diff --git a/backend/routers/admin_modules.py b/backend/routers/admin_modules.py index 8f9dc6c..833c621 100644 --- a/backend/routers/admin_modules.py +++ b/backend/routers/admin_modules.py @@ -49,6 +49,15 @@ def admin_sfm_page(request: Request): }) +@router.get("/admin/events", response_class=HTMLResponse) +def admin_events_page(request: Request): + """SFM Event DB Manager — browse, flag, and delete events across all units.""" + return templates.TemplateResponse("admin_events.html", { + "request": request, + "sfm_base_url": SFM_BASE_URL, + }) + + @router.get("/api/admin/sfm/overview") async def admin_sfm_overview() -> JSONResponse: """Aggregated SFM diagnostic snapshot. diff --git a/templates/admin_events.html b/templates/admin_events.html new file mode 100644 index 0000000..9f45799 --- /dev/null +++ b/templates/admin_events.html @@ -0,0 +1,359 @@ +{% extends "base.html" %} + +{% block title %}SFM Event DB Manager - Seismo Fleet Manager{% endblock %} + +{% block content %} +
+
+ ← Back to Developer Tools +

SFM Event DB Manager

+

Browse, flag, and delete triggered events from SFM's events table. Destructive actions also clean up on-disk waveform / sidecar / pickle / hdf5 files.

+
+ +
+ + +
+
+ ⚠️ +
+ Destructive operations. + Delete actions remove rows from SFM's events table AND delete associated waveform files on disk. Both are permanent — there is no undo. Use filters carefully, dry-run first, and verify the match count before confirming. +
+
+
+ + +
+

Filters

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+ 0 selected + + + + + +
+ + + + +
+ + +
+
+ Apply filters above to load events. +
+
+
No query run yet.
+
+
+ + +{% endblock %} diff --git a/templates/settings.html b/templates/settings.html index cb586a7..ba5532e 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -589,6 +589,20 @@ + +
+
+
SFM Event DB Manager
+
+ Browse, flag, and delete events from SFM's events table across all units. Destructive — also cleans up on-disk waveform files. Use for cleaning bogus events from a misbehaving unit. +
+
+ + Open + +
+ {# Metadata Backfill + Project Tidy moved to Tools (they're operator workflows, not admin/dev surfaces). Find them at /tools. #} diff --git a/templates/unit_detail.html b/templates/unit_detail.html index beec92b..c2eb60f 100644 --- a/templates/unit_detail.html +++ b/templates/unit_detail.html @@ -379,6 +379,20 @@ + +
+ 0 selected + + + For deletion / DB cleanup, use the Event DB Manager. +
+
@@ -2652,7 +2666,13 @@ function renderUnitEventTable(events, total, container, bucket, assignmentsTotal ? 'FT' : ''; + const checked = _ueSelectedEventIds.has(ev.id) ? 'checked' : ''; return ` + + + ${ts} ${tran} ${vert} @@ -2668,6 +2688,11 @@ function renderUnitEventTable(events, total, container, bucket, assignmentsTotal + ${_ueSortableTh('Timestamp', 'timestamp')} ${_ueSortableTh('Tran', 'tran_ppv')} ${_ueSortableTh('Vert', 'vert_ppv')} @@ -2679,6 +2704,85 @@ function renderUnitEventTable(events, total, container, bucket, assignmentsTotal ${rows}
+ +
`; + _ueRefreshBulkButton(); +} + +// ===== Bulk false-trigger flagging ===== +// Selection is keyed by event ID and persists across table re-renders, so +// users can paginate / re-sort without losing their selection. +const _ueSelectedEventIds = new Set(); + +function _ueRefreshBulkButton() { + const n = _ueSelectedEventIds.size; + const lbl = document.getElementById('ue-bulk-selected'); + const flag = document.getElementById('ue-bulk-flag-ft'); + const clr = document.getElementById('ue-bulk-clear-ft'); + if (lbl) lbl.textContent = `${n} selected`; + if (flag) flag.disabled = (n === 0); + if (clr) clr.disabled = (n === 0); +} + +function onUnitEventRowCheck(input) { + const id = input.getAttribute('data-event-id'); + if (input.checked) { + _ueSelectedEventIds.add(id); + } else { + _ueSelectedEventIds.delete(id); + // If we just unchecked a row, the master "all" checkbox shouldn't stay checked. + const master = document.getElementById('ue-check-all'); + if (master) master.checked = false; + } + _ueRefreshBulkButton(); +} + +function toggleAllUnitEventRows(checked) { + document.querySelectorAll('.ue-row-check').forEach(cb => { + const id = cb.getAttribute('data-event-id'); + cb.checked = checked; + if (checked) _ueSelectedEventIds.add(id); + else _ueSelectedEventIds.delete(id); + }); + _ueRefreshBulkButton(); +} + +async function flagSelectedUnitEvents(value) { + // value = true → flag as false trigger + // value = false → clear false-trigger flag + if (_ueSelectedEventIds.size === 0) return; + const ids = Array.from(_ueSelectedEventIds); + const verb = value ? 'flag as false trigger' : 'clear false-trigger flag on'; + if (!confirm(`${verb} ${ids.length} event${ids.length === 1 ? '' : 's'}?`)) { + return; + } + + // SFM exposes single-row PATCH only. Fan out concurrently with a + // modest cap so we don't open hundreds of sockets at once. + const concurrency = 8; + let ok = 0, failed = 0; + let cursor = 0; + async function worker() { + while (cursor < ids.length) { + const i = cursor++; + const id = ids[i]; + try { + const resp = await fetch( + `/api/sfm/db/events/${encodeURIComponent(id)}/false_trigger?value=${value ? 'true' : 'false'}`, + { method: 'PATCH' } + ); + if (resp.ok) ok++; + else failed++; + } catch (_) { + failed++; + } + } + } + await Promise.all(Array.from({ length: concurrency }, worker)); + + if (failed) { + alert(`${ok} updated, ${failed} failed. Refreshing table.`); + } + _ueSelectedEventIds.clear(); + loadUnitEvents(); } // ===== Pair Device Modal Functions =====