// 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 '
' +
'
' +
'
' +
'
';
}
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 };
}