feat(sfm): shared event-detail modal with rich BW report fields

Clicking any event row in any of the three event tables (/sfm Events,
project-location Events tab, unit detail SFM Events) now opens a modal
populated from the SFM .sfm.json sidecar.  Previously the /sfm page had
a basic inline modal showing only the columns already in the table;
this rebuilds it as a shared component and exposes the rich fields
that the BW ASCII report unlocks.

Shared component:
- backend/static/event-modal.js — single ~250-line module.  Public API:
  showEventDetail(eventId) fetches /api/sfm/db/events/{id}/sidecar
  live (no extra terra-view caching) and renders sections for:
    • Event (serial, timestamp, record type, sample rate, rec time,
      waveform key)
    • Project Info (operator-typed user notes — project / client /
      operator / sensor_location — flagged in the UI as "as typed
      into the seismograph at session start", not the terra-view
      assignment)
    • Peak Particle Velocity (per-channel + vector sum, with the
      time-of-vector-sum-peak when bw_report is available)
    • Microphone (Peak dB(L) + psi, ZC frequency, time of peak)
    • Sensor Self-Check table (per-channel freq + ratio/amplitude +
      pass/fail)
    • Device & Recording Metadata (firmware, battery, calibration
      date + by-whom, geo range, stop mode, units)
    • Source File (Blastware filename, size, SHA-256, capture time)
  closeEventDetailModal() closes; Escape key also closes.

- templates/partials/event_detail_modal.html — modal shell partial
  (sticky title bar, scrollable body, click-outside-to-close).

Wired into three pages:
- templates/sfm.html: removed the old inline modal + showEventDetail /
  ppvCard / closeEventModal functions (replaced by the shared module).
  Row onclick now passes just the event id instead of the full JSON.
- templates/vibration_location_detail.html: row click on the Events
  tab opens the modal.  The /unit/{serial} link inside the row has
  event.stopPropagation() so the link navigates instead of opening
  the modal.
- templates/unit_detail.html: row click on the SFM Events table opens
  the modal.  The attribution-cell project/location links also got
  stopPropagation.

Graceful degradation: older events forwarded before the watcher's
_ASCII.TXT pairing fix don't have a bw_report block in their sidecar.
The modal renders an amber banner explaining that and shows just the
event + project_info + peak_values + source-file sections.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 03:55:41 +00:00
parent f1f3da8e61
commit 80fa76208a
5 changed files with 342 additions and 72 deletions
+10 -4
View File
@@ -2221,19 +2221,21 @@ function _ueEsc(s) {
}
function _ueAttrCell(ev) {
// Inline links use onclick="event.stopPropagation()" so clicking the
// project/location link navigates instead of opening the event-detail
// modal (which fires from the row-level onclick).
const a = ev.attribution;
if (a) {
// Attributed: project / location link.
const projLabel = _ueEsc(a.project_name || '—');
const locLabel = _ueEsc(a.location_name || '—');
return `<a href="/projects/${_ueEsc(a.project_id)}/nrl/${_ueEsc(a.location_id)}"
onclick="event.stopPropagation()"
class="text-seismo-orange hover:text-seismo-navy"
title="${projLabel}${locLabel}">
📍 ${locLabel}
</a>
<div class="text-xs text-gray-500 dark:text-gray-400">${projLabel}</div>`;
}
// Unattributed: show nearest assignment + delta for context.
const n = ev.nearest_assignment;
if (n) {
const sign = n.delta_days < 0 ? 'before' : (n.delta_days > 0 ? 'after' : 'within boundary');
@@ -2243,7 +2245,7 @@ function _ueAttrCell(ev) {
: `${days.toFixed(1)}d`;
return `<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300">⚠ Unattributed</span>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
${daysLabel} ${_ueEsc(sign)} <a href="/projects/${_ueEsc(n.project_id)}/nrl/${_ueEsc(n.location_id)}" class="text-seismo-orange hover:text-seismo-navy">${_ueEsc(n.location_name || '?')}</a>
${daysLabel} ${_ueEsc(sign)} <a href="/projects/${_ueEsc(n.project_id)}/nrl/${_ueEsc(n.location_id)}" onclick="event.stopPropagation()" class="text-seismo-orange hover:text-seismo-navy">${_ueEsc(n.location_name || '?')}</a>
</div>`;
}
return `<span class="px-2 py-0.5 rounded text-xs bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-300">⚠ No assignments</span>`;
@@ -2277,7 +2279,7 @@ function renderUnitEventTable(events, total, container, bucket, assignmentsTotal
? '<span class="px-2 py-0.5 rounded text-xs bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300">FT</span>'
: '';
return `<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50 ${ev.attribution ? '' : 'bg-amber-50/40 dark:bg-amber-900/10'}">
return `<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50 cursor-pointer ${ev.attribution ? '' : 'bg-amber-50/40 dark:bg-amber-900/10'}" onclick="showEventDetail('${_dtEsc(ev.id)}')">
<td class="px-4 py-2.5 text-sm text-gray-900 dark:text-white whitespace-nowrap">${ts}</td>
<td class="px-4 py-2.5 text-sm font-mono ${_uePpvClass(ev.tran_ppv)}">${tran}</td>
<td class="px-4 py-2.5 text-sm font-mono ${_uePpvClass(ev.vert_ppv)}">${vert}</td>
@@ -2854,4 +2856,8 @@ function showToast(message, type = 'info') {
</div>
</div>
{# Shared event-detail modal (clicking a row in the SFM Events table) #}
{% include 'partials/event_detail_modal.html' %}
<script src="/static/event-modal.js"></script>
{% endblock %}