From 9794d8076c4e004969a8a0042abe2e5aa0692515 Mon Sep 17 00:00:00 2001 From: Leo Date: Fri, 22 May 2026 23:36:40 -0400 Subject: [PATCH] Add clear asset GPS endpoint + UI - DELETE /api/assets/{id}/gps: clears lat/lng to NULL - GET /api/assets/{id}: asset lookup by ID - GET /api/assets/search?machine_id=: asset lookup by machine ID - Export & Admin page: Clear Asset GPS card with lookup + confirmation - Tests: 204 success + 404 not found --- admin_server.py | 45 +++++++++++++++++++++++++++++++++ static/index.html | 63 ++++++++++++++++++++++++++++++++++++++++++++++ test_admin_crud.py | 29 +++++++++++++++++++++ 3 files changed, 137 insertions(+) diff --git a/admin_server.py b/admin_server.py index 3f035e0..48d9755 100644 --- a/admin_server.py +++ b/admin_server.py @@ -1583,6 +1583,51 @@ def export_checkins_csv(asset_id: Optional[int] = Query(None)): ) +# ─── Asset GPS Management ────────────────────────────────────────────────── + + +@app.get("/api/assets/{asset_id}") +def get_asset_admin(asset_id: int): + """Get a single asset by ID (admin).""" + conn = get_db() + row = conn.execute("SELECT * FROM assets WHERE id = ?", (asset_id,)).fetchone() + conn.close() + if row is None: + raise HTTPException(status_code=404, detail="Asset not found") + return row_to_dict(row) + + +@app.get("/api/assets/search") +def search_assets_admin(machine_id: str = Query(...)): + """Search for an asset by machine_id (admin).""" + conn = get_db() + row = conn.execute( + "SELECT * FROM assets WHERE machine_id = ?", (machine_id,) + ).fetchone() + conn.close() + if row is None: + raise HTTPException(status_code=404, detail="Asset not found") + return row_to_dict(row) + + +@app.delete("/api/assets/{asset_id}/gps", status_code=204) +def clear_asset_gps(asset_id: int): + """Clear GPS coordinates from an asset (sets latitude/longitude to NULL).""" + conn = get_db() + existing = conn.execute("SELECT id, name FROM assets WHERE id = ?", (asset_id,)).fetchone() + if existing is None: + conn.close() + raise HTTPException(status_code=404, detail="Asset not found") + conn.execute( + "UPDATE assets SET latitude = NULL, longitude = NULL, updated_at = datetime('now') WHERE id = ?", + (asset_id,), + ) + _log_activity(conn, "updated", "asset", asset_id, + f"GPS data cleared for '{existing['name']}'") + conn.commit() + conn.close() + + # ─── Icon Upload API ──────────────────────────────────────────────────────── diff --git a/static/index.html b/static/index.html index 5717b30..4730602 100644 --- a/static/index.html +++ b/static/index.html @@ -878,6 +878,17 @@ 📄 Export Check-Ins CSV +
+
Clear Asset GPS
+

+ Clear GPS coordinates from an asset. Enter the asset ID or machine ID. +

+
+ + +
+ +
Database
@@ -2110,6 +2121,58 @@ async function confirmResetDb() { } } +// ── Clear Asset GPS ────────────────────────────────────────────────────── +async function clearAssetGps() { + const input = document.getElementById('clearGpsAssetId').value.trim(); + if (!input) { + showToast('Enter an asset ID or machine ID', true); + return; + } + const resultEl = document.getElementById('clearGpsResult'); + resultEl.style.display = 'none'; + + try { + // Try numeric asset ID first, otherwise search by machine_id + let asset; + const idNum = parseInt(input); + if (!isNaN(idNum) && idNum > 0) { + try { + asset = await api('/api/assets/' + idNum); + } catch (e) { + // Fall through to machine_id search + } + } + if (!asset) { + asset = await api('/api/assets/search?machine_id=' + encodeURIComponent(input)); + } + + if (!asset || !asset.id) { + resultEl.style.display = ''; + resultEl.innerHTML = 'Asset not found'; + return; + } + + const confirmed = await showModal( + 'Clear GPS Data', + `${esc(asset.name)} (#${asset.id})
+ Machine ID: ${esc(asset.machine_id)}
+ Current: lat=${asset.latitude || 'none'}, lng=${asset.longitude || 'none'}

+ Remove GPS coordinates from this asset?`, + 'Clear GPS', 'btn-danger' + ); + if (!confirmed) return; + + await api('/api/assets/' + asset.id + '/gps', { method: 'DELETE' }); + resultEl.style.display = ''; + resultEl.innerHTML = '✅ GPS data cleared for ' + esc(asset.name) + ' (#' + asset.id + ')'; + document.getElementById('clearGpsAssetId').value = ''; + showToast('GPS data cleared'); + } catch (e) { + resultEl.style.display = ''; + resultEl.innerHTML = '' + esc(e.message) + ''; + } +} + // ═════════════════════════════════════════════════════════════════════════════ // CANTALOUPE SYNC // ═════════════════════════════════════════════════════════════════════════════ diff --git a/test_admin_crud.py b/test_admin_crud.py index c837720..77f46ba 100644 --- a/test_admin_crud.py +++ b/test_admin_crud.py @@ -288,3 +288,32 @@ class TestSettings: cid = _assert_created(client.post("/api/settings/categories", json={"name": name}), "category") resp = client.delete(f"/api/settings/categories/{cid}") assert resp.status_code == 204 + + +# ─── Asset GPS ────────────────────────────────────────────────────────────── + +class TestClearAssetGps: + def test_clear_gps_returns_204(self, client): + """Clear GPS returns 204 and sets lat/lng to null.""" + # Create an asset with GPS coords via direct DB insert + import sqlite3 + conn = sqlite3.connect(DB_PATH) + conn.execute( + "INSERT INTO assets (machine_id, name, latitude, longitude) VALUES (?, ?, ?, ?)", + ("GPS-ADMIN-TEST", "Test GPS Asset", 28.5, -81.3), + ) + asset_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0] + conn.commit() + conn.close() + + resp = client.delete(f"/api/assets/{asset_id}/gps") + assert resp.status_code == 204 + + # Verify + data = _assert_ok(client.get(f"/api/assets/{asset_id}"), "asset") + assert data["latitude"] is None + assert data["longitude"] is None + + def test_clear_gps_not_found_returns_404(self, client): + resp = client.delete("/api/assets/99999/gps") + assert resp.status_code == 404