// 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 += '
' + catIcon(cat, 12) + " " + esc(CATEGORY_LABELS[cat] || cat) + '' + rows.length + "
"; for (const { c, s } of rows) { const gotoAttr = s.goto ? ' data-goto-code="' + esc(((findingsByCheck.get(c.id) || [])[0] || {}).code || "") + '" data-goto-check="' + esc(c.id) + '" tabindex="0" role="button" aria-label="Show ' + esc(c.id) + ' finding"' : ""; const why = s.why ? '' + s.why + "" : ""; const cls = MX_CLS[s.key]; body += '
" + '' + '' + esc(c.id) + '' + '' + esc(c.title) + '' + '' + MX_STATE[s.key] + "" + why + "
"; } } 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) => '
' + esc(r.v) + '' + '' + (r.kind === "code" ? "code" : "title") + '' + '
' ).join("") : '
Nothing is ignored. Dismiss a finding to hide it here.
'; return '
Ignored findings
' + '
Dismissed findings and patterns, saved to ' + esc(ig.path || "~/.config/linux-doctor/config.json") + '
' + list; } async function openChecksMatrix() { openModal('
All checks
Loading\u2026
', "All checks"); const checks = STATIC_DATA ? [] : await fetchChecks(); const ignore = await fetchIgnore(); openModal(checksMatrixHtml(lastReportData || {}, checks) + ignoredSectionHtml(ignore), "All checks"); } function gotoFinding(check, code) { closeModal(); activeFilter = "all"; document.querySelectorAll(".fpill").forEach((b) => b.classList.toggle("active", b.dataset.sev === "all")); syncGroupsOpen(); applyFilters(); const card = (code && document.querySelector('#report details[data-code="' + code.replace(/"/g, '\\"') + '"]')) || document.querySelector('#report details[data-check="' + check.replace(/"/g, '\\"') + '"]'); if (card) { card.setAttribute("open", ""); card.scrollIntoView({ behavior: scrollBehavior(), block: "start" }); if (card.tabIndex >= 0) card.focus({ preventScroll: true }); showToast("Showing " + check); } } function setupChecksMatrix() { $("#checksmatrixbtn")?.addEventListener("click", openChecksMatrix); // Deep-link rows: mouse and keyboard share one persistent handler. const body = $("#modal-body"); if (!body) return; body.addEventListener("click", async (e) => { const rm = e.target.closest("[data-ig-remove]"); if (rm && isModalOpen()) { rm.disabled = true; const payload = rm.dataset.igKind === "code" ? { code: rm.dataset.igRemove, remove: true } : { pattern: rm.dataset.igRemove, remove: true }; const ok = await editIgnore(payload); showToast(ok ? "Removed from ignore list" : "Could not remove"); if (ok) openChecksMatrix(); return; } const row = e.target.closest("[data-goto-check]"); if (!row || !isModalOpen()) return; gotoFinding(row.dataset.gotoCheck, row.dataset.gotoCode); }); body.addEventListener("keydown", (e) => { if (e.key !== "Enter" && e.key !== " ") return; const row = e.target.closest("[data-goto-check]"); if (!row || !isModalOpen()) return; e.preventDefault(); gotoFinding(row.dataset.gotoCheck, row.dataset.gotoCode); }); } /* Checks view: the same matrix, inline and framed as its own view. The inventory needs to be scannable (a real column rule), countable (a count per category), and quietable (a filter that drops everything clean). Cached so the filter repaints without another fetch. */ let lastChecks = null; let checksProblemsOnly = false; function paintChecksView() { const box = $("#checksview"); if (!box) return; const data = lastReportData || {}; const checks = lastChecks || []; const m = checksMatrixBody(data, checks, { problemsOnly: checksProblemsOnly }); const empty = checksProblemsOnly && m.problems === 0 ? '
Nothing needs attention — every check came back clean.
' : ""; box.innerHTML = '
' + '
All checks \u00b7 ' + m.summary + "
" + '" + "
" + '' + empty + '
' + m.body + "
"; } async function renderChecksView() { const box = $("#checksview"); if (!box) return; const checks = STATIC_DATA ? [] : await fetchChecks(); lastChecks = checks; paintChecksView(); } function setupChecksView() { // Deep-link rows inside the Checks view: same goto as the modal, but the // finding lives in Overview, so switch views first. Bound once. const cv = $("#view-checks"); if (cv && !cv.dataset.bound) { cv.dataset.bound = "1"; cv.addEventListener("click", (e) => { if (e.target.closest("#mx-problems")) { checksProblemsOnly = !checksProblemsOnly; paintChecksView(); return; } const row = e.target.closest("[data-goto-check]"); if (!row) return; switchView("overview"); gotoFinding(row.dataset.gotoCheck, row.dataset.gotoCode); }); cv.addEventListener("keydown", (e) => { if (e.key !== "Enter" && e.key !== " ") return; const row = e.target.closest("[data-goto-check]"); if (!row) return; e.preventDefault(); switchView("overview"); gotoFinding(row.dataset.gotoCheck, row.dataset.gotoCode); }); } }