ui: surface per-channel ZC Freq (and ">100") in event modals
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) <noreply@anthropic.com>
This commit is contained in:
+26
-11
@@ -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 = `
|
||||
<table class="stats-table">
|
||||
<thead>
|
||||
<tr><th>Channel</th><th>PPV (in/s)</th></tr>
|
||||
<tr><th>Channel</th><th>PPV (in/s)</th><th>ZC Freq</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${rows.map(([ch, ppv]) => `<tr><td>${ch}</td><td>${fmt(ppv)}</td></tr>`).join('')}
|
||||
<tr><td>MicL</td><td>${micStr}</td></tr>
|
||||
${rows.map(([ch, ppv, zc]) => `<tr><td>${ch}</td><td>${fmt(ppv)}</td><td>${zc}</td></tr>`).join('')}
|
||||
<tr><td>MicL</td><td>${micStr}</td><td>${fmtZc(bwrMic)}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
+18
-4
@@ -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 || '—';
|
||||
|
||||
Reference in New Issue
Block a user