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
@@ -423,6 +423,12 @@ def portal_device_settings_page():
)
@app.get("/dtc")
@require_login
def portal_dtc_page():
return render_template("dtc.html")
# ── Portal JSON API ────────────────────────────────────────────────────────
@app.get("/api/v1/portal/stats")
@@ -850,6 +856,45 @@ def api_device_config_update():
return jsonify(config)
# ── DTC sync (FEAT-05) ────────────────────────────────────────────────────────
@app.post("/api/v1/dtc/batch")
@require_device_auth
def upload_dtc_batch():
"""Receive a DTC snapshot from the Pi. Body: {mil_on: bool, codes: [{code, description}, ...]}."""
body = request.get_json(silent=True) or {}
mil_on = bool(body.get("mil_on", False))
codes = body.get("codes")
if not isinstance(codes, list):
return jsonify(error="expected 'codes' as a list"), 400
for c in codes:
if not isinstance(c.get("code"), str) or not c["code"].strip():
return jsonify(error="each code must have a non-empty 'code' string"), 400
result = db.upsert_dtc_snapshot(g.device_id, mil_on, codes)
return jsonify(result), 200
@app.get("/api/v1/portal/dtc")
@require_login
def portal_dtc():
device_id = _portal_device_id()
if device_id is None:
return jsonify(error="device not configured"), 404
return jsonify(db.list_dtc_events(device_id))
@app.delete("/api/v1/portal/dtc/<int:dtc_id>")
@require_login
def portal_dtc_delete(dtc_id):
device_id = _portal_device_id()
if device_id is None:
return jsonify(error="device not configured"), 404
deleted = db.delete_dtc_event(device_id, dtc_id)
if not deleted:
return jsonify(error="DTC event not found"), 404
return jsonify(status="ok")
def main():
app.run(host="0.0.0.0", port=8098)