Commit Graph
204 Commits
Author SHA1 Message Date
Mathieu MoranandCopilot 5a5058adca fix: reuse upload credentials for device config sync
- load server sync creds from db/upload_creds.env like uploads do
- fall back from YARIS_DEVICE_KEY to existing YARIS_API_KEY
- document shared credential flow for server-synced identity
- add test coverage for sync credential fallback

Co-authored-by: Copilot <[email protected]>
2026-08-10 17:37:52 -04:00
Mathieu MoranandCopilot 585da18225 feat: add remote device settings portal and Pi hot reload
- add /settings/device portal page and portal device-config GET endpoint
- add portal navigation and client-side device settings editor UI
- hot-reload Account/Car display config after local saves and server syncs
- document portal workflow and Phase 25.5b completion
- add runtime reload tests for account, car, and sync callback

Co-authored-by: Copilot <[email protected]>
2026-08-10 17:34:22 -04:00
Mathieu MoranandCopilot c4ccc3efb9 docs: record verified Pi and unraid deployment state
- note verified Pi IP after WiFi card change
- record successful 2026-08-10 Pi/unraid rollout
- document unraid checkout divergence and temp-clone deploy workaround

Co-authored-by: Copilot <[email protected]>
2026-08-10 17:26:17 -04:00
Mathieu MoranandCopilot cd004fbdde fix: wire Phase 25.5a deploy docs and sync connectivity
- use app_network status for device sync connectivity checks
- mount migrate_phase25_5a.sql in server docker compose
- document manual server upgrade migration steps
- record verified Pi and unraid SSH access details in PLAN

Co-authored-by: Copilot <[email protected]>
2026-08-10 17:22:02 -04:00
Mathieu MoranandCopilot 0bda44a7bc Phase 25.5a: Server-side device config API and Pi sync thread
- Created app_device_sync.py: background sync thread with connectivity-driven scheduling (5min online, 60sec backoff when offline)
- Server /api/device-config endpoints: GET for Pi device auth, POST for admin user auth
- Database migration adds config_version, owner_name, car_model, device_name, avatar_path to devices table
- Pi syncs config from server on network availability; offline-first design (loads from local JSON at startup)
- Settings UI fields read-only when server sync enabled

All 63 tests passing. Ready for testing with actual server connection.

Co-authored-by: Copilot <[email protected]>
2026-08-10 17:17:04 -04:00
Mathieu MoranandCopilot 2395cf6dc4 feat: add Phase 25.5a server-side device config API endpoints
- Add database migration (migrate_phase25_5a.sql):
  * Add owner_name, car_model, device_name, avatar_path columns to devices table
  * Add config_version and config_updated_at for cache invalidation

- Add server/modules/device_config.py:
  * get_device_config(device_id): retrieve config for Pi sync
  * update_device_config(device_id, data): update config with validation

- Add server API endpoints:
  * GET /api/device-config (authenticated with device_key, Pi fetches config)
  * POST /api/device-config (authenticated as user, admin updates config)

- Enhance server/modules/db.py:
  * Add get_device_by_id() function to retrieve all device config fields

Phase 25.5a enables server-side device identity management and connection-driven sync.
Pi will fetch from this endpoint when internet available, keeping config in sync.

Co-authored-by: Copilot <[email protected]>
2026-08-10 16:46:52 -04:00
Mathieu MoranandCopilot ed78700005 docs: update Phase 25.5 to reflect settings UI implementation
- Settings UI now implemented for device config editing
- Sync toggle blocks field edits when enabled
- All behaviors tested and verified on Pi
- Phase 25.5a will add server-side config endpoints and connection-driven sync

Co-authored-by: Copilot <[email protected]>
2026-08-10 16:44:03 -04:00
Mathieu MoranandCopilot d7b9bb634f feat: add device config settings UI with optional server sync
- Add app_device_config.py module for managing device configuration (owner_name, car_model, avatar_path, device_name)
- Add sync_enabled flag to control whether Pi syncs config from server
- When sync is enabled, device config fields are read-only (server is source of truth)
- When sync is disabled, user can edit device config via Settings page
- Add /api/device-config endpoint (GET to retrieve, POST to update)
- Add Device identity section to settings.html with form fields for all config values
- Update settings.js to load/save device config with sync toggle
- Gracefully handles both config file (at startup) and database (live updates via settings UI)
- All 63 tests passing

