feat: add server-side device deletion
- add portal API and UI action to delete non-default devices - protect the configured portal default device from deletion - document that trip/history pages still scope to the single DEVICE_NAME Co-authored-by: Copilot <[email protected]>
This commit is contained in:
@@ -699,6 +699,19 @@ def portal_device_request_sync(device_id):
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
@app.delete("/api/v1/portal/devices/<int:device_id>")
|
||||
@require_login
|
||||
def portal_device_delete(device_id):
|
||||
success, errors = device_registry.delete_device(
|
||||
device_id,
|
||||
protected_device_name=os.environ.get("DEVICE_NAME", "yaris-2013"),
|
||||
)
|
||||
if not success:
|
||||
status = 404 if errors.get("error") == "Device not found." else 409
|
||||
return jsonify(error=errors.get("error", "Could not delete device.")), status
|
||||
return jsonify(status="deleted")
|
||||
|
||||
|
||||
@app.get("/api/v1/portal/device-config")
|
||||
@require_login
|
||||
def portal_device_config_get():
|
||||
|
||||
@@ -187,6 +187,13 @@ def acknowledge_device_sync(device_id, config_version):
|
||||
)
|
||||
|
||||
|
||||
def delete_device(device_id):
|
||||
with get_cursor(commit=True) as cur:
|
||||
cur.execute("DELETE FROM devices WHERE id = %s RETURNING id", (device_id,))
|
||||
row = cur.fetchone()
|
||||
return row[0] if row else None
|
||||
|
||||
|
||||
def insert_tracking_points(device_id, points):
|
||||
"""Bulk-insert tracking points, skipping any that already exist
|
||||
(device_id, recorded_at) so a client can safely re-send a batch after a
|
||||
|
||||
@@ -63,6 +63,21 @@ def request_sync(device_id):
|
||||
}
|
||||
|
||||
|
||||
def delete_device(device_id, protected_device_name=None):
|
||||
device = db.get_device_by_id(device_id)
|
||||
if device is None:
|
||||
return False, {"error": "Device not found."}
|
||||
if protected_device_name and device["name"] == protected_device_name:
|
||||
return False, {
|
||||
"error": "Cannot delete the portal's default device while DEVICE_NAME still points to it.",
|
||||
}
|
||||
|
||||
deleted_id = db.delete_device(device_id)
|
||||
if deleted_id is None:
|
||||
return False, {"error": "Device not found."}
|
||||
return True, {}
|
||||
|
||||
|
||||
def acknowledge_sync(device_id, config_version):
|
||||
db.acknowledge_device_sync(device_id, config_version)
|
||||
|
||||
|
||||
@@ -57,6 +57,9 @@
|
||||
<button class="btn btn-outline-primary btn-sm" id="request-sync-button" type="button" disabled>
|
||||
<i class="fas fa-rotate me-1"></i>Request sync
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" id="delete-device-button" type="button" disabled>
|
||||
<i class="fas fa-trash-alt me-1"></i>Delete device
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@@ -144,6 +147,7 @@
|
||||
<li><strong>Request sync</strong> means “fetch this config on the Pi's next online contact”.</li>
|
||||
<li>The car can still be offline; the remote server cannot push directly into it.</li>
|
||||
<li>Use the Pi's local <strong>Sync now</strong> button for immediate pull attempts.</li>
|
||||
<li><strong>Delete device</strong> removes that device row and cascades its trips, tracking logs, and dashcam files from the server.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user