const $ = (sel) => document.querySelector(sel);
const $$ = (sel) => Array.from(document.querySelectorAll(sel));
function badgeClass(sev) {
const s = (sev || "").toLowerCase();
if (["ok", "info", "low"].includes(s)) return "good";
if (["medium", "warn", "warning"].includes(s)) return "warn";
if (["high", "critical", "bad"].includes(s)) return "bad";
return "";
}
function fmtTime(ts) {
if (!ts) return "—";
try {
if (typeof ts === "number") return new Date(ts * 1000).toLocaleString();
return new Date(ts).toLocaleString();
} catch {
return String(ts);
}
}
function toast(msg) {
const t = $("#toast");
t.textContent = msg;
t.classList.add("show");
setTimeout(() => t.classList.remove("show"), 3500);
}
async function api(path, opts) {
const res = await fetch(path, opts);
const ct = res.headers.get("content-type") || "";
const data = ct.includes("application/json") ? await res.json() : await res.text();
return { ok: res.ok, status: res.status, data };
}
let state = {
view: "dashboard",
reports: [],
selected: null,
};
function setView(v) {
state.view = v;
$$(".nav-item").forEach((a) => a.classList.toggle("active", a.dataset.view === v));
$("#view-dashboard").classList.toggle("hidden", v !== "dashboard");
$("#view-reports").classList.toggle("hidden", v !== "reports");
$("#view-settings").classList.toggle("hidden", v !== "settings");
if (v === "dashboard") {
$("#page-title").textContent = "Dashboard";
$("#page-sub").textContent = "Обзор состояния и последние отчёты";
}
if (v === "reports") {
$("#page-title").textContent = "Reports";
$("#page-sub").textContent = "Просмотр отчётов и применение действий (по согласию)";
}
if (v === "settings") {
$("#page-title").textContent = "Settings";
$("#page-sub").textContent = "Локальные параметры интерфейса";
}
}
function renderTable(el, rows, { limit = null } = {}) {
const r = limit ? rows.slice(0, limit) : rows;
const header = `
`;
const body = r
.map((it) => {
const cls = badgeClass(it.severity);
return `
${escapeHtml(it.summary || "(no summary)")}
${escapeHtml(it.severity || "unknown")}
${escapeHtml(fmtTime(it.created_utc || it.mtime))}
open
`;
})
.join("");
el.innerHTML = header + body;
el.querySelectorAll(".row.clickable").forEach((row) => {
row.addEventListener("click", () => openReport(row.dataset.id));
});
}
function escapeHtml(s) {
return String(s)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
async function refresh() {
const h = await api("/api/health");
if (h.ok) {
$("#health-pill").textContent = "UI: OK";
$("#status-value").textContent = "Protected";
$("#status-hint").textContent = "local UI service running";
} else {
$("#health-pill").textContent = "UI: error";
$("#status-value").textContent = "Degraded";
$("#status-hint").textContent = "health check failed";
}
const r = await api("/api/reports");
if (r.ok) {
state.reports = r.data.reports || [];
$("#reports-count").textContent = String(state.reports.length);
renderTable($("#latest-table"), state.reports, { limit: 6 });
renderTable($("#reports-table"), state.reports);
} else {
toast("Failed to load reports");
}
}
async function openReport(reportId) {
const p = await api(`/api/reports/${encodeURIComponent(reportId)}`);
if (!p.ok) {
toast("Report not found");
return;
}
const report = p.data.report;
state.selected = { id: reportId, report };
$("#report-details").classList.remove("hidden");
$("#details-title").textContent = `Report: ${reportId}`;
$("#details-sub").textContent = "Read-only details";
$("#details-summary").textContent = report.summary || "—";
const sev = report.severity || "unknown";
const cls = badgeClass(sev);
const badge = $("#details-severity");
badge.textContent = sev;
badge.className = `badge ${cls}`;
$("#details-created").textContent = fmtTime(report.created_utc || "—");
$("#raw").textContent = JSON.stringify(report, null, 2);
const fixesEl = $("#fixes");
fixesEl.classList.add("fixes");
const fixes = report.suggested_fixes || [];
if (!fixes.length) {
fixesEl.innerHTML = `No suggested fixes
`;
} else {
fixesEl.innerHTML = fixes
.map((f) => {
return `
${escapeHtml(f.title || f.id || "Fix")}
${escapeHtml(f.description || "")}
consent
`;
})
.join("");
fixesEl.querySelectorAll("button[data-fix]").forEach((b) => {
b.addEventListener("click", () => applyFix(b.dataset.fix));
});
}
}
async function applyFix(fixId) {
if (!state.selected) return;
const reportId = state.selected.id;
const confirm1 = window.confirm(
`Apply fix '${fixId}' for report '${reportId}'?\n\nThis action requires explicit operator confirmation.`
);
if (!confirm1) return;
const res = await api("/api/apply-fix", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ report_id: reportId, fix_id: fixId, confirm: true }),
});
if (res.ok) {
toast("Fix applied");
} else {
if (res.status === 501) {
toast("Fix runner not implemented yet (MVP UI only)");
return;
}
if (res.status === 409) {
toast("Confirmation required");
return;
}
toast("Failed to apply fix");
}
}
function bindEvents() {
$$(".nav-item").forEach((a) => {
a.addEventListener("click", (e) => {
e.preventDefault();
setView(a.dataset.view);
});
});
$("#refresh-btn").addEventListener("click", () => refresh());
$("#close-details").addEventListener("click", () => {
$("#report-details").classList.add("hidden");
state.selected = null;
});
$("#filter").addEventListener("input", () => {
const q = $("#filter").value.trim().toLowerCase();
const rows = !q
? state.reports
: state.reports.filter((r) =>
String(r.summary || "").toLowerCase().includes(q) || String(r.id || "").toLowerCase().includes(q)
);
renderTable($("#reports-table"), rows);
});
}
bindEvents();
setView("dashboard");
refresh();