From 8cbda09917a622743c76b20ba9a91ce55beb83d7 Mon Sep 17 00:00:00 2001 From: serversdown Date: Wed, 27 May 2026 22:30:43 +0000 Subject: [PATCH] viewers: render timestamps in browser-local time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Spotted on the SFM webapp event modal — "Received by server at" was showing the raw ISO string "2026-05-27T21:59:57.213043Z" because we were assigning ev.timestamp / src.captured_at directly to the textContent of the modal fields, bypassing the existing _fmtTs() helper that wraps them in toLocaleString(). Net effect for operators: confusing "21:59 vs it's 6 PM" mismatch when the displayed UTC timestamp didn't match wall-clock time. The values were always correct; the display was just ambiguous. After this fix: - "Recorded at" (naive ISO from BW = unit local time) renders cleanly as the unit wrote it: "5/27/2026, 6:00:13 AM" - "Received by server at" (UTC with Z suffix) converts to browser local: "5/27/2026, 5:59:57 PM" - Timestamp column in the history table already used _fmtTs — unchanged - Same fix applied to the standalone /events page (sidebar event list + meta header) via a new _fmtTsLocal helper Note: did NOT add file-mtime-on-watcher-PC tracking as a separate "Called in at" column — discussed and decided created_at is close enough for schedule-compliance monitoring (worst case lag = watcher poll interval ~60s, indistinguishable from BW write time at the operationally-relevant resolution). Co-Authored-By: Claude Opus 4.7 (1M context) --- sfm/event_browser.html | 14 ++++++++++++-- sfm/sfm_webapp.html | 9 +++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/sfm/event_browser.html b/sfm/event_browser.html index 9f5fd31..30542af 100644 --- a/sfm/event_browser.html +++ b/sfm/event_browser.html @@ -356,6 +356,16 @@ function _psiToDbl(psi) { return 20 * Math.log10(psi / DBL_REF); } +// Format an ISO timestamp in the browser's local timezone — UTC values +// (with 'Z' suffix) convert; naive values are interpreted as local clock. +// Returns '—' for null/empty/unparseable. +function _fmtTsLocal(iso) { + if (!iso) return '—'; + const d = new Date(iso); + if (isNaN(d)) return iso; + return d.toLocaleString(); +} + // Adaptive decimal formatter — scientific notation only for truly extreme // values. Normal-range peaks render as plain decimals with sensible // precision (was previously forcing toExponential(3) which produced ugly @@ -458,7 +468,7 @@ function renderEventList() { const row = document.createElement('div'); row.className = 'event-row' + (ev.false_trigger ? ' false_trigger' : ''); if (ev.id === currentEventId) row.className += ' active'; - const ts = (ev.timestamp || '').replace('T', ' ').replace('Z', ''); + const ts = _fmtTsLocal(ev.timestamp); const pvs = ev.peak_vector_sum != null ? `${ev.peak_vector_sum.toFixed(3)} in/s` : '—'; row.innerHTML = `
@@ -510,7 +520,7 @@ function renderMeta(data, ev) { const metaDiv = document.getElementById('event-meta'); const fields = [ ['Serial', data.serial || ev?.serial || '—'], - ['Timestamp', (data.timestamp || ev?.timestamp || '—').replace('T', ' ').replace('Z', '')], + ['Timestamp', _fmtTsLocal(data.timestamp || ev?.timestamp)], ['Record', data.record_type || ev?.record_type || '—'], ['Sample rate', data.sample_rate ? `${data.sample_rate} sps` : '—'], ['Geo range', data.geo_range ? `${data.geo_range} (${data.geo_full_scale_ips} in/s FS)` : '—'], diff --git a/sfm/sfm_webapp.html b/sfm/sfm_webapp.html index 9b0f862..856c8f5 100644 --- a/sfm/sfm_webapp.html +++ b/sfm/sfm_webapp.html @@ -2864,7 +2864,9 @@ function _renderSidecar(data) { }; document.getElementById('sc-f-serial').textContent = ev.serial || '—'; - document.getElementById('sc-f-ts').textContent = ev.timestamp || '—'; + // Route through _fmtTs so the unit-local naive timestamp shows as + // "5/27/2026, 6:00:13 AM" instead of "2026-05-27T06:00:13". + document.getElementById('sc-f-ts').textContent = _fmtTs(ev.timestamp); document.getElementById('sc-f-rt').textContent = ev.record_type || '—'; document.getElementById('sc-f-sr').textContent = (ev.sample_rate ?? '—') + (ev.sample_rate ? ' sps' : ''); document.getElementById('sc-f-key').textContent = ev.waveform_key || '—'; @@ -2884,7 +2886,10 @@ function _renderSidecar(data) { document.getElementById('sc-f-bwsize').textContent = bw.filesize != null ? `${bw.filesize} bytes` : '—'; document.getElementById('sc-f-sha').textContent = bw.sha256 || '—'; document.getElementById('sc-f-src').textContent = src.kind || '—'; - document.getElementById('sc-f-cap').textContent = src.captured_at || '—'; + // captured_at has a "Z" suffix (UTC); _fmtTs converts to browser local + // — matches the BW-reported recorded-at, no more "21:59:57 vs it's 6 PM" + // confusion from operators reading the raw UTC value. + document.getElementById('sc-f-cap').textContent = _fmtTs(src.captured_at); document.getElementById('sc-edit-ft').checked = !!rev.false_trigger; document.getElementById('sc-edit-reviewer').value = rev.reviewer || '';