Files
warpbox/static/js/admin/dashboard.js

329 lines
13 KiB
JavaScript
Raw Normal View History

(() => {
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();
})();