feat(locations): soft-remove monitoring locations without destroying history
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>
This commit is contained in:
@@ -318,13 +318,18 @@ async def events_for_unit(
|
||||
loc = loc_map.get(a.location_id)
|
||||
proj = proj_map.get(a.project_id)
|
||||
return {
|
||||
"assignment_id": a.id,
|
||||
"location_id": a.location_id,
|
||||
"location_name": loc.name if loc else None,
|
||||
"project_id": a.project_id,
|
||||
"project_name": proj.name if proj else None,
|
||||
"assigned_at": _iso_utc(a.assigned_at),
|
||||
"assigned_until": _iso_utc(a.assigned_until),
|
||||
"assignment_id": a.id,
|
||||
"location_id": a.location_id,
|
||||
"location_name": loc.name if loc else None,
|
||||
# Soft-removal indicator so the UI can render a "(removed)"
|
||||
# badge next to historical attributions whose location is no
|
||||
# longer actively monitored.
|
||||
"location_removed_at": (loc.removed_at.isoformat()
|
||||
if loc and loc.removed_at else None),
|
||||
"project_id": a.project_id,
|
||||
"project_name": proj.name if proj else None,
|
||||
"assigned_at": _iso_utc(a.assigned_at),
|
||||
"assigned_until": _iso_utc(a.assigned_until),
|
||||
}
|
||||
|
||||
# 2. Fetch all events for this serial in one shot.
|
||||
@@ -515,6 +520,10 @@ async def vibration_summary_for_project(
|
||||
"event_count": ec,
|
||||
"peak_pvs": ev_peak,
|
||||
"last_event": ev_last,
|
||||
# Soft-removal state — UI can show a "(removed)" badge in the
|
||||
# per-location list so operators see at a glance that a row's
|
||||
# numbers are historical-only.
|
||||
"removed_at": loc.removed_at.isoformat() if loc.removed_at else None,
|
||||
})
|
||||
|
||||
per_location.sort(key=lambda r: r["event_count"], reverse=True)
|
||||
|
||||
Reference in New Issue
Block a user