e05f2189c4
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>
76 lines
2.5 KiB
Python
76 lines
2.5 KiB
Python
"""
|
|
Migration: add `pending_deployments` table.
|
|
|
|
Stores "I just installed this seismograph" captures from the field.
|
|
A pending deployment is the prospective form of a UnitAssignment —
|
|
captured at install time (photo + coords + maybe a free-text note),
|
|
classified later (project + location chosen at a desk).
|
|
|
|
Once classified, a real UnitAssignment is created, the pending row's
|
|
status flips to "assigned", and resulting_assignment_id points at the
|
|
new assignment for audit.
|
|
|
|
Idempotent — safe to re-run. Non-destructive — adds only.
|
|
|
|
Run with:
|
|
docker exec terra-view-terra-view-1 python3 /app/backend/migrate_add_pending_deployments.py
|
|
"""
|
|
|
|
import os
|
|
import sqlite3
|
|
|
|
DB_PATH = "./data/seismo_fleet.db"
|
|
|
|
|
|
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()
|
|
|
|
cur.execute("""
|
|
CREATE TABLE IF NOT EXISTS pending_deployments (
|
|
id TEXT PRIMARY KEY,
|
|
unit_id TEXT NOT NULL,
|
|
captured_at DATETIME NOT NULL,
|
|
coordinates TEXT,
|
|
operator_note TEXT,
|
|
photo_filename TEXT,
|
|
status TEXT NOT NULL DEFAULT 'awaiting',
|
|
promoted_at DATETIME,
|
|
resulting_assignment_id TEXT,
|
|
cancelled_at DATETIME,
|
|
cancelled_reason TEXT,
|
|
created_at DATETIME NOT NULL DEFAULT (datetime('now')),
|
|
updated_at DATETIME NOT NULL DEFAULT (datetime('now'))
|
|
)
|
|
""")
|
|
print(" Table 'pending_deployments' ready.")
|
|
|
|
# Indexes — operators will query by status (hopper list) and by
|
|
# unit_id (per-unit detail page → "is there a pending capture?").
|
|
cur.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_pending_deployments_status
|
|
ON pending_deployments (status)
|
|
""")
|
|
cur.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_pending_deployments_unit_id
|
|
ON pending_deployments (unit_id)
|
|
""")
|
|
cur.execute("""
|
|
CREATE INDEX IF NOT EXISTS idx_pending_deployments_captured_at
|
|
ON pending_deployments (captured_at)
|
|
""")
|
|
print(" Indexes ready.")
|
|
|
|
conn.commit()
|
|
conn.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print("Running migration: add pending_deployments table")
|
|
migrate_database()
|
|
print("Done.")
|