Commit Graph
323 Commits
Author SHA1 Message Date
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
Mathieu MoranandCopilot 91d475fd24 fix(map): show raw coords as fallback when geocode fails or times out
- 5s timeout on Nominatim fetch (AbortSignal.timeout)
- On network error, non-200, or empty result: show 'lat, lon' (5 decimal places)
- Applies to both dashboard live location and history scrubber

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:46:33 -04:00
Mathieu MoranandCopilot 548808fd83 feat(history): show reverse-geocoded location during trip scrubbing
- Location name (road + city) appears below time/speed in the timeline panel
- Debounced 600ms so dragging the scrubber doesn't spam Nominatim
- Deduped by ~3 decimal lat/lon so stationary replay doesn't re-fetch
- Cleared when a new trip is loaded

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:45:41 -04:00
Mathieu MoranandCopilot f1145a4dbb feat(map): reverse geocode location + recenter button on dashboard and history
- Dashboard: recenter button in GPS card header (hidden until first fix)
- Dashboard: estimated location name under lat/lon via Nominatim reverse geocode (throttled 30s)
- History: recenter button fits map back to trip bounds
- Both buttons use crosshairs icon, hidden until data is available

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:43:23 -04:00
Mathieu MoranandCopilot 08a3bef558 ui: tap any row to replay trip on mobile
- data-trip-id on the <tr> itself
- click handler checks row if no button found
- cursor: pointer on rows for discoverability

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:40:45 -04:00
Mathieu MoranandCopilot f27e2f2d4b ui: remove imperial units, hide Replay button on mobile
- formatDistance/formatSpeed: km/km/h only, no mi/mph
- Replay button hidden on mobile (d-none d-sm-table-cell)
- Action column header hidden on mobile to match
- data_table macro supports optional 'class' on column headers

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:38:50 -04:00
Mathieu MoranandCopilot 908e86d169 feat(network): wlan0 fallback if T3U dongle is absent
- PRIMARY_INTERFACE=wlan1, FALLBACK_INTERFACE=wlan0
- resolve_interface() checks /sys/class/net at startup and picks whichever exists
- INTERFACE is module-level, so all functions use the correct iface automatically
- restore_fallback() now also tries home-wifi-fb / Hotspot on wlan0 if wlan1 is primary but failed
- install_netwatch.sh creates home-wifi-fb profile on wlan0 (lower autoconnect-priority=5)
- wlan0 stays managed=yes (fallback) instead of being hard-disabled
- Status JSON now includes 'interface' field for diagnostics

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:35:06 -04:00
Mathieu MoranandCopilot e533f52e78 feat(network): switch all WiFi to wlan1 (TP-Link T3U dongle)
- INTERFACE = 'wlan1' in yaris_uplink.py
- install_netwatch.sh binds home-wifi, Hotspot, phone-hotspot to wlan1
- Disables wlan0 (onboard) via nmcli device set managed no
- WLAN variable at top of script for easy revert to wlan0 if needed

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:29:22 -04:00
Mathieu MoranandCopilot cc6e9c8de0 feat: add download button to Pi videos list
- /api/local-video/file?download=1 serves file as attachment
- Download link (green DL button) added to each row in local-videos.js
- Dashcam USB files skipped: no persistent mount for direct serve;
  use Import then download from Pi Videos

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:17:29 -04:00
Mathieu MoranandCopilot b711d421cc fix(videos): real ffmpeg progress via -progress pipe, exclude .converting.mp4
- ffmpeg -progress pipe:1 streams out_time_ms lines; we parse them in the
  reading loop so % = out_time_s / duration_s (actual encode position)
- find_video_files() skips *.converting.mp4 temp files so they are never
  listed, uploaded, or shown in the UI

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:13:45 -04:00
Mathieu MoranandCopilot afce229310 feat: odometer in trips/overview, trips upload toggle, fix video count
- OBD odometer now logged to tracking rows on every GPS snapshot
- trips table: START_ODOMETER/END_ODOMETER columns (migration)
- build_trip_summaries: picks odometer from first/last tracking row
- list_trips: uses real OBD odo, falls back to cumulative distance
- overview stats: odometer_km (max seen from OBD), video_count fixed
- history page: Odometer column shows start→end km per trip
- app_upload: TRIPS_UPLOAD_SETTING_KEY, is_trips_upload_enabled()
- _upload_tracking_batch: skips when trips upload is off
- settings: separate 'Upload trips & GPS' toggle from dashcam toggle

