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>
|
||||||
</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">
|
<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 %}
|
{% if not item.assignment %}
|
||||||
<!-- Primary action: visible because the unassigned card
|
<!-- Primary action: visible because the unassigned card
|
||||||
is most likely getting clicked on right after creation -->
|
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('');
|
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() {
|
async function loadLiveStats() {
|
||||||
// Skip work while the tab is hidden in the background.
|
// Skip work while the tab is hidden in the background.
|
||||||
if (document.hidden) return;
|
if (document.hidden) return;
|
||||||
@@ -2240,10 +2277,19 @@ async function loadLiveStats() {
|
|||||||
const r = await fetch(`/api/projects/${projectId}/live-stats`);
|
const r = await fetch(`/api/projects/${projectId}/live-stats`);
|
||||||
if (!r.ok) return;
|
if (!r.ok) return;
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
lsRender(j.locations || []);
|
lsLastData = j.locations || [];
|
||||||
|
lsRender(lsLastData);
|
||||||
|
lsPaintInline(lsLastData);
|
||||||
} catch (e) { /* keep last render */ }
|
} 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() {
|
function startLiveStats() {
|
||||||
if (liveStatsTimer) return; // already running
|
if (liveStatsTimer) return; // already running
|
||||||
loadLiveStats();
|
loadLiveStats();
|
||||||
|
|||||||
Reference in New Issue
Block a user