Phase 24: remote trip + dashcam portal

- Add Flask-Login browser session auth (separate from device X-Api-Key auth)
  - modules/user_auth.py: User model, require_login decorator, bcrypt helpers
  - /login with CSRF token, /logout
  - scripts/create_user.py to provision admin user

- Add server-side trip computation from tracking_points
  - db.compute_trips_for_device(): gap > 5 min = new trip, haversine distance,
    upserts into trips table, auto-triggered in bg thread after each upload batch
  - db.list_trips(), db.get_trip_route(): same JSON shape as Pi API
  - db.get_overview_stats(): aggregated totals for overview page
  - db.list_dashcam_files(), db.get_dashcam_file(): browse/stream helpers

- Add web portal to server/app.py
  - Overview page (/): stat cards + recent trips + recent clips
  - Trip history (/history): reuses Pi history.js + data-list.js unchanged
    via /api/trips + /api/trip/<id> compatibility aliases
  - Dashcam archive (/dashcam): file list + inline video player
  - /api/v1/portal/dashcam/stream/<id>: Range-request streaming, path traversal guard
  - /api/geocode: same Nominatim→reverse_geocoder offline fallback as Pi

- Add server templates (server/templates/)
  - base.html: shared shell with 'Remote server' badge in sidebar + mobile topbar
  - login.html, index.html, history.html, dashcam.html, components.html

- DB schema: users + trips tables added to init.sql (fresh deploys) +
  migrate_phase24.sql (existing deployments)

- Infrastructure: Dockerfile uses repo-root build context to copy shared
  static/ assets; docker-compose updated; SECRET_KEY + DEVICE_NAME in .env;
  requirements: Flask-Login, bcrypt, reverse_geocoder

