Files
warpbox/static/js/admin/dashboard.js
Daniel Legt a2c80ac105 feat(admin): make dashboard live and disk-aware
Wire dashboard panels to real alerts, activity, boxes, and users data instead of static mock rows.

Enable working dashboard actions (close alerts, close low alerts, cleanup expired boxes, exports, and navigation).

Update storage overview to use real filesystem free/total space from the uploads volume.

Make top alert chip data-driven across admin pages.
2026-05-04 10:54:44 +03:00

329 lines
13 KiB
JavaScript

(() => {
const menuController = window.WarpBoxUI?.bindMenuBar?.() || {
close() {
document.querySelectorAll(".menu-item.is-open").forEach((item) => {
item.classList.remove("is-open");
item.querySelector(".menu-button")?.setAttribute("aria-expanded", "false");
});
}
};
const dataNode = document.getElementById("dashboard-data");
const toast = document.getElementById("toast");
const statusText = document.getElementById("statusText");
const modal = document.querySelector("[data-alert-modal]");
const backdrop = document.querySelector("[data-modal-backdrop]");
const modalTitle = document.getElementById("modalTitle");
const modalMeta = document.getElementById("modalMeta");
const alertCountValue = document.getElementById("alertCountValue");
const alertStatNote = document.getElementById("alertStatNote");
const alertsCard = document.getElementById("alertsCard");
const topAlertChip = document.getElementById("topAlertChip");
const topTaskbar = document.querySelector(".admin-taskbar");
const dashboardData = parseDashboardData();
if (!statusText || !alertsCard || !topAlertChip) return;
function parseDashboardData() {
try {
return JSON.parse(dataNode?.textContent || "{}");
} catch (_) {
return {};
}
}
function showToast(message, type = "info", duration = 2200) {
if (window.WarpBoxUI) {
window.WarpBoxUI.toast(message, type, { target: toast, duration });
return;
}
if (!toast) return;
toast.textContent = message;
toast.classList.add("is-visible");
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => toast.classList.remove("is-visible"), duration);
}
function setStatus(message) {
statusText.textContent = message;
}
function openModal(title, meta) {
if (!modal || !backdrop || !modalTitle || !modalMeta) return;
modalTitle.textContent = title;
modalMeta.textContent = meta;
modal.classList.add("is-visible");
modal.setAttribute("aria-hidden", "false");
backdrop.classList.add("is-visible");
}
function closeModal() {
modal?.classList.remove("is-visible");
modal?.setAttribute("aria-hidden", "true");
backdrop?.classList.remove("is-visible");
}
function visibleAlertRows() {
return Array.from(document.querySelectorAll(".alert-row")).filter((row) => !row.classList.contains("is-dismissed"));
}
function updateStickyHeader() {
topTaskbar?.classList.toggle("is-scrolled", window.scrollY > 4);
}
function updateAlertSummary() {
const rows = visibleAlertRows();
const counts = rows.reduce((acc, row) => {
const severity = row.dataset.severity || "low";
acc[severity] = (acc[severity] || 0) + 1;
return acc;
}, { high: 0, medium: 0, low: 0 });
const score = counts.high * 5 + counts.medium * 2 + counts.low;
const total = rows.length;
const stateClass = counts.high > 0 || score >= 12 ? "is-danger" : counts.medium >= 2 || score >= 5 ? "is-warning" : total > 0 ? "is-info" : "is-ok";
alertsCard.classList.remove("is-ok", "is-info", "is-warning", "is-danger");
alertsCard.classList.add(stateClass);
topAlertChip.classList.remove("is-ok", "is-info", "is-warning", "is-danger");
topAlertChip.classList.add(stateClass);
if (alertCountValue) alertCountValue.textContent = String(total);
topAlertChip.textContent = total === 0 ? "OK no alerts" : `! ${total} alerts`;
if (alertStatNote) {
alertStatNote.innerHTML = total === 0
? '<span class="stat-note-pill">all clear</span>'
: `<span class="stat-note-pill">${counts.high} high</span><span class="stat-note-pill">${counts.medium} medium</span><span class="stat-note-pill">${counts.low} low</span>`;
}
}
function scrollToSection(id) {
const target = document.getElementById(id);
if (!target) return;
target.scrollIntoView({ behavior: "smooth", block: "start" });
setStatus(`Focused ${id.replace("-", " ")}`);
}
function downloadFile(filename, content, type) {
const blob = new Blob([content], { type });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = filename;
anchor.click();
URL.revokeObjectURL(url);
}
function csvEscape(value) {
const text = String(value ?? "");
if (!/[",\n]/.test(text)) return text;
return `"${text.replaceAll('"', '""')}"`;
}
function exportBoxesCSV() {
const rows = dashboardData.boxes || [];
const header = ["id", "status", "files", "size", "created", "expires", "flags"];
const lines = rows.map((box) => [
box.id,
box.status_label,
`${box.complete_files}/${box.file_count}`,
box.total_size_label,
box.created_at_label,
box.expires_at_label,
(box.flags || []).join("|")
].map(csvEscape).join(","));
downloadFile(`warpbox-dashboard-boxes-${new Date().toISOString().replaceAll(":", "-")}.csv`, [header.join(","), ...lines].join("\n"), "text/csv;charset=utf-8");
showToast("Dashboard boxes exported", "success");
}
function exportAlertsJSON() {
downloadFile(`warpbox-dashboard-alerts-${new Date().toISOString().replaceAll(":", "-")}.json`, JSON.stringify(dashboardData.alerts || [], null, 2), "application/json;charset=utf-8");
showToast("Dashboard alerts exported", "success");
}
function exportSnapshot() {
downloadFile(`warpbox-dashboard-${new Date().toISOString().replaceAll(":", "-")}.json`, JSON.stringify(dashboardData, null, 2), "application/json;charset=utf-8");
showToast("Dashboard snapshot exported", "success");
}
async function postAlertAction(action, ids) {
const response = await fetch("/admin/alerts/actions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action, ids })
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || "Alert action failed");
return payload;
}
async function postBoxAction(action, extra = {}) {
const response = await fetch("/admin/boxes/actions", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action, ...extra })
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || "Box action failed");
return payload;
}
async function closeAlert(row) {
const id = row?.dataset.alertId;
if (!id) return;
await postAlertAction("close", [id]);
row.classList.add("is-dismissed");
updateAlertSummary();
showToast(`Closed alert ${row.dataset.alertCode || id}`, "success");
setStatus(`Closed alert ${row.dataset.alertCode || id}`);
}
async function closeLowAlerts() {
const rows = Array.from(document.querySelectorAll('.alert-row[data-severity="low"]'));
const ids = rows.map((row) => row.dataset.alertId).filter(Boolean);
if (!ids.length) {
showToast("No low alerts to close");
return;
}
await postAlertAction("close", ids);
rows.forEach((row) => row.classList.add("is-dismissed"));
updateAlertSummary();
showToast(`Closed ${ids.length} low alert(s)`, "success");
setStatus(`Closed ${ids.length} low alert(s)`);
}
async function cleanupExpiredBoxes() {
if (!window.confirm("Clean up expired boxes now? This can delete expired box data.")) return;
const payload = await postBoxAction("cleanup_expired");
showToast(payload.message || "Expired cleanup complete", payload.ok ? "success" : "warning", 3200);
setStatus(payload.message || "Expired cleanup complete");
window.setTimeout(() => window.location.reload(), 900);
}
async function runCommand(command) {
if (command === "refresh") {
window.location.reload();
return;
}
if (command === "dashboard-snapshot") return exportSnapshot();
if (command === "logout") {
window.location.href = "/admin/logout";
return;
}
if (command === "compact-mode") {
document.body.classList.toggle("is-compact");
showToast("Toggled compact density");
return;
}
if (command === "show-all-boxes") {
window.location.href = "/admin/boxes";
return;
}
if (command === "show-all-alerts") {
window.location.href = "/admin/alerts";
return;
}
if (command === "open-users") {
window.location.href = "/admin/users";
return;
}
if (command === "open-activity") {
window.location.href = "/admin/activity";
return;
}
if (command === "open-settings") {
window.location.href = "/admin/settings";
return;
}
if (command === "export-boxes") return exportBoxesCSV();
if (command === "export-alerts") return exportAlertsJSON();
if (command === "close-low-alerts") return closeLowAlerts();
if (command === "cleanup-expired") return cleanupExpiredBoxes();
if (command === "shortcuts") {
showToast("Shortcuts: F5 refresh, Alt+A alerts, Alt+B boxes, Alt+R activity, Esc close menus/modal.", "info", 3600);
return;
}
if (command === "about") {
showToast("Live WarpBox admin dashboard backed by alerts, activity, boxes, users, and settings.", "info", 3600);
}
}
document.querySelectorAll("[data-command]").forEach((button) => {
button.addEventListener("click", async () => {
menuController.close();
try {
await runCommand(button.dataset.command);
} catch (error) {
showToast(error.message || "Command failed", "error", 3600);
setStatus(error.message || "Command failed");
}
});
});
document.querySelectorAll("[data-scroll-to]").forEach((button) => {
button.addEventListener("click", () => {
menuController.close();
scrollToSection(button.dataset.scrollTo);
});
});
document.querySelectorAll("[data-view-meta]").forEach((button) => {
button.addEventListener("click", () => {
const row = button.closest(".alert-row");
const title = row?.dataset.alertTitle || "Alert Metadata";
let meta = row?.dataset.alertMeta || "{}";
try {
meta = JSON.stringify(JSON.parse(meta), null, 2);
} catch (_) {
meta = row?.dataset.alertMeta || "{}";
}
openModal(`${title} (${row?.dataset.alertCode || "alert"})`, meta);
});
});
document.querySelectorAll("[data-close-alert]").forEach((button) => {
button.addEventListener("click", async () => {
try {
await closeAlert(button.closest(".alert-row"));
} catch (error) {
showToast(error.message || "Could not close alert", "error", 3600);
}
});
});
document.querySelector("[data-close-modal]")?.addEventListener("click", closeModal);
backdrop?.addEventListener("click", closeModal);
topAlertChip.addEventListener("click", (event) => {
if (document.getElementById("alerts")) {
event.preventDefault();
scrollToSection("alerts");
}
});
window.addEventListener("scroll", updateStickyHeader, { passive: true });
document.addEventListener("keydown", async (event) => {
if (event.key === "Escape") {
menuController.close();
closeModal();
}
if (event.key === "F5") {
event.preventDefault();
await runCommand("refresh");
}
if (event.altKey && event.key.toLowerCase() === "a") {
event.preventDefault();
scrollToSection("alerts");
}
if (event.altKey && event.key.toLowerCase() === "b") {
event.preventDefault();
scrollToSection("recent-boxes");
}
if (event.altKey && event.key.toLowerCase() === "r") {
event.preventDefault();
scrollToSection("recent-activity");
}
});
updateAlertSummary();
updateStickyHeader();
})();