EH/MU/FR-C1: live sensor-health KPIs, stale detection, refresh interval - #962
EH/MU/FR-C1: live sensor-health KPIs, stale detection, refresh interval#962Mustafa-1089 wants to merge 4 commits into
Conversation
- Replace static KPI cards with live Total/Online/Degraded/Offline/Low Battery counts computed from the same data as the table - Surface backend Degraded/Offline status and last-seen time as stale indicators - Add Degraded status pill and filter option - Drive refresh interval from a single REFRESH_MS constant and display it - Map lastAudioMinutesAgo into the Last Audio column
Vamshi-Gollapelly
left a comment
There was a problem hiding this comment.
I went through the changes and there are a couple of things that should be addressed before this is ready to merge. I left inline comments on the battery/status handling and KPI counting logic, as both could produce incorrect results in some scenarios. Once those are updated and verified, I'm happy to review it again.
| return `<span class="pill pill-success">${s}</span>`; | ||
| } | ||
|
|
||
| if (s === "Degraded") { |
There was a problem hiding this comment.
The batteryPct < 20 check above still runs before any status check, so a Degraded/Offline sensor with low battery will show "Low Battery" here even though the KPI card and row highlight correctly show Degraded/Offline. Since status is the source of truth everywhere else in this PR, can you fold it in here too and drop the raw threshold branch?
```js
function pillHtml(status) {
const s = String(status || "").trim();
if (s === "Online" || s === "Success") return <span class="pill pill-success">${s}</span>;
if (s === "Degraded" || s === "Low Battery") return <span class="pill pill-warning">${s}</span>;
if (s === "Offline" || s === "Failed") return <span class="pill pill-danger">${s}</span>;
return <span class="pill pill-warning">${s || "Unknown"}</span>;
}
```
There was a problem hiding this comment.
The battery check was overriding status. I've dropped the raw batteryPct threshold from pillHtml and it now trusts the backend status everywhere (pill, row highlight, and the filter). So a Degraded or Offline sensor with low battery now shows its correct status. Pushed in the latest commit.
| try { | ||
| const data = await apiFetch("/sensors/updates"); | ||
| lastItems = Array.isArray(data.items) ? data.items : []; | ||
| updateSensorKpis(lastItems); |
There was a problem hiding this comment.
When backend status is something other than Online/Degraded/Offline/Low Battery (your testing notes mention nodes coming back Offline/null), updateSensorKpis counts it toward Total but doesn't add it to any bucket. Can you confirm the backend always returns one of those four values, or should this have a fallback bucket so the cards can't silently under-count?
There was a problem hiding this comment.
The backend derives exactly four statuses (Online / Degraded / Offline / Low Battery), so those are the full set today, but you are right that the cards shouldn't silently under-count if that ever changes. I've added an other fallback bucket so every sensor is counted and the buckets always reconcile with the total. Pushed in the latest commit.
…I fallback bucket
…o real backend field
| style="margin-bottom: 12px;" | ||
| > | ||
| Last updated at: -- | ||
| Last updated at: -- · auto-refresh every <span id="refresh-interval-label">--</span> |
There was a problem hiding this comment.
The interval is inside #last-updated-at, but updateLastUpdated() replaces the parent’s entire textContent at script.js (line 209). Nothing assigns REFRESH_MS to the span, so the required visible interval changes from -- to nothing
| const tr = document.createElement("tr"); | ||
|
|
||
| if (typeof item.batteryPct === "number" && item.batteryPct < 20) { | ||
| if (item.status === "Low Battery") { |
There was a problem hiding this comment.
The backend overwrites Offline/Degraded with Low Battery when the battery is below its threshold.The frontend then adds the stale treatment only for Degraded or Offline. So a low-battery sensor whose heartbeat is old or missing appears only as low battery?
Summary
Turns the Sensor Health dashboard (FR-C1) from a static shell into a live view. The KPI cards, status pills, stale indicators, and refresh interval are now all driven by real data from the backend's /sensors/updates endpoint instead of hardcoded placeholder values.
Why
Before this change the page had the right structure but wasn't meeting the FR-C1 acceptance criteria:
The three summary cards showed fixed text ("Live", "MQTT / Seeded", "15s") that didn't reflect the actual sensors, so the KPIs and the table could say different things.
Nothing surfaced stale or offline sensors, the backend already derives a Degraded/Offline status, but the frontend had no pill, filter, or visual treatment for it.
The "Last Audio" column read a field (item.lastAudio) that the backend doesn't return, so it was always blank.
The refresh interval was hardcoded in three separate places that could drift apart.
What changed
Live KPIs: Replaced the 3 static cards with 5 (Total / Online / Degraded / Offline / Low Battery), computed from the same array that fills the table, so the cards can never disagree with the rows.
Stale detection: Degraded and Offline rows are now visually highlighted, each status pill shows a "seen Xm ago" subtext from lastSeenMinutesAgo, and a "Degraded" option was added to the status filter.
Refresh interval: Introduced a single REFRESH_MS constant driving both the auto-refresh timer and the displayed "auto-refresh every 15s" label, one source of truth.
Last Audio column: Now maps the real lastAudioMinutesAgo field instead of a non-existent one.
Low-battery highlight: Keys off the backend's derived Low Battery status rather than a hardcoded <20 threshold, so frontend and backend agree.
Testing
Verified against the real backend, not just a syntax check:
Seeded 5 of the existing topology nodes with health fields; GET :9000/sensors/updates returned all 14 nodes with the expected mix (2 Online, 1 Degraded, 1 Offline, 1 Low Battery, rest Offline/null).
Confirmed KPI counts sum correctly to the total and match the table.
Loaded the page and confirmed the new cards, filter, and layout render.
Not in scope / known follow-ups