feat: FEAT-05 — DTC history sync to remote server
- Server: dtc_events table (migrate_phase27_dtc.sql), db.py helpers (upsert_dtc_snapshot, list_dtc_events, delete_dtc_event) - Server API: POST /api/v1/dtc/batch (device auth), GET/DELETE /api/v1/portal/dtc - Server portal: /dtc page with code, description, MIL, status, timestamps, delete - Server nav: DTC history in sidebar + mobile bottom nav - Pi: _upload_dtc_snapshot() in app_upload.py — uploads when snapshot changes, skips when OBD disconnected or unchanged
This commit is contained in:
@@ -249,6 +249,83 @@ def insert_dashcam_file(device_id, relative_path, size_bytes, sha256, storage_pa
|
||||
)
|
||||
|
||||
|
||||
# ── DTC events (FEAT-05) ────────────────────────────────────────────────────
|
||||
|
||||
def upsert_dtc_snapshot(device_id, mil_on, codes):
|
||||
"""Receive a DTC snapshot from the Pi. Inserts new codes, updates existing,
|
||||
marks codes absent from this snapshot as cleared. Returns stats dict."""
|
||||
with get_cursor(commit=True) as cur:
|
||||
cur.execute(
|
||||
"SELECT code FROM dtc_events WHERE device_id = %s AND is_active = TRUE",
|
||||
(device_id,),
|
||||
)
|
||||
previously_active = {row[0] for row in cur.fetchall()}
|
||||
current_codes = {c["code"] for c in codes}
|
||||
|
||||
new_codes = current_codes - previously_active
|
||||
cleared_codes = previously_active - current_codes
|
||||
unchanged = previously_active & current_codes
|
||||
|
||||
with get_cursor(commit=True) as cur:
|
||||
for c in codes:
|
||||
if c["code"] in new_codes:
|
||||
cur.execute(
|
||||
"""INSERT INTO dtc_events (device_id, code, description, mil_on, is_active)
|
||||
VALUES (%s, %s, %s, %s, TRUE)
|
||||
ON CONFLICT (device_id, code) DO UPDATE SET
|
||||
description = EXCLUDED.description,
|
||||
mil_on = EXCLUDED.mil_on,
|
||||
is_active = TRUE,
|
||||
last_seen_at = now()""",
|
||||
(device_id, c["code"], c.get("description", ""), mil_on),
|
||||
)
|
||||
elif c["code"] in unchanged:
|
||||
cur.execute(
|
||||
"""UPDATE dtc_events SET mil_on = %s, last_seen_at = now(), is_active = TRUE
|
||||
WHERE device_id = %s AND code = %s""",
|
||||
(mil_on, device_id, c["code"]),
|
||||
)
|
||||
for code in cleared_codes:
|
||||
cur.execute(
|
||||
"""UPDATE dtc_events SET is_active = FALSE, cleared_at = now()
|
||||
WHERE device_id = %s AND code = %s""",
|
||||
(device_id, code),
|
||||
)
|
||||
return {"received": len(codes), "cleared": len(cleared_codes), "unchanged": len(unchanged)}
|
||||
|
||||
|
||||
def list_dtc_events(device_id):
|
||||
with get_cursor() as cur:
|
||||
cur.execute(
|
||||
"""SELECT id, code, description, mil_on, is_active, first_seen_at, last_seen_at, cleared_at
|
||||
FROM dtc_events WHERE device_id = %s ORDER BY last_seen_at DESC""",
|
||||
(device_id,),
|
||||
)
|
||||
rows = cur.fetchall()
|
||||
return [
|
||||
{
|
||||
"id": r[0],
|
||||
"code": r[1],
|
||||
"description": r[2],
|
||||
"mil_on": r[3],
|
||||
"is_active": r[4],
|
||||
"first_seen_at": r[5].isoformat() if r[5] else None,
|
||||
"last_seen_at": r[6].isoformat() if r[6] else None,
|
||||
"cleared_at": r[7].isoformat() if r[7] else None,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
|
||||
def delete_dtc_event(device_id, dtc_id):
|
||||
with get_cursor(commit=True) as cur:
|
||||
cur.execute(
|
||||
"DELETE FROM dtc_events WHERE id = %s AND device_id = %s RETURNING id",
|
||||
(dtc_id, device_id),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
# ── User auth ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_user_by_id(user_id):
|
||||
|
||||
Reference in New Issue
Block a user