feat(deployments): mobile capture wizard + classify hopper + dashboard banner
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
<input capture="environment"> 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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(...),
|
||||
|
||||
Reference in New Issue
Block a user