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:
+6
-66
@@ -115,21 +115,9 @@
|
||||
|
||||
</div><!-- end tab container -->
|
||||
|
||||
<!-- Event detail modal -->
|
||||
<div id="event-modal" class="fixed inset-0 z-50 hidden">
|
||||
<div class="absolute inset-0 bg-black/60" onclick="closeEventModal()"></div>
|
||||
<div class="absolute inset-x-4 top-1/2 -translate-y-1/2 max-w-2xl mx-auto bg-white dark:bg-slate-800 rounded-xl shadow-2xl p-6 max-h-[85vh] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-bold text-gray-900 dark:text-white" id="modal-title">Event Detail</h3>
|
||||
<button onclick="closeEventModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="modal-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
{# Shared event-detail modal — rendered by /static/event-modal.js #}
|
||||
{% include 'partials/event_detail_modal.html' %}
|
||||
<script src="/static/event-modal.js"></script>
|
||||
|
||||
<style>
|
||||
.sfm-tab {
|
||||
@@ -275,7 +263,7 @@ function renderEventsTable(events, total, container) {
|
||||
const ft = ev.false_trigger ? `<span class="px-2 py-0.5 rounded text-xs bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">FT</span>` : '';
|
||||
const proj = ev.project ? `<span class="truncate max-w-[120px] inline-block" title="${esc(ev.project)}">${esc(ev.project)}</span>` : '<span class="text-gray-400">—</span>';
|
||||
|
||||
return `<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50 cursor-pointer" onclick="showEventDetail(${JSON.stringify(JSON.stringify(ev))})">
|
||||
return `<tr class="hover:bg-gray-50 dark:hover:bg-slate-700/50 cursor-pointer" onclick="showEventDetail('${esc(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 font-medium text-seismo-orange">${esc(ev.serial)}</td>
|
||||
<td class="px-4 py-2.5 text-sm text-gray-700 dark:text-gray-300 max-w-[140px]">${proj}</td>
|
||||
@@ -354,56 +342,8 @@ async function toggleFalseTrigger(id, newValue, btn) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Event detail modal ───────────────────────────────────────────────────────
|
||||
function showEventDetail(jsonStr) {
|
||||
const ev = JSON.parse(jsonStr);
|
||||
const ts = ev.timestamp ? ev.timestamp.replace('T', ' ').slice(0, 19) : '—';
|
||||
|
||||
document.getElementById('modal-title').textContent = `Event — ${esc(ev.serial)} @ ${ts}`;
|
||||
document.getElementById('modal-content').innerHTML = `
|
||||
<div class="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mb-4">
|
||||
<div><span class="text-gray-500">Serial</span><span class="ml-2 font-medium">${esc(ev.serial)}</span></div>
|
||||
<div><span class="text-gray-500">Key</span><span class="ml-2 font-mono text-xs">${esc(ev.waveform_key)}</span></div>
|
||||
<div><span class="text-gray-500">Timestamp</span><span class="ml-2 font-medium">${ts}</span></div>
|
||||
<div><span class="text-gray-500">Sample Rate</span><span class="ml-2 font-medium">${ev.sample_rate || '—'} sps</span></div>
|
||||
<div><span class="text-gray-500">Record Type</span><span class="ml-2 font-medium">${ev.record_type || '—'}</span></div>
|
||||
<div><span class="text-gray-500">False Trigger</span><span class="ml-2 font-medium">${ev.false_trigger ? '⚠ Yes' : 'No'}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-x-6 gap-y-2 text-sm mb-4">
|
||||
<div><span class="text-gray-500">Project</span><span class="ml-2 font-medium">${esc(ev.project || '—')}</span></div>
|
||||
<div><span class="text-gray-500">Client</span><span class="ml-2 font-medium">${esc(ev.client || '—')}</span></div>
|
||||
<div><span class="text-gray-500">Operator</span><span class="ml-2 font-medium">${esc(ev.operator || '—')}</span></div>
|
||||
<div><span class="text-gray-500">Sensor Loc</span><span class="ml-2 font-medium">${esc(ev.sensor_location || '—')}</span></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-4">
|
||||
<h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Peak Particle Velocity</h4>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4 text-center">
|
||||
${ppvCard('Transverse', ev.tran_ppv)}
|
||||
${ppvCard('Vertical', ev.vert_ppv)}
|
||||
${ppvCard('Longitudinal', ev.long_ppv)}
|
||||
${ppvCard('Peak Vector Sum', ev.peak_vector_sum, true)}
|
||||
</div>
|
||||
${ev.mic_ppv != null ? `<div class="mt-3 text-sm text-center text-gray-600 dark:text-gray-400">Mic: <span class="font-mono font-medium">${ev.mic_ppv.toFixed(3)}</span></div>` : ''}
|
||||
</div>`;
|
||||
|
||||
document.getElementById('event-modal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function ppvCard(label, v, bold = false) {
|
||||
const val = v != null ? v.toFixed(4) : '—';
|
||||
const cls = ppvClass(v) + (bold ? ' text-lg' : '');
|
||||
return `<div>
|
||||
<div class="text-xs text-gray-500 mb-1">${label}</div>
|
||||
<div class="font-mono font-${bold ? 'bold' : 'medium'} ${cls}">${val}</div>
|
||||
<div class="text-xs text-gray-400">in/s</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function closeEventModal() {
|
||||
document.getElementById('event-modal').classList.add('hidden');
|
||||
}
|
||||
// Event detail modal now lives in /static/event-modal.js (shared component).
|
||||
// `showEventDetail(eventId)` is exposed globally; row onclick handlers call it.
|
||||
|
||||
// ── Units tab ────────────────────────────────────────────────────────────────
|
||||
async function loadUnits() {
|
||||
|
||||
Reference in New Issue
Block a user