From 1af5a94f57de01753445690fa4408c9f24b38f80 Mon Sep 17 00:00:00 2001 From: serversdown Date: Sat, 16 May 2026 03:45:18 +0000 Subject: [PATCH] feat(deployments): mobile capture wizard + classify hopper + dashboard banner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI for the pending-deployment workflow (commits 2 + 3 from the plan, landed together since commit 1 already shipped the full backend). New surfaces - /deploy — mobile-first 3-step wizard. Pick unit → take photo (uses so it opens the phone camera) → add optional note + submit. EXIF GPS auto-extracted on the server. Success page shows the captured coords + links to either "Deploy another" or "View pending hopper." Whole flow is meant to take under 90 seconds on site. - /tools/pending-deployments — the hopper. Filter pills: Awaiting / Assigned / Cancelled. Each card shows photo thumbnail, unit serial link, captured-at timestamp, coordinates, operator note, and status-appropriate actions. - Classify modal on the hopper: two modes — "Assign to existing location" (project + location pickers, scoped to vibration_monitoring) or "Create new location" (with new-or-existing project, plus a "use captured coords" checkbox that writes the pending row's coords onto the new location). Calls /pending/{id}/promote on submit. - Cancel button uses prompt() for the optional reason → POSTs to /pending/{id}/cancel. Backend additions - GET /api/deployments/seismograph-picker — JSON list of non-retired seismograph units for the /deploy unit picker. Annotates each unit with has_pending so the picker can flag units that already have a pending capture waiting. Discovery - New "Field Deploy" + "Pending Deployments" cards on /tools. - Dashboard banner: auto-shows when there are awaiting captures, polled every 30s. Hides when count drops to 0. Click → /tools/ pending-deployments. Co-Authored-By: Claude Opus 4.7 --- backend/main.py | 16 + backend/routers/pending_deployments.py | 44 +++ templates/admin/pending_deployments.html | 462 +++++++++++++++++++++++ templates/dashboard.html | 47 +++ templates/deploy.html | 300 +++++++++++++++ templates/tools.html | 37 ++ 6 files changed, 906 insertions(+) create mode 100644 templates/admin/pending_deployments.html create mode 100644 templates/deploy.html diff --git a/backend/main.py b/backend/main.py index 7accdab..48fc117 100644 --- a/backend/main.py +++ b/backend/main.py @@ -275,6 +275,22 @@ async def tools_page(request: Request): return templates.TemplateResponse("tools.html", {"request": request}) +@app.get("/deploy", response_class=HTMLResponse) +async def deploy_page(request: Request): + """Mobile-first field-capture wizard. Pick a seismograph, snap a + photo of the install, optionally add a memo — drop into the pending + hopper for later classification.""" + return templates.TemplateResponse("deploy.html", {"request": request}) + + +@app.get("/tools/pending-deployments", response_class=HTMLResponse) +async def pending_deployments_page(request: Request): + """List of field captures awaiting classification, plus filters for + historical assigned / cancelled rows. Operators promote a capture + into a real UnitAssignment from here.""" + return templates.TemplateResponse("admin/pending_deployments.html", {"request": request}) + + @app.get("/modems", response_class=HTMLResponse) async def modems_page(request: Request): """Field modems management dashboard""" diff --git a/backend/routers/pending_deployments.py b/backend/routers/pending_deployments.py index 06ae3c8..06cf2e7 100644 --- a/backend/routers/pending_deployments.py +++ b/backend/routers/pending_deployments.py @@ -67,6 +67,50 @@ def _record_history( )) +@router.get("/seismograph-picker") +def seismograph_picker( + q: str = "", + limit: int = 20, + db: Session = Depends(get_db), +): + """JSON list of seismograph units for the /deploy mobile picker. + + Filters out retired units. Sorts by recency of pending captures + first, then alphabetically — so units the operator is actively + deploying with surface at the top. + """ + q_clean = (q or "").strip() + qb = db.query(RosterUnit).filter( + RosterUnit.device_type == "seismograph", + RosterUnit.retired == False, # noqa: E712 + ) + if q_clean: + qb = qb.filter( + (RosterUnit.id.ilike(f"%{q_clean}%")) + | (RosterUnit.note.ilike(f"%{q_clean}%")) + ) + units = qb.order_by(RosterUnit.id).limit(limit).all() + + # Annotate with "has an awaiting pending deployment" so the picker + # can de-emphasize / warn on units that are already mid-deploy. + pending_unit_ids = { + r[0] for r in db.query(PendingDeployment.unit_id) + .filter_by(status="awaiting").distinct().all() + } + + return { + "units": [ + { + "id": u.id, + "note": u.note, + "deployed": u.deployed, + "has_pending": u.id in pending_unit_ids, + } + for u in units + ], + } + + @router.post("/capture") async def capture_deployment( unit_id: str = Form(...), diff --git a/templates/admin/pending_deployments.html b/templates/admin/pending_deployments.html new file mode 100644 index 0000000..2157f52 --- /dev/null +++ b/templates/admin/pending_deployments.html @@ -0,0 +1,462 @@ +{% extends "base.html" %} + +{% block title %}Pending Deployments - Seismo Fleet Manager{% endblock %} + +{% block content %} +
+ ← Back to Tools +

Pending Deployments

+

+ Captures from the field waiting to be classified. + Capture a new one → +

+
+ + +
+ + + +
+ +
+
Loading…
+
+ + + + + +{% endblock %} diff --git a/templates/dashboard.html b/templates/dashboard.html index 8b8e22c..c7f8b90 100644 --- a/templates/dashboard.html +++ b/templates/dashboard.html @@ -21,6 +21,53 @@ + + + + +
+
+

Field Deploy

+

+ Capture an install while you're still on site. Project + location can be picked later at a desk. +

+
+ + +
+
+ 1 + Unit +
+
+
+ 2 + Photo +
+
+
+ 3 + Confirm +
+
+ + +
+ +
+
+ + + + + + + + + +
+ + +{% endblock %} diff --git a/templates/tools.html b/templates/tools.html index 91c8613..d9649f6 100644 --- a/templates/tools.html +++ b/templates/tools.html @@ -14,6 +14,43 @@