Phase 25.5: Offline-first device config with eventual consistency. Config loads from JSON at startup (no network needed), can be edited via settings if not syncing, and will sync from server on next connectivity when Phase 25.5a server endpoints are implemented.

Co-authored-by: Copilot <[email protected]>
2026-08-10 16:41:45 -04:00
Mathieu MoranandCopilot c65a635906 implement: Phase 25.5 device config from JSON file
- Create config/device.json with owner_name, car_model, device_name, avatar_path
- Update app_account.py to load owner_name and avatar_path from config
- Update app_car.py to load car_model and device_name from config
- Update base.html template to use avatar_path from config instead of hardcoded
- Add graceful fallback to hardcoded defaults if config file missing
- Document in deploy/readme.md and readme.md how to customize device identity
- Update PLAN.md Phase 25.5 to reflect current implementation and future server sync

Benefits:
- No code edits needed to customize Pi (edit JSON file only)
- Offline-first: all values loaded from local file at startup
- Connection-driven sync planned for Phase 25.5a (sync when internet available, not on timer)
- Server becomes source of truth after Phase 25.5a implementation

All 63 tests passing. Database filename still based on owner_name, with migration warning in docs.

Co-authored-by: Copilot <[email protected]>
2026-08-10 16:31:59 -04:00
Mathieu MoranandCopilot 69beb07430 docs: add Phase 25.5 device config sync plan
Plan for replacing hardcoded user/car/avatar with managed server-synced configuration, enabling multi-device and zero-code-edit deployment scenarios.

Co-authored-by: Copilot <[email protected]>
2026-08-10 16:21:49 -04:00
Mathieu MoranandCopilot 235ca65822 docs: add profile and car customization guide
Co-authored-by: Copilot <[email protected]>
2026-08-10 16:21:05 -04:00
Mathieu MoranandCopilot 2c0e9f7e25 docs: add RTL8812BU driver install to deploy guide
Co-authored-by: Copilot <[email protected]>
2026-08-10 16:14:38 -04:00
Mathieu MoranandCopilot d2f9ea4209 Re-add redacted UI screenshot after history purge
Co-authored-by: Copilot <[email protected]>
2026-08-10 16:10:04 -04:00
Mathieu MoranandCopilot ae0bcc7ea8 Add fresh-Pi deploy guide and uplink consistency fixes
- add deploy/readme.md with full fresh install + quick bootstrap block
- update README with deploy guide link and hardware photo
- fix app_network interface detection for wlan1-first setup
- make dispatcher hook handle wlan0 and wlan1
- update PLAN.md with Phase 24.8 completion notes

