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();