feat: live status chip on NRL list cards
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 "● <Leq> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -78,8 +78,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right column: small assign/unassign pill + 3-dot menu -->
|
||||
<!-- Right column: live status chip + small assign/unassign pill + 3-dot menu -->
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<!-- Live status chip — painted by the Overview live poller
|
||||
(paintInlineLive) keyed on data-loc-live. Hidden until
|
||||
there's live data for this location. -->
|
||||
<span class="nrl-live-chip hidden" data-loc-live="{{ item.location.id }}"></span>
|
||||
{% if not item.assignment %}
|
||||
<!-- Primary action: visible because the unassigned card
|
||||
is most likely getting clicked on right after creation -->
|
||||
|
||||
@@ -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 `<span class="${base}bg-red-100 dark:bg-red-900/40 text-red-600 dark:text-red-300">Wedged</span>`;
|
||||
if (!reachable || !hasData)
|
||||
return `<span class="${base}bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400">Offline</span>`;
|
||||
if (measuring) {
|
||||
const leqStr = (loc.leq == null || loc.leq === '') ? '--' : loc.leq;
|
||||
return `<span class="${base}${lsInlineLevelPill(lsNum(loc.leq))}">`
|
||||
+ `<span class="w-1.5 h-1.5 rounded-full bg-current"></span>${lsEsc(leqStr)} dB</span>`;
|
||||
}
|
||||
return `<span class="${base}bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400">Stopped</span>`;
|
||||
}
|
||||
// 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();
|
||||
|
||||
Reference in New Issue
Block a user