// SPDX-License-Identifier: GPL-3.0-or-later /* === Sections: next step, fixed, diff, skipped, check errors === */ function firstSentence(text) { return String(text || "").split(/(?<=\.)\s/)[0].trim(); } function renderSecurityPosture(data) { const el = $("#security-posture"); if (!el) return; const sec = securityPostureFindings(data.findings || []); if (!sec.length) { el.hidden = true; el.innerHTML = ""; syncNotices(); return; } const counts = { high: 0, medium: 0, info: 0 }; for (const f of sec) counts[f.severity] = (counts[f.severity] || 0) + 1; // Only visually compete if high-severity security issue is present const hasHigh = counts.high > 0; const hasMedium = counts.medium > 0; if (!hasHigh && !hasMedium) { // Quiet informational: one subtle line inside the shared notices row. el.hidden = false; el.className = "security-posture"; el.innerHTML = '
' + '' + sec.length + ' security observations — no action needed' + ''; } else { const tone = hasHigh ? "high" : "warn"; el.hidden = false; el.className = "security-posture " + tone; const label = hasHigh ? counts.high + " high" : hasMedium ? counts.medium + " warning" + (counts.medium > 1 ? "s" : "") : ""; el.innerHTML = '' + 'Security posture · ' + esc(label + (counts.info ? " · " + counts.info + " info" : "") + ' — firewall, SSH, login & boot') + '' + ''; } syncNotices(); el.querySelector("[data-spjump]")?.addEventListener("click", () => { activeFilter = "all"; document.querySelectorAll(".fpill").forEach((b) => b.classList.toggle("active", b.dataset.sev === "all")); setGroupBy("category"); requestAnimationFrame(() => { applyFilters(); scrollToGroup("cat", "security"); }); }, { once: true }); } function renderNextStep(data) { const el = $("#nexthep"); if (!el) return; const order = { high: 0, medium: 1, info: 2 }; const pick = data.nextAction || [...(data.findings || [])].sort((a, b) => (order[a.severity] ?? 9) - (order[b.severity] ?? 9)).find((f) => f.fix); if (!pick || !pick.fix) { el.hidden = true; syncNoticeGrid(); return; } // Find the full finding for evidence/detail const full = (data.findings || []).find((f) => f.code === pick.code) || pick; const detail = full.detail || pick.detail || ""; // "Why it matters" must add context, never echo: it shows everything // AFTER the first sentence (What happened already prints the full text). // Single-sentence details hide the section instead of repeating line one. const whyText = String(detail).split(/(?<=\.)\s/).slice(1).join(" ").trim(); const evidence = full.evidence || ""; const code = pick.code || full.code || ""; const sev = pick.severity || full.severity || "medium"; const sevLabel = sev === "high" ? "High priority" : sev === "medium" ? "Recommended" : "Informational"; el.hidden = false; el.innerHTML = '