d5a0163852
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>
64 lines
2.2 KiB
Python
64 lines
2.2 KiB
Python
"""
|
|
Migration: add `removed_at` + `removal_reason` columns to `monitoring_locations`.
|
|
|
|
Lets operators mark a location as no longer actively monitored without
|
|
deleting it (so historical events stay attributed correctly). Mirrors
|
|
the timestamp-based "closed state" pattern already used by
|
|
`unit_assignments.assigned_until`.
|
|
|
|
Behavior:
|
|
- `removed_at IS NULL` → location is active (default for all existing
|
|
rows after this migration)
|
|
- `removed_at` set → location is removed; historical events still
|
|
attribute to it but it's hidden from active
|
|
surfaces (assign dropdowns, calendar, etc.)
|
|
- `removal_reason` → optional operator note (e.g. "client dropped
|
|
from scope")
|
|
|
|
Idempotent — safe to re-run. Non-destructive — adds only.
|
|
|
|
Run with:
|
|
docker exec terra-view-terra-view-1 python3 /app/backend/migrate_add_location_removed.py
|
|
"""
|
|
|
|
import os
|
|
import sqlite3
|
|
|
|
DB_PATH = "./data/seismo_fleet.db"
|
|
|
|
|
|
def _has_column(cur: sqlite3.Cursor, table: str, column: str) -> bool:
|
|
cur.execute(f"PRAGMA table_info({table})")
|
|
return any(row[1] == column for row in cur.fetchall())
|
|
|
|
|
|
def migrate_database() -> None:
|
|
if not os.path.exists(DB_PATH):
|
|
print(f"Database not found at {DB_PATH}")
|
|
return
|
|
|
|
conn = sqlite3.connect(DB_PATH)
|
|
cur = conn.cursor()
|
|
|
|
added = []
|
|
if not _has_column(cur, "monitoring_locations", "removed_at"):
|
|
cur.execute("ALTER TABLE monitoring_locations ADD COLUMN removed_at DATETIME")
|
|
added.append("removed_at")
|
|
if not _has_column(cur, "monitoring_locations", "removal_reason"):
|
|
cur.execute("ALTER TABLE monitoring_locations ADD COLUMN removal_reason TEXT")
|
|
added.append("removal_reason")
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
if added:
|
|
print(f" Added columns to monitoring_locations: {', '.join(added)}")
|
|
else:
|
|
print(" monitoring_locations already has removed_at + removal_reason — nothing to do.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("Running migration: add removed_at + removal_reason to monitoring_locations")
|
|
migrate_database()
|
|
print("Done.")
|