Refresh docs and reorder roadmap

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-10 18:48:59 -04:00
co-authored by Copilot
parent 457e3dcc08
commit d216aac97e
3 changed files with 425 additions and 386 deletions
+214 -209
View File
@@ -1398,89 +1398,9 @@ including two UX polish passes and multiple bug-fix rounds.
- **Dispatcher hook fix (`deploy/90-yaris-uplink`)** — previously ignored wlan1 events; now listens for both `wlan0` and `wlan1` so uplink transition recording remains accurate on current hardware.
- **README hardware photo** — added a current hardware build image (`pics/yaris-obd-hardware.jpg`) at the bottom of `readme.md`; saved without EXIF metadata and resized for lighter page load.
### Phase 25: Pre-install Yaris 2013 compatibility test — 🚧 PLANNED (core-system gate)
**Added 2026-08-09; intentionally immediately before the Android app.** Bench tests, synthetic
routes, attached-hardware tests, and stationary Pi runs cannot prove the complete system works
when temporarily connected to and powered by the actual 2013 Toyota Yaris. This phase is the
pre-installation acceptance gate for the complete core Pi/web/server system after Phase 24. It is
verification and targeted correction—not permanent installation or another redesign—and the
system is not considered ready to install until the results are recorded here. Permanent mounting,
cable routing, trim work, and final installation are explicitly outside the roadmap and will be
handled by the owner after this phase passes.
Prepare a temporary, removable test setup before driving:
- Connect the Pi, SIM868 HAT/antennas, USB OBD adapter, dashcam, and representative intended power
source without permanently mounting or routing anything. Secure every temporary component and
cable so it cannot interfere with pedals, steering, airbags, visibility, occupants, or loose-item
safety during the controlled drive; remove the setup after testing.
- Resolve the Phase 19/20 undervoltage/throttling warning before the test; confirm
`vcgencmd get_throttled` shows no current power fault at idle and under load with the temporary
representative power setup. This validates electrical compatibility, not final cable routing.
- Back up the Pi SQLite database and record baseline row/trip/video/upload counts on the Pi and
remote server. Use a uniquely timestamped test window so generated records can be reconciled
without deleting unrelated history.
- Verify the service, watchdog, storage mounts, hotspot, `http://yaris.local/`, alert engine, GPS
antenna view, dashcam recording mode, free space, and remote server health before moving.
- Use a second person for UI/log observations while the vehicle is moving, or record diagnostics
for review afterward. The driver must not operate or troubleshoot the UI while driving.
Run a controlled route containing ignition-off/on, stationary idle, low-speed streets, normal road
speed, several complete stops, turns, a short GPS-obstructed segment if safely available, and a
clean trip end:
- **OBD/ECU:** confirm the ELM327 reaches `CAR_CONNECTED`, record negotiated Toyota protocol and
adapter firmware, and verify real speed/RPM/coolant/fuel/throttle/MIL/DTC values. Compare wheel
speed against the instrument cluster at several safe speeds. Record unsupported PIDs (especially
odometer PID A6) as capabilities, not failures, and verify reconnect after ignition cycling and a
deliberate USB disconnect/reconnect.
- **GPS and motion logging:** confirm fixes while stationary and moving, route coordinates on the
offline map, OBD-speed-first motion detection, GPS displacement fallback when OBD is deliberately
unavailable, one-minute idle heartbeat behavior, and no false trip from stationary GPS drift.
- **Trip boundary/replay:** after the inactivity timeout, verify exactly one trip (or the expected
number for deliberate ignition gaps), correct start/end time and coordinates, plausible
distance/average/max speed, km/mi and km/h/mph values, route polyline, start/end markers, timeline
scrubber, play/pause marker position, and stopped portions of the route.
- **Local web UI:** test the final Phase 23 interface on the actual phone over the Pi/phone network at
`http://yaris.local/`; verify the explicit **In-car / Local Pi** identity, dashboard telemetry,
DTC page, trip history, dashcam library, settings, alerts, responsive layout, reconnect behavior,
and no new browser console/API errors.
- **Dashcam workflow:** create a known short real clip, switch the camera safely into USB storage
mode, detect/list it without confusing the OBD adapter, import only that selected file, verify
thumbnail/playback/Range seeking, preserve the source until checks finish, and confirm the camera
returns to recording afterward. Test deletion only with the disposable clip.
- **Alerts:** safely exercise conditions that can be simulated without damaging hardware (upload
backlog, temporary GPS loss while OBD reports motion, and disposable threshold overrides).
Confirm local bell transition/recovery/cooldown behavior and offline alert-state reconciliation
with the Phase 24 server after connectivity returns. Do not intentionally overheat, undervolt,
fill storage, or trigger a real MIL. With a real SIM, explicitly determine whether the SIM868
transport can reliably deliver ntfy notifications while WiFi/Ethernet is unavailable, including
authentication, timeout, retry, and recovery. If that cannot be made reliable on the actual
modem/network, remove the ntfy feature and its settings rather than retain a transport that fails
in the only situation where it is useful.
- **Power/reliability:** perform multiple normal ignition/power cycles, confirm automatic boot,
service/watchdog startup, SIM868 power-on, OBD/GPS recovery, no SQLite corruption, and no current
undervoltage/throttling. The deferred Phase 14 clean-shutdown limitation must be explicitly
accepted or resolved with representative delayed-power/ignition-sense hardware before declaring
the design ready for permanent installation.
Verify the complete remote path after connectivity returns:
- Confirm every test-window tracking row uploads exactly once and the selected dashcam clip resumes
or completes without corruption; compare counts, timestamps, hashes, and byte sizes locally and
remotely.
- On the Phase 24 portal, verify the explicit **Remote server** identity, authenticated
vehicle-scoped trip summary/replay, route and timeline agreement with the local Pi, archived clip
thumbnail/playback/Range seeking/download, synchronized alert history/current state without
duplicate transitions after an offline interval, and absence of local-only OBD/cellular/Pi
controls.
- The Android companion is intentionally outside this core acceptance gate and remains the final,
low-priority Phase 26. Its own hotspot/cellular binding, trip display, and disposable clip relay
require separate real-phone/in-car acceptance when that phase is implemented.
Acceptance requires a written result for every item, captured negotiated protocol/PID support,
measured trip/count/hash comparisons, relevant logs/screenshots, no unexplained tracebacks or data
loss, and cleanup of every disposable record/file while preserving the real drive. Any defect found
must be fixed, redeployed uncommitted, and the affected in-car test repeated before the final commit
and **ready-to-install** sign-off. Afterward, the owner handles permanent installation outside the
phase roadmap.
**Roadmap ordering note (2026-08-10):** the late phases are intentionally sequenced as
Phase 25.5a–25.5d (configuration/sync/status work) -> Phase 25 (real-car acceptance gate) ->
Phase 26 (Android, always last).
### Phase 25.5a: Device configuration sync backend — ✅ COMPLETE (connection-driven server sync)
**Server API and Pi sync thread implemented.** Server stores device config; Pi fetches when internet available and applies changes without restart.
@@ -1570,27 +1490,27 @@ updates its display-facing values without a manual restart once sync lands.
---
### Phase 25.5c: Multi-device provisioning + force-sync UX — ✅ COMPLETE
Extend the current single-device portal page into a real multi-device management
surface: register devices from the server, mint/rotate API keys, help the Pi
consume those credentials without SSH-heavy manual steps, and expose an honest
**force sync** action that works with the system's pull-based/offline-first
architecture.
### Phase 25.5c: Multi-device provisioning + force-sync UX — ✅ COMPLETE
Extend the current single-device portal page into a real multi-device management
surface: register devices from the server, mint/rotate API keys, help the Pi
consume those credentials without SSH-heavy manual steps, and expose an honest
**force sync** action that works with the system's pull-based/offline-first
architecture.
#### Goals
- **Multi-device server view**: stop assuming one `DEVICE_NAME` in the portal;
#### Goals
- **Multi-device server view**: stop assuming one `DEVICE_NAME` in the portal;
show all registered devices and let the user select which one is being edited
- **Device registration from the portal**: create a new device row and return a
- **Device registration from the portal**: create a new device row and return a
one-time raw API key directly in the browser UI (same behavior as
`server/scripts/create_device.py`, but web-managed)
- **Credential handoff to the Pi**: make it possible to put `YARIS_SERVER_URL`,
- **Credential handoff to the Pi**: make it possible to put `YARIS_SERVER_URL`,
`YARIS_DEVICE_NAME`, and `YARIS_API_KEY` onto the Pi from the app UI rather
than editing `db/upload_creds.env` manually over SSH
- **Force sync button**: let the user request an immediate config pull without
- **Force sync button**: let the user request an immediate config pull without
pretending the cloud server can magically push into an offline car
#### Proposed implementation
1. **Server: device management model + portal**
#### Proposed implementation
1. **Server: device management model + portal**
- Add a device list page/card in the remote portal, with:
- registered devices table
- "Add device" action
@@ -1605,7 +1525,7 @@ updates its display-facing values without a manual restart once sync lands.
- never store raw keys server-side
- only store hash as today
2. **Server: browser APIs**
2. **Server: browser APIs**
- Add login-protected endpoints for:
- list devices
- create device + return one-time API key
@@ -1615,7 +1535,7 @@ updates its display-facing values without a manual restart once sync lands.
- Device settings UI should populate from the selected device row, not from a
single hardcoded portal `DEVICE_NAME`
3. **Pi: credential management UI**
3. **Pi: credential management UI**
- Add a local Settings section for remote server credentials:
- server URL
- device name
@@ -1628,7 +1548,7 @@ updates its display-facing values without a manual restart once sync lands.
- reload upload client / sync client config
- allow enabling **Sync with server** only when creds test cleanly
4. **Force sync semantics**
4. **Force sync semantics**
- **Constraint**: the server cannot directly reach a Pi behind hotspot/NAT,
and the car is often offline. So a server-side force sync button cannot be
a true push.
@@ -1641,44 +1561,44 @@ updates its display-facing values without a manual restart once sync lands.
- from server: "sync on next online contact"
- from Pi: "sync immediately if internet is currently available"
#### Data model changes
- Add server columns such as:
#### Data model changes
- Add server columns such as:
- `last_config_sync_at`
- `last_config_sync_version`
- `force_sync_requested_at`
- `api_key_rotated_at`
- Optionally add a lightweight pairing/claim model later if copying raw API keys
- Optionally add a lightweight pairing/claim model later if copying raw API keys
manually into the Pi UI feels too clunky
#### UI/UX shape
- **Remote portal**
#### UI/UX shape
- **Remote portal**
- left: device list / selector
- right: settings form for selected device
- actions: Add device, Rotate key, Copy creds, Request sync
- status badges: last seen, last config version, last sync requested, maybe
last successful contact
- **Pi local app**
- **Pi local app**
- server credentials card in Settings
- Test connection button
- Sync now button
- clear warning when local edits are locked because server sync is enabled
#### Security / operational notes
- Never display a stored API key after the creation/rotation response is gone
- Keep key rotation explicit; rotating a key should warn that the old Pi config
#### Security / operational notes
- Never display a stored API key after the creation/rotation response is gone
- Keep key rotation explicit; rotating a key should warn that the old Pi config
will stop authenticating until updated
- Avoid logging raw keys in Flask logs, JS errors, or shell output captured in
- Avoid logging raw keys in Flask logs, JS errors, or shell output captured in
committed docs
- Because the Pi is usually offline, all flows must degrade gracefully when the
- Because the Pi is usually offline, all flows must degrade gracefully when the
remote portal requests work the Pi cannot act on yet
#### Suggested execution order
1. Server multi-device DB/API refactor
2. Remote portal device list + register/rotate key UI
3. Pi local server-credentials UI + connection test
4. Pi/local immediate **Sync now** button
5. Server "request sync" flag + per-device status indicators
6. End-to-end validation:
#### Suggested execution order
1. Server multi-device DB/API refactor
2. Remote portal device list + register/rotate key UI
3. Pi local server-credentials UI + connection test
4. Pi/local immediate **Sync now** button
5. Server "request sync" flag + per-device status indicators
6. End-to-end validation:
- create device on server
- copy/paste creds into Pi UI
- test connection
@@ -1687,87 +1607,87 @@ updates its display-facing values without a manual restart once sync lands.
- verify Pi receives it
- rotate key and verify old key stops working until Pi updated
#### Open constraint to keep in mind
- If you want **zero manual copy/paste** for API key provisioning, that becomes
#### Open constraint to keep in mind
- If you want **zero manual copy/paste** for API key provisioning, that becomes
a pairing problem (claim code / QR / short-lived token). That is doable, but
it is a bigger scope than simply exposing key create/copy fields in the two
existing UIs. My recommendation is to ship copy/paste provisioning first, then
add a pairing flow only if it still feels too annoying.
#### Implementation progress (2026-08-10)
- **Server-side multi-device API/UI**
#### Implementation progress (2026-08-10)
- **Server-side multi-device API/UI**
- Added device listing, create-device, rotate-key, per-device config, and
request-sync endpoints in the remote portal
- Reworked `/settings/device` into a multi-device management page with a
registered-device list, selected-device editor, one-time API-key reveal, and
copy-credentials helper
- **Server schema**
- **Server schema**
- Added Phase 25.5c migration for `last_config_sync_at`,
`last_config_sync_version`, `force_sync_requested_at`, and
`api_key_rotated_at`
- Added device sync acknowledgement endpoint so the Pi can report when a
config version was actually applied
- **Pi-side provisioning**
- **Pi-side provisioning**
- Added local Settings UI/API for `YARIS_SERVER_URL`, `YARIS_DEVICE_NAME`, and
`YARIS_API_KEY`
- Added **Test connection** and **Sync now** actions in the Pi app
- Reused the existing gitignored `db/upload_creds.env` store instead of
inventing a second credential location
- **Runtime behavior fixes**
- **Runtime behavior fixes**
- Upload client now reads credentials dynamically instead of only at import
- Device sync thread can restart after credentials are added later
- Enabling **Sync with server** now validates that remote credentials exist
#### Validation
- Local tests expanded to `72 passed`
- Deployed Phase 25.5c to unraid and applied `migrate_phase25_5c.sql`
- Deployed Phase 25.5c to the Pi at `192.168.50.213`; `yaris-obd.service`
#### Validation
- Local tests expanded to `72 passed`
- Deployed Phase 25.5c to unraid and applied `migrate_phase25_5c.sql`
- Deployed Phase 25.5c to the Pi at `192.168.50.213`; `yaris-obd.service`
restarted cleanly
- Pi local API checks:
- Pi local API checks:
- `GET /api/remote-config` shows the saved remote server credentials state
- `POST /api/remote-config/test` succeeds with authenticated device creds
- `POST /api/remote-config/sync-now` succeeds (`no_change` when already current)
- Server sync-ack metadata verified live: `last_config_sync_version` matched the
- Server sync-ack metadata verified live: `last_config_sync_version` matched the
current `config_version` for `yaris-2013`, and `last_config_sync_at` populated
- Temporary create/delete validation of the new device-provisioning helper
- Temporary create/delete validation of the new device-provisioning helper
succeeded on the live server (`phase25c-validate-temp`)
#### Remaining polish ideas (not blockers)
- Exercise the authenticated browser flow for create/rotate/request-sync in the
#### Remaining polish ideas (not blockers)
- Exercise the authenticated browser flow for create/rotate/request-sync in the
live portal UI itself, not just the backing APIs/helpers
- If copy/paste provisioning feels too clunky in practice, add a later QR /
- If copy/paste provisioning feels too clunky in practice, add a later QR /
claim-code pairing flow
- Main portal overview/history/dashcam pages are **not** multi-device yet:
- Main portal overview/history/dashcam pages are **not** multi-device yet:
the data model already separates rows by `device_id`, but those pages still
read from the single configured `DEVICE_NAME`. Today that means the new
multi-device management page can manage several devices, but the normal trip
history view still shows one configured car/device at a time until a later
portal-wide device selector/filter is implemented.
### Phase 25.5d: GPS dashboard visibility / live-status debugging — 🚧 IN PROGRESS
- Add an explicit Pi dashboard GPS status indicator (thread alive / live fix / stale fix / no fix)
- Expose a local `/api/gps/status` endpoint so GPS health can be checked without relying on
### Phase 25.5d: GPS dashboard visibility / live-status debugging — ✅ COMPLETE
- Add an explicit Pi dashboard GPS status indicator (thread alive / live fix / stale fix / no fix)
- Expose a local `/api/gps/status` endpoint so GPS health can be checked without relying on
socket events alone
- Verify on the real Pi whether GPS is actually still producing fixes or whether the issue is
- Verify on the real Pi whether GPS is actually still producing fixes or whether the issue is
now hardware/module/fix availability rather than a front-end live-update regression
- **Live Pi check 2026-08-10**: after deploying the new endpoint, the real Pi reported
- **Live Pi check 2026-08-10**: after deploying the new endpoint, the real Pi reported
`thread_alive=true` but `has_fix=false`, `latitude=null`, `longitude=null`, and
`fix_age_seconds=null`. That means the local app/socket path is still running, but there is
currently no GNSS fix to stream into the dashboard.
- **Follow-up recheck 2026-08-10**: the Pi later recovered a live fix without further code
- **Follow-up recheck 2026-08-10**: the Pi later recovered a live fix without further code
changes. `/api/gps/status` returned `thread_alive=true`, `has_fix=true`,
`latitude=45.544198`, `longitude=-73.536626`, and a fresh `fix_age_seconds` (~2.7s), which
confirms the live-update path still works and the earlier issue was temporary lack of GNSS fix,
not a dashboard/socket regression.
- **Dashboard fit polish 2026-08-10**: tightened the Pi dashboard layout so the GPS and Overview
- **Dashboard fit polish 2026-08-10**: tightened the Pi dashboard layout so the GPS and Overview
cards no longer stretch to the speed graph's full height, shortened the speed graph on
Pi-sized screens, and reduced excess map/footer whitespace that previously forced unnecessary
scrolling.
- **Next local UX follow-up 2026-08-10**: split the bulky Pi-health block out of the dashboard
- **Next local UX follow-up 2026-08-10**: split the bulky Pi-health block out of the dashboard
into a dedicated local **Status** page and expand it into a broader runtime diagnostics view:
local `systemd` service states, server reachability/auth status, Wi-Fi uplink state, GPS fix
state, SIM868/cellular-HAT responsiveness, upload backlog, and OBD adapter presence.
- **Implemented + verified 2026-08-10**: added a new local `/status` page plus `/api/status`
- **Implemented + verified 2026-08-10**: added a new local `/status` page plus `/api/status`
aggregator. The dashboard no longer carries the bulky Pi-health block; instead the new page
shows Pi health, remote-server reachability/auth, upload backlog, Wi-Fi/uplink mode, GPS fix
status, SIM868 UART/modem responsiveness, OBD presence, and key `systemd` units
@@ -1776,10 +1696,10 @@ updates its display-facing values without a manual restart once sync lands.
page/API served live values: server reachable+authenticated, uplink on Wi-Fi with internet,
GPS thread alive with live fix, SIM868 responding on UART, OBD adapter present, and the local
services reporting expected active/inactive states.
- **Next status-page polish 2026-08-10**: add quick action buttons directly on `/status` for
- **Next status-page polish 2026-08-10**: add quick action buttons directly on `/status` for
server test, sync-now, uplink reconnect, and app-service restart so the Pi can be diagnosed and
nudged from one screen instead of bouncing back to Settings/SSH.
- **Implemented + verified 2026-08-10**: `/status` now includes quick actions for server test,
- **Implemented + verified 2026-08-10**: `/status` now includes quick actions for server test,
sync-now, uplink reconnect, and app restart. The first three reuse the existing local APIs; app
restart adds a minimal sudo-backed `systemctl restart --no-block yaris-obd.service` path,
installed by `deploy/install_service.sh` via `/etc/sudoers.d/yaris-status-actions`. Verified on
@@ -1789,91 +1709,176 @@ updates its display-facing values without a manual restart once sync lands.
returned to `active` and `/api/status` recovered with live values. The uplink-reconnect button
was left unclicked during verification because this Pi currently has a saved phone hotspot and
the action intentionally may switch networks/drop the active SSH session.
- **Dashboard card order tweak 2026-08-10**: swapped the local dashboard layout so the Overview
- **Dashboard card order tweak 2026-08-10**: swapped the local dashboard layout so the Overview
card now appears before the Live GPS map. Deployed to the real Pi and confirmed the rendered
`/` HTML places `Overview` before `Live GPS`.
---
---
### Phase 25: Pre-install Yaris 2013 compatibility test — 🚧 PLANNED (core-system gate)
**Added 2026-08-09; intentionally immediately before the Android app.** Bench tests, synthetic
routes, attached-hardware tests, and stationary Pi runs cannot prove the complete system works
when temporarily connected to and powered by the actual 2013 Toyota Yaris. This phase is the
pre-installation acceptance gate for the complete core Pi/web/server system after Phase 24. It is
verification and targeted correction—not permanent installation or another redesign—and the
system is not considered ready to install until the results are recorded here. Permanent mounting,
cable routing, trim work, and final installation are explicitly outside the roadmap and will be
handled by the owner after this phase passes.
### Phase 26: Native Android companion app — 🚧 PLANNED (low priority, always last)
Revives the phone-relay goal (Phase 11/12) properly this time, using a real native app instead of
a web page - the only way to actually hold a WiFi connection to the Pi's hotspot and a cellular
connection to the internet open simultaneously (`ConnectivityManager.bindProcessToNetwork()`,
Prepare a temporary, removable test setup before driving:
- Connect the Pi, SIM868 HAT/antennas, USB OBD adapter, dashcam, and representative intended power
source without permanently mounting or routing anything. Secure every temporary component and
cable so it cannot interfere with pedals, steering, airbags, visibility, occupants, or loose-item
safety during the controlled drive; remove the setup after testing.
- Resolve the Phase 19/20 undervoltage/throttling warning before the test; confirm
`vcgencmd get_throttled` shows no current power fault at idle and under load with the temporary
representative power setup. This validates electrical compatibility, not final cable routing.
- Back up the Pi SQLite database and record baseline row/trip/video/upload counts on the Pi and
remote server. Use a uniquely timestamped test window so generated records can be reconciled
without deleting unrelated history.
- Verify the service, watchdog, storage mounts, hotspot, `http://yaris.local/`, alert engine, GPS
antenna view, dashcam recording mode, free space, and remote server health before moving.
- Use a second person for UI/log observations while the vehicle is moving, or record diagnostics
for review afterward. The driver must not operate or troubleshoot the UI while driving.
Run a controlled route containing ignition-off/on, stationary idle, low-speed streets, normal road
speed, several complete stops, turns, a short GPS-obstructed segment if safely available, and a
clean trip end:
- **OBD/ECU:** confirm the ELM327 reaches `CAR_CONNECTED`, record negotiated Toyota protocol and
adapter firmware, and verify real speed/RPM/coolant/fuel/throttle/MIL/DTC values. Compare wheel
speed against the instrument cluster at several safe speeds. Record unsupported PIDs (especially
odometer PID A6) as capabilities, not failures, and verify reconnect after ignition cycling and a
deliberate USB disconnect/reconnect.
- **GPS and motion logging:** confirm fixes while stationary and moving, route coordinates on the
offline map, OBD-speed-first motion detection, GPS displacement fallback when OBD is deliberately
unavailable, one-minute idle heartbeat behavior, and no false trip from stationary GPS drift.
- **Trip boundary/replay:** after the inactivity timeout, verify exactly one trip (or the expected
number for deliberate ignition gaps), correct start/end time and coordinates, plausible
distance/average/max speed, km/mi and km/h/mph values, route polyline, start/end markers, timeline
scrubber, play/pause marker position, and stopped portions of the route.
- **Local web UI:** test the final Phase 23 interface on the actual phone over the Pi/phone network at
`http://yaris.local/`; verify the explicit **In-car / Local Pi** identity, dashboard telemetry,
DTC page, trip history, dashcam library, settings, alerts, responsive layout, reconnect behavior,
and no new browser console/API errors.
- **Dashcam workflow:** create a known short real clip, switch the camera safely into USB storage
mode, detect/list it without confusing the OBD adapter, import only that selected file, verify
thumbnail/playback/Range seeking, preserve the source until checks finish, and confirm the camera
returns to recording afterward. Test deletion only with the disposable clip.
- **Alerts:** safely exercise conditions that can be simulated without damaging hardware (upload
backlog, temporary GPS loss while OBD reports motion, and disposable threshold overrides).
Confirm local bell transition/recovery/cooldown behavior and offline alert-state reconciliation
with the Phase 24 server after connectivity returns. Do not intentionally overheat, undervolt,
fill storage, or trigger a real MIL. With a real SIM, explicitly determine whether the SIM868
transport can reliably deliver ntfy notifications while WiFi/Ethernet is unavailable, including
authentication, timeout, retry, and recovery. If that cannot be made reliable on the actual
modem/network, remove the ntfy feature and its settings rather than retain a transport that fails
in the only situation where it is useful.
- **Power/reliability:** perform multiple normal ignition/power cycles, confirm automatic boot,
service/watchdog startup, SIM868 power-on, OBD/GPS recovery, no SQLite corruption, and no current
undervoltage/throttling. The deferred Phase 14 clean-shutdown limitation must be explicitly
accepted or resolved with representative delayed-power/ignition-sense hardware before declaring
the design ready for permanent installation.
Verify the complete remote path after connectivity returns:
- Confirm every test-window tracking row uploads exactly once and the selected dashcam clip resumes
or completes without corruption; compare counts, timestamps, hashes, and byte sizes locally and
remotely.
- On the Phase 24 portal, verify the explicit **Remote server** identity, authenticated
vehicle-scoped trip summary/replay, route and timeline agreement with the local Pi, archived clip
thumbnail/playback/Range seeking/download, synchronized alert history/current state without
duplicate transitions after an offline interval, and absence of local-only OBD/cellular/Pi
controls.
- The Android companion is intentionally outside this core acceptance gate and remains the final,
low-priority Phase 26. Its own hotspot/cellular binding, trip display, and disposable clip relay
require separate real-phone/in-car acceptance when that phase is implemented.
Acceptance requires a written result for every item, captured negotiated protocol/PID support,
measured trip/count/hash comparisons, relevant logs/screenshots, no unexplained tracebacks or data
loss, and cleanup of every disposable record/file while preserving the real drive. Any defect found
must be fixed, redeployed uncommitted, and the affected in-car test repeated before the final commit
and **ready-to-install** sign-off. Afterward, the owner handles permanent installation outside the
phase roadmap.
---
### Phase 26: Native Android companion app — 🚧 PLANNED (low priority, always last)
Revives the phone-relay goal (Phase 11/12) properly this time, using a real native app instead of
a web page - the only way to actually hold a WiFi connection to the Pi's hotspot and a cellular
connection to the internet open simultaneously (`ConnectivityManager.bindProcessToNetwork()`,
unavailable to browser JS - see the Phase 11 update). Explicitly **not urgent** and intentionally
kept at the end of the roadmap, after all web/server work and the Phase 25 core in-car acceptance
test are done. Permanent vehicle installation remains owner-handled and is not an implementation
phase. Scope, at a high level (not yet designed in detail):
- **Dashcam file transfer**: browse/select/delete dashcam clips already imported to the Pi (same
data as the Phase 17 local video browser, just a native client instead of/alongside the web UI),
download them to the phone, and upload to the remote server over cellular while the phone stays
on the Pi's hotspot for the download leg - the true simultaneous dual-network relay the web
version could never deliver.
data as the Phase 17 local video browser, just a native client instead of/alongside the web UI),
download them to the phone, and upload to the remote server over cellular while the phone stays
on the Pi's hotspot for the download leg - the true simultaneous dual-network relay the web
version could never deliver.
- **Trip/location info**: surface live GPS position, speed, and trip history (Phase 18) pulled
from the Pi while connected to its hotspot (reusing the existing `/api/gps`-style data feeds),
so the app is useful as a real "check on the car" companion, not just a file-transfer tool.
from the Pi while connected to its hotspot (reusing the existing `/api/gps`-style data feeds),
so the app is useful as a real "check on the car" companion, not just a file-transfer tool.
- Not yet scoped: distribution (sideload vs Play Store), whether it talks to the Pi directly over
the hotspot only or also to the remote server directly for trip history when away from the car,
and how much of the existing REST API surface can be reused as-is vs needs new
mobile-friendly endpoints. To be designed once Phases 12-24 (web-side cleanup + hardening,
redesign, remote portal, and core in-car acceptance) are done.
the hotspot only or also to the remote server directly for trip history when away from the car,
and how much of the existing REST API surface can be reused as-is vs needs new
mobile-friendly endpoints. To be designed once Phases 12-24 (web-side cleanup + hardening,
redesign, remote portal, and core in-car acceptance) are done.
## Working Notes / Environment Gotchas
- **Pi credentials/SSH**: `ssh yaris@<pi-ip>`. Confirm current IP before connecting (varies by
network — check locally, not recorded here) — usually reachable via `yaris.local` too (mDNS,
see Phase 6). **Current verified LAN IP (2026-08-10): `192.168.50.213`** after the TP-Link
Archer T3U WiFi change.
network — check locally, not recorded here) — usually reachable via `yaris.local` too (mDNS,
see Phase 6). **Current verified LAN IP (2026-08-10): `192.168.50.213`** after the TP-Link
Archer T3U WiFi change.
- **Local dev repo**: work happens on a separate dev machine, not on the Pi. Push to
`origin/shameless-aislop`, the repo's current default branch (repo is public).
`origin/shameless-aislop`, the repo's current default branch (repo is public).
- **unraid server SSH**: `ssh [email protected]` (alias `unraid`). Real git checkout lives at
`/mnt/user/app/git/yaris-obd` (not `/mnt/user/appdata/...`, which is only the docker-managed data
volume for the `db`/`dashcam` containers). **Re-verified 2026-08-10**: alias works and lands in
`/root`. Deploy with
`cd /mnt/user/app/git/yaris-obd && git pull --ff-only && cd server && docker compose up -d --build`.
Container names: `yaris-obd-db` (Postgres 17), `yaris-obd-api`. For existing databases, also run
the mounted migration scripts manually, e.g.
`docker compose exec db psql -U yarisobd -d yarisobd -f /docker-entrypoint-initdb.d/migrate_phase25_5a.sql`.
**Important 2026-08-10**: this checkout had a long-diverged local `shameless-aislop`
(`ahead 136, behind 146`), so `git pull --ff-only` failed. To avoid destroying local history, the
Phase 25.5a deploy was performed from a fresh temporary clone while reusing the same `.env` and
Docker data. Reconcile or replace the old checkout before relying on it for future ff-only deploys.
`/mnt/user/app/git/yaris-obd` (not `/mnt/user/appdata/...`, which is only the docker-managed data
volume for the `db`/`dashcam` containers). **Re-verified 2026-08-10**: alias works and lands in
`/root`. Deploy with
`cd /mnt/user/app/git/yaris-obd && git pull --ff-only && cd server && docker compose up -d --build`.
Container names: `yaris-obd-db` (Postgres 17), `yaris-obd-api`. For existing databases, also run
the mounted migration scripts manually, e.g.
`docker compose exec db psql -U yarisobd -d yarisobd -f /docker-entrypoint-initdb.d/migrate_phase25_5a.sql`.
**Important 2026-08-10**: this checkout had a long-diverged local `shameless-aislop`
(`ahead 136, behind 146`), so `git pull --ff-only` failed. To avoid destroying local history, the
Phase 25.5a deploy was performed from a fresh temporary clone while reusing the same `.env` and
Docker data. Reconcile or replace the old checkout before relying on it for future ff-only deploys.
- **Pi deploy loop**: `ssh yaris@<pi-ip>` (alias `yarisobd`) ->
`cd ~/app/yaris-obd && git pull --ff-only` -> `sudo systemctl restart yaris-obd.service`
(passwordless sudo confirmed via `yaris ALL=(ALL) NOPASSWD: ALL`). App stdout/stderr goes to
`~/app/yaris-obd/yaris-obd.log` (not journald - the systemd unit redirects both streams there),
so use `tail`/`grep` on that file, not `journalctl`, to see application-level prints (upload
progress, OBD/GNSS errors, etc.) - `journalctl -u yaris-obd.service` only shows service
start/stop/crash lifecycle events. **Update 2026-08-09**: plain `print()` calls were silently
invisible in that log for months because Python fully-buffers stdout when it's not a TTY (a
file, in this case) — `logging`-based lines (e.g. the `obd` library's own logger, which uses
stderr) showed up fine, but bare `print()` diagnostics from `app_gps.py` never did. Fixed by
adding `Environment=PYTHONUNBUFFERED=1` to the systemd unit
(`deploy/yaris-obd.service`) — **remember to also copy the updated unit file to
`/etc/systemd/system/yaris-obd.service` and run `sudo systemctl daemon-reload`** on the Pi, a
plain `git pull` + restart does NOT pick up systemd unit file changes (the live
`/etc/systemd/system/` copy is independent of the repo's `deploy/` copy).
`cd ~/app/yaris-obd && git pull --ff-only` -> `sudo systemctl restart yaris-obd.service`
(passwordless sudo confirmed via `yaris ALL=(ALL) NOPASSWD: ALL`). App stdout/stderr goes to
`~/app/yaris-obd/yaris-obd.log` (not journald - the systemd unit redirects both streams there),
so use `tail`/`grep` on that file, not `journalctl`, to see application-level prints (upload
progress, OBD/GNSS errors, etc.) - `journalctl -u yaris-obd.service` only shows service
start/stop/crash lifecycle events. **Update 2026-08-09**: plain `print()` calls were silently
invisible in that log for months because Python fully-buffers stdout when it's not a TTY (a
file, in this case) — `logging`-based lines (e.g. the `obd` library's own logger, which uses
stderr) showed up fine, but bare `print()` diagnostics from `app_gps.py` never did. Fixed by
adding `Environment=PYTHONUNBUFFERED=1` to the systemd unit
(`deploy/yaris-obd.service`) — **remember to also copy the updated unit file to
`/etc/systemd/system/yaris-obd.service` and run `sudo systemctl daemon-reload`** on the Pi, a
plain `git pull` + restart does NOT pick up systemd unit file changes (the live
`/etc/systemd/system/` copy is independent of the repo's `deploy/` copy).
- **Pi deploy loop, requirements.txt changes**: after any `requirements.txt` change (e.g. adding
`RPi.GPIO`), also run `~/app/yaris-obd/venv/bin/pip install -r requirements.txt` on the Pi — a
`git pull` alone doesn't install new dependencies into the venv.
`RPi.GPIO`), also run `~/app/yaris-obd/venv/bin/pip install -r requirements.txt` on the Pi — a
`git pull` alone doesn't install new dependencies into the venv.
- **Pi memory budget**: ~900MB usable RAM, no swap. Any code path that might buffer a whole
dashcam-sized file (hundreds of MB) in memory is unsafe on this hardware - stream, don't buffer
(see Phase 10).
dashcam-sized file (hundreds of MB) in memory is unsafe on this hardware - stream, don't buffer
(see Phase 10).
- **Git push auth**: if `git push` fails with a credential/auth error, run `gh auth login`
(device-code browser flow) then `gh auth setup-git` to wire git to use the `gh` CLI's stored
credentials — this fixed push auth issues in this environment without needing SSH keys or PATs.
(device-code browser flow) then `gh auth setup-git` to wire git to use the `gh` CLI's stored
credentials — this fixed push auth issues in this environment without needing SSH keys or PATs.
- **Pi environment gap**: Debian Bookworm blocks system-wide `pip install` (PEP 668,
"externally-managed-environment"). Use a venv for the real app dependency install (Phase 5)
rather than `--break-system-packages` everywhere. (Resolved in Phase 5 — venv at
`/home/yaris/app/yaris-obd/venv`.)
"externally-managed-environment"). Use a venv for the real app dependency install (Phase 5)
rather than `--break-system-packages` everywhere. (Resolved in Phase 5 — venv at
`/home/yaris/app/yaris-obd/venv`.)
- **Pi → GitHub auth for `git pull` (private repo)**: the Pi's pre-existing
`~/.ssh/id_ed25519.pub` is an *inbound* key only (private half is on the dev machine) — can't
be reused for the Pi's own outbound git auth. Generated a dedicated keypair on the Pi
(`~/.ssh/yaris_deploy_key`), added it as a GitHub **read-only deploy key** on this repo, and
added an `~/.ssh/config` entry pinning `github.com` to that key. `[email protected]:...` (SSH
remote) then works standalone on the Pi, no PAT/interactive login needed.
`~/.ssh/id_ed25519.pub` is an *inbound* key only (private half is on the dev machine) — can't
be reused for the Pi's own outbound git auth. Generated a dedicated keypair on the Pi
(`~/.ssh/yaris_deploy_key`), added it as a GitHub **read-only deploy key** on this repo, and
added an `~/.ssh/config` entry pinning `github.com` to that key. `[email protected]:...` (SSH
remote) then works standalone on the Pi, no PAT/interactive login needed.
- **piwheels**: pip on the Pi automatically uses `piwheels.org` as an extra index (Raspberry Pi
OS default `pip.conf`) — this is why `gevent` (which normally needs a C compile) installed in
seconds from a prebuilt ARM wheel instead of taking many minutes to build from source.
OS default `pip.conf`) — this is why `gevent` (which normally needs a C compile) installed in
seconds from a prebuilt ARM wheel instead of taking many minutes to build from source.
- **python-obd note**: `obd.OBD()` (or with `portstr=`) performs the ELM327 handshake at
connection time; if there's no car on the other end, it raises rather than silently returning
a dead handle — `app_obd.py`'s `connect_obd()` already catches this and sets `connection = None`.
connection time; if there's no car on the other end, it raises rather than silently returning
a dead handle — `app_obd.py`'s `connect_obd()` already catches this and sets `connection = None`.
+34 -15
View File
@@ -59,8 +59,8 @@ Dashboard — mobile (390 px) and desktop (1280 px):
trip history with OSM/CARTO live map and route replay, dashcam archive browsing with inline
video player and Range-request seeking, overview stats dashboard, delete buttons for trips and
clips, and a device-settings page for editing the synced owner/car/avatar values that the Pi
pulls from the server. The portal can register multiple devices, reveal a one-time API key at
creation/rotation time, and request a sync for a selected device. When the Pi has
pulls from the server. The portal can register/delete multiple devices, reveal a one-time API
key at creation/rotation time, and request a sync for a selected device. When the Pi has
**Sync with server** enabled, those identity changes hot-reload into the running local UI on the
next successful sync instead of waiting for a manual restart. Shared `history.js` code runs on
both the Pi and the server — tile source and zoom limits are configured per-deployment via HTML
@@ -71,8 +71,9 @@ Dashboard — mobile (390 px) and desktop (1280 px):
diagnostics, plus quick actions for server test, sync-now, uplink reconnect, and app restart
- 🗄️ **Motion-aware local-first SQLite logging** — five-second motion checks without five-second
parked writes, plus retry-safe remote sync and automatic backoff
- 🌡️ **Pi health monitoring** — dashboard temperature, RAM, disk space, uptime, and explicit
undervoltage/throttling warnings from the Raspberry Pi firmware
- 🌡️ **Pi health + diagnostics** — a dedicated local Status page shows temperature, RAM, disk,
uptime, undervoltage/throttling warnings, service state, uplink/server reachability, GPS state,
OBD presence, and safe quick actions
- 🔔 **Offline-first alerts** — a notification bell on every page reports check-engine state,
storage pressure, upload backlog, temperature/power faults, and GPS loss while moving. Optional
ntfy delivery is available but remains disabled until configured
@@ -122,7 +123,14 @@ For the remote server (receives trip logs + dashcam uploads), see [server/README
## Customizing device identity
The Pi displays a username, car model, and avatar image. All customizable values are stored in `config/device.json` (no code edits needed).
The Pi displays an owner name, car model, device name, and avatar. You can manage those values in
two ways:
- **Locally on the Pi** through the Settings page
- **From the server portal** when **Sync with server** is enabled on the Pi
The local fallback/default source remains [config/device.json](./config/device.json), so the car
still boots with sensible identity values even when fully offline.
### Edit device configuration
@@ -141,10 +149,10 @@ Edit [config/device.json](./config/device.json):
|-------|----------|---------|
| `owner_name` | Top-right of app topbar | Your name/username |
| `car_model` | Left sidebar (next to car icon) | Vehicle model/year |
| `device_name` | (Reserved) | Future server-side use in Phase 25.5 |
| `device_name` | Local settings / remote portal | Device identity used by server sync/provisioning |
| `avatar_path` | Top-right avatar circle | Profile picture (relative to `static/`) |
After editing, restart the app:
If you edit the JSON file directly, restart the app:
```bash
# If running as a systemd service:
@@ -187,15 +195,26 @@ rm db/sql/quoije_Yaris2013.db
## Project status
Phases 1–24.7 are complete. The system is fully built and audited, ready for the Phase 25 real-vehicle test:
The build is now at the endgame stage: the Pi app, remote server, and device-management/sync work
are implemented and verified on the real Pi/unraid setup. The remaining roadmap phases are the
temporary real-vehicle acceptance test and the low-priority Android companion.
- **Phases 1–23.5**: core Pi system — GPS, OBD, dashcam, trip history, web UI redesign, WiFi upgrade, cellular fallback, alerts, settings, upload
- **Phase 24**: remote web portal at `https://yaris.ceaf.dev/` — login-protected, trip history with CARTO live map, dashcam archive, delete, overview stats
- **Phase 24.5**: full code audit — 6 bugs fixed across Pi modules and server, 63 tests passing
- **Phase 24.6**: Yaris 2013 OBD compatibility hardening — `_decode_odometer` no longer kills the OBD session on unsupported PIDs, sensor globals reset on reconnect, `OBD_SKIP_VOLTAGE_CHECK` escape hatch for clone ELM327 adapters
- **Phase 24.7**: diagnostic script (`scripts/diagnose.sh`), map tile switch to CARTO, zoom limit fix
- **Phase 24.8**: deployment/readme cleanup and uplink interface consistency
- **Phases 25.5a–25.5d**: offline-first device identity sync, remote portal device settings,
Pi-side hot-reload, multi-device provisioning, one-time API key rotation/registration, local
Settings UI for remote credentials, Sync now / Test connection, GPS live-status verification,
dedicated local Status page, and status-page quick actions
- **Current known limitation**: the remote portal's device-management page is multi-device, but the
main remote overview/history/dashcam browsing views still follow one configured `DEVICE_NAME`
until a later portal-wide device selector/filter is added
**Next**: Phase 25 — temporary real-vehicle test in the 2013 Yaris (see PLAN.md for the full acceptance checklist). Phase 26 (Android companion app) follows after that.
**Next**: Phase 25 — temporary real-vehicle test in the 2013 Yaris (see [PLAN.md](./PLAN.md) for
the full acceptance checklist). Phase 26 (Android companion app) follows after that.
Phase 14 is partially complete: the Pi's hardware watchdog is deployed and reboot-verified.
Read-only-root storage hardening is deferred until persistent storage is moved off the SD root
@@ -205,12 +224,12 @@ The browser-based phone relay was removed after real-world testing confirmed tha
cannot reliably use the Pi's WiFi and cellular data simultaneously. A native Android companion is
planned later instead (Phase 26).
The local Web UI includes a Settings page for safe runtime configuration: dashcam uploads,
uploaded-video retention, cellular APN fallback, optional ntfy delivery, and alert thresholds.
Notification secrets are write-only in the browser API, and network credentials that could lock
the Pi out remain SSH/file-managed. When cellular is configured, small ntfy notifications use the
SIM868 first and WiFi/Ethernet only as fallback; that modem path is implemented and mock-tested but
still needs real-SIM/carrier verification.
The local web UI includes a Settings page for safe runtime configuration: dashcam uploads,
uploaded-video retention, cellular APN fallback, optional ntfy delivery, alert thresholds, local
device identity, and remote server credentials/API key. Notification secrets remain write-only in
the browser API. When cellular is configured, small ntfy notifications use the SIM868 first and
WiFi/Ethernet only as fallback; that modem path is implemented and mock-tested but still needs
real-SIM/carrier verification.
The Pi can also join a configured phone hotspot for outbound internet when no SIM is available,
including trip synchronization and WiFi dashcam uploads, with automatic recovery to home WiFi or
+33 -18
View File
@@ -21,6 +21,13 @@ docker-compose stack on an unraid server (or any Docker host).
staged on disk and reassembled + sha256-verified once all have arrived, then
recorded the same way as the single-shot endpoint. This is what the Pi
actually uses for real dashcam files — see "Why chunked uploads" below.
- `GET /api/device-config` — device-authenticated config fetch used by the Pi's
identity-sync thread and local **Sync now** action.
- `POST /api/v1/device-config/ack` — device-authenticated config-sync
acknowledgement so the server can record the last applied version/time.
- `/settings/device` + `/api/v1/portal/devices*` — login-protected multi-device
management UI/APIs for create, delete, rotate key, edit config, and request
sync.
Both `/api/v1/tracking/batch` and the dashcam upload endpoints require two
headers: `X-Device-Name` and `X-Api-Key` (see
@@ -28,13 +35,11 @@ headers: `X-Device-Name` and `X-Api-Key` (see
Postgres is **not** published to the LAN — only the `api` container talks to
it, over the internal compose network.
**CORS is enabled on every response** (`Access-Control-Allow-Origin: *`) so a
device's registered API key can be used directly from browser JS running on a
*different* origin — specifically, the Pi's phone-relay feature (see
[PLAN.md](../PLAN.md) Phase 11), where a phone's browser calls this API
straight from a page served by the Pi itself. This is safe here because
auth is header-based (`X-Device-Name`/`X-Api-Key`), not cookie/session-based,
so a permissive origin can't be used to steal a logged-in session.
**CORS is enabled on every response** (`Access-Control-Allow-Origin: *`) for the
device-authenticated API surface. This keeps header-authenticated device calls
usable from browser-based tooling and same-user cross-origin utilities without
introducing cookie/session theft risk, because device auth is explicit
(`X-Device-Name`/`X-Api-Key`) rather than ambient session auth.
## Deploying on unraid
@@ -65,12 +70,11 @@ so a permissive origin can't be used to steal a logged-in session.
docker compose ps
curl http://localhost:8098/health
```
5. Sign in to the remote portal and open `/settings/device` to edit the synced
owner/car/device/avatar values for the configured `DEVICE_NAME`. If the Pi
has local **Sync with server** enabled, those values hot-reload into the
running Pi UI after the next successful sync. Phase 25.5c also adds a
multi-device list, one-time API-key reveal, key rotation, and a request-sync
action on that page.
5. Sign in to the remote portal and open `/settings/device` to manage devices:
create/delete devices, reveal a one-time API key, rotate keys, edit synced
owner/car/device/avatar values, and request a sync. If the Pi has local
**Sync with server** enabled, those values hot-reload into the running Pi UI
after the next successful sync.
### Upgrading an existing unraid deployment
@@ -87,10 +91,12 @@ docker compose up -d --build
Phase 25.5a adds the device identity sync columns (`owner_name`, `car_model`,
`device_name`, `avatar_path`, `config_version`, `config_updated_at`) used by the
Pi/server config sync flow. The browser portal page at `/settings/device` edits
those values; the Pi applies them on its next successful sync when local
**Sync with server** is enabled, and Phase 25.5b now hot-reloads the in-process
UI fields instead of requiring an app restart.
Pi/server config sync flow. Phase 25.5c adds multi-device provisioning/status
columns (`last_config_sync_at`, `last_config_sync_version`,
`force_sync_requested_at`, `api_key_rotated_at`). The browser portal page at
`/settings/device` edits those values; the Pi applies them on its next
successful sync when local **Sync with server** is enabled, and Phase 25.5b
hot-reloads the in-process UI fields instead of requiring an app restart.
## Registering a device and getting an API key
@@ -104,14 +110,23 @@ This prints a one-time API key — copy the `YARIS_DEVICE_NAME` /
`YARIS_API_KEY` values into the Pi's upload-client config. Only the
key's sha256 hash is stored server-side; the raw key is never shown again.
On the Pi, those credentials can now be entered through the local Settings page
instead of editing `db/upload_creds.env` over SSH. The same page also exposes
**Test connection** and **Sync now** actions.
The browser portal at `/settings/device` can now do the same workflow without
shell access:
- create a new device
- delete a non-default device
- reveal a one-time API key
- rotate the API key later
- request a sync for the selected device
Current limitation: device management is multi-device, but the main remote
overview/history/dashcam browsing pages still scope themselves to one
configured `DEVICE_NAME` until a portal-wide device selector/filter is added.
## Exposing it outside the LAN
Two options, either works with this API unchanged:
@@ -149,7 +164,7 @@ byte-for-byte sha256-verified against the source on the Pi.
The Pi's web UI has a setting to pause dashcam file uploads while keeping
GPS/trip log uploads running — no server-side change needed, the Pi simply
stops calling `/api/v1/dashcam/upload` while the toggle is off.
stops calling the dashcam upload status/chunk endpoints while the toggle is off.
## Local development / testing without unraid