Two related operator-facing improvements after the nav reorg.
1) Events as a top-level sidebar entry.
The /sfm page (fleet-wide event database) was demoted to Settings →
Developer in the previous reorg. Bringing it back to main nav as
"Events" — operators do reach for the cross-project, sortable
event list, so it earns a top-level slot.
Sidebar now (7 items):
Dashboard · Devices · Projects · Events · Tools · Job Planner · Settings
Settings → Developer card pointing at /sfm is removed. /sfm page
title/subtitle updated from "SFM Event Data" to just "Events". URL
unchanged.
2) "Peak PVS" KPI tile becomes "Overall Peak" and excludes false
triggers from the calculation.
When operators ask "what's the biggest event at this location/unit/
project?" they mean the biggest REAL event, not the biggest sensor
glitch. A single mis-flagged false trigger could otherwise dominate
the tile (the 14.13 in/s spike at Loc 1 was a prime example).
backend/services/sfm_events.py:
- _compute_stats() skips false_trigger=True events when computing
peak_pvs / peak_pvs_at / peak_pvs_serial. Continues counting them
in false_trigger_count so the separate "False Triggers" tile still
reflects what got filtered out. last_event unchanged (recency, not
magnitude).
- Same change automatically propagates to events_for_unit() and
vibration_summary_for_project() — both call _compute_stats().
Templates: "Peak PVS" → "Overall Peak" in 3 KPI tile locations
(vibration_location_detail.html, partials/projects/vibration_summary
.html, unit_detail.html). The physical-quantity name "Peak Vector
Sum" in the event-detail modal stays — that's the actual physics
term, not a summary stat.
Verified end-to-end: Overall Peak renders on real data; peak event
false_trigger flag confirmed False.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Operators couldn't change a unit's assigned_at / assigned_until after
creating the assignment, so a unit physically deployed in December 2025
but only recorded in terra-view today would show "deployed today" and
all its real events would be invisible on the project's location page.
Backend:
- PATCH /api/projects/{project_id}/assignments/{assignment_id}
Accepts JSON body with optional assigned_at, assigned_until, notes.
- assigned_at is required (cannot be cleared)
- assigned_until can be null to mark active / indefinite
- assigned_until must be after assigned_at
- rejects overlaps with other assignments of the same unit at the
same location (different units overlapping is fine — that's a
legitimate swap window)
- assignment.status flips to "active" when assigned_until is cleared,
"completed" when set
- 404 if the assignment doesn't belong to {project_id} (security)
Frontend (vibration_location_detail.html):
- Pencil icon next to each row in the "Seismographs deployed at this
location" card. Click to open a modal with datetime-local inputs for
From + Until (blank = active) and a Notes textarea. Save reloads the
Events tab so KPI tiles and the event table reflect the new window.
- Helper line under the assignment list explains the workflow:
"Click the pencil to backdate a deployment so historical events get
attributed to this location."
Verified end-to-end against real data: backdating BE11529's assignment
on a vibration location from 2026-04-14 to 2025-12-01 surfaced 10
additional events (24 -> 34) that were previously invisible.
Validation suite (all returning correct HTTP codes):
- assigned_until < assigned_at -> 400
- cross-project assignment_id -> 404
- assigned_at cleared -> 400
- notes-only update -> 200
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 1 of the SFM project/location integration. When viewing a vibration
monitoring location, operators now see the events that were actually
recorded there — fanned out across every seismograph that was ever
assigned to that location (handles mid-project unit swaps).
Backend:
- backend/services/sfm_events.py: new events_for_location() async helper.
Walks UnitAssignment rows for the location (active + closed), intersects
each assignment's [assigned_at, assigned_until] window with the requested
filter, and concurrently queries SFM /db/events for each (serial, window)
pair via httpx.AsyncClient. Unions, sorts newest-first, computes summary
stats (event count, peak PVS + when/who, last event, false-trigger count)
over the full set, and trims to the user's display limit. Over-fetches
per-window (up to 5000) so stats stay accurate even with a small display
limit.
- backend/routers/project_locations.py: new GET endpoint
/api/projects/{project_id}/locations/{location_id}/events. Validates
project/location pairing (404 on mismatch). SLM locations return an
empty payload rather than 404 so the frontend can render gracefully.
Frontend:
- templates/vibration_location_detail.html: new "Events" tab on the
location detail page. KPI tiles (total / peak PVS / last event / false
triggers), "Seismographs deployed at this location" assignment list
(transparency: shows each assignment's date range and contributed event
count), date / false-trigger / limit filters, and the paginated event
table. Lazy-loaded on first tab visit; manual refresh button.
Architectural notes:
- SFM remains the single source of truth for events. No event sync; live
HTTP per page load.
- UnitAssignment is the join key (not MonitoringSession).
- Events whose timestamp falls outside every assignment window are NOT
surfaced here. Those orphan events get a dedicated "Unattributed
events" view on the per-unit detail page in Phase 2.
Out of scope (this commit):
- Phase 2 (per-unit history view) and Phase 3 (project-level roll-up)
reuse this helper but ship separately.
- Phase 4 (deprecating deployment_records) is independent.
- Extracting the event-table JS to a shared file is a follow-up.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>