Co-authored-by: Copilot <[email protected]>
2026-08-10 02:07:31 -04:00
Mathieu MoranandCopilot 414fb78543 fix(videos): base convert % on elapsed/duration not file size
File size ratio was wrong since output bitrate != input bitrate.
Now uses ffprobe to get video duration, then elapsed/duration for %.

Co-authored-by: Copilot <[email protected]>
2026-08-10 01:50:49 -04:00
Mathieu MoranandCopilot 1fde8732c2 fix(videos): stop thumbnails reloading on every convert poll
Instead of full re-render every 10s, patch converting badges in-place.
Only does a full renderFiles() when file count changes (conversion done)
or on the final poll when all conversions finish.

Co-authored-by: Copilot <[email protected]>
2026-08-10 01:49:03 -04:00
Mathieu MoranandCopilot 7d5755180a fix(videos): set explicit 4Mbps bitrate for hw encode
Default h264_v4l2m2m bitrate was ~200kbps producing unusable quality.
4M video + 128k audio = ~100MB per 3-min clip, proper dashcam quality.

Co-authored-by: Copilot <[email protected]>
2026-08-10 01:43:28 -04:00
Mathieu MoranandCopilot e8ce33eded fix(videos): fix FileNotFoundError crashing convert thread
safe_video_path checks file existence — mp4 output doesn't exist yet.
Add _local_path_unchecked() for output paths that don't exist yet.

Co-authored-by: Copilot <[email protected]>
2026-08-10 01:35:31 -04:00
Mathieu MoranandCopilot d9d1537a13 feat(videos): show conversion progress % and elapsed time
- _converting dict now tracks started_at, source_size, tmp_path per file
- list_local_videos() reads tmp file size to compute pct complete
- Desktop badge: 'Converting 42% 1m30s'
- Mobile badge: '42%' (compact, fits under thumbnail)

Co-authored-by: Copilot <[email protected]>
2026-08-10 01:30:48 -04:00
Mathieu MoranandCopilot c995599512 feat(videos): add manual Convert button per MOV file on Pi
- New POST /api/local-video/convert route triggers queue_convert(force=True)
- queue_convert() now returns True/False/None and accepts force param
  to bypass the auto-convert toggle (manual always works)
- Convert button shown for non-MP4, non-converting files in video list
- Clicking spins the button and refreshes list after 1s to show badge

Co-authored-by: Copilot <[email protected]>
2026-08-10 01:28:56 -04:00
Mathieu MoranandCopilot ba712bd6db fix(dashcam): prevent site freeze during import/convert
- _convert_bg now runs in real threading.Thread (not gevent greenlet)
  so ffmpeg proc.wait() doesn't block gevent event loop
- _device_lock released before copy loop; only held for mount/unmount
  so /api/dash/files and other routes don't block during long copies
- Date now shows time (Aug 9, 10:30 PM) on mobile video rows
- Spacing added above converting spinner badge

Co-authored-by: Copilot <[email protected]>
2026-08-10 01:23:53 -04:00
Mathieu MoranandCopilot 77cd3fdadd feat(dashcam): show converting badge and date on mobile video rows
Co-authored-by: Copilot <[email protected]>
2026-08-10 01:18:03 -04:00
Mathieu MoranandCopilot 384050e78a fix(dashcam): show checkbox on mobile for file selection
Co-authored-by: Copilot <[email protected]>
2026-08-10 01:15:00 -04:00
Mathieu MoranandCopilot 71f8ce2993 feat(dashcam): async import with polling status
- Import now returns immediately with {status: 'started'}
- Backend runs copy in a daemon thread, tracks job state in _import_job
- New GET /api/dash/import/status endpoint for polling
- UI polls every 3s, shows progress bar until done
- Page reload mid-import resumes polling automatically
- Prevents double-import with 'busy' guard

Co-authored-by: Copilot <[email protected]>
2026-08-10 01:12:00 -04:00
Mathieu MoranandCopilot 7d124b0fa0 fix(dashcam): remove internal td borders on mobile rows
Co-authored-by: Copilot <[email protected]>
2026-08-10 01:01:17 -04:00
Mathieu MoranandCopilot 219d559de9 fix: dashcam mobile shows filename, RPM/fuel/throttle don't append unit when disconnected
- yaris.css: split mobile rules by list ID — video-list shows thumb+actions,
  dashcam-file-list shows dash-col-file+actions; thead display:none !important
