From bb5b407c982f84504c7800431d52abc800586e34 Mon Sep 17 00:00:00 2001 From: serversdown Date: Sun, 21 Jun 2026 21:30:54 +0000 Subject: [PATCH] feat: live status chip on NRL list cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a compact live-status chip to each NRL card in the location list, so the inline list next to the map (Overview tab) and the Sound > NRLs tab show live state alongside the new live tiles. Both surfaces share location_list.html, so this lands in both. - Chip shows "● dB" when measuring (tinted green/amber/red at the same 55/70 thresholds as the live tiles), else Stopped / Offline / Wedged. Hidden for cards with no assigned unit and for vibration locations. - Painted by the existing 15s Overview poller (no extra requests). Repaints on htmx:afterSwap so the chips survive the NRL list reloading (e.g. the 30s dashboard swap). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../partials/projects/location_list.html | 6 ++- templates/projects/detail.html | 48 ++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/templates/partials/projects/location_list.html b/templates/partials/projects/location_list.html index d5ccecb..8dfb08e 100644 --- a/templates/partials/projects/location_list.html +++ b/templates/partials/projects/location_list.html @@ -78,8 +78,12 @@ - +
+ + {% if not item.assignment %} diff --git a/templates/projects/detail.html b/templates/projects/detail.html index 2f214fe..ad3a85e 100644 --- a/templates/projects/detail.html +++ b/templates/projects/detail.html @@ -2233,6 +2233,43 @@ function lsRender(locations) { document.getElementById('ls-tiles').innerHTML = locations.map(lsRenderTile).join(''); } +// Compact level-tinted pill classes for the inline NRL-card chips. +function lsInlineLevelPill(leq) { + if (leq == null) return 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300'; + if (leq >= LS_LEVEL_RED) return 'bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-300'; + if (leq >= LS_LEVEL_AMBER) return 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300'; + return 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'; +} +function lsInlineChipHtml(loc) { + if (!loc.unit_id) return ''; // no unit assigned → no chip + const base = 'inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-medium '; + const measuring = loc.measurement_state === 'Start' || loc.measurement_state === 'Measure'; + const hasData = loc.measurement_state != null || loc.leq != null; + const reachable = loc.is_reachable !== false; + if (loc.connection_state === 'wedged') + return `Wedged`; + if (!reachable || !hasData) + return `Offline`; + if (measuring) { + const leqStr = (loc.leq == null || loc.leq === '') ? '--' : loc.leq; + return `` + + `${lsEsc(leqStr)} dB`; + } + return `Stopped`; +} +// Paint the inline chips on the NRL list cards (Overview + Sound tab). +function lsPaintInline(locations) { + const byId = {}; + for (const l of locations) byId[l.id] = l; + document.querySelectorAll('[data-loc-live]').forEach(el => { + const loc = byId[el.getAttribute('data-loc-live')]; + const html = loc ? lsInlineChipHtml(loc) : ''; + el.innerHTML = html; + el.classList.toggle('hidden', !html); + }); +} + +let lsLastData = []; async function loadLiveStats() { // Skip work while the tab is hidden in the background. if (document.hidden) return; @@ -2240,10 +2277,19 @@ async function loadLiveStats() { const r = await fetch(`/api/projects/${projectId}/live-stats`); if (!r.ok) return; const j = await r.json(); - lsRender(j.locations || []); + lsLastData = j.locations || []; + lsRender(lsLastData); + lsPaintInline(lsLastData); } catch (e) { /* keep last render */ } } +// The NRL list partial reloads via htmx (e.g. the 30s dashboard swap), which +// wipes the painted chips — repaint from the last poll as soon as it settles. +document.body.addEventListener('htmx:afterSwap', (e) => { + const id = e.target && e.target.id; + if (id === 'project-locations' || id === 'sound-locations') lsPaintInline(lsLastData); +}); + function startLiveStats() { if (liveStatsTimer) return; // already running loadLiveStats();