2026-05-01 00:29:06 +03:00
|
|
|
(() => {
|
|
|
|
|
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");
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-05-04 10:54:44 +03:00
|
|
|
const dataNode = document.getElementById("dashboard-data");
|
2026-05-01 00:29:06 +03:00
|
|
|
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");
|
|
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
const dashboardData = parseDashboardData();
|
|
|
|
|
|
2026-05-01 00:29:06 +03:00
|
|
|
if (!statusText || !alertsCard || !topAlertChip) return;
|
|
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
function parseDashboardData() {
|
|
|
|
|
try {
|
|
|
|
|
return JSON.parse(dataNode?.textContent || "{}");
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showToast(message, type = "info", duration = 2200) {
|
2026-05-01 00:29:06 +03:00
|
|
|
if (window.WarpBoxUI) {
|
2026-05-04 10:54:44 +03:00
|
|
|
window.WarpBoxUI.toast(message, type, { target: toast, duration });
|
2026-05-01 00:29:06 +03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!toast) return;
|
|
|
|
|
toast.textContent = message;
|
|
|
|
|
toast.classList.add("is-visible");
|
|
|
|
|
window.clearTimeout(showToast.timer);
|
2026-05-04 10:54:44 +03:00
|
|
|
showToast.timer = window.setTimeout(() => toast.classList.remove("is-visible"), duration);
|
2026-05-01 00:29:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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("-", " ")}`);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
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");
|
|
|
|
|
}
|
2026-05-01 00:29:06 +03:00
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
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;
|
2026-05-01 00:29:06 +03:00
|
|
|
}
|
2026-05-04 10:54:44 +03:00
|
|
|
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)`);
|
|
|
|
|
}
|
2026-05-01 00:29:06 +03:00
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
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);
|
|
|
|
|
}
|
2026-05-01 00:29:06 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.querySelectorAll("[data-command]").forEach((button) => {
|
2026-05-04 10:54:44 +03:00
|
|
|
button.addEventListener("click", async () => {
|
2026-05-01 00:29:06 +03:00
|
|
|
menuController.close();
|
2026-05-04 10:54:44 +03:00
|
|
|
try {
|
|
|
|
|
await runCommand(button.dataset.command);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showToast(error.message || "Command failed", "error", 3600);
|
|
|
|
|
setStatus(error.message || "Command failed");
|
|
|
|
|
}
|
2026-05-01 00:29:06 +03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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 || "{}";
|
|
|
|
|
}
|
2026-05-04 10:54:44 +03:00
|
|
|
openModal(`${title} (${row?.dataset.alertCode || "alert"})`, meta);
|
2026-05-01 00:29:06 +03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
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);
|
|
|
|
|
}
|
2026-05-01 00:29:06 +03:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
document.querySelector("[data-close-modal]")?.addEventListener("click", closeModal);
|
|
|
|
|
backdrop?.addEventListener("click", closeModal);
|
|
|
|
|
topAlertChip.addEventListener("click", (event) => {
|
2026-05-04 10:54:44 +03:00
|
|
|
if (document.getElementById("alerts")) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
scrollToSection("alerts");
|
|
|
|
|
}
|
2026-05-01 00:29:06 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
window.addEventListener("scroll", updateStickyHeader, { passive: true });
|
|
|
|
|
|
2026-05-04 10:54:44 +03:00
|
|
|
document.addEventListener("keydown", async (event) => {
|
2026-05-01 00:29:06 +03:00
|
|
|
if (event.key === "Escape") {
|
|
|
|
|
menuController.close();
|
|
|
|
|
closeModal();
|
|
|
|
|
}
|
|
|
|
|
if (event.key === "F5") {
|
|
|
|
|
event.preventDefault();
|
2026-05-04 10:54:44 +03:00
|
|
|
await runCommand("refresh");
|
2026-05-01 00:29:06 +03:00
|
|
|
}
|
|
|
|
|
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();
|
|
|
|
|
})();
|