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.
This commit is contained in:
@@ -111,6 +111,18 @@
|
||||
.alerts-scroll { height: 326px; }
|
||||
.boxes-scroll { height: 352px; }
|
||||
.activity-scroll { height: 326px; }
|
||||
.dashboard-empty-state {
|
||||
margin: 8px;
|
||||
padding: 10px;
|
||||
color: #333333;
|
||||
background: #ffffcc;
|
||||
border-top: 1px solid #ffffff;
|
||||
border-left: 1px solid #ffffff;
|
||||
border-right: 1px solid #a08000;
|
||||
border-bottom: 1px solid #a08000;
|
||||
font-size: 12px;
|
||||
line-height: 15px;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert-list { display: grid; min-width: 0; }
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
});
|
||||
}
|
||||
};
|
||||
const dataNode = document.getElementById("dashboard-data");
|
||||
const toast = document.getElementById("toast");
|
||||
const statusText = document.getElementById("statusText");
|
||||
const modal = document.querySelector("[data-alert-modal]");
|
||||
@@ -19,18 +20,28 @@
|
||||
const topAlertChip = document.getElementById("topAlertChip");
|
||||
const topTaskbar = document.querySelector(".admin-taskbar");
|
||||
|
||||
const dashboardData = parseDashboardData();
|
||||
|
||||
if (!statusText || !alertsCard || !topAlertChip) return;
|
||||
|
||||
function showToast(message, type = "info") {
|
||||
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 });
|
||||
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"), 2600);
|
||||
showToast.timer = window.setTimeout(() => toast.classList.remove("is-visible"), duration);
|
||||
}
|
||||
|
||||
function setStatus(message) {
|
||||
@@ -91,45 +102,159 @@
|
||||
setStatus(`Focused ${id.replace("-", " ")}`);
|
||||
}
|
||||
|
||||
const commandMessages = {
|
||||
refresh: "CURRENTLY_MOCKED_LEAVE_AS_IS: dashboard refresh would re-fetch dashboard data.",
|
||||
"dashboard-snapshot": "CURRENTLY_MOCKED_LEAVE_AS_IS: dashboard snapshot export would start here.",
|
||||
logout: "CURRENTLY_MOCKED_LEAVE_AS_IS: logout would submit to the account logout route.",
|
||||
"compact-mode": "Toggled compact density.",
|
||||
"show-all-boxes": "TO-DO: navigate to the admin boxes view when that page exists.",
|
||||
"show-all-alerts": "TO-DO: navigate to /admin/alerts.",
|
||||
"export-boxes": "CURRENTLY_MOCKED_LEAVE_AS_IS: boxes CSV export would be requested.",
|
||||
"export-alerts": "CURRENTLY_MOCKED_LEAVE_AS_IS: alerts JSON export would be requested.",
|
||||
"cleanup-dry-run": "CURRENTLY_MOCKED_LEAVE_AS_IS: cleanup dry run would calculate affected boxes without deleting.",
|
||||
"dismiss-low-alerts": "Closed visible low-severity alerts in this mock.",
|
||||
"config-snapshot": "CURRENTLY_MOCKED_LEAVE_AS_IS: config snapshot would summarize runtime settings and sources.",
|
||||
"support-summary": "CURRENTLY_MOCKED_LEAVE_AS_IS: support summary would collect safe diagnostic information.",
|
||||
"thumbnail-rebuild": "CURRENTLY_MOCKED_LEAVE_AS_IS: thumbnail rebuild would enqueue preview regeneration.",
|
||||
"open-users": "TO-DO: navigate to the admin users view when that page exists.",
|
||||
"open-settings": "TO-DO: navigate to the admin settings view when that page exists.",
|
||||
"alerts-help": "Alerts use title, description, severity, metadata JSON, trace identifier, and unique numeric code.",
|
||||
shortcuts: "Shortcuts: F5 refresh, Alt+A alerts, Alt+B boxes, Alt+R activity, Esc close menus/modal.",
|
||||
about: "WarpBox dashboard mock v5, single-window Win98 account dashboard."
|
||||
};
|
||||
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 runCommand(command) {
|
||||
if (command === "compact-mode") document.body.classList.toggle("is-compact");
|
||||
if (command === "dismiss-low-alerts") {
|
||||
document.querySelectorAll('.alert-row[data-severity="low"]').forEach((row) => row.classList.add("is-dismissed"));
|
||||
updateAlertSummary();
|
||||
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;
|
||||
}
|
||||
if (command === "show-all-boxes") window.location.hash = "recent-boxes";
|
||||
if (command === "show-all-alerts") window.location.hash = "alerts";
|
||||
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)`);
|
||||
}
|
||||
|
||||
const message = commandMessages[command] || `Command: ${command}`;
|
||||
showToast(message);
|
||||
setStatus(message);
|
||||
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", () => {
|
||||
button.addEventListener("click", async () => {
|
||||
menuController.close();
|
||||
runCommand(button.dataset.command);
|
||||
try {
|
||||
await runCommand(button.dataset.command);
|
||||
} catch (error) {
|
||||
showToast(error.message || "Command failed", "error", 3600);
|
||||
setStatus(error.message || "Command failed");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,37 +275,39 @@
|
||||
} catch (_) {
|
||||
meta = row?.dataset.alertMeta || "{}";
|
||||
}
|
||||
openModal(`${title} (${row?.dataset.alertCode || "mock"})`, meta);
|
||||
openModal(`${title} (${row?.dataset.alertCode || "alert"})`, meta);
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-dismiss-alert]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const row = button.closest(".alert-row");
|
||||
row?.classList.add("is-dismissed");
|
||||
updateAlertSummary();
|
||||
showToast(`Closed alert ${row?.dataset.alertCode || "mock"}.`);
|
||||
setStatus(`Closed alert ${row?.dataset.alertCode || "mock"}`);
|
||||
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) => {
|
||||
event.preventDefault();
|
||||
scrollToSection("alerts");
|
||||
if (document.getElementById("alerts")) {
|
||||
event.preventDefault();
|
||||
scrollToSection("alerts");
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener("scroll", updateStickyHeader, { passive: true });
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
document.addEventListener("keydown", async (event) => {
|
||||
if (event.key === "Escape") {
|
||||
menuController.close();
|
||||
closeModal();
|
||||
}
|
||||
if (event.key === "F5") {
|
||||
event.preventDefault();
|
||||
runCommand("refresh");
|
||||
await runCommand("refresh");
|
||||
}
|
||||
if (event.altKey && event.key.toLowerCase() === "a") {
|
||||
event.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user