@@ -324,6 +415,175 @@ function switchTab(tabName) {
button.classList.remove('border-transparent', 'text-gray-600', 'dark:text-gray-400');
button.classList.add('border-seismo-orange', 'text-seismo-orange');
}
+ // Lazy-load Events tab on first visit (or whenever it's reopened).
+ if (tabName === 'events' && !_eventsLoaded) {
+ loadLocationEvents();
+ }
+}
+
+// ── Events tab ───────────────────────────────────────────────────────────────
+let _eventsLoaded = false;
+
+function clearEventFilters() {
+ document.getElementById('ev-filter-from').value = '';
+ document.getElementById('ev-filter-to').value = '';
+ document.getElementById('ev-filter-ft').value = '';
+ document.getElementById('ev-filter-limit').value = '500';
+ loadLocationEvents();
+}
+
+async function loadLocationEvents() {
+ const container = document.getElementById('events-container');
+ container.innerHTML = '
';
+
+ const params = new URLSearchParams();
+ const from = document.getElementById('ev-filter-from').value;
+ const to = document.getElementById('ev-filter-to').value;
+ const ft = document.getElementById('ev-filter-ft').value;
+ const limit = document.getElementById('ev-filter-limit').value;
+ if (from) params.set('from_dt', from.replace('T', ' '));
+ if (to) params.set('to_dt', to.replace('T', ' '));
+ if (ft) params.set('false_trigger', ft);
+ params.set('limit', limit);
+
+ try {
+ const r = await fetch(`/api/projects/${projectId}/locations/${locationId}/events?${params.toString()}`);
+ if (!r.ok) {
+ const err = await r.json().catch(() => ({detail: 'HTTP ' + r.status}));
+ throw new Error(err.detail || 'HTTP ' + r.status);
+ }
+ const d = await r.json();
+ _eventsLoaded = true;
+ renderEventStats(d.stats);
+ renderAssignmentsUsed(d.assignments_used);
+ renderEventTable(d.events, d.count, container);
+ } catch (e) {
+ container.innerHTML = `
Failed to load events: ${e.message}
`;
+ }
+}
+
+function renderEventStats(stats) {
+ const s = stats || {};
+ document.getElementById('ev-stat-count').textContent = (s.event_count ?? 0).toLocaleString();
+ document.getElementById('ev-stat-ft').textContent = (s.false_trigger_count ?? 0).toLocaleString();
+
+ if (s.peak_pvs != null) {
+ document.getElementById('ev-stat-peak').textContent = s.peak_pvs.toFixed(4) + ' in/s';
+ const when = s.peak_pvs_at ? s.peak_pvs_at.slice(0, 10) : '';
+ const who = s.peak_pvs_serial || '';
+ document.getElementById('ev-stat-peak-when').textContent = [when, who].filter(Boolean).join(' · ') || '—';
+ } else {
+ document.getElementById('ev-stat-peak').textContent = '—';
+ document.getElementById('ev-stat-peak-when').textContent = '—';
+ }
+
+ if (s.last_event) {
+ const dt = s.last_event.slice(0, 19).replace('T', ' ');
+ document.getElementById('ev-stat-last').textContent = dt;
+ } else {
+ document.getElementById('ev-stat-last').textContent = '—';
+ }
+}
+
+function renderAssignmentsUsed(assignments) {
+ const card = document.getElementById('events-assignments-card');
+ const listEl = document.getElementById('ev-assignments-list');
+ const countEl = document.getElementById('ev-assignments-count');
+
+ if (!assignments || assignments.length === 0) {
+ card.classList.add('hidden');
+ return;
+ }
+ card.classList.remove('hidden');
+ countEl.textContent = `${assignments.length} assignment${assignments.length === 1 ? '' : 's'}`;
+
+ listEl.innerHTML = assignments.map(a => {
+ const start = a.assigned_at ? a.assigned_at.slice(0, 10) : '?';
+ const end = a.assigned_until ? a.assigned_until.slice(0, 10) : 'present';
+ const isActive = !a.assigned_until;
+ const badge = isActive
+ ? '
active'
+ : '';
+ return `
+
+
${(a.events_in_window || 0).toLocaleString()} event${a.events_in_window === 1 ? '' : 's'}
+
`;
+ }).join('');
+}
+
+function renderEventTable(events, total, container) {
+ if (!events || events.length === 0) {
+ const haveAssignments = !document.getElementById('events-assignments-card').classList.contains('hidden');
+ const msg = haveAssignments
+ ? 'No events recorded for the assignments above within the current filter.'
+ : 'No seismographs have been assigned to this location yet. Assign one to start collecting events.';
+ container.innerHTML = `
${msg}
`;
+ return;
+ }
+
+ const rows = events.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);
+ const lng = fmtPPV(ev.long_ppv);
+ const pvs = fmtPPV(ev.peak_vector_sum);
+ const mic = ev.mic_ppv != null ? ev.mic_ppv.toFixed(3) : '—';
+ const ft = ev.false_trigger
+ ? '
FT'
+ : '';
+
+ return `
+ | ${ts} |
+
+ ${esc(ev.serial)}
+ |
+ ${tran} |
+ ${vert} |
+ ${lng} |
+ ${pvs} |
+ ${mic} |
+ ${ft} |
+
`;
+ }).join('');
+
+ container.innerHTML = `
+
Showing ${events.length} of ${total.toLocaleString()} events
+
+
+
+ | Timestamp |
+ Serial |
+ Tran |
+ Vert |
+ Long |
+ PVS |
+ Mic |
+ Flags |
+
+
+ ${rows}
+
`;
+}
+
+function fmtPPV(v) {
+ if (v == null) return '—';
+ return v.toFixed(4);
+}
+
+function ppvClass(v) {
+ if (v == null) return 'text-gray-400';
+ if (v < 0.5) return 'text-green-600 dark:text-green-400';
+ if (v < 2.0) return 'text-amber-600 dark:text-amber-400';
+ return 'text-red-600 dark:text-red-400 font-semibold';
+}
+
+function esc(s) {
+ if (s == null) return '';
+ return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"');
}
// Location settings form submission