Phase 4. Rebuilds the seismograph "Deployment History" + "Timeline"
sections on the unit detail page as a single derived view computed from
three sources: unit_assignments (authoritative project/location windows),
unit_history (calibration/retirement/deployed state changes), and SFM
events overlaid per assignment window (count + peak PVS + last event).
Fixes the wonky-timeline symptoms: missing entries, duplicate/contradictory
rows, and no visibility into what the unit was actually doing during each
deployment window.
Backend:
- backend/services/deployment_timeline.py: new deployment_timeline_for_unit()
helper. Merges UnitAssignment rows (with SFM event overlay fetched
concurrently via httpx), UnitHistory state-change rows (filtered to
meaningful change_types and de-noised by dropping rows where
old_value == new_value — there's noise in legacy audit log from
record_history() being called on every save), and synthetic "gap"
entries between assignments >= 1 day apart. Sorts newest first.
- backend/routers/units.py: new GET /api/units/{unit_id}/deployment_timeline
endpoint with optional include_events=false flag.
- backend/routers/project_locations.py: assign / unassign / swap /
update endpoints now write UnitHistory rows on every assignment
lifecycle event. New change_types: assignment_created,
assignment_ended, assignment_swapped, assignment_updated. These
surface in the unified timeline (where the assignment row itself
shows the structural data; the audit row is filtered out to avoid
double-rendering). Closes a real gap — assignment changes were
previously invisible to any audit consumer.
- backend/migrate_deprecate_deployment_records.py: non-destructive
migration. Adds deployment_records.deprecated_at column. For each
legacy row without a matching UnitAssignment, best-effort
synthesizes one (with the free-text location_name preserved in
notes). Marks every processed row. Idempotent. DROP TABLE
deferred to a follow-up release.
Frontend (templates/unit_detail.html):
- Removed legacy "Deployment History" card (with Log Deployment button)
and the separate "Timeline" card. Replaced with a single
"Deployment Timeline" section.
- Three entry visual styles: assignment rows (orange dot, location +
project link, event-overlay summary), gap rows (dashed outline, idle
day count), and state_change rows (navy dot, friendly label, old →
new value). Active assignments get a green dot + "active" badge.
- Existing loadUnitHistory() and loadDeploymentHistory() functions kept
as shims that delegate to loadDeploymentTimeline(), so modal-save
callbacks that referenced them still trigger a refresh of the visible
section. Legacy function bodies preserved under _legacy_*_unused
names for archeology; not called by anything.
Verified end-to-end:
- BE11529 timeline now shows 2 entries (active assignment with 24-event
overlay + the deployed→benched state change), compared to the previous
noisy mix that included 6 no-op state-change rows.
- Migration ran against real DB: 1 legacy row processed (had no
project_id, marked deprecated without backfill).
- Assign / unassign / swap / edit now leave a paper trail in
unit_history.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Phase 2 of the SFM integration. Adds a "SFM Events" section to the
seismograph unit detail page (/unit/{id}). Every event SFM has for the
serial is shown, with each event annotated by which project/location
assignment window it falls into. Events outside every assignment window
get the "⚠ Unattributed" badge plus a "<N>d before/after <nearest location>"
hint — that's the operator's signal that backdating an assignment (Phase 1
edit-pencil) will absorb the orphan events.
Backend:
- backend/services/sfm_events.py: new events_for_unit() helper. Fetches
all events for the serial via SFM /db/events (one call, ceiling 5000),
loads every UnitAssignment for the unit + resolves MonitoringLocation +
Project names, then annotates each event with attribution or
nearest_assignment (signed delta_days). Bucket filter: all /
attributed / unattributed. Stats always reflect the full event set so
the "Unattributed" KPI tile is meaningful regardless of which bucket
is being viewed.
- backend/routers/units.py: new GET /api/units/{unit_id}/events with
bucket / date-range / false_trigger / limit query params. 404s on
unknown unit_id; returns an empty payload for non-seismograph
device_types so the page can render the section conditionally.
Frontend (templates/unit_detail.html):
- New "SFM Events" section between "Deployment History" and "Timeline",
styled to match the existing card pattern (border-t divider, same
heading weight).
- Hidden by default; revealed only when currentUnit.device_type ===
'seismograph' after the unit data loads.
- Four KPI tiles: Total Events / Unattributed (highlighted amber when
> 0) / Peak PVS / Last Event.
- Filters: Bucket (all|attributed|unattributed), From/To, False
Triggers, Limit, + Refresh.
- Event table with Attribution column. Attributed rows link to the
project/location detail page; unattributed rows are tinted amber
and show "<N>d before/after <nearest location>" with a link to the
nearest location.
- Empty-state copy varies by bucket: e.g. unattributed-with-zero shows
"✅ All events for this unit are attributed to a project/location".
Verified end-to-end against BE11529 (81 events total, 24 attributed,
57 unattributed — all 57 unattributed events emitted within hours of
the assignment start, which means backdating the assignment by a day
would attribute every one of them).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Implemented a modal for renaming units with validation and confirmation prompts.
- Added JavaScript functions to handle opening, closing, and submitting the rename unit form.
- Enhanced the back navigation in the SLM detail page to check referrer history.
- Updated breadcrumb navigation in the legacy dashboard to accommodate NRL locations.
- Improved the sound level meters page with a more informative header and device list.
- Introduced a live measurement chart with WebSocket support for real-time data streaming.
- Added functionality to manage active devices and projects with auto-refresh capabilities.
- Created complete frontend structure with Jinja2 templates
- Implemented three main pages: Dashboard, Fleet Roster, and Unit Detail
- Added HTMX auto-refresh for real-time updates (10s interval)
- Integrated dark/light mode toggle with localStorage persistence
- Built responsive card-based UI with sidebar navigation
- Created API endpoints for status snapshot, roster, unit details, and photos
- Added mock data service for development (emit_status_snapshot)
- Implemented tabbed interface on unit detail page (Photos, Map, History)
- Integrated Leaflet maps for unit location visualization
- Configured static file serving and photo management
- Updated requirements.txt with Jinja2 and aiofiles
- Reorganized backend structure into routers and services
- Added comprehensive FRONTEND_README.md documentation
Frontend features:
- Auto-refreshing dashboard with fleet summary and alerts
- Sortable fleet roster table (prioritizes Missing > Pending > OK)
- Unit detail view with status, deployment info, and notes
- Photo gallery with thumbnail navigation
- Interactive maps showing unit coordinates
- Consistent styling with brand colors (orange, navy, burgundy)
Ready for integration with real Series3 emitter data.