Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ace542cba5 | |||
| 8cbda09917 |
+12
-2
@@ -356,6 +356,16 @@ function _psiToDbl(psi) {
|
|||||||
return 20 * Math.log10(psi / DBL_REF);
|
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
|
// Adaptive decimal formatter — scientific notation only for truly extreme
|
||||||
// values. Normal-range peaks render as plain decimals with sensible
|
// values. Normal-range peaks render as plain decimals with sensible
|
||||||
// precision (was previously forcing toExponential(3) which produced ugly
|
// precision (was previously forcing toExponential(3) which produced ugly
|
||||||
@@ -458,7 +468,7 @@ function renderEventList() {
|
|||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'event-row' + (ev.false_trigger ? ' false_trigger' : '');
|
row.className = 'event-row' + (ev.false_trigger ? ' false_trigger' : '');
|
||||||
if (ev.id === currentEventId) row.className += ' active';
|
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` : '—';
|
const pvs = ev.peak_vector_sum != null ? `${ev.peak_vector_sum.toFixed(3)} in/s` : '—';
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="er-top">
|
<div class="er-top">
|
||||||
@@ -510,7 +520,7 @@ function renderMeta(data, ev) {
|
|||||||
const metaDiv = document.getElementById('event-meta');
|
const metaDiv = document.getElementById('event-meta');
|
||||||
const fields = [
|
const fields = [
|
||||||
['Serial', data.serial || ev?.serial || '—'],
|
['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 || '—'],
|
['Record', data.record_type || ev?.record_type || '—'],
|
||||||
['Sample rate', data.sample_rate ? `${data.sample_rate} sps` : '—'],
|
['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)` : '—'],
|
['Geo range', data.geo_range ? `${data.geo_range} (${data.geo_full_scale_ips} in/s FS)` : '—'],
|
||||||
|
|||||||
+70
-17
@@ -97,6 +97,7 @@ class ReportData:
|
|||||||
mic_pspl_dbl: Optional[float] = None
|
mic_pspl_dbl: Optional[float] = None
|
||||||
mic_pspl_psi: Optional[float] = None
|
mic_pspl_psi: Optional[float] = None
|
||||||
mic_pspl_time_s: Optional[float] = None
|
mic_pspl_time_s: Optional[float] = None
|
||||||
|
mic_pspl_when_str: Optional[str] = None # histogram absolute date+time, BW-formatted
|
||||||
mic_zc_freq_hz: Optional[float] = None
|
mic_zc_freq_hz: Optional[float] = None
|
||||||
mic_channel_test_result: Optional[str] = None
|
mic_channel_test_result: Optional[str] = None
|
||||||
mic_channel_test_freq_hz: Optional[float] = None
|
mic_channel_test_freq_hz: Optional[float] = None
|
||||||
@@ -220,12 +221,19 @@ def gather_report_data(
|
|||||||
rd.mic_channel_test_freq_hz = sc_mic.get("freq_hz")
|
rd.mic_channel_test_freq_hz = sc_mic.get("freq_hz")
|
||||||
rd.mic_channel_test_amp_mv = sc_mic.get("amplitude_mv")
|
rd.mic_channel_test_amp_mv = sc_mic.get("amplitude_mv")
|
||||||
|
|
||||||
# Per-channel stats (Tran / Vert / Long)
|
# Per-channel stats (Tran / Vert / Long). Per-channel peak
|
||||||
|
# date+time for histograms comes from bw_report.histogram.channel_peak_when
|
||||||
|
# (populated when the parser captured it; see the bw_ascii_report
|
||||||
|
# parser's histogram-fields handler).
|
||||||
peaks = bw.get("peaks") or {}
|
peaks = bw.get("peaks") or {}
|
||||||
sc_block = bw.get("sensor_check") or {}
|
sc_block = bw.get("sensor_check") or {}
|
||||||
|
hist_block = bw.get("histogram") or {}
|
||||||
|
peak_when = hist_block.get("channel_peak_when") or {}
|
||||||
for ch_lc, ch_label in (("tran", "Tran"), ("vert", "Vert"), ("long", "Long")):
|
for ch_lc, ch_label in (("tran", "Tran"), ("vert", "Vert"), ("long", "Long")):
|
||||||
ch = peaks.get(ch_lc) or {}
|
ch = peaks.get(ch_lc) or {}
|
||||||
sc_ch = sc_block.get(ch_lc) or {}
|
sc_ch = sc_block.get(ch_lc) or {}
|
||||||
|
ch_when_iso = peak_when.get(ch_label)
|
||||||
|
peak_date, peak_time = _split_iso_to_date_time(ch_when_iso)
|
||||||
rd.channel_stats.append({
|
rd.channel_stats.append({
|
||||||
"name": ch_label,
|
"name": ch_label,
|
||||||
"ppv_ips": ch.get("ppv_ips"),
|
"ppv_ips": ch.get("ppv_ips"),
|
||||||
@@ -234,25 +242,30 @@ def gather_report_data(
|
|||||||
"peak_accel_g": ch.get("peak_accel_g"),
|
"peak_accel_g": ch.get("peak_accel_g"),
|
||||||
"peak_disp_in": ch.get("peak_disp_in"),
|
"peak_disp_in": ch.get("peak_disp_in"),
|
||||||
"sensor_check": sc_ch.get("result"),
|
"sensor_check": sc_ch.get("result"),
|
||||||
|
"peak_date": peak_date,
|
||||||
|
"peak_time": peak_time,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# MicL peak time (used in the mic block — "PSPL ... on DATE at TIME")
|
||||||
|
mic_when_iso = peak_when.get("MicL")
|
||||||
|
rd.mic_pspl_when_str = _fmt_iso_to_bw(mic_when_iso) if mic_when_iso else None
|
||||||
|
|
||||||
# Peak Vector Sum
|
# Peak Vector Sum
|
||||||
vs = peaks.get("vector_sum") or {}
|
vs = peaks.get("vector_sum") or {}
|
||||||
rd.peak_vector_sum_ips = vs.get("ips")
|
rd.peak_vector_sum_ips = vs.get("ips")
|
||||||
rd.peak_vector_sum_time_s = vs.get("time_s")
|
rd.peak_vector_sum_time_s = vs.get("time_s")
|
||||||
|
# PVS absolute date+time (histograms). Same formatting as Mic.
|
||||||
|
pvs_when_iso = vs.get("when")
|
||||||
|
rd.peak_vector_sum_when_str = _fmt_iso_to_bw(pvs_when_iso) if pvs_when_iso else None
|
||||||
|
|
||||||
# Histogram-specific header fields. These come from the BW XML
|
# Histogram-specific header fields — keys match the projection in
|
||||||
# at ingest time (when present); the parsed bw_report dict
|
# _bw_report_to_dict ("start" / "stop", not "_str" suffixed).
|
||||||
# carries them under the 'histogram' sub-block (added by the
|
|
||||||
# BW XML parser once that lands). For now, derive from the
|
|
||||||
# event timestamp + recording config as a best-effort.
|
|
||||||
if rd.is_histogram:
|
if rd.is_histogram:
|
||||||
hist = bw.get("histogram") or {}
|
rd.histogram_start_str = hist_block.get("start") or rd.event_datetime_str
|
||||||
rd.histogram_start_str = hist.get("start_str") or rd.event_datetime_str
|
rd.histogram_stop_str = hist_block.get("stop")
|
||||||
rd.histogram_stop_str = hist.get("stop_str")
|
rd.histogram_n_intervals = hist_block.get("n_intervals")
|
||||||
rd.histogram_n_intervals = hist.get("n_intervals")
|
rd.histogram_interval_size = hist_block.get("interval_size")
|
||||||
rd.histogram_interval_size = hist.get("interval_size")
|
rd.histogram_interval_times = hist_block.get("interval_times") or []
|
||||||
rd.histogram_interval_times = hist.get("interval_times") or []
|
|
||||||
|
|
||||||
# ── Waveform samples — from the .h5 via the existing helper ──
|
# ── Waveform samples — from the .h5 via the existing helper ──
|
||||||
from sfm import event_hdf5
|
from sfm import event_hdf5
|
||||||
@@ -376,6 +389,24 @@ def _fmt_iso_to_bw(iso: Optional[str]) -> Optional[str]:
|
|||||||
return iso
|
return iso
|
||||||
|
|
||||||
|
|
||||||
|
def _split_iso_to_date_time(iso: Optional[str]) -> tuple[Optional[str], Optional[str]]:
|
||||||
|
"""Split an ISO timestamp into BW-formatted ("May 27 /26", "06:06:14")
|
||||||
|
date+time strings. Used for the histogram stats table where the
|
||||||
|
Date and Time rows are presented separately. Returns (None, None)
|
||||||
|
if the input isn't a valid ISO datetime."""
|
||||||
|
if not iso:
|
||||||
|
return (None, None)
|
||||||
|
try:
|
||||||
|
import datetime as _dt
|
||||||
|
dt = _dt.datetime.fromisoformat(iso.replace("Z", "+00:00"))
|
||||||
|
# BW format: "May 27 /26" (3-letter month + 2-digit year)
|
||||||
|
date_str = dt.strftime("%b %d /%y").replace(" 0", " ")
|
||||||
|
time_str = dt.strftime("%H:%M:%S")
|
||||||
|
return (date_str, time_str)
|
||||||
|
except Exception:
|
||||||
|
return (None, None)
|
||||||
|
|
||||||
|
|
||||||
def _kv(ax, x, y, label, value, *, label_w=0.18):
|
def _kv(ax, x, y, label, value, *, label_w=0.18):
|
||||||
"""Render a 'Label Value' row at axes-coordinates (x, y)."""
|
"""Render a 'Label Value' row at axes-coordinates (x, y)."""
|
||||||
ax.text(x, y, label, fontsize=8, color="#555", ha="left", va="top",
|
ax.text(x, y, label, fontsize=8, color="#555", ha="left", va="top",
|
||||||
@@ -489,11 +520,28 @@ def _draw_mic_and_usbm(ax, rd: ReportData) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _mic_rows(rd: ReportData) -> list[tuple[str, Optional[str]]]:
|
def _mic_rows(rd: ReportData) -> list[tuple[str, Optional[str]]]:
|
||||||
"""Build the mic-section value rows (shared by both layouts)."""
|
"""Build the mic-section value rows (shared by both layouts).
|
||||||
|
|
||||||
|
For histograms, BW formats the PSPL line as
|
||||||
|
"125.7 dB(L) on May 27, 2026 at 06:19:14"
|
||||||
|
(absolute date+time of peak). Waveform events show the relative
|
||||||
|
"at 0.012 sec." instead. Both formats covered here based on which
|
||||||
|
field is populated.
|
||||||
|
"""
|
||||||
rows: list[tuple[str, Optional[str]]] = []
|
rows: list[tuple[str, Optional[str]]] = []
|
||||||
if rd.mic_pspl_dbl is not None:
|
if rd.mic_pspl_dbl is not None:
|
||||||
line = f"{rd.mic_pspl_dbl:.1f} dB(L)"
|
line = f"{rd.mic_pspl_dbl:.1f} dB(L)"
|
||||||
if rd.mic_pspl_time_s is not None:
|
if rd.mic_pspl_when_str:
|
||||||
|
# Histogram-style: "PSPL 125.7 dB(L) on May 27, 2026 at 06:19:14"
|
||||||
|
# mic_pspl_when_str is already "HH:MM:SS Month DD, YYYY";
|
||||||
|
# reformat to "on Month DD, YYYY at HH:MM:SS" for BW match.
|
||||||
|
parts = rd.mic_pspl_when_str.split(" ", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
line += f" on {parts[1]} at {parts[0]}"
|
||||||
|
else:
|
||||||
|
line += f" on {rd.mic_pspl_when_str}"
|
||||||
|
elif rd.mic_pspl_time_s is not None:
|
||||||
|
# Waveform-style: relative-to-trigger seconds.
|
||||||
line += f" at {rd.mic_pspl_time_s:.3f} sec."
|
line += f" at {rd.mic_pspl_time_s:.3f} sec."
|
||||||
rows.append(("PSPL", line))
|
rows.append(("PSPL", line))
|
||||||
if rd.mic_zc_freq_hz is not None:
|
if rd.mic_zc_freq_hz is not None:
|
||||||
@@ -545,10 +593,15 @@ def _draw_channel_stats_histogram(ax, rd: ReportData) -> None:
|
|||||||
]
|
]
|
||||||
_draw_stats_table(ax, rd, rows_spec)
|
_draw_stats_table(ax, rd, rows_spec)
|
||||||
if rd.peak_vector_sum_ips is not None:
|
if rd.peak_vector_sum_ips is not None:
|
||||||
when = rd.peak_vector_sum_when_str or ""
|
|
||||||
line = f"Peak Vector Sum {rd.peak_vector_sum_ips:.3f} in/s"
|
line = f"Peak Vector Sum {rd.peak_vector_sum_ips:.3f} in/s"
|
||||||
if when:
|
# Histograms: "0.091 in/s on May 27, 2026 At 06:06:14"
|
||||||
line += f" on {when}"
|
# The when_str is "HH:MM:SS Month DD, YYYY" — reformat for BW match.
|
||||||
|
if rd.peak_vector_sum_when_str:
|
||||||
|
parts = rd.peak_vector_sum_when_str.split(" ", 1)
|
||||||
|
if len(parts) == 2:
|
||||||
|
line += f" on {parts[1]} At {parts[0]}"
|
||||||
|
else:
|
||||||
|
line += f" on {rd.peak_vector_sum_when_str}"
|
||||||
ax.text(0.0, -0.08, line, fontsize=9, weight="bold",
|
ax.text(0.0, -0.08, line, fontsize=9, weight="bold",
|
||||||
ha="left", va="top", transform=ax.transAxes)
|
ha="left", va="top", transform=ax.transAxes)
|
||||||
ax.text(0.0, -0.18, "NA: Not Applicable", fontsize=7, color="#888",
|
ax.text(0.0, -0.18, "NA: Not Applicable", fontsize=7, color="#888",
|
||||||
|
|||||||
+7
-2
@@ -2864,7 +2864,9 @@ function _renderSidecar(data) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
document.getElementById('sc-f-serial').textContent = ev.serial || '—';
|
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-rt').textContent = ev.record_type || '—';
|
||||||
document.getElementById('sc-f-sr').textContent = (ev.sample_rate ?? '—') + (ev.sample_rate ? ' sps' : '');
|
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-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-bwsize').textContent = bw.filesize != null ? `${bw.filesize} bytes` : '—';
|
||||||
document.getElementById('sc-f-sha').textContent = bw.sha256 || '—';
|
document.getElementById('sc-f-sha').textContent = bw.sha256 || '—';
|
||||||
document.getElementById('sc-f-src').textContent = src.kind || '—';
|
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-ft').checked = !!rev.false_trigger;
|
||||||
document.getElementById('sc-edit-reviewer').value = rev.reviewer || '';
|
document.getElementById('sc-edit-reviewer').value = rev.reviewer || '';
|
||||||
|
|||||||
Reference in New Issue
Block a user