Co-authored-by: Copilot <[email protected]>
This commit is contained in:
Mathieu Moran
2026-08-10 03:58:45 -04:00
co-authored by Copilot
parent 51c6be3a94
commit c4dfcbaf25
18 changed files with 1453 additions and 59 deletions
+59 -53
View File
@@ -1241,60 +1241,66 @@ including two UX polish passes and multiple bug-fix rounds.
- Status JSON now includes `interface` field for diagnostics.
- SSH alias updated to new wlan1 IP (192.168.50.213).
### Phase 24: Remote trip + dashcam portal with shared web UI — 🚧 PLANNED
**Added 2026-08-09; intentionally scheduled after Phase 23.** Phase 23 first establishes the final
visual language and complete in-car interface. Building the remote portal afterward avoids
duplicating the current unfinished theme and lets both deployments deliberately share the same
finished shell, components, trip list, route map, timeline replay, dashcam library, units, and
responsive behavior.
### Phase 24: Remote trip + dashcam portal with shared web UI — ✅ DONE
**Completed 2026-08-10.** Extended the existing `server/` Flask deployment from an upload-only API into a full read-only browser portal backed by Postgres tracking data and the uploaded dashcam archive.
#### What was built
**Database (server/sql/)**
- `migrate_phase24.sql`: adds `users` (bcrypt browser auth) and `trips` (server-side computed trip index) tables
- `init.sql` updated to include both tables so fresh deployments don't need a separate migration step
- `trips` table: `(device_id, start_time)` unique key, full summary stats, start/end coordinates
**DB layer (server/modules/db.py)**
- `get_user_by_id()`, `get_user_by_username()`, `create_user()` — browser auth helpers
- `compute_trips_for_device()` — splits raw `tracking_points` into trip segments (gap > 5 min = new trip), filters out <50m blips, computes distance (haversine), duration, avg/max speed, upserts into `trips` table
- `list_trips()`, `get_trip_route()` — portal trip API helpers returning same JSON shape as Pi `/api/trips`/`/api/trip/<id>`
- `list_dashcam_files()`, `get_dashcam_file()` — dashcam browse/stream helpers
- `get_overview_stats()` — aggregated counts/totals for the Overview page
**Auth (server/modules/user_auth.py)**
- Flask-Login based browser session auth, completely separate from device `X-Api-Key` auth
- `User` class, `load_user()` loader, `check_password()`, `hash_password()`, `require_login` decorator
- `login_manager.login_view = "login_page"` — unauthenticated requests redirected to `/login`
**Application (server/app.py)**
- Added Flask-Login init, `SECRET_KEY` from env
- `_geocode_cached()` — same Nominatim→reverse_geocoder offline fallback as Pi, LRU cached
- `/login` GET/POST with CSRF token (stored in session, validated on POST)
- `/logout`
- Portal pages: `GET /` → Overview, `GET /history` → Trip history (reuses Pi `history.js`), `GET /dashcam` → Dashcam archive
- Portal JSON API: `/api/v1/portal/stats`, `/api/v1/portal/trips`, `/api/v1/portal/trip/<id>`, `/api/v1/portal/dashcam`, `/api/v1/portal/dashcam/stream/<id>` (Range-request streaming), `/api/v1/portal/recompute-trips`
- Pi history.js compatibility aliases: `/api/trips`, `/api/trip/<id>`, `/api/geocode`
- Auto-recompute trips in background thread after each tracking batch upload
- Path traversal guard on dashcam stream: rejects any storage path that escapes `STORAGE_DIR`
**Templates (server/templates/)**
- `base.html` — shared shell matching Pi visual style; "Remote server" badge in sidebar and mobile topbar; navigation has Overview / Trip history / Dashcam archive only (no Pi-hardware nav items)
- `login.html` — standalone login page with CSRF token, flash messages
- `index.html` — Overview page: 6 stat cards (trips, distance, drive time, top speed, videos, odometer), recent trips table, recent dashcam clips table
- `history.html` — identical layout to Pi history page; reuses `history.js` + `data-list.js` unchanged
- `dashcam.html` — file list table (name, size, uploaded date, download button) + inline video player with seek/Range support
- `components.html` — same `data_table` macro as Pi (needed by history.html)
**Scripts (server/scripts/)**
- `create_user.py` — create or update browser portal user: `docker compose exec server python scripts/create_user.py admin yourpassword`
- `recompute_trips.py` — recompute trips for all devices: `docker compose exec server python scripts/recompute_trips.py`
**Infrastructure**
- `server/requirements.txt`: added `Flask-Login`, `bcrypt`, `reverse_geocoder`
- `server/Dockerfile`: added `ffmpeg`, updated COPY paths to use repo-root build context (`context: ..`), copies shared `static/` assets
- `server/docker-compose.yml`: build `context: ..`, `dockerfile: server/Dockerfile`; added `SECRET_KEY` and `DEVICE_NAME` env vars; added `migrate_phase24.sql` mount (for existing deployments)
- `server/.env.example`: added `SECRET_KEY`, `DEVICE_NAME` entries with generation instructions
#### Deployment steps (existing unraid stack)
1. `git pull` on unraid (or however you deploy the server)
2. Run migration: `docker compose exec db psql -U yarisobd -d yarisobd -f /docker-entrypoint-initdb.d/migrate_phase24.sql`
3. Add `SECRET_KEY` and `DEVICE_NAME` to `.env`
4. Rebuild: `docker compose up -d --build`
5. Create portal user: `docker compose exec api python scripts/create_user.py admin yourpassword`
6. Run initial trip computation: `docker compose exec api python scripts/recompute_trips.py`
7. Browse to `https://your-cloudflare-tunnel-url/` → login page
Extend the existing self-hosted `server/` deployment from an upload-only API into a secure remote
web portal backed by its Postgres tracking data and uploaded dashcam archive, so trip history,
recorded trip details, and footage can be accessed away from the car:
- Add server-side trip boundary/index data equivalent to Phase 18, generated from uploaded tracking
points without changing or deleting the raw records. Expose trip summaries, full/downsampled
routes, timestamps, distance in km/mi, average/max speed in km/h and mph, and timeline replay.
- Reuse the redesigned Phase 23 Jinja/Bootstrap UI rather than creating a visually separate app.
Extract/share presentation assets where practical so local and remote pages do not drift, while
keeping their data providers and available actions explicit.
- Make the uploaded dashcam archive a required first-class remote page, matching the redesigned
in-car imported-video library: sortable/searchable file list, relative path, size, timestamp,
thumbnails, inline H.264 playback with Range/seeking support, and an explicit download action for
individual original files. Downloads must stream from server storage with safe
`Content-Disposition`, bounded/traversal-safe paths, and no whole-video memory buffering. Remote
deletion is not included unless it is designed later with separate safeguards; the server remains
the archive/backup.
- Put an always-visible environment indicator in the shared shell: **In-car / Local Pi** on
`yaris.local`, and **Remote server** on the hosted portal. Page title, navigation, and status
styling must also make the context unambiguous so the user cannot mistake remote historical data
for a live connection to the car.
- The remote portal is historical/read-only. It should feel like the same product, but render only
server-relevant navigation and data—not redacted/empty imitations of unavailable hardware.
Physical dashcam mount/import/delete, live OBD/GPS, cellular status/configuration, Pi health, and
local Pi settings are omitted entirely. Trip history/replay and the uploaded dashcam archive are
the required remote scope.
- Add real human-user authentication and secure sessions for the internet-facing UI. Existing
device `X-Device-Name`/`X-Api-Key` upload authentication remains separate and must not become a
browser login mechanism. Preserve Cloudflare Tunnel compatibility and do not expose secrets in
HTML/JavaScript.
- Support selecting a vehicle/device if more than one is registered later, while defaulting
cleanly to the current Yaris. Keep trip APIs scoped to the authenticated user/device.
- Synchronize alert transitions, recoveries, timestamps, severity/details, and current state from
the Pi to the server through an authenticated, idempotent offline queue. The Pi remains
authoritative for vehicle/hardware detection and its local bell must never depend on internet
access. The server stores per-vehicle alert history/current state, reconciles reconnects without
duplicates, and exposes it in the remote portal; remote acknowledgement must not silently clear
the underlying Pi condition.
- Synchronize alert transitions, recoveries, timestamps, severity/details, and current state from
the Pi to the server through an authenticated, idempotent offline queue. The Pi remains
authoritative for vehicle/hardware detection and its local bell must never depend on internet
access. The server stores per-vehicle alert history/current state, reconciles reconnects without
duplicates, and exposes it in the remote portal; remote acknowledgement must not silently clear
the underlying Pi condition.
- Deploy only after Phase 23 is complete: rebuild the unraid Docker stack, verify migrations against
copied data first, compare remote trip calculations with uploaded Pi records, verify playback,
Range requests, and downloads against disposable copies of real server footage, and browser-test
desktop/mobile through the real hosted endpoint before committing.
### 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