From 5e3645e2295997989307984e0ddf6963d2023c82 Mon Sep 17 00:00:00 2001 From: serversdown Date: Wed, 10 Jun 2026 19:52:57 +0000 Subject: [PATCH] fix(slm): show real cache freshness in device list + draw L1/L10 on card chart 1. "No recent check-in" was always shown because the row's last-check text read unit.slm_last_check (a Terra-View roster field the monitor never updates), while the live freshness lives in SLMM's cached NL43Status.last_seen. Carry that last_seen onto the unit (unit.cache_last_seen) and display it (falling back to slm_last_check). Also treat "Measure" as Measuring in the badge, to match the panel and the cache's MEASURING_STATES. 2. The dashboard card chart only had Lp + Leq datasets, so L1/L10 never drew even though the cards showed them. Add L1 (purple) and L10 (orange) datasets and feed ln1/ln2 in both the /history backfill and the live /monitor frames. Percentiles parse via numOrNull so a missing "-.-" leaves a gap (spanGaps) instead of dropping the line to 0. Co-Authored-By: Claude Opus 4.8 --- backend/routers/slm_dashboard.py | 2 + templates/partials/slm_device_list.html | 6 ++- templates/sound_level_meters.html | 72 +++++++++++++++++++------ 3 files changed, 62 insertions(+), 18 deletions(-) diff --git a/backend/routers/slm_dashboard.py b/backend/routers/slm_dashboard.py index f3d3fcd..d35746c 100644 --- a/backend/routers/slm_dashboard.py +++ b/backend/routers/slm_dashboard.py @@ -94,6 +94,7 @@ async def get_slm_units( # Legacy default from the roster field; refined from SLMM's cached status below. unit.is_recent = bool(unit.slm_last_check and unit.slm_last_check > one_hour_ago) unit.measurement_state = None + unit.cache_last_seen = None # SLMM cache last_seen (real monitoring freshness) if include_measurement: # SLMM's /roster carries each unit's CACHED status (last_seen, @@ -124,6 +125,7 @@ async def get_slm_units( try: ls = datetime.fromisoformat(last_seen.replace("Z", "")) unit.is_recent = ls > recent_cutoff + unit.cache_last_seen = ls # the real freshness the monitor updates except Exception: pass diff --git a/templates/partials/slm_device_list.html b/templates/partials/slm_device_list.html index 810bd0f..56e47a7 100644 --- a/templates/partials/slm_device_list.html +++ b/templates/partials/slm_device_list.html @@ -49,7 +49,7 @@ Retired {% elif not unit.deployed %} Benched - {% elif unit.measurement_state == "Start" %} + {% elif unit.measurement_state in ["Start", "Measure"] %} Measuring {% elif unit.is_recent %} Active @@ -57,7 +57,9 @@ Idle {% endif %} - {% if unit.slm_last_check %} + {% if unit.cache_last_seen %} + Last check: {{ unit.cache_last_seen|local_datetime }} + {% elif unit.slm_last_check %} Last check: {{ unit.slm_last_check|local_datetime }} {% else %} No recent check-in diff --git a/templates/sound_level_meters.html b/templates/sound_level_meters.html index 360ddbd..123e1d8 100644 --- a/templates/sound_level_meters.html +++ b/templates/sound_level_meters.html @@ -168,9 +168,18 @@ window.selectedUnitId = null; window.dashboardChartData = { timestamps: [], lp: [], - leq: [] + leq: [], + ln1: [], + ln2: [] }; +// Parse a metric to a number, or null (so a missing/"-.-" percentile leaves a gap +// in the line instead of dropping it to 0). +function numOrNull(v) { + const f = parseFloat(v); + return isNaN(f) ? null : f; +} + // Initialize Chart.js function initializeDashboardChart() { if (typeof Chart === 'undefined') { @@ -212,6 +221,26 @@ function initializeDashboardChart() { tension: 0.3, borderWidth: 2, pointRadius: 0 + }, + { + label: 'L1', + data: [], + borderColor: 'rgb(168, 85, 247)', + backgroundColor: 'rgba(168, 85, 247, 0.1)', + tension: 0.3, + borderWidth: 2, + pointRadius: 0, + spanGaps: true + }, + { + label: 'L10', + data: [], + borderColor: 'rgb(249, 115, 22)', + backgroundColor: 'rgba(249, 115, 22, 0.1)', + tension: 0.3, + borderWidth: 2, + pointRadius: 0, + spanGaps: true } ] }, @@ -263,11 +292,10 @@ function showLiveChart(unitId) { } // Reset data for the newly-selected unit (clears any prior unit's line) - window.dashboardChartData = { timestamps: [], lp: [], leq: [] }; + window.dashboardChartData = { timestamps: [], lp: [], leq: [], ln1: [], ln2: [] }; if (window.dashboardChart) { window.dashboardChart.data.labels = []; - window.dashboardChart.data.datasets[0].data = []; - window.dashboardChart.data.datasets[1].data = []; + window.dashboardChart.data.datasets.forEach(ds => ds.data = []); window.dashboardChart.update('none'); } @@ -366,22 +394,30 @@ function updateDashboardMetrics(data) { } function updateDashboardChart(data) { + const cd = window.dashboardChartData; const now = new Date(); - window.dashboardChartData.timestamps.push(now.toLocaleTimeString()); - window.dashboardChartData.lp.push(parseFloat(data.lp || 0)); - window.dashboardChartData.leq.push(parseFloat(data.leq || 0)); + cd.timestamps.push(now.toLocaleTimeString()); + cd.lp.push(numOrNull(data.lp)); + cd.leq.push(numOrNull(data.leq)); + // /monitor (DOD) frames carry ln1/ln2; a DRD frame would omit them -> null gap. + cd.ln1.push(numOrNull(data.ln1)); + cd.ln2.push(numOrNull(data.ln2)); // Keep a generous window (backfill seeds up to ~120 points from the 2h trail). - if (window.dashboardChartData.timestamps.length > 600) { - window.dashboardChartData.timestamps.shift(); - window.dashboardChartData.lp.shift(); - window.dashboardChartData.leq.shift(); + if (cd.timestamps.length > 600) { + cd.timestamps.shift(); + cd.lp.shift(); + cd.leq.shift(); + cd.ln1.shift(); + cd.ln2.shift(); } if (window.dashboardChart) { - window.dashboardChart.data.labels = window.dashboardChartData.timestamps; - window.dashboardChart.data.datasets[0].data = window.dashboardChartData.lp; - window.dashboardChart.data.datasets[1].data = window.dashboardChartData.leq; + window.dashboardChart.data.labels = cd.timestamps; + window.dashboardChart.data.datasets[0].data = cd.lp; + window.dashboardChart.data.datasets[1].data = cd.leq; + window.dashboardChart.data.datasets[2].data = cd.ln1; + window.dashboardChart.data.datasets[3].data = cd.ln2; window.dashboardChart.update('none'); } } @@ -417,13 +453,17 @@ async function backfillDashboardChart(unitId) { // Trail timestamps are naive UTC; append 'Z' to render in local time // consistently with the live frames (which use local Date.now()). cd.timestamps.push(row.timestamp ? new Date(row.timestamp + 'Z').toLocaleTimeString() : ''); - cd.lp.push(parseFloat(row.lp || 0)); - cd.leq.push(parseFloat(row.leq || 0)); + cd.lp.push(numOrNull(row.lp)); + cd.leq.push(numOrNull(row.leq)); + cd.ln1.push(numOrNull(row.ln1)); + cd.ln2.push(numOrNull(row.ln2)); } if (window.dashboardChart) { window.dashboardChart.data.labels = cd.timestamps; window.dashboardChart.data.datasets[0].data = cd.lp; window.dashboardChart.data.datasets[1].data = cd.leq; + window.dashboardChart.data.datasets[2].data = cd.ln1; + window.dashboardChart.data.datasets[3].data = cd.ln2; window.dashboardChart.update('none'); } } catch (e) {