feat(slm): backfill the live chart from the DOD trail on open

On opening the live view, fetch GET /api/slmm/{unit}/history?hours=2 and
seed the chart with the recent trend BEFORE connecting the live socket, so
it opens with context instead of blank. Live frames then append in order.

- backfillChart() populates all four series (Lp/Leq/L1/L10) from the trail.
- initLiveDataStream is async and awaits the backfill before opening the WS.
- Chart rolling window raised 60 -> 600 points so the ~2h backfill (1/min)
  isn't immediately shifted out.
- Trail timestamps are naive UTC -> append 'Z' so they localize consistently
  with the live frames.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 20:00:41 +00:00
parent bdc91177e2
commit f5e93d5612
+38 -3
View File
@@ -513,7 +513,37 @@ if (typeof window.currentWebSocket === 'undefined') {
window.currentWebSocket = null; window.currentWebSocket = null;
} }
function initLiveDataStream(unitId) { // Backfill the chart with the recent DOD trail so it opens with context.
async function backfillChart(unitId) {
try {
const r = await fetch(`/api/slmm/${encodeURIComponent(unitId)}/history?hours=2`);
if (!r.ok) return;
const d = await r.json();
const readings = d.readings || [];
if (!window.chartData) return;
for (const row of readings) {
// Trail timestamps are naive UTC; append 'Z' so they convert to local
// consistently with the live frames (which use local Date.now()).
window.chartData.timestamps.push(row.timestamp ? new Date(row.timestamp + 'Z').toLocaleTimeString() : '');
window.chartData.lp.push(parseFloat(row.lp || 0));
window.chartData.leq.push(parseFloat(row.leq || 0));
window.chartData.ln1.push(parseFloat(row.ln1 || 0));
window.chartData.ln2.push(parseFloat(row.ln2 || 0));
}
if (window.liveChart) {
window.liveChart.data.labels = window.chartData.timestamps;
window.liveChart.data.datasets[0].data = window.chartData.lp;
window.liveChart.data.datasets[1].data = window.chartData.leq;
window.liveChart.data.datasets[2].data = window.chartData.ln1;
window.liveChart.data.datasets[3].data = window.chartData.ln2;
window.liveChart.update('none');
}
} catch (e) {
console.warn('Chart backfill failed:', e);
}
}
async function initLiveDataStream(unitId) {
// Close existing connection if any // Close existing connection if any
if (window.currentWebSocket) { if (window.currentWebSocket) {
window.currentWebSocket.close(); window.currentWebSocket.close();
@@ -533,6 +563,10 @@ function initLiveDataStream(unitId) {
window.liveChart.update(); window.liveChart.update();
} }
// Seed the chart with recent history BEFORE opening the live socket, so live
// frames append after the backfill (right order) and the chart isn't blank.
await backfillChart(unitId);
// WebSocket URL for SLMM backend via proxy. // WebSocket URL for SLMM backend via proxy.
// /monitor = the shared fan-out DOD feed (many viewers, one device connection, // /monitor = the shared fan-out DOD feed (many viewers, one device connection,
// and it carries L1/L10 which the DRD /stream cannot). // and it carries L1/L10 which the DRD /stream cannot).
@@ -649,8 +683,9 @@ function updateLiveChart(data) {
window.chartData.ln1.push(parseFloat(data.ln1 || 0)); window.chartData.ln1.push(parseFloat(data.ln1 || 0));
window.chartData.ln2.push(parseFloat(data.ln2 || 0)); window.chartData.ln2.push(parseFloat(data.ln2 || 0));
// Keep only last 60 data points // Keep a rolling window large enough to hold the ~2h backfill (one point/min)
if (window.chartData.timestamps.length > 60) { // plus a good run of live points before the oldest scroll off.
if (window.chartData.timestamps.length > 600) {
window.chartData.timestamps.shift(); window.chartData.timestamps.shift();
window.chartData.lp.shift(); window.chartData.lp.shift();
window.chartData.leq.shift(); window.chartData.leq.shift();