// SPDX-License-Identifier: GPL-3.0-or-later /* === Schedule strip: is the daily check actually scheduled? === The dashboard used to know only "Re-run now". This strip answers the follow-up question — will the machine check itself tomorrow? Read-only: installing stays a CLI decision; the strip shows state plus the exact command to copy. Hidden when the backend has no schedule to report (static --html exports, unreachable API). */ async function fetchSchedule() { if (STATIC_DATA) return null; try { const res = await fetch("/api/schedule", { cache: "no-store" }); if (!res.ok) return null; return (await res.json()).schedule || null; } catch { return null; } } function browserNotifyState() { try { if (!("Notification" in window)) return "unavailable"; if (Notification.permission === "granted") return "on"; return "off"; } catch { return "unavailable"; } } /* Pure (string in, string out) so dashboard-state.test.js can pin it. */ function scheduleHtml(s, notify) { if (!s) return ""; const dot = (on) => ''; // Dot and label wrap as one unit — a separator stranded at line end reads // as a dangling bullet on narrow screens. const notifyBit = notify === "unavailable" ? "" : '' + dot(notify === "on") + "Browser alerts " + (notify === "on" ? "on" : "off") + ""; if (!s.installed) { return dot(false) + "Daily check off — install the timer and the machine notifies you only when something new breaks." + '' + notifyBit; } if (s.active) { return dot(true) + "Daily check on — runs after boot, then daily · notifies only on new findings." + notifyBit; } return dot(false) + "Daily check needs attention — timer units exist but the timer isn't active. Re-run linux-doctor --install-timer." + '' + notifyBit; } /* The shared notices row is visible when at least one notice is. Both renderers call this after updating, so async schedule fetches and sync posture renders converge on the same visibility. */ function syncNotices() { const wrap = $("#notices"); if (!wrap) return; const kids = [...wrap.children]; wrap.hidden = kids.length === 0 || kids.every((k) => k.hidden); syncNoticeGrid(); } function renderSchedule(s) { const el = $("#schedstrip"); if (!el) return; const html = scheduleHtml(s, browserNotifyState()); if (!html) { el.hidden = true; el.innerHTML = ""; } else { el.hidden = false; el.innerHTML = html; } syncNotices(); } /* Schedule view: the full picture — last run, next run, timer state, and the exact commands as recipes. Pure builders kept separate for tests. */ /* Coarse relative freshness for the "last run" cell. */ function schedAgo(iso) { const t = Date.parse(iso || ""); if (!Number.isFinite(t)) return "\u2014"; const s = Math.max(0, Math.round((Date.now() - t) / 1000)); if (s < 60) return "just now"; if (s < 3600) return Math.floor(s / 60) + "m ago"; if (s < 86400) return Math.floor(s / 3600) + "h ago"; return Math.floor(s / 86400) + "d ago"; } /* The next run is read as a distance first ("in 24h") and a date second — the same reading order as "just now" for the last one. */ function schedAhead(ms) { if (!Number.isFinite(ms)) return "\u2014"; const h = Math.round((ms - Date.now()) / 3600000); if (h <= 0) return "any moment"; if (h < 48) return "in " + h + "h"; return "in " + Math.round(h / 24) + "d"; } /* The command IS the information, so it is printed, not hidden behind a button: an accent-ruled mono slip — the same recipe language as the findings' Recommended next step — with a quiet copy next to it. */ function recipeHtml(cmd, primary) { return '
' + esc(cmd) + "" +
'