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:
Mathieu Moran
2026-08-12 01:23:23 -04:00
parent 13cb8b4361
commit 3916f9879e
8 changed files with 302 additions and 14 deletions
+45
View File
@@ -310,6 +310,50 @@ def _upload_dashcam_files():
return uploaded_count
_last_dtc_hash = None # tracks last-uploaded DTC snapshot to avoid redundant pushes
def _upload_dtc_snapshot():
"""Upload current DTC snapshot to server if it differs from last upload.
Skips when OBD is disconnected (no point sending empty snapshots that
would clear all server-side DTC events)."""
global _last_dtc_hash
import hashlib as _hl
from . import app_obd, app_sql
# Only upload when OBD is actually connected — don't clear server DTCs
# just because the Pi rebooted or the adapter is unplugged.
if app_obd.connection is None:
return
mil_on = app_obd.mil_on
codes = app_obd.dtc_codes or []
if not codes and mil_on is None:
return # No data yet
# Hash the current snapshot to detect changes
payload = {"mil_on": bool(mil_on), "codes": codes}
h = _hl.sha256(repr(payload).encode()).hexdigest()
if h == _last_dtc_hash:
return
_last_dtc_hash = h
try:
creds = _credentials()
if not creds["server_url"] or not creds["device_name"] or not creds["api_key"]:
return
resp = requests.post(
f"{creds['server_url']}/api/v1/dtc/batch",
json=payload,
headers=_headers(creds),
timeout=5,
)
resp.raise_for_status()
except requests.RequestException:
_last_dtc_hash = None # retry next cycle
raise
def _upload_loop():
global _last_error, _last_success_at
import datetime as _dt
@@ -323,6 +367,7 @@ def _upload_loop():
sent = _upload_tracking_batch()
uploaded = _upload_dashcam_files()
cleanup = app_video.cleanup_uploaded_videos()
_upload_dtc_snapshot() # silently skips when unchanged or OBD offline
if sent or uploaded:
print(f"Upload: sent {sent} tracking rows, {uploaded} dashcam files")
if cleanup["deleted"]: