266 lines
10 KiB
JavaScript
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();
|
|
})();
|