// SPDX-License-Identifier: GPL-3.0-or-later /* === Status zone: gauge, score, status message — PREMIUM HIERARCHY === */ const SCORE_RING_R = 26; const SCORE_RING_C = 2 * Math.PI * SCORE_RING_R; function renderGauge(score, size) { const s = size || 96; const scoreColor = score >= 80 ? "var(--green)" : score >= 50 ? "var(--yellow)" : "var(--red)"; return '
' + '' + '' + '' + '' + '
0
/100
' + '
'; } function animateScore(target) { const num = $("#scorenum"); const live = $("#scorering"); if (!num) return; let reduce = false; try { reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches; } catch {} /* The trace is drawn up to the score — the machine's own vital sign. pathLength="100" makes the dash arithmetic trivial. The dot that marks the end of the trace is placed by renderStatus itself (an inline `left`), so nothing here can leave it behind: it is HTML, not a circle with a `cx`. */ const setPulse = (v) => { if (live) live.style.strokeDashoffset = String(100 - v); }; if (reduce) { num.textContent = target; setPulse(target); return; } const t0 = performance.now(); const dur = 900; const step = (now) => { const p = Math.min(1, (now - t0) / dur); const v = target * (1 - Math.pow(1 - p, 3)); num.textContent = Math.round(v); setPulse(v); if (p < 1) requestAnimationFrame(step); }; requestAnimationFrame(step); } /* The product's own motif: a heartbeat, not a dashboard gauge. Same waveform as the brand mark and the favicon, but the shape comes from the report — a steady monitoring rhythm, with one taller beat for every finding that needs attention (taller still for a high severity). A clean machine draws the rhythm alone. */ function pulsePath(w, amps) { const mid = 14; if (!amps.length) return "M0 " + mid + " H" + w; const step = w / amps.length; let d = "M0 " + mid; amps.forEach((amp, i) => { const a = 3.5 + 9 * amp; const peak = 16 - a; const trough = peak + a * 0.6 + 3; d += " H" + (i * step + step * 0.42).toFixed(1); d += " l2 2 l1 " + (-a).toFixed(1) + " l1 " + (a * 0.6 + 3).toFixed(1) + " l2 " + (-(trough - mid)).toFixed(1); d += " H" + ((i + 1) * step).toFixed(1); }); return d; } /* What the trace is made of: a fixed monitoring rhythm, with the beats that carry a finding made taller in proportion to its severity. */ function pulseTrace(findings) { const high = findings.filter((f) => f.severity === "high").length; const med = findings.filter((f) => f.severity === "medium").length; const issues = Math.min(5, high + med); const amp = high ? 1 : med ? 0.55 : 0; return pulsePath(120, [1, 2, 3, 4, 5].map((_, i) => (i < issues ? amp : 0.16))); } function renderStatusMessage(findings) { const high = findings.filter((f) => f.severity === "high").length; const med = findings.filter((f) => f.severity === "medium").length; const inf = findings.filter((f) => f.severity === "info").length; let headline, sublabel, level; if (high + med === 0) { if (findings.length === 0) { headline = "Healthy"; sublabel = "No high or medium issues — no issues detected"; } else { headline = "Healthy"; sublabel = "No high or medium issues — " + inf + " informational " + (inf === 1 ? "note" : "notes") + " — no action needed"; } level = "ok"; } else if (high > 0) { headline = high === 1 ? "Needs attention" : "Needs attention"; sublabel = high + " high-severity " + (high === 1 ? "issue" : "issues") + (med ? " · " + med + " warnings" : ""); level = "critical"; } else { headline = "Needs attention"; sublabel = med + " " + (med === 1 ? "issue needs" : "issues need") + " attention"; level = "warn"; } return { headline, sublabel, level, high, med, inf }; } function renderStatus(data) { const { findings = [], system = {} } = data; const ver = (data.version || "").replace(/^v/, ""); $(".sysinfo").innerHTML = [esc(system.distro), esc(system.kernel), esc((system.cores || "?") + " core(s)"), "up " + esc(system.uptime)].filter(Boolean).join(" · ") + (ver ? ' · v' + esc(ver) + "" : ""); const high = findings.filter((f) => f.severity === "high").length; const med = findings.filter((f) => f.severity === "medium").length; const inf = findings.filter((f) => f.severity === "info").length; const { headline, sublabel } = renderStatusMessage(findings); const scoreDelta = typeof data.scoreDelta === "number" ? data.scoreDelta : null; const deltaCls = scoreDelta === null ? "" : scoreDelta > 0 ? "delta-up" : scoreDelta < 0 ? "delta-down" : "delta-flat"; const deltaIcon = scoreDelta === null ? "" : scoreDelta > 0 ? "↗" : scoreDelta < 0 ? "↘" : "→"; const deltaTxt = scoreDelta === null ? "no baseline yet" : (scoreDelta > 0 ? "+" + scoreDelta : String(scoreDelta)) + " since last check"; const score = typeof data.score === "number" ? data.score : 0; const chips = (data.newCount > 0 ? 'NEW ' + data.newCount + "" : "") + (data.fixedCount > 0 ? 'FIXED ' + data.fixedCount + "" : ""); const status = $("#status"); /* The score lives in the rail now: a compact Health block — number, a progress hairline, the headline and one counts line. No gauge, no KPI tiles: those belong to a dashboard, and this is an application. */ const band = score >= 80 ? "ok" : score >= 50 ? "warn" : "bad"; status.className = "status " + band; const deltaShort = scoreDelta === null ? "" : (scoreDelta > 0 ? "+" + scoreDelta : String(scoreDelta)); const trace = pulseTrace(findings); status.innerHTML = '
' + 'Health' + (scoreDelta === null ? "" : '' + deltaIcon + " " + deltaShort + "") + "
" + '
0/100
' + '" + '
' + esc(headline) + "
" + '
' + (high + med > 0 ? '' + high + " high · " + med + " medium" + (inf ? " · " + inf + " info" : "") : esc(sublabel)) + "
" + '
' + chips + '' + '' + "
" + '
' + ' ' + "
"; animateScore(score); return { high, medium: med, info: inf, score }; }