feat grey out stale quick cards instead of hiding

Cards now always display, even with old or missing data: - Stale readings (>10 min) grey out the card (opacity-50) - Last known value still shown with its age - Status pill shows 'Stale' instead of 'Unreachable' - Cards with no data show '— · no data' instead of disappearing

Commit
6df5721bf8d997426dc2147f71a6fde59c5b179a
Author
GPT-5 medium <codex@openai.com>
Author date
Committer
GPT-5 medium <codex@openai.com>
Committer date
Changed files
roles/dashboard/public/js/dashboard/quick-readings.js
index 1b4c9f0a..abe8ba7e 100644..100644
@@ -91,13 +91,22 @@
91 91 const state = qualityState(quality);
92 92 const percent = Number.isFinite(value) ? rangePercent(value, range) : 50;
93 93 const timestamp = reading?.received_at || reading?.timestamp;
94 Removed: const age = timestamp ? formatAge(ageMs(timestamp)) : "unknown age";
94 Added: const age = timestamp ? ageMs(timestamp) : null;
95 Added: const ageText = age !== null ? formatAge(age) : "no data";
95 96
97 Added: // Determine if the reading is stale (older than 10 minutes)
98 Added: const isStale = age === null || age > 10 * 60 * 1000;
99 Added:
96 100 configureRange(item, range, unit);
97 Removed: item.classList.remove("border-success", "border-warning", "border-danger");
98 Removed: const borderClass = qualityBorderClass(state);
99 Removed: if (borderClass) item.classList.add(borderClass);
101 Added: item.classList.remove("border-success", "border-warning", "border-danger", "opacity-50");
100 102
103 Added: if (isStale) {
104 Added: item.classList.add("opacity-50");
105 Added: } else {
106 Added: const borderClass = qualityBorderClass(state);
107 Added: if (borderClass) item.classList.add(borderClass);
108 Added: }
109 Added:
101 110 const marker = item.querySelector("[data-reading-quick-marker]");
102 111 marker.style.left = `${percent}%`;
103 112 marker.style.backgroundColor = Number.isFinite(value) ? markerColor(quality) : "";
@@ -106,8 +115,8 @@
106 115 meter.setAttribute("aria-valuenow", Math.round(percent).toString());
107 116 meter.setAttribute("aria-label", `${label} ${qualityLabel(quality)} at ${formatValue(value, range)} ${unit}`);
108 117 item.querySelector("[data-reading-quick-value]").textContent = Number.isFinite(value)
109 Removed: ? `${formatValue(value, range)} ${unit} · ${age}`
110 Removed: : "—";
118 Added: ? `${formatValue(value, range)} ${unit} · ${ageText}`
119 Added: : `— · ${ageText}`;
111 120 }
112 121
113 122 export async function loadQuickReadings() {
roles/dashboard/public/js/dashboard/status.js
index 54409c80..26a9effc 100644..100644
@@ -85,6 +85,16 @@
85 85 });
86 86 }
87 87
88 Added: function quickCardLabel(state) {
89 Added: return {
90 Added: healthy: "OK",
91 Added: stale: "Stale",
92 Added: "probe-error": "Error",
93 Added: unreachable: "Stale",
94 Added: unknown: ""
95 Added: }[state] || "";
96 Added: }
97 Added:
88 98 function loadQuickCardStatuses(statuses) {
89 99 const pills = Array.from(document.querySelectorAll("[data-reading-quick-status]"));
90 100 if (!pills.length) return;
@@ -92,8 +102,10 @@
92 102 pills.forEach(pill => {
93 103 const probe = pill.dataset.readingQuickStatus;
94 104 const state = stateForNodeStatus(statuses[probe] || null);
95 Removed: pill.className = `status-pill is-${state}`;
96 Removed: pill.textContent = statusLabel(state);
105 Added: // Use 'stale' styling for unreachable on quick cards (less alarming)
106 Added: const displayState = state === "unreachable" ? "stale" : state;
107 Added: pill.className = `status-pill is-${displayState}`;
108 Added: pill.textContent = quickCardLabel(state);
97 109 });
98 110 }
99 111