feat(events): event modal + sortable tables polish
Event modal (event-modal.js): - Record Type now derived from Blastware filename's last-char code (H=Histogram, W=Waveform, M=Manual, E=Event, C=Combo). Falls back to whatever SFM reported if the code isn't recognized. Client-side workaround — SFM still hardcodes "Waveform" server-side and needs a proper fix in its sidecar parser. - PSI mic tile dropped; mic section now renders 3 tiles (dB(L), ZC Frequency, Time of Peak) instead of 4. - New "View JSON" toggle exposes a prettified inline JSON viewer with a Copy-to-clipboard button alongside the existing "Download sidecar JSON" link. - "Project Info" section header renamed to "User Notes" to reflect that these are operator-typed fields, not the terra-view project assignment. Sortable tables (sfm.html + unit_detail.html): - Both Events tables now have clickable column headers with ↕/↓/↑ indicators. Default sort is Timestamp DESC. Clicking the same column toggles direction; clicking a different column switches and resets to DESC. Sort is purely client-side over the cached rowset, so no extra fetches. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+59
-11
@@ -220,6 +220,12 @@ async function loadStats() {
|
||||
}
|
||||
|
||||
// ── Events tab ───────────────────────────────────────────────────────────────
|
||||
// Module-level cache so sort can re-render without re-fetching.
|
||||
let _eventsCache = [];
|
||||
let _eventsTotal = 0;
|
||||
let _eventsSortKey = 'timestamp';
|
||||
let _eventsSortDir = 'desc'; // 'asc' | 'desc'
|
||||
|
||||
async function loadEvents() {
|
||||
const container = document.getElementById('events-container');
|
||||
container.innerHTML = '<div class="text-center py-8 text-gray-500"><div class="animate-spin rounded-full h-8 w-8 border-b-2 border-seismo-orange mx-auto mb-3"></div>Loading events…</div>';
|
||||
@@ -241,19 +247,61 @@ async function loadEvents() {
|
||||
const r = await fetch('/api/sfm/db/events?' + params.toString());
|
||||
if (!r.ok) { throw new Error('HTTP ' + r.status); }
|
||||
const d = await r.json();
|
||||
renderEventsTable(d.events, d.count, container);
|
||||
_eventsCache = d.events || [];
|
||||
_eventsTotal = d.count || 0;
|
||||
renderEventsTable(_eventsCache, _eventsTotal, container);
|
||||
} catch (e) {
|
||||
container.innerHTML = `<div class="text-center py-8 text-red-500">Failed to load events: ${e.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function sortEvents(key) {
|
||||
// Toggle direction if same column clicked; otherwise default to desc.
|
||||
if (_eventsSortKey === key) {
|
||||
_eventsSortDir = _eventsSortDir === 'desc' ? 'asc' : 'desc';
|
||||
} else {
|
||||
_eventsSortKey = key;
|
||||
_eventsSortDir = 'desc';
|
||||
}
|
||||
renderEventsTable(_eventsCache, _eventsTotal, document.getElementById('events-container'));
|
||||
}
|
||||
|
||||
function _applySort(events) {
|
||||
const key = _eventsSortKey;
|
||||
const dir = _eventsSortDir === 'asc' ? 1 : -1;
|
||||
return [...events].sort((a, b) => {
|
||||
let av = a[key], bv = b[key];
|
||||
// Nulls always sort last regardless of dir.
|
||||
if (av == null && bv == null) return 0;
|
||||
if (av == null) return 1;
|
||||
if (bv == null) return -1;
|
||||
if (typeof av === 'number' && typeof bv === 'number') return (av - bv) * dir;
|
||||
return String(av).localeCompare(String(bv)) * dir;
|
||||
});
|
||||
}
|
||||
|
||||
function _sortIndicator(key) {
|
||||
if (_eventsSortKey !== key) return '<span class="text-gray-400 opacity-50 ml-1">↕</span>';
|
||||
return _eventsSortDir === 'desc'
|
||||
? '<span class="text-seismo-orange ml-1">↓</span>'
|
||||
: '<span class="text-seismo-orange ml-1">↑</span>';
|
||||
}
|
||||
|
||||
function _sortableTh(label, key) {
|
||||
return `<th onclick="sortEvents('${key}')"
|
||||
class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider cursor-pointer select-none hover:bg-gray-100 dark:hover:bg-slate-600 transition-colors">
|
||||
${label}${_sortIndicator(key)}
|
||||
</th>`;
|
||||
}
|
||||
|
||||
function renderEventsTable(events, total, container) {
|
||||
if (!events || events.length === 0) {
|
||||
container.innerHTML = '<div class="text-center py-12 text-gray-500 dark:text-gray-400"><p class="text-sm">No events found matching the current filters.</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = events.map(ev => {
|
||||
const sorted = _applySort(events);
|
||||
const rows = sorted.map(ev => {
|
||||
const ts = ev.timestamp ? ev.timestamp.replace('T', ' ').slice(0, 19) : '—';
|
||||
const tran = fmtPPV(ev.tran_ppv);
|
||||
const vert = fmtPPV(ev.vert_ppv);
|
||||
@@ -288,15 +336,15 @@ function renderEventsTable(events, total, container) {
|
||||
<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-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Timestamp</th>
|
||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Serial</th>
|
||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Project</th>
|
||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Tran</th>
|
||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Vert</th>
|
||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Long</th>
|
||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">PVS</th>
|
||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Mic</th>
|
||||
<th class="px-4 py-3 text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Flags</th>
|
||||
${_sortableTh('Timestamp', 'timestamp')}
|
||||
${_sortableTh('Serial', 'serial')}
|
||||
${_sortableTh('Project', 'project')}
|
||||
${_sortableTh('Tran', 'tran_ppv')}
|
||||
${_sortableTh('Vert', 'vert_ppv')}
|
||||
${_sortableTh('Long', 'long_ppv')}
|
||||
${_sortableTh('PVS', 'peak_vector_sum')}
|
||||
${_sortableTh('Mic', 'mic_ppv')}
|
||||
${_sortableTh('Flags', 'false_trigger')}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">${rows}</tbody>
|
||||
|
||||
Reference in New Issue
Block a user