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