// SPDX-License-Identifier: GPL-3.0-or-later
/* === All-checks matrix ===
A diagnostic tool earns trust by showing what it looked AT, not only
what it found. This modal lists every registered check with its live
outcome on this machine: findings (worst severity first), clean,
not-applicable for this profile, atomic skips, or runtime errors.
Rows with findings deep-link back to their cards in the report. */
/* Result states — a fixed vocabulary, not free prose. The three finding
words are the ones the report and the severity legend already use, so a
result column reads the same everywhere in the product. */
const MX_STATE = {
high: "needs attention", medium: "warning", info: "observation",
ok: "clean", na: "not applicable", skip: "skipped", err: "failed to run",
};
const MX_CLS = { high: "high", medium: "medium", info: "info", ok: "ok", na: "na", skip: "na", err: "err" };
/* Build the matrix once, returned in parts: the view frames it with its own
header and column rule, the modal keeps the "All checks" heading. */
function checksMatrixBody(data, checks, opts) {
const problemsOnly = !!(opts && opts.problemsOnly);
const findingsByCheck = new Map();
for (const f of data.findings || []) {
if (!findingsByCheck.has(f.check)) findingsByCheck.set(f.check, []);
findingsByCheck.get(f.check).push(f);
}
const errors = new Map((data.checkErrors || []).map((e) => [e.check, e.error]));
const atomic = new Set((data.skippedChecks || []).map((s) => s.id));
const SEV_RANK = { high: 0, medium: 1, info: 2 };
const dot = (cls) => '';
function statusOf(c) {
if (errors.has(c.id)) return { rank: 0, key: "err", icon: dot("err"), why: esc(errors.get(c.id) || "failed to run") };
const fs = findingsByCheck.get(c.id) || [];
if (fs.length) {
fs.sort((a, b) => (SEV_RANK[a.severity] ?? 9) - (SEV_RANK[b.severity] ?? 9));
const sev = fs[0].severity;
return { rank: 1, key: sev, icon: dot(sev), why: fs.length > 1 ? fs.length + " findings" : "", goto: true };
}
if (!c.appliesHere) return { rank: 3, key: "na", icon: dot("na"), why: "runs on: " + (c.appliesTo || []).join(", ") };
if (c.skipOnAtomic || atomic.has(c.id)) return { rank: 3, key: "skip", icon: dot("atomic"), why: c.atomicReason || "skipped on immutable/atomic systems" };
return { rank: 2, key: "ok", icon: dot("ok"), why: "" };
}
const cats = new Map();
for (const c of checks) {
const cat = c.category || "other";
if (!cats.has(cat)) cats.set(cat, []);
cats.get(cat).push(c);
}
// Stats are computed over the whole registry, so the summary never changes
// shape when the "problems only" filter is on.
const all = new Map();
let problemCount = 0, errorCount = 0, cleanCount = 0, naCount = 0;
for (const [cat, list] of cats) {
const rows = list.map((c) => ({ c, s: statusOf(c) }))
.sort((a, b) => a.s.rank - b.s.rank || a.c.id.localeCompare(b.c.id));
for (const { s } of rows) {
if (s.rank === 1) problemCount++;
else if (s.rank === 0) errorCount++;
else if (s.rank === 2) cleanCount++;
else naCount++;
}
all.set(cat, rows);
}
let body = "";
for (const cat of CATEGORY_ORDER) {
const rows = (all.get(cat) || []).filter((r) => !problemsOnly || r.s.rank <= 1);
if (!rows.length) continue;
body += '
";
}
}
const total = checks.length;
const summary = total + " registered \u00b7 " +
problemCount + " with findings \u00b7 " +
cleanCount + " clean \u00b7 " +
(errorCount ? errorCount + " failed to run \u00b7 " : "") +
naCount + " not applicable here" +
(STATIC_DATA ? " \u00b7 static report: live check list unavailable" : "");
return { body, summary, problems: problemCount + errorCount };
}
function checksMatrixHtml(data, checks, opts) {
const m = checksMatrixBody(data, checks, opts);
const head = opts && opts.head === false ? "" : '
All checks
';
return head + '
' + m.summary + "
" + m.body;
}
/* The persistent state the GUI can otherwise only set (Dismiss) lives here:
the ignore list, with a remove for each entry and the config path. */
function ignoredSectionHtml(ig) {
const rows = [];
for (const p of ig.patterns || []) rows.push({ kind: "title", v: p });
for (const c of ig.codes || []) rows.push({ kind: "code", v: c });
const list = rows.length
? rows.map((r) =>
'