Implements a master toggle for security features across config, CLI, and application logic. This allows granular control over whether the advanced security middleware and protections are active globally.
268 lines
11 KiB
JavaScript
268 lines
11 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
|
|
};
|
|
const initialQuery = new URLSearchParams(window.location.search).get("q");
|
|
if (initialQuery) searchInput.value = initialQuery;
|
|
|
|
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();
|
|
})();
|