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 = `
Summary
Severity
Updated
`; 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();