Files
warpbox/static/js/admin/alerts.js

266 lines
10 KiB
JavaScript

(() => {
const menuController = window.WarpBoxUI?.bindMenuBar?.() || { close() {} };
const dataNode = document.getElementById("alerts-data");
const alertsBody = document.getElementById("alerts-body");
const searchInput = document.getElementById("search-input");
const severityFilter = document.getElementById("severity-filter");
const statusFilter = document.getElementById("status-filter");
const sourceFilter = document.getElementById("source-filter");
const sortFilter = document.getElementById("sort-filter");
const selectAll = document.getElementById("select-all");
const selectedCountEl = document.getElementById("selected-count");
const totalPill = document.getElementById("alerts-total-pill");
const toast = document.getElementById("toast");
const detailEls = {
title: document.getElementById("detail-title"),
severity: document.getElementById("detail-severity"),
status: document.getElementById("detail-status"),
code: document.getElementById("detail-code"),
trace: document.getElementById("detail-trace"),
time: document.getElementById("detail-time"),
description: document.getElementById("detail-description"),
metadata: document.getElementById("detail-metadata")
};
if (!dataNode || !alertsBody) return;
const state = {
alerts: parseData(),
selected: new Set(),
activeID: null
};
function parseData() {
try {
return JSON.parse(dataNode.textContent || "[]");
} catch (_) {
return [];
}
}
function showToast(message, type = "info", duration = 1800) {
window.WarpBoxUI?.toast?.(message, type, { target: toast, duration });
}
function createdLabel(value) {
const parsed = new Date(value);
if (Number.isNaN(parsed.getTime())) return "-";
return parsed.toISOString().replace("T", " ").slice(0, 16) + " UTC";
}
function allAlerts() {
return state.alerts.slice();
}
function filteredAlerts() {
const query = searchInput.value.trim().toLowerCase();
const severity = severityFilter.value;
const status = statusFilter.value;
const group = sourceFilter.value;
const rows = allAlerts().filter((alert) => {
const haystack = [
alert.title,
alert.message,
alert.code,
alert.trace,
alert.group
].join(" ").toLowerCase();
const matchesSearch = !query || haystack.includes(query);
const matchesSeverity = severity === "all" || alert.severity === severity;
const matchesStatus = status === "all" || alert.status === status;
const matchesGroup = group === "all" || alert.group === group;
return matchesSearch && matchesSeverity && matchesStatus && matchesGroup;
});
const order = { high: 3, medium: 2, low: 1 };
rows.sort((a, b) => {
if (sortFilter.value === "severity") return (order[b.severity] || 0) - (order[a.severity] || 0);
if (sortFilter.value === "oldest") return String(a.created_at).localeCompare(String(b.created_at));
return String(b.created_at).localeCompare(String(a.created_at));
});
return rows;
}
function ensureActive(rows) {
if (rows.length === 0) {
state.activeID = null;
return null;
}
const found = rows.find((item) => item.id === state.activeID);
if (found) return found;
state.activeID = rows[0].id;
return rows[0];
}
function render() {
const rows = filteredAlerts();
alertsBody.innerHTML = "";
rows.forEach((alert) => alertsBody.appendChild(buildRow(alert)));
const active = ensureActive(rows);
if (active) renderDetails(active);
renderSummary(rows);
syncSelected();
syncSelectAll(rows);
}
function buildRow(alert) {
const row = document.createElement("tr");
if (state.activeID === alert.id) row.classList.add("is-selected");
row.innerHTML = `
<td><input type="checkbox" class="row-check"${state.selected.has(alert.id) ? " checked" : ""}></td>
<td>${escapeHtml(alert.title || "-")}</td>
<td><span class="alerts-pill ${escapeHtml(alert.severity || "low")}">${escapeHtml(alert.severity || "low")}</span></td>
<td><span class="alerts-pill ${escapeHtml(alert.status || "open")}">${escapeHtml(alert.status || "open")}</span></td>
<td>${escapeHtml(alert.code || "-")}</td>
<td>${escapeHtml(alert.trace || "-")}</td>
<td>${createdLabel(alert.created_at)}</td>
<td><button class="win98-button alerts-row-button row-open" type="button">Open</button></td>
`;
row.addEventListener("click", (event) => {
if (event.target.closest("button") || event.target.closest("input")) return;
state.activeID = alert.id;
render();
});
row.querySelector(".row-open")?.addEventListener("click", () => {
state.activeID = alert.id;
render();
});
row.querySelector(".row-check")?.addEventListener("change", (event) => {
if (event.target.checked) state.selected.add(alert.id);
else state.selected.delete(alert.id);
syncSelected();
syncSelectAll(filteredAlerts());
});
return row;
}
function renderDetails(alert) {
detailEls.title.textContent = alert.title || "";
detailEls.severity.textContent = alert.severity || "";
detailEls.status.textContent = alert.status || "";
detailEls.code.textContent = alert.code || "";
detailEls.trace.textContent = alert.trace || "";
detailEls.time.textContent = createdLabel(alert.created_at);
detailEls.description.textContent = alert.message || "";
detailEls.metadata.textContent = JSON.stringify(alert.meta || {}, null, 2);
}
function renderSummary(rows) {
const open = rows.filter((item) => item.status === "open").length;
const high = rows.filter((item) => item.severity === "high" && item.status !== "closed").length;
const ack = rows.filter((item) => item.status === "acked").length;
const closed = rows.filter((item) => item.status === "closed").length;
document.querySelector("[data-open-count]").textContent = String(open);
document.querySelector("[data-high-count]").textContent = String(high);
document.querySelector("[data-ack-count]").textContent = String(ack);
document.querySelector("[data-closed-count]").textContent = String(closed);
totalPill.textContent = `${rows.length} alerts`;
}
function syncSelected() {
selectedCountEl.textContent = `Selected: ${state.selected.size}`;
}
function syncSelectAll(rows) {
selectAll.checked = rows.length > 0 && rows.every((alert) => state.selected.has(alert.id));
}
async function postAction(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 || "Request failed");
state.alerts = payload.alerts || [];
}
async function runAction(action) {
const ids = Array.from(state.selected);
if (!ids.length && (action === "ack" || action === "close" || action === "delete")) {
showToast("Select one or more alerts first", "warning");
return;
}
if (action === "open-only") {
statusFilter.value = "open";
render();
showToast("Showing open alerts only");
return;
}
if (action === "refresh") {
window.location.reload();
return;
}
if (action === "copy-meta") {
const active = allAlerts().find((item) => item.id === state.activeID);
if (active) {
navigator.clipboard?.writeText(JSON.stringify(active.meta || {}, null, 2)).catch(() => {});
}
showToast("Metadata copied");
return;
}
if (action === "export") {
const blob = new Blob([JSON.stringify(filteredAlerts(), null, 2)], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
const anchor = document.createElement("a");
anchor.href = url;
anchor.download = `warpbox-alerts-${new Date().toISOString().replaceAll(":", "-")}.json`;
anchor.click();
URL.revokeObjectURL(url);
showToast("Visible alerts exported");
return;
}
if (action === "help-codes") {
showToast("Codes map to internal security and service traces.");
return;
}
if (action === "help-meta") {
showToast("Metadata shows extra context for each alert.");
return;
}
await postAction(action, ids);
state.selected.clear();
render();
showToast(`Action complete: ${action}`, "success");
}
function escapeHtml(value) {
return window.WarpBoxUI?.htmlEscape?.(value) || String(value ?? "");
}
[searchInput, severityFilter, statusFilter, sourceFilter, sortFilter].forEach((control) => {
control.addEventListener(control.tagName === "INPUT" ? "input" : "change", render);
});
selectAll?.addEventListener("change", () => {
const rows = filteredAlerts();
rows.forEach((alert) => {
if (selectAll.checked) state.selected.add(alert.id);
else state.selected.delete(alert.id);
});
render();
});
document.querySelectorAll("[data-command]").forEach((button) => {
button.addEventListener("click", async () => {
menuController.close();
try {
await runAction(button.dataset.command);
} catch (error) {
showToast(error.message, "error", 3200);
}
});
});
document.addEventListener("keydown", async (event) => {
if (event.key === "Escape") menuController.close();
if (event.key === "F5") {
event.preventDefault();
await runAction("refresh");
}
});
render();
})();