feat: add multi-device provisioning and local remote-config UI

- add multi-device portal device management, key rotation, and sync request APIs
- add Pi settings UI for remote server URL, device name, and API key
- add local test-connection and sync-now actions
- make upload and config sync reuse dynamic shared credentials
- add Phase 25.5c migration and docs updates

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-10 18:02:09 -04:00
co-authored by Copilot
parent fbc41a5db5
commit ba682fbf3a
23 changed files with 1357 additions and 167 deletions
+160 -4
View File
@@ -1570,10 +1570,166 @@ updates its display-facing values without a manual restart once sync lands.
---
### 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()`,
### Phase 25.5c: Multi-device provisioning + force-sync UX — 🚧 PLANNED
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;
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
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`,
`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
pretending the cloud server can magically push into an offline car
#### 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
- "Rotate API key" action
- per-device "Edit settings" action
- Replace the single-device lookup helper with:
- list devices
- fetch one device by id
- update one device's identity/config
- Keep raw API keys **write-only/one-time reveal**:
- show only immediately after create/rotate
- never store raw keys server-side
- only store hash as today
2. **Server: browser APIs**
- Add login-protected endpoints for:
- list devices
- create device + return one-time API key
- rotate device API key + return one-time API key
- get/update selected device config
- request force sync for a selected device
- Device settings UI should populate from the selected device row, not from a
single hardcoded portal `DEVICE_NAME`
3. **Pi: credential management UI**
- Add a local Settings section for remote server credentials:
- server URL
- device name
- API key
- connection test button
- Persist those values in the existing gitignored
`db/upload_creds.env` (or an equivalent sqlite-backed secret store if we
later choose to migrate away from env files)
- After saving:
- reload upload client / sync client config
- allow enabling **Sync with server** only when creds test cleanly
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.
- Proposed behavior:
- server sets a per-device `force_sync_requested_at` flag/version bump
- Pi checks that flag during normal online polling
- local Pi UI also gets a **Sync now** button that triggers an immediate
fetch against the server using the saved credentials
- Result:
- from server: "sync on next online contact"
- from Pi: "sync immediately if internet is currently available"
#### 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
manually into the Pi UI feels too clunky
#### 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**
- 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
will stop authenticating until updated
- 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
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:
- create device on server
- copy/paste creds into Pi UI
- test connection
- enable sync
- change device identity on server
- 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
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**
- 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**
- 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**
- 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**
- 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
#### Pending validation
- Deploy Phase 25.5c to unraid + Pi
- Live smoke-test device creation / key rotation / local credential paste flow
- Confirm server-side **Request sync** and Pi-side **Sync now** behavior against
the running hardware
---
### 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