feat: portal-wide device selector — session-based, all pages now use selected device

- _portal_device_id() reads from session, falls back to DEVICE_NAME env var
- GET/POST /api/v1/portal/select-device for selector data + set
- Dropdown in server base.html topbar, JS populates from API, page reloads on change
- All portal pages (overview, trips, dashcam, DTC, device settings) now use selected device
This commit is contained in:
Mathieu Moran
2026-08-12 01:40:51 -04:00
parent 1556c65131
commit e30b85d218
6 changed files with 133 additions and 87 deletions
+8 -62
View File
@@ -82,69 +82,15 @@ dedicated PLAN.md phase (or fold it into the relevant existing work) and mark it
run the Pi's hotspot/SSID the same way `app_network.py` currently does.
### FEAT-05 — DTC history synced to remote server with code descriptions
- **Status**: `IDEA`
- **Area**: `modules/app_upload.py`, `modules/app_sql.py`, `server/sql/`, `server/modules/db.py`,
`server/app.py`, server portal templates
- **Status**: `SHIPPED` 2026-08-12
- **Area**: `modules/app_upload.py`, `server/sql/migrate_phase27_dtc.sql`, `server/modules/db.py`, `server/app.py`, `server/templates/dtc.html`
- **Phase**: Phase 27
- **Requested**: 2026-08-11 — owner wants DTC codes persisted to the server (not just local SQLite)
with code + description, surviving until user deletes them or OBD reports the code cleared.
- **Current state (2026-08-11)**:
- Pi: `app_obd.py` polls DTCs from ECU → `app_sql.save_dtc_state()` persists MIL + codes
(code, description, IS_SET, FIRST_SEEN_AT, LAST_SEEN_AT) to local SQLite `dtc` table.
- Pi: `app_upload.py` uploads tracking rows and dashcam files — NO DTC upload path exists.
- Server: NO `dtc_events` table, NO DTC API, NO portal DTC page.
- **Design**:
**Server schema** (`server/sql/migrate_phase26_dtc.sql`):
```sql
CREATE TABLE IF NOT EXISTS dtc_events (
id SERIAL PRIMARY KEY,
device_id INTEGER NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
code VARCHAR(10) NOT NULL,
description TEXT NOT NULL DEFAULT '',
mil_on BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
first_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
last_seen_at TIMESTAMPTZ NOT NULL DEFAULT now(),
cleared_at TIMESTAMPTZ,
UNIQUE (device_id, code)
);
```
**Server API** (`server/app.py`):
- `POST /api/v1/dtc/batch` (device auth): receives current DTC snapshot from Pi.
Body: `{"mil_on": true/false, "codes": [{"code": "P0301", "description": "Cylinder 1 Misfire"}, ...]}`
- Insert new codes, update `last_seen_at` for existing active codes.
- Mark any previously-active codes NOT in this batch as `is_active=false, cleared_at=now()`.
- Return `{received: N, cleared: M, unchanged: K}`.
- `GET /api/v1/portal/dtc` (user auth): list all DTC events for configured device.
- `DELETE /api/v1/portal/dtc/<id>` (user auth): manual deletion.
**Server portal**: new DTC page or section showing code, description, MIL state, first/last seen,
active/cleared status, cleared timestamp.
**Pi upload** (`modules/app_upload.py`):
- New `_upload_dtc_snapshot()` — called after tracking batch in `_upload_loop()`.
- Only uploads when the local DTC snapshot differs from last-uploaded (hash or version compare).
- Reuses same `_credentials()`/`_headers()` as tracking uploads.
- Same WiFi-first, cellular-fallback (if configured) path as tracking batches.
- **Deduplication logic**: server-side per `(device_id, code)` unique key. Pi sends the full
current snapshot each time. Server diffs: new codes = insert, existing = touch last_seen_at,
missing from snapshot = mark cleared. This means if a code disappears from the ECU, it
auto-clears on the server on the next upload — no manual "clear" step needed.
- **Manual deletion**: only via the server portal delete button. Cleared codes stay visible as
history. The Pi never deletes from the server automatically.
- **Outstanding considerations**:
- The Pi's local `save_dtc_state()` already handles IS_SET/cleared tracking — the upload path
just needs to read the current active snapshot from SQLite (reuse `get_persisted_dtc_state()`).
- DTC descriptions come directly from the ECU (python-obd's `GET_DTC` response) — they're the
standard SAE J2012 descriptions, no external lookup needed.
- Should we upload on every poll (every ~2s OBD cycle) or batch? Answer: batch. Upload alongside
tracking batches (every 30s idle, 1s catchup). DTCs don't change mid-drive that often.
- First upload after Pi reboot with no OBD connected: snapshot is empty → server marks all
previously-active codes as cleared. Is that desired? Probably not — should skip upload when
OBD is disconnected/unavailable (the Pi already knows this from `connection` state).
- **What**: Pi pushes current DTC snapshot to server via `POST /api/v1/dtc/batch`.
Server diffs against stored state — new codes inserted, existing updated, missing
codes auto-cleared. Portal has a new `/dtc` page showing code, description, MIL
state, active/cleared status, first/last seen timestamps, and manual delete.
Upload skips when OBD disconnected (avoids clearing server DTCs on Pi reboot).
Snapshot is hash-compared to avoid redundant uploads.
### FEAT-08 — Predictable IP on phone hotspot (DHCP client-id)
- **Status**: `SHIPPED` 2026-08-11 (Phase 22)