v0.13.0 shipped the mic_unit_pref default as "dBL", which made the
website chart's mic axis inconsistent with the PDF report (which
renders psi). Original brief was always "psi on charts, dBL on
peaks" — I implemented the default backwards. Operator caught it
within an hour of rollout.
Same-day patch:
- backend/models.py: default "dBL" → "psi"
- migrate_add_mic_unit_pref.py: idempotent across both fresh DB
("add column with psi default") and v0.13.0 upgrade ("flip dBL
rows to psi"). One-row table, freshness assumed.
- backend/routers/settings.py: GET/PUT fallback "dBL" → "psi"
- templates/settings.html: dropdown's `selected` flag moves to psi
+ reorders options + relabels with "(matches PDF report)" hint
- backend/static/event-modal.js: module-level fallback + branch
conditions flip to make psi the unset/error default
Includes the "Captured at" → "Time received" relabel from earlier
in the day (already-shipped commit 43c804d) rolled into the
release notes.
Migration is idempotent + safe to re-run; rolled out on the dev
container during this commit's smoke test.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New UserPreferences field controls the mic channel's unit on the
SFM event-detail modal's waveform chart only. "dBL" default,
"psi" alternate. Peaks everywhere else (tables, KPI tiles, modal
summary) stay in dBL regardless — this is strictly a chart-axis
preference.
Surfaced as a single dropdown on Settings → General, below the
auto-refresh interval.
Setting up the storage half ahead of the chart port in the next
commit, so the chart can read the value from /api/settings/preferences
on first render instead of needing a follow-up wiring pass.
Includes idempotent backend/migrate_add_mic_unit_pref.py for fleets
already on an older schema.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Field-install workflow needs to be fast: arrive on site, snap a photo
of the seismograph in place, leave. Project / location classification
happens later at a desk. This adds the data model + capture endpoint
for that workflow.
Data model
- New PendingDeployment table. Lifecycle: awaiting → assigned (when
promoted to a real UnitAssignment) or → cancelled (operator's
mistake). Photos are filesystem files under data/photos/{unit_id}/
with the filename stored on the row.
- Migration: backend/migrate_add_pending_deployments.py (idempotent).
Endpoints
- POST /api/deployments/capture — multipart upload (unit_id, photo,
optional note). Refuses non-seismographs. Extracts EXIF GPS
(cribbing extract_exif_data from routers/photos.py) and stores
the captured "lat,lon" on the row. Saves the photo under
data/photos/{unit_id}/install_YYYYMMDD_HHMMSS_<uuid8>.<ext>.
Returns the new pending_deployment_id + extracted coords + photo
URL for the client to render confirmation.
- GET /api/deployments/pending — list by status (default awaiting)
- GET /api/deployments/pending/{id} — single row detail
- POST /api/deployments/pending/{id}/promote — classify → create
UnitAssignment. Body accepts two shapes: assign-to-existing-location
OR create-new-location (with new-or-existing project). Sets
status=assigned, resulting_assignment_id, promoted_at.
- POST /api/deployments/pending/{id}/cancel — abandon with optional reason.
All four routes write UnitHistory audit rows
(pending_deployment_captured / _promoted / _cancelled).
Events from a unit with an unclassified pending deployment land in the
unit's "Unattributed" events bucket as usual. Once promoted, the new
UnitAssignment's window retroactively attributes them — same mechanism
the metadata-backfill tool uses.
Seismograph-only for v1. SLM deployments don't follow this pattern
and are tracked elsewhere. Capture refuses non-seismograph unit_ids
with HTTP 400.
UI (commits 2 + 3) lands next.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Project location cards now reorderable via drag-and-drop, and the
four inline action buttons (Unassign/Edit/Remove/Delete) collapse into
a single three-dot kebab menu — much cleaner card layout, especially
for projects with many locations.
Data
- MonitoringLocation.sort_order: nullable Integer, default 0.
Migration `migrate_add_location_sort_order.py` adds the column and
seeds existing rows with sort_order = alphabetical index per project
(so the post-migration display order matches what operators see
today — no surprise reordering).
- get_project_locations + locations-json: ORDER BY sort_order, name.
- Location-create: assigns max(sort_order) + 1 so new locations land
at the END of the list rather than being interleaved alphabetically.
Reorder endpoint
- POST /api/projects/{p}/locations/reorder
Body: { location_ids: [uuid, uuid, ...] }
Validates: all ids belong to this project; raises 404 on missing.
Applies 0-indexed sort_order matching the provided order.
UI changes (templates/partials/projects/location_list.html)
- Active cards get a draggable="true" attribute + native HTML5
drag/drop handlers. Drop reorders the DOM immediately, then posts
the new order to the reorder endpoint. Drop-zone visual feedback
(orange ring on hover, opacity on source during drag).
- Six-dot drag handle icon on the left of each active card; whole
card body is the drag source but the handle is the visual cue.
- Right side: small Assign pill (only shown when unassigned) +
three-dot kebab menu containing Unassign/Edit/Remove/Delete.
Click ⋮ to toggle; click outside or Escape to close. Only one
menu open at a time.
- Removed locations are NOT draggable (their order is historical) and
keep their existing Restore button visible.
The card also shows "{N} events" instead of "Sessions: N" when the
location_type is vibration AND the backend passes event_count in
the payload — which lands in commit 2 of this redesign.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a client drops a location from scope mid-project (e.g. the office
half of a museum+office monitoring job), operators couldn't previously
mark it as no-longer-active without either deleting it (which would
orphan historical events) or leaving it in the active list looking
deployable. Now there's a proper middle ground.
Data model
- MonitoringLocation gets two new nullable columns:
- removed_at — NULL means active; set means soft-removed
- removal_reason — optional operator note
Migration: backend/migrate_add_location_removed.py (idempotent)
Endpoints
- POST /api/projects/{p}/locations/{l}/remove
Body: { effective_date?: ISO-datetime, reason?: str }
Side effects (cascade):
1. Closes active UnitAssignment rows at this location
(assigned_until = effective_date, status = "completed")
2. Cancels pending ScheduledActions at this location
3. Marks location.removed_at = effective_date
Returns counts of assignments closed + actions cancelled.
- POST /api/projects/{p}/locations/{l}/restore
Clears removed_at + removal_reason. Does NOT auto-reopen
assignments — operator creates new ones if resuming monitoring.
Active-surface filters
- locations-json defaults to active-only; pass include_removed=true
for historical / reporting views. Schedule modal dropdowns now
exclude removed locations automatically.
- Metadata-backfill fuzzy matcher excludes removed locations from
proposed targets (don't want backfill creating new assignments at
decommissioned locations).
- Vibration-summary per_location rollup includes removed locations
(so historical event totals stay accurate) but tags each with
removed_at so the UI can show a badge.
UI
- Project detail page's Monitoring Locations section now splits into:
Active locations (full card with Assign / Edit / Remove / Delete)
Removed locations (collapsed <details>, greyed cards, Restore button,
shows removal date + reason)
- New per-card "Remove" button → opens confirmation modal explaining
the cascade, with optional effective-date (defaults to now,
backdateable) and reason fields.
- Unit detail's SFM Events attribution cell shows a small "removed"
badge next to historical attributions whose location is no longer
active. Same pattern in vibration_summary's top-locations list.
- Soft-removal indicator surfaced through the events_for_unit
attribution payload as location_removed_at.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Operator clicks one button. Parser reads SFM's events table (operator-typed
project / client / sensor_location strings), clusters by serial + time +
metadata, fuzzy-matches against existing projects, and proposes
Project / MonitoringLocation / UnitAssignment chains to create.
Auto-applies high-confidence non-conflicting clusters in bulk; queues
medium/low confidence for individual review.
Verified against real data: 10,052 events → 59 clusters → 37 high-
confidence + 14 medium + 8 low. Test-applied one cluster end-to-end;
Project + Module + Location + Assignment + UnitHistory + Decision rows
all created correctly, and Phase 2's attribution walk picked up the
events automatically on the new location's detail page.
Pipeline (backend/services/metadata_backfill.py, ~700 lines):
1. Pull all SFM events via /db/events per serial.
2. Pre-filter: drop events already covered by an existing UnitAssignment
window (Phase 2 handles those automatically).
3. Time-cluster what's left: serial + 7-day gap is the cluster identity.
4. Metadata-split each time-cluster on persistent metadata transitions
(≥ 2 consecutive events) so a single typo doesn't fork the cluster.
5. Match against existing graph (rapidfuzz.WRatio multi-signal scoring,
normalisation that handles abbreviations / reorders / separator
variations). Thresholds: 0.95 exact, 0.80 fuzzy, min-shorter-input
5 chars to guardrail false positives on single common words.
6. Score confidence (high/medium/low) using event count, span,
blank-meta, conflict, ambiguity rules.
7. Detect conflicts: overlap with existing UnitAssignment at a different
location for the same serial → blocking. Operator must reconcile.
8. Apply: ensure auto_imported ProjectType exists, ensure
vibration_monitoring ProjectModule on the project, write
Project / MonitoringLocation / UnitAssignment / UnitHistory all in
one transaction.
Migration (backend/migrate_add_metadata_backfill.py): adds
unit_assignments.source column (default 'manual') and
metadata_backfill_decisions table. Idempotent, non-destructive.
API (backend/routers/metadata_backfill.py):
GET /api/admin/metadata_backfill/scan — clusters + suggestions
POST /api/admin/metadata_backfill/apply — bulk apply by cluster_ids
w/ optional per-cluster
project/location overrides
POST /api/admin/metadata_backfill/skip — mark skipped (persistent)
UI (templates/admin/metadata_backfill.html, accessible at
/settings/developer/metadata-backfill via the Developer tab of Settings):
- One-button "Run scan" entry.
- Summary KPI tiles (scanned / already attributed / pending / conflicts).
- "Apply all high-confidence" bulk button at the top — primary path.
- Per-cluster cards below with Apply / Skip / Preview event actions.
- Blank-meta clusters get inline input fields for operator-typed project +
location names before applying.
- Blocking-conflict clusters render with the conflicting assignment
information and a disabled Apply button.
- Live progress toast during apply.
- Reuses the Phase 1+2+4 event-detail modal for "Preview event" — operator
can sanity-check the BW report data against the cluster's sample event.
Dependencies: rapidfuzz==3.10.1 added to requirements.txt. Pre-built C
wheels for all platforms, ~5s docker build hit.
Phase 5b (deferred to next session): swap-detection daily background job,
notification inbox for auto-applied swaps, recently-applied audit view,
"Tidy" page for renaming/merging auto-created projects.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Updated project creation modal to allow selection of optional modules (Sound and Vibration Monitoring).
- Modified project dashboard and header to display active modules and provide options to add/remove them.
- Enhanced project detail view to dynamically adjust UI based on enabled modules.
- Implemented a new migration script to create a `project_modules` table and seed it based on existing project types.
- Adjusted form submissions to handle module selections and ensure proper API interactions for module management.
- Fix UTC display bug: upload_nrl_data now wraps RNH datetimes with
local_to_utc() before storing, matching patch_session behavior.
Period type and label are derived from local time before conversion.
- Add period_start_hour / period_end_hour to MonitoringSession model
(nullable integers 0–23). Migration: migrate_add_session_period_hours.py
- Update patch_session to accept and store period_start_hour / period_end_hour.
Response now includes both fields.
- Update get_project_sessions to compute "Effective: M/D H:MM AM → M/D H:MM AM"
string from period hours and pass it to session_list.html.
- Rework period edit UI in session_list.html: clicking the period badge now
opens an inline editor with period type selector + start/end hour inputs.
Selecting a period type pre-fills default hours (Day: 7–19, Night: 19–7).
- Wire period hours into _build_location_data_from_sessions: uses
period_start/end_hour when set, falls back to hardcoded defaults.
- RND viewer: inject SESSION_PERIOD_START/END_HOUR from template context.
renderTable() dims rows outside the period window (opacity-40) with a
tooltip; shows "(N in period window)" in the row count.
- New session detail page at /api/projects/{id}/sessions/{id}/detail:
shows breadcrumb, files list with View/Download/Report actions,
editable session info form (label, period type, hours, times).
- Add local_datetime_input Jinja filter for datetime-local input values.
- Monthly calendar view: new get_sessions_calendar endpoint returns
sessions_calendar.html partial; added below sessions list in detail.html.
Color-coded per NRL with legend, HTMX prev/next navigation, session dots
link to detail page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Updated dashboard to display allocated units alongside deployed and benched units.
- Introduced a quick-info modal for units, showing detailed information including calibration status, project allocation, and upcoming jobs.
- Enhanced fleet calendar with a new quick-info modal for units, allowing users to view unit details without navigating away.
- Modified devices table to include allocated status and visual indicators for allocated units.
- Added allocated filter option in the roster view for better unit management.
- Implemented backend migration to add 'allocated' and 'allocated_to_project_id' columns to the roster table.
- Updated unit detail view to reflect allocated status and allow for project allocation input.
- Updated reservation list to display estimated units and improved count display.
- Added "Upcoming" status to project dashboard and header with corresponding styles.
- Implemented a dropdown for quick status updates in project header.
- Modified project list compact view to reflect new status labels.
- Updated project overview to include a tab for upcoming projects.
- Added migration script to introduce estimated_units column in job_reservations table.
- Each monitoring location slot can now have a named location (e.g. "North Gate")
- Location names and slot order are persisted and restored in the planner
- Location names display in the expanded reservation card view
- Added "Promote to Project" button that converts a reservation into a
tracked project with monitoring locations and unit assignments pre-filled
Requires DB migration on prod:
ALTER TABLE job_reservation_units ADD COLUMN location_name TEXT;
ALTER TABLE job_reservation_units ADD COLUMN slot_index INTEGER;
- Add POST /api/projects/{project_id}/nrl/{location_id}/upload-data endpoint
accepting a ZIP or multi-file select of .rnd/.rnh files from an SD card.
Parses .rnh metadata for session start/stop times, serial number, and store
name. Creates a MonitoringSession (no unit assignment required) and DataFile
records for each measurement file.
- Add Upload Data button and collapsible upload panel to the NRL detail Data
Files tab, with inline success/error feedback and automatic file list refresh
via HTMX after import.
- Rename RecordingSession -> MonitoringSession throughout the codebase
(models.py, projects.py, project_locations.py, scheduler.py, roster_rename.py,
main.py, init_projects_db.py, scripts/rename_unit.py). DB table renamed from
recording_sessions to monitoring_sessions; old indexes dropped and recreated.
- Update all template UI copy from Recording Sessions to Monitoring Sessions
(nrl_detail, projects/detail, session_list, schedule_oneoff, roster).
- Add backend/migrate_rename_recording_to_monitoring_sessions.py for applying
the table rename on production databases before deploying this build.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Implemented a new API router for managing report templates, including endpoints for listing, creating, retrieving, updating, and deleting templates.
- Added a new HTML partial for a unified SLM settings modal, allowing users to configure SLM settings with dynamic modem selection and FTP credentials.
- Created a report preview page with an editable data table using jspreadsheet, enabling users to modify report details and download the report as an Excel file.
- Updated all instances of device_type from "sound_level_meter" to "slm" across the codebase.
- Enhanced documentation to reflect the new device type standardization.
- Added migration script to convert legacy device types in the database.
- Updated relevant API endpoints, models, and frontend templates to use the new device type.
- Ensured backward compatibility by deprecating the old device type without data loss.
- 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.