// SPDX-License-Identifier: GPL-3.0-or-later /* === History section + chart interaction === */ let statusTimer = null; function setStatus(generatedAt) { const txt = $("#statuspill-txt"); const dot = $("#statusdot"); if (!txt || !dot) return; const ms = Date.now() - new Date(generatedAt).getTime(); const secs = Math.max(0, Math.round(ms / 1000)); let label; if (secs < 5) label = "checked just now"; else if (secs < 60) label = "checked " + secs + "s ago"; else if (secs < 3600) label = "checked " + Math.floor(secs / 60) + "m ago"; else if (secs < 86400) label = "checked " + Math.floor(secs / 3600) + "h ago"; else label = "checked " + Math.floor(secs / 86400) + "d ago"; txt.textContent = label; dot.className = "dot" + (secs >= 3600 ? " stale" : ""); // Status bar (wide desktop chrome) mirrors the hero's freshness line. const sbar = document.getElementById("sbar-check"); if (sbar) { sbar.textContent = label; sbar.hidden = false; sbar.classList.toggle("stale", secs >= 3600); } } async function renderTrend() { if (STATIC_DATA) return; const trend = $("#trend"); const runs = await fetchHistory(); const data = runs.filter((r) => typeof r.score === "number").slice(-12); const histEl = $("#history"); const ledger = $("#runledger"); if (ledger) ledger.innerHTML = ""; if (data.length < 2) { /* First recorded run: a bare hidden section reads as "broken". Explain what will grow here instead, using the score we already have. */ if (data.length === 1 && !STATIC_DATA) { histEl.hidden = false; trend.innerHTML = '
Only one run on record so far \u2014 it scored ' + data[0].score + "/100." + "Run another check and your health-score trend appears here.
"; } else { histEl.hidden = true; } return; } histEl.hidden = false; renderHistoryLedger(data); updateHistoryBadge(data.length); // Hero mini-sparkline: the score's recent direction, visible at a glance. const spark = $("#hero-spark"); if (spark) { spark.innerHTML = miniSpark(data); spark.hidden = false; } trend.innerHTML = '
' + scoreChart(data) + severityChart(data) + '
'; setTimeout(() => { const histData = data; const showRun = (idx) => { const r = histData[idx]; if (!r) return; const prev = idx > 0 ? histData[idx - 1] : null; let msg = "Run " + (idx + 1) + "/" + histData.length + " \u00b7 " + fmtWhen(r.at) + " \u00b7 score " + r.score + "/100"; if (prev) msg += " (prev " + prev.score + ")"; showToast(msg + " \u2014 opening Changes"); const diffEl = $("#diff"); if (diffEl) { diffEl.hidden = false; diffEl.open = true; diffEl.scrollIntoView({ behavior: scrollBehavior(), block: "start" }); } }; // Keyboard parity: the chart marks are role="button", so Enter and Space // must do what the click does — the finding cards behave the same way. const activate = (el) => { const idx = Number(el.dataset.idx || el.closest("[data-idx]")?.dataset.idx); if (Number.isFinite(idx)) showRun(idx); }; trend.querySelectorAll(".hist-dot, .hist-bar").forEach(el => { el.addEventListener("click", () => activate(el)); el.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") { e.preventDefault(); activate(el); } }); }); }, 80); } /* Run ledger: every recorded run as a row — date, score, delta vs the previous run, severity counts. Newest first. Pure-ish (string building only) so dashboard-state.test.js can pin the vocabulary. */ function historyLedgerHtml(runs) { const rows = (runs || []).filter((r) => typeof r.score === "number").slice(-12); if (rows.length < 2) return ""; let html = '
Past runs newest first' + '
' + '
' + ''; for (let i = rows.length - 1; i >= 0; i -= 1) { const r = rows[i]; const prev = i > 0 ? rows[i - 1] : null; const d = prev ? r.score - prev.score : null; const dCls = d === null ? "" : d > 0 ? "up" : d < 0 ? "down" : "flat"; const dTxt = d === null ? "—" : (d > 0 ? "+" + d : String(d)); const c = r.counts || {}; html += '
' + esc(fmtWhen(r.at) || "—") + "" + '' + r.score + "/100" + '' + dTxt + "" + '' + (c.high || 0) + " high · " + (c.medium || 0) + " med · " + (c.info || 0) + " info
"; } return html + "
"; } function renderHistoryLedger(runs) { const box = $("#runledger"); if (!box) return; box.innerHTML = historyLedgerHtml(runs); $("#histclear")?.addEventListener("click", async () => { if (!window.confirm("Delete all stored runs? This cannot be undone.")) return; const ok = await clearHistoryApi(); showToast(ok ? "History cleared" : "Could not clear history"); if (ok) renderTrend(lastReportData ? lastReportData.score : undefined); }); } function setTabBadge(id, text) { const b = document.getElementById(id); if (!b) return; if (text) { b.textContent = text; b.hidden = false; } else { b.textContent = ""; b.hidden = true; } } function updateHistoryBadge(n) { setTabBadge("tabbadge-history", n >= 2 ? String(n) : ""); } /* Checks-tab badge: findings with a problem (high/medium) on this report. */ function updateChecksBadge(data) { const n = (data.findings || []).filter((f) => f.severity === "high" || f.severity === "medium").length; setTabBadge("tabbadge-checks", n > 0 ? String(n) : ""); }