Co-authored-by: Copilot <[email protected]>
2026-08-10 15:54:36 -04:00
Mathieu MoranandCopilot 0cd798ee19 fix: use total trip distance as odometer fallback when PID 0xA6 unavailable
If no real odometer data exists (all NULL — expected on 2013 Yaris which
likely doesn't support PID 0xA6), fall back to SUM(distance_km) from the
computed trips table so the overview shows something meaningful.

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:59:53 -04:00
Mathieu MoranandCopilot 8f8c03a26a fix: server odometer stat shows 0 when no odometer data exists
COALESCE(MAX(odometer), 0) returned 0 when all tracking rows have NULL
odometer (expected before the car is connected — PID 0xA6 likely unsupported
on 2013 Yaris). Now returns NULL which the overview JS renders as '—'.

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:58:22 -04:00
Mathieu MoranandCopilot a7ab088cf5 docs: update PLAN.md and readme for phases 24.5-24.7
- PLAN.md: add Phase 24.7 section (diagnose.sh, map tile fixes, app.py cleanup)
- readme.md: update project status to reflect phases 24-24.7 complete,
  add remote portal to features list, add diagnostics/recovery section

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:55:37 -04:00
Mathieu MoranandCopilot f90a5e5826 fix: switch server portal map tiles to CARTO (OSM was rate-limiting/blocking)
OSM tile.openstreetmap.org was returning grey tiles on the portal (likely
rate-limiting the Cloudflare tunnel egress IP). Switch to CARTO Voyager
tiles which are free, CDN-backed, and don't have the same restrictions.

Also add data-tile-subdomains attribute support in history.js so tile
provider subdomains can be configured per-deployment.

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:51:18 -04:00
Mathieu MoranandCopilot 5e0b30759d fix: always add tile layer at map init, fix zoom bounds
- tiles.addTo(map) now called at init instead of lazily on first trip click
  (fixes blank map on page load on the server portal)
- minZoom/maxZoom read from data-min-zoom/data-max-zoom attributes with
  correct NaN fallback (parseInt || default)
- Pi template: data-min-zoom=8 data-max-zoom=15 (matches cached tile range)
- Server template: data-min-zoom=3 data-max-zoom=19 (full OSM range)

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:46:45 -04:00
Mathieu MoranandCopilot c287ab67a3 fix: map zoom limits configurable per-deployment via data attributes
Pi offline tiles only cover zoom 8-15; OSM live tiles support 0-19.
history.js now reads data-min-zoom/data-max-zoom from the #history-map div
instead of hardcoding 3/19. Pi template caps to 8-15 (prevents blank tiles
at extreme zooms), server template allows full 3-19 OSM range.

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:42:04 -04:00
Mathieu MoranandCopilot 264d38a6dc cleanup: fix duplicate app_gps import, remove debug prints from index route
Co-authored-by: Copilot <[email protected]>
2026-08-10 05:38:49 -04:00
Mathieu MoranandCopilot d96a971f15 fix: diagnose.sh DB backup line shows literal $DB_PATH
Co-authored-by: Copilot <[email protected]>
2026-08-10 05:31:50 -04:00
Mathieu MoranandCopilot 437434b972 feat: add scripts/diagnose.sh — system diagnostic & recovery tool
Read-only health check (no args) or auto-fix mode (--fix):
  - Service: active state, crash loop detection, recent log
  - Hardware: OBD USB adapter (/dev/ttyUSBx), SIM868 UART (/dev/serial0),
    dashcam USB mount, permissions
  - Network: hotspot IP, web UI reachability, internet, upload server
  - Storage: disk usage, SQLite integrity check, row counts, upload backlog,
    log file size
  - OBD status: parsed from log (last connect/disconnect/speed/errors)
  - Python env: venv present, key packages importable
  - Pi health: throttling flags decoded, CPU temp, free memory
  - Manual recovery command cheatsheet printed at the end

Exit code = number of critical issues found (0 = healthy).

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:31:03 -04:00
Mathieu MoranandCopilot c03cd6d48d fix: add OBD_SKIP_VOLTAGE_CHECK support for clone ELM327 adapters
Counterfeit ELM327 chips (common in cheap PL2303 USB adapters) often report
bad/zero voltage even when the OBD connection is working. python-obd's
check_voltage=True default rejects these. Add OBD_SKIP_VOLTAGE_CHECK=1 env
var to pass check_voltage=False to obd.OBD(), documented in yaris-obd.service.

Also document Phase 24.6 Yaris 2013 compatibility analysis in PLAN.md.

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:26:40 -04:00
Mathieu MoranandCopilot 10dea22f9a fix: Yaris 2013 OBD compatibility hardening
- _decode_odometer: catch all exceptions and return None instead of raising.
  PID 0xA6 (odometer) is not guaranteed on 2013 Toyota Yaris — a garbage
  response or unsupported PID previously raised ValueError through python-obd's
  decoder, which app_obd._query() caught and treated as a lost connection,
  killing the entire OBD session. Now it gracefully returns None.

- connect_obd: reset all sensor globals (rpm, coolant_temp, fuel_level,
  throttle_position, mil_on, dtc_codes, odometer, timestamps) on each new
  connection so the UI doesn't display stale readings from the previous
  ignition cycle while fresh polls are completing.

- yaris-obd.service: add commented OBD_PROTOCOL=6 hint for post-confirmation
  pinning (eliminates 3-5s auto-probe delay after first confirmed connection).

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:22:36 -04:00
Mathieu MoranandCopilot a56e4252d3 docs: document Phase 24.5 code audit in PLAN.md
Co-authored-by: Copilot <[email protected]>
2026-08-10 05:17:32 -04:00
Mathieu MoranandCopilot 1b39ac0cc8 fix: pre-car-test bug fixes from full code audit
- app_alerts.py: null guard on list_local_videos() result (.get('files', []))
- app_cellular.py: remove erroneous .encode(errors='replace') on str response body
- app_settings.py: initialize apn='' when isinstance check fails (NameError guard)
- app_upload.py: move json import to top of file instead of inside except handler
- server/app.py: wrap int(request.args.get('limit')) in try/except ValueError
- server/app.py: wrap os.path.commonpath() in try/except ValueError (cross-root paths)
- tests/test_app_settings.py: add missing trips_upload_enabled/video_convert_enabled
  to valid_payload() (fields added in Phase 23.5 but test not updated)

All 63 tests pass.

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:15:27 -04:00
Mathieu MoranandCopilot f30143c7b7 docs: document Phase 24 bug fixes and UI polish in PLAN.md
Co-authored-by: Copilot <[email protected]>
2026-08-10 05:07:20 -04:00
Mathieu MoranandCopilot 2f1e35414d Fix sidebar arrow direction: use CSS rotation not JS icon swap
The CSS already handles the arrow direction with transform:rotate(180deg) on
.app-sidebar.collapsed .sidebar-collapse-btn. The JS icon className swap was
fighting with CSS rotation causing a double-flip. Remove JS icon manipulation,
let CSS handle it. Also suppress the button transition on page load.

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:04:57 -04:00
Mathieu MoranandCopilot 3c3c8bba71 Fix sidebar collapse flash on page navigation
Suppress CSS transitions during initial state restore from localStorage.
Re-enable after two rAF frames so the toggle button still animates smoothly.

Co-authored-by: Copilot <[email protected]>
2026-08-10 05:00:22 -04:00
Mathieu MoranandCopilot 21e44a8586 Fix server sidebar collapsed state: hide badge and user footer text
- sidebar-env-badge hidden when collapsed
- sidebar-user-text collapses like nav link spans
- User icon stays centered when collapsed via sidebar-user-footer flex

Co-authored-by: Copilot <[email protected]>
2026-08-10 04:56:44 -04:00
Mathieu MoranandCopilot b172b89318 Simplify delete confirmation to native window.confirm
Bootstrap Modal async was unreliable. Replace with synchronous
window.confirm() which always works and needs no dependencies.

Co-authored-by: Copilot <[email protected]>
2026-08-10 04:52:03 -04:00
Mathieu MoranandCopilot 72e4c1b1d7 Fix trip delete button: layout and click handler
- text-nowrap on action cell so Replay+trash stay on same line
- Delete button inline with Replay (no whitespace between)
- Explicit deletable guard in click handler

Co-authored-by: Copilot <[email protected]>
2026-08-10 04:45:05 -04:00
Mathieu MoranandCopilot 7aa9ffda16 Add delete with confirmation for trips and dashcam
- Confirmation modal in server base.html (window.confirmDelete promise helper)
- Trip delete: trash button in Replay column (server-only via data-deletable on
  trip-list div), DELETE /api/v1/portal/trip/<id>, removes computed trip only
  (raw tracking_points kept)
- Dashcam delete: trash button next to download, DELETE /api/v1/portal/dashcam/<id>,
  removes from DB + disk (best-effort file removal)
- history.js: delete button gated on data-deletable='1' attribute; click handler
  intercepts before replay logic
- db.py: delete_trip(), delete_dashcam_file() helpers

Co-authored-by: Copilot <[email protected]>
2026-08-10 04:40:22 -04:00
Mathieu MoranandCopilot a13d74e18e Fix remote portal map tiles and geocode
- history.js reads tile URL from data-tile-url attribute on #history-map div,
  falling back to local offline tiles (Pi behavior unchanged)
- server history.html sets data-tile-url to live OSM tiles
- maxZoom raised to 19 (OSM supports it, local tiles were limited to 15)
- server geocode returns 'label' key to match what history.js/yaris.js expect

Co-authored-by: Copilot <[email protected]>
2026-08-10 04:34:49 -04:00
Mathieu MoranandCopilot e2a319ae08 Fix history.js null guard for speed and distance
formatSpeed and formatDistance return '—' on null/undefined values.
Server trips may have null avg/max speed if no speed data was recorded.

Co-authored-by: Copilot <[email protected]>
2026-08-10 04:30:54 -04:00
Mathieu MoranandCopilot 693848aba1 Fix remote portal trip list
- /api/trips now returns {trips:[...]} to match what history.js expects (result.trips)
- odo_start_km null guard in history.js (server trips have no odometer data)
- require_login returns 401 JSON for /api/* paths instead of HTML redirect

Co-authored-by: Copilot <[email protected]>
2026-08-10 04:27:57 -04:00
Mathieu MoranandCopilot 64b9481ce0 Phase 24: add password change page
- GET/POST /account/password with CSRF token and bcrypt re-hash
- 'Password' link in sidebar next to Logout
- change_password.html template

Co-authored-by: Copilot <[email protected]>
2026-08-10 04:22:28 -04:00
Mathieu MoranandCopilot c4dfcbaf25 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]>
2026-08-10 03:58:45 -04:00
Mathieu MoranandCopilot 51c6be3a94 docs: update PLAN.md phase 23.5, README features/hardware/status, new screenshots
- PLAN.md: Phase 23.5 documents all work from this session
- README: hardware table adds T3U, features add geocoding/offline maps/T3U, status updated
- pics/ui.png: desktop + mobile side-by-side screenshot (updated)

Co-authored-by: Copilot <[email protected]>
2026-08-10 03:43:09 -04:00
Mathieu MoranandCopilot 58faded602 fix(dashboard): parseFloat GPS coords before toFixed call
Co-authored-by: Copilot <[email protected]>
2026-08-10 03:31:55 -04:00
Mathieu MoranandCopilot a2ae15c2f0 fix(dashboard): geocode fires on first GPS fix, gated by distance not timer
Co-authored-by: Copilot <[email protected]>
2026-08-10 03:29:32 -04:00
Mathieu MoranandCopilot c5982e905f fix(geocode): replace AbortSignal.timeout with AbortController for compatibility
- AbortSignal.timeout not supported on all mobile browsers
- Manual AbortController + setTimeout works everywhere
- Fixes location not showing on dashboard and history first point

Co-authored-by: Copilot <[email protected]>
2026-08-10 03:25:14 -04:00
Mathieu MoranandCopilot 11f07dec38 fix(geocode): accurate street name + always shows on dashboard
- Cache key uses 4 decimal places (~11m) instead of 3 (~111m) for correct street
- Dashboard geocodes on first GPS fix, then again every >50m moved (throttled 15s)
- Removed stale console.log

Co-authored-by: Copilot <[email protected]>
2026-08-10 03:21:37 -04:00
Mathieu MoranandCopilot c188fb9d14 feat(geocode): street-level via Nominatim when online, city fallback offline
- Try Nominatim first (road + city), 4s timeout
- Fall back to reverse_geocoder (city + region) if no internet
- LRU cache (512 entries, ~111m precision) — repeated lookups are instant
- No external calls from browser, all via /api/georecode on Pi

Co-authored-by: Copilot <[email protected]>
2026-08-10 03:18:36 -04:00
Mathieu MoranandCopilot e96c6a16bb fix(network): stable phone hotspot IP via dhcp-client-id=yaris-obd
- Phone DHCP caches by client-id so Pi always gets same lease
- Remove dashboard phone-hotspot banner (didn't work)

Co-authored-by: Copilot <[email protected]>
2026-08-10 03:11:41 -04:00
Mathieu MoranandCopilot a6c6cddf30 feat(dashboard): show Pi IP banner when connected to phone hotspot
- Banner appears on dashboard only when uplink mode is 'phone'
- Shows the Pi's current IP so you can reach it from your phone's browser
- Polls every 15s, hides automatically when not on phone hotspot

Co-authored-by: Copilot <[email protected]>
2026-08-10 03:06:39 -04:00
Mathieu MoranandCopilot 9f9974e197 fix(network): home-wifi-fb autoconnect=no, only activated by restore_fallback()
Co-authored-by: Copilot <[email protected]>
2026-08-10 02:56:25 -04:00
Mathieu MoranandCopilot 1299e35a88 fix(network): use wifi-sec shorthand in nmcli connection add
Co-authored-by: Copilot <[email protected]>
2026-08-10 02:53:41 -04:00
Mathieu MoranandCopilot 11e98de0ee feat(geocode): fully offline reverse geocode via local Pi API
- Remove all Nominatim/external calls from frontend
- Add /api/geocode?lat=&lon= route using reverse_geocoder (20MB offline KD-tree)
- JS hits /api/geocode only — works with no internet at all
- Falls back to raw coords if Pi route fails
- Add reverse_geocoder to requirements.txt

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:49:48 -04:00