feat(events): add SFM Event DB Manager for browsing, flagging, and deleting events

This commit is contained in:
2026-05-17 07:57:27 +00:00
parent 8cffd7dd5e
commit ef6484c350
4 changed files with 486 additions and 0 deletions
+104
View File
@@ -379,6 +379,20 @@
</button>
</div>
<!-- Bulk false-trigger flagging -->
<div id="ue-bulk-actions" class="flex flex-wrap items-center gap-2 mb-3 text-sm">
<span id="ue-bulk-selected" class="text-gray-600 dark:text-gray-400">0 selected</span>
<button id="ue-bulk-flag-ft" onclick="flagSelectedUnitEvents(true)" disabled
class="px-3 py-1.5 text-sm rounded-lg border border-yellow-300 dark:border-yellow-700 text-yellow-700 dark:text-yellow-300 hover:bg-yellow-50 dark:hover:bg-yellow-900/30 disabled:opacity-40 disabled:cursor-not-allowed">
🚩 Flag as false trigger
</button>
<button id="ue-bulk-clear-ft" onclick="flagSelectedUnitEvents(false)" disabled
class="px-3 py-1.5 text-sm rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-40 disabled:cursor-not-allowed">
✓ Clear false trigger
</button>
<span class="text-xs text-gray-500 dark:text-gray-400 ml-2">For deletion / DB cleanup, use the <a href="/admin/events" class="text-seismo-orange hover:underline">Event DB Manager</a>.</span>
</div>
<!-- Event table -->
<div id="ue-events-container" class="overflow-x-auto rounded-lg border border-gray-200 dark:border-gray-700">
<div class="text-center py-12 text-gray-500 dark:text-gray-400 text-sm">
@@ -2652,7 +2666,13 @@ 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>'
: '';
const checked = _ueSelectedEventIds.has(ev.id) ? 'checked' : '';
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-3 py-2.5 text-sm" onclick="event.stopPropagation()">
<input type="checkbox" class="ue-row-check rounded border-gray-300 dark:border-gray-600"
data-event-id="${_dtEsc(ev.id)}" ${checked}
onchange="onUnitEventRowCheck(this)">
</td>
<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>
@@ -2668,6 +2688,11 @@ function renderUnitEventTable(events, total, container, bucket, assignmentsTotal
<table class="w-full text-left">
<thead class="bg-gray-50 dark:bg-slate-700 border-b border-gray-200 dark:border-gray-600">
<tr>
<th class="px-3 py-2">
<input type="checkbox" id="ue-check-all"
class="rounded border-gray-300 dark:border-gray-600"
onchange="toggleAllUnitEventRows(this.checked)">
</th>
${_ueSortableTh('Timestamp', 'timestamp')}
${_ueSortableTh('Tran', 'tran_ppv')}
${_ueSortableTh('Vert', 'vert_ppv')}
@@ -2679,6 +2704,85 @@ function renderUnitEventTable(events, total, container, bucket, assignmentsTotal
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">${rows}</tbody>
</table>`;
_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 =====