From 6a73523e4d2d245cfa3967e8ed258c9beee6c117 Mon Sep 17 00:00:00 2001 From: serversdown Date: Thu, 28 May 2026 18:47:37 +0000 Subject: [PATCH] ui: surface per-channel ZC Freq (and ">100") in event modals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PDF report shows per-channel ZC Freq alongside PPV in the stats block, but neither modal exposed it. Now that the sidecar projection carries zc_freq_hz + zc_freq_above_range, plumb them through: - sfm_webapp.html: inline suffix on existing Peaks cells, e.g. "Tran 0.04500 in/s · >100 Hz". Empty suffix when no ZC is available (legacy events without a preserved .TXT). - event_browser.html: new ZC Freq column on the per-channel stats table. Required adding a parallel sidecar fetch in loadEvent() (waveform.json alone doesn't carry bw_report). Fetch failure is non-fatal — falls back to "—" in the new column. Above-range ZC peaks (BW ">100 Hz") render with a literal ">" prefix mirroring the PDF, so operators don't have to generate the PDF to see when a channel hit the zero-crossing ceiling. Co-Authored-By: Claude Opus 4.7 (1M context) --- sfm/event_browser.html | 37 ++++++++++++++++++++++++++----------- sfm/sfm_webapp.html | 22 ++++++++++++++++++---- 2 files changed, 44 insertions(+), 15 deletions(-) diff --git a/sfm/event_browser.html b/sfm/event_browser.html index bbd960f..ca19794 100644 --- a/sfm/event_browser.html +++ b/sfm/event_browser.html @@ -499,6 +499,14 @@ async function loadEvent(eventId) { renderEventList(); setStatus('Loading waveform…'); try { + // Sidecar fetch runs in parallel — its bw_report block carries ZC + // Freq + above-range flags + sensor-check results that the per- + // channel stats table surfaces. Failures are non-fatal (legacy + // events without a preserved .TXT have no sidecar bw_report). + const sidecarP = fetch(`${apiBase}/db/events/${eventId}/sidecar`) + .then(r => r.ok ? r.json() : null) + .catch(() => null); + const r = await fetch(`${apiBase}/db/events/${eventId}/waveform.json`); if (!r.ok) { if (r.status === 404) { @@ -511,7 +519,8 @@ async function loadEvent(eventId) { renderWaveform(data); // Also fetch metadata from the events list for richer header const ev = allEvents.find(e => e.id === eventId); - renderMeta(data, ev); + const sidecar = await sidecarP; + renderMeta(data, ev, sidecar); setStatus(`Event loaded.`, 'ok'); } catch (e) { setStatus(`Failed to load event: ${e.message}`, 'error'); @@ -528,7 +537,7 @@ function showEmpty(msg) { charts = {}; } -function renderMeta(data, ev) { +function renderMeta(data, ev, sidecar) { const metaDiv = document.getElementById('event-meta'); const fields = [ ['Serial', data.serial || ev?.serial || '—'], @@ -543,14 +552,20 @@ function renderMeta(data, ev) { ]; // Per-channel stats table mirroring the printout's middle block. - // Pulls per-channel PPV from the events row (DB columns) and additional - // details (peak time, peak accel, peak displacement, sensor check) from - // bw_report when present. + // PPV from the events DB row; ZC Freq + saturation flags from the + // sidecar's bw_report block (when a .TXT was preserved on ingest). + const bwrPeaks = (sidecar?.bw_report || {}).peaks || {}; + const bwrMic = (sidecar?.bw_report || {}).mic || {}; const fmt = v => (v == null ? '—' : (typeof v === 'number' ? v.toFixed(3) : v)); + const fmtZc = bwr => { + if (!bwr || bwr.zc_freq_hz == null) return '—'; + const prefix = bwr.zc_freq_above_range ? '>' : ''; + return `${prefix}${Math.round(bwr.zc_freq_hz)} Hz`; + }; const rows = [ - ['Tran', ev?.tran_ppv], - ['Vert', ev?.vert_ppv], - ['Long', ev?.long_ppv], + ['Tran', ev?.tran_ppv, fmtZc(bwrPeaks.tran)], + ['Vert', ev?.vert_ppv, fmtZc(bwrPeaks.vert)], + ['Long', ev?.long_ppv, fmtZc(bwrPeaks.long)], ]; // Mic display honors the current user preference (dBL default). // mic_ppv is stored as raw psi on series3 events; convert when needed. @@ -568,11 +583,11 @@ function renderMeta(data, ev) { const statsHtml = ` - + - ${rows.map(([ch, ppv]) => ``).join('')} - + ${rows.map(([ch, ppv, zc]) => ``).join('')} +
ChannelPPV (in/s)
ChannelPPV (in/s)ZC Freq
${ch}${fmt(ppv)}
MicL${micStr}
${ch}${fmt(ppv)}${zc}
MicL${micStr}${fmtZc(bwrMic)}
`; diff --git a/sfm/sfm_webapp.html b/sfm/sfm_webapp.html index 5021c79..7f283a4 100644 --- a/sfm/sfm_webapp.html +++ b/sfm/sfm_webapp.html @@ -2886,6 +2886,12 @@ function _renderSidecar(data) { const bw = data.blastware || {}; const src = data.source || {}; const rev = data.review || {}; + // bw_report carries the per-channel ASCII-derived stats (ZC Freq, + // saturation flags, peak time, etc.). Only present on events + // ingested with a preserved .TXT (post-2026-05-27); falls back to + // empty for legacy events. + const bwrPeaks = (data.bw_report || {}).peaks || {}; + const bwrMic = (data.bw_report || {}).mic || {}; document.getElementById('sc-title').textContent = `Event — ${bw.filename || ev.waveform_key || 'unknown'}`; @@ -2918,11 +2924,19 @@ function _renderSidecar(data) { document.getElementById('sc-f-sr').textContent = (ev.sample_rate ?? '—') + (ev.sample_rate ? ' sps' : ''); document.getElementById('sc-f-key').textContent = ev.waveform_key || '—'; - document.getElementById('sc-f-tran').textContent = fmtPpv(pv.transverse); - document.getElementById('sc-f-vert').textContent = fmtPpv(pv.vertical); - document.getElementById('sc-f-long').textContent = fmtPpv(pv.longitudinal); + // Suffix with " · {prefix}{N} Hz" when bw_report has a ZC Freq. + // Above-range ZC peaks (BW ">100 Hz") get a literal ">" prefix so + // operators see the same indicator the PDF shows. + const fmtZc = bwr => { + if (!bwr || bwr.zc_freq_hz == null) return ''; + const prefix = bwr.zc_freq_above_range ? '>' : ''; + return ` · ${prefix}${Math.round(bwr.zc_freq_hz)} Hz`; + }; + document.getElementById('sc-f-tran').textContent = fmtPpv(pv.transverse) + fmtZc(bwrPeaks.tran); + document.getElementById('sc-f-vert').textContent = fmtPpv(pv.vertical) + fmtZc(bwrPeaks.vert); + document.getElementById('sc-f-long').textContent = fmtPpv(pv.longitudinal) + fmtZc(bwrPeaks.long); document.getElementById('sc-f-pvs').textContent = fmtPpv(pv.vector_sum); - document.getElementById('sc-f-mic').textContent = fmtMic(pv.mic_psi); + document.getElementById('sc-f-mic').textContent = fmtMic(pv.mic_psi) + fmtZc(bwrMic); document.getElementById('sc-f-project').textContent = pi.project || '—'; document.getElementById('sc-f-client').textContent = pi.client || '—';