From 93f01be4710cd045d6650f780a60bce74b6a5ac6 Mon Sep 17 00:00:00 2001 From: serversdown Date: Mon, 22 Jun 2026 18:12:56 +0000 Subject: [PATCH] fix: deployment capture coords now reach existing locations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /deploy classify "Assign to existing location" path dropped the captured GPS — only "Create new location" applied it — so units assigned to pre-existing coordless locations left those locations without a pin. - Classify (promote) now backfills the captured GPS onto an existing location that has no coordinates (doesn't clobber operator-set coords). - Add "Reforward info" button on Assigned deployment cards + endpoint POST /pending/{id}/resync-location that re-pushes a capture's GPS onto its assigned location (explicit action, overwrites). Fixes already-classified locations and guards against this recurring. Logged to unit history. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/routers/pending_deployments.py | 50 ++++++++++++++++++++++++ templates/admin/pending_deployments.html | 28 ++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/backend/routers/pending_deployments.py b/backend/routers/pending_deployments.py index 7496d74..608c409 100644 --- a/backend/routers/pending_deployments.py +++ b/backend/routers/pending_deployments.py @@ -296,6 +296,12 @@ async def promote_pending( if not location: raise HTTPException(status_code=404, detail=f"Location {location_id!r} not found.") project_id = location.project_id + # Backfill the captured GPS onto the existing location if it doesn't + # have coordinates yet. (Previously the captured coords were dropped on + # the assign-to-existing path, so only create-new locations got a pin.) + # Don't clobber coordinates an operator already set. + if pd.coordinates and not (location.coordinates or "").strip(): + location.coordinates = pd.coordinates else: # Create-new path. Need a project (existing or new). project_id = payload.get("project_id") @@ -456,6 +462,50 @@ async def cancel_pending( return {"success": True, "cancelled_at": pd.cancelled_at.isoformat()} +@router.post("/pending/{pending_id}/resync-location") +async def resync_location(pending_id: str, db: Session = Depends(get_db)): + """Re-push a promoted capture's GPS onto its assigned location. + + Use when a capture's coordinates didn't land on the location (e.g. it was + assigned to a pre-existing location that had none). Unlike the auto-backfill + on classify, this is an explicit operator action and OVERWRITES the + location's coordinates with the captured GPS. + """ + pd = db.query(PendingDeployment).filter_by(id=pending_id).first() + if not pd: + raise HTTPException(status_code=404, detail="Pending deployment not found.") + if pd.status != "assigned" or not pd.resulting_assignment_id: + raise HTTPException(status_code=400, detail="Only a promoted (assigned) capture can be re-forwarded.") + if not (pd.coordinates or "").strip(): + raise HTTPException(status_code=400, detail="This capture has no GPS coordinates to forward.") + + asg = db.query(UnitAssignment).filter_by(id=pd.resulting_assignment_id).first() + if not asg: + raise HTTPException(status_code=404, detail="Resulting assignment not found.") + location = db.query(MonitoringLocation).filter_by(id=asg.location_id).first() + if not location: + raise HTTPException(status_code=404, detail="Assigned location not found.") + + old = location.coordinates + location.coordinates = pd.coordinates.strip() + + _record_history( + db, unit_id=pd.unit_id, + change_type="deployment_coords_reforwarded", + old_value=old, + new_value=location.coordinates, + notes=f"Re-forwarded capture GPS to location '{location.name}'", + ) + + db.commit() + return { + "success": True, + "location_id": location.id, + "location_name": location.name, + "coordinates": location.coordinates, + } + + # ── Helpers ─────────────────────────────────────────────────────────────────── def _to_dict(pd: PendingDeployment, unit: Optional[RosterUnit] = None, detail: bool = False) -> dict: diff --git a/templates/admin/pending_deployments.html b/templates/admin/pending_deployments.html index 04df4fe..edb1eda 100644 --- a/templates/admin/pending_deployments.html +++ b/templates/admin/pending_deployments.html @@ -207,9 +207,19 @@ function _renderPdCard(pd) { `; } else if (pd.status === 'assigned') { + const reforward = pd.coordinates + ? `` + : ''; footerActions = `
Promoted ${_fmtDateTime(pd.promoted_at)} → assignment ${_esc((pd.resulting_assignment_id || '').slice(0, 8))}… -
`; + ${reforward}`; } else if (pd.status === 'cancelled') { footerActions = `
Cancelled ${_fmtDateTime(pd.cancelled_at)}${pd.cancelled_reason ? ` — ${_esc(pd.cancelled_reason)}` : ''} @@ -459,6 +469,22 @@ async function cancelPending(pendingId) { } } +// Re-push a promoted capture's GPS coordinates onto its assigned location. +async function reforwardInfo(pendingId) { + try { + const r = await fetch(`/api/deployments/pending/${pendingId}/resync-location`, { + method: 'POST', + }); + const j = await r.json().catch(() => ({})); + if (!r.ok) throw new Error(j.detail || 'HTTP ' + r.status); + const msg = `Coordinates synced to "${j.location_name}": ${j.coordinates}`; + if (window.showToast) showToast(msg, 'success'); else alert(msg); + } catch (e) { + const msg = 'Reforward failed: ' + e.message; + if (window.showToast) showToast(msg, 'error'); else alert(msg); + } +} + // Kick off the initial load. loadPdList(); // Refresh awaiting count every 30s for the badge.