- yaris.js: check data !== '-' before appending rpm/°C/% units so disconnected
  state shows — instead of '- rpm'

Co-authored-by: Copilot <[email protected]>
2026-08-10 00:56:48 -04:00
Mathieu MoranandCopilot 522674c907 fix(dashcam): remove inline width from thumb td, control via CSS with !important
Inline style.width on td was overriding the mobile media query width rule.
Use dash-thumb-td class for desktop (9rem) and mobile (5rem !important).

Co-authored-by: Copilot <[email protected]>
2026-08-10 00:53:02 -04:00
Mathieu MoranandCopilot 5b2854fbec fix(dashcam): use style.display instead of hidden attr (d-flex overrides hidden)
Bootstrap d-flex sets display:flex !important which beats the HTML hidden
attribute. Switch to style.display='none'/'flex' for both status bars.

Co-authored-by: Copilot <[email protected]>
2026-08-10 00:51:27 -04:00
Mathieu MoranandCopilot b064b4f902 fix(dashcam): auto-hide import result bar after 10s
Co-authored-by: Copilot <[email protected]>
2026-08-10 00:48:31 -04:00
Mathieu MoranandCopilot ba760a04d6 fix(dashcam): proper mobile row reflow + import status at top with ETA
- yaris.css: use .dash-row class on all rows; only td:first-child and
  .dash-actions-td visible on mobile (<576px); buttons never overflow
- dash.js: add dash-row class + dash-actions-td wrapper div to dashcam
  file rows; import status moved to card footer at top with spinner + ETA
- local-videos.js: add dash-row class to video rows
- dash.html: import status bar (spinner + ETA + result) in device card
  at top; removed old footer import status from files card

Co-authored-by: Copilot <[email protected]>
2026-08-10 00:46:05 -04:00
Mathieu MoranandCopilot 9f0f53a59c fix(dashcam): reflow table rows as flex cards on mobile so buttons are always visible
Replace column-hiding approach (still caused 691px overflow) with full
table reflow: on <576px each tr becomes a flex row, all tds hidden except
first (thumb+name) and .dash-actions-td (buttons). Buttons can never
overflow because the row is constrained to viewport width.

Co-authored-by: Copilot <[email protected]>
2026-08-10 00:33:57 -04:00
Mathieu MoranandCopilot 01704d5429 feat(dashcam): mobile layout fix + optional hw-encode conversion on import
- yaris.css: on mobile (<576px) hide file/size/date/upload cols, show
  filename inside thumb cell, icon-only buttons, dash-actions-td nowrap
- local-videos.js: filename in thumb cell for mobile; Converting badge with
  spinner; auto-refresh every 10s while any file is converting
- app_video.py: CONVERT_SETTING, is_convert_enabled(), queue_convert(),
  _convert_bg() — h264_v4l2m2m + aac faststart, deletes original on success
- app_dash.py: call queue_convert() after each successful copy on import
- app_settings.py: expose video_convert_enabled in get/save settings
- settings.html: toggle for 'Convert imported videos for web playback'

Co-authored-by: Copilot <[email protected]>
2026-08-10 00:30:59 -04:00
Mathieu MoranandCopilot dd2f95b9c1 revert(video): back to simple send_file for video serving (RPi3 can't transcode)
Co-authored-by: Copilot <[email protected]>
2026-08-10 00:21:19 -04:00
Mathieu MoranandCopilot a3659e8a8c fix(video): transcode audio to AAC for MP4 streaming (PCM unsupported in MP4)
Co-authored-by: Copilot <[email protected]>
2026-08-10 00:18:41 -04:00
Mathieu MoranandCopilot 51322d0e00 fix(dashcam): wrap action buttons in div, ffmpeg stream for MOV playback
- local-videos.js: action buttons go in a div.data-list-actions inside td
  so td stays a table cell (not flex); thumbnail td gets fixed width 9rem
- app.py: video route now pipes ffmpeg with -c copy -movflags
  frag_keyframe+empty_moov so browsers get a fragmented MP4 stream that
  starts playing immediately without needing the moov atom at the end

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