/* event-modal.js — shared event-detail modal.
*
* Used by:
* - /sfm (admin Events tab)
* - /projects/{p}/nrl/{l} (project-location Events tab)
* - /unit/{id} (unit-detail SFM Events table)
*
* Pages must include partials/event_detail_modal.html in the body
* before this script is loaded.
*
* Public API:
* showEventDetail(eventId)
* Open the modal and fetch /api/sfm/db/events/{id}/sidecar to
* populate the rich BW report fields (peaks, ZC freq, sensor
* self-check, device info, etc.) into a tabbed/sectioned view.
*
* closeEventDetailModal()
* Close the modal.
*
* Notes:
* - Fetches sidecar live from SFM via terra-view's /api/sfm proxy.
* - Renders gracefully when the sidecar lacks a bw_report block
* (older events forwarded before the _ASCII.TXT pairing fix).
* - All functions are global on window so inline onclick handlers
* can reach them across all three host pages.
*/
(function () {
const MODAL_ID = 'event-detail-modal';
function _esc(s) {
if (s == null) return '';
return String(s).replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"');
}
function _fmt(v, digits = 4, suffix = '') {
if (v == null || (typeof v === 'number' && Number.isNaN(v))) return '—';
if (typeof v === 'number') {
return v.toFixed(digits) + (suffix ? ` ${suffix}` : '');
}
return _esc(v) + (suffix ? ` ${suffix}` : '');
}
function _ppvClass(v) {
if (v == null) return 'text-gray-400';
if (v < 0.5) return 'text-green-600 dark:text-green-400';
if (v < 2.0) return 'text-amber-600 dark:text-amber-400';
return 'text-red-600 dark:text-red-400 font-semibold';
}
function _kvCard(label, value, options = {}) {
// Single key-value tile. `value` is pre-rendered HTML (or text).
const colorCls = options.colorCls || '';
const valCls = `font-mono font-semibold ${colorCls}`;
return `
${_esc(label)}
${value}
${options.sub ? `
${options.sub}
` : ''}
`;
}
function _deriveRecordType(filename, fallback) {
// SFM currently hardcodes record_type="Waveform" for every event.
// The actual type is encoded in the LAST character of the Blastware
// filename's extension (e.g. "O121LL5E.IS0H" → "H" → Histogram).
// We derive it client-side until SFM is fixed; if the suffix isn't
// a known code we fall back to whatever SFM reported.
if (!filename) return fallback || '—';
const dotIdx = filename.lastIndexOf('.');
if (dotIdx < 0 || dotIdx === filename.length - 1) return fallback || '—';
const ext = filename.slice(dotIdx + 1);
const lastChar = ext.slice(-1).toUpperCase();
const typeMap = {
'H': 'Histogram',
'W': 'Waveform',
'M': 'Manual',
'E': 'Event',
'C': 'Combo',
};
return typeMap[lastChar] || (fallback || '—');
}
function _sectionHeader(title, sub) {
return `
`;
}
function _renderUserNotes(s) {
// The "user notes" metadata the operator typed into the BW device.
// These are the strings the future metadata-driven parser will use.
// NOTE: SFM's sidecar JSON still names this block `project_info` —
// we render it as "User Notes" (the actual BW term) but read the
// field by its SFM-API name. Rename in SFM is a future cleanup.
const p = s.project_info || {};
return `
Project${_esc(p.project || '—')}
Client${_esc(p.client || '—')}
Operator${_esc(p.operator || '—')}
Sensor Location${_esc(p.sensor_location || '—')}
Values are as typed into the seismograph at session start — not the terra-view project/location assignment.
`;
}
function _renderMic(s) {
// Operators only care about dB(L); PSI tile was dropped 2026-05.
// We still render the row if any mic data is present so ZC freq /
// time-of-peak stay visible even when bw_report.mic is missing.
const mic = (s.bw_report && s.bw_report.mic) || null;
const pv = s.peak_values || {};
if (!mic && pv.mic_psi == null) return '';
const dbl = mic?.pspl_dbl;
const zcHz = mic?.zc_freq_hz;
const tPk = mic?.time_of_peak_s;
const wt = mic?.weighting;
return `
No BW ASCII report paired with this event.
Older events forwarded before the watcher's _ASCII.TXT pairing fix landed lack this data.
PPV is still available from the binary event file.