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]>
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]>
- 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]>
- 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]>
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]>
- /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]>
- 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]>
- 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]>
- 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]>
- 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]>
- 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]>
- 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]>
- 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]>
- 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]>
- 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]>
- /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]>
- 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]>
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]>
- 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]>
- _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]>
- 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]>
- 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]>
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]>
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]>
- 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]>
- 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]>
- 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]>
- app_sql.py: add get_overview_stats() with single SQL query (trip count,
total km, duration, max speed, active DTC count)
- app.py: add /api/stats route (was missing after scp overwrote the file)
- local-videos.js: remove leftover old closePlayer/playVideo/renderFiles
duplicate block that still had scrollIntoView and references to the old
panel elements; new modal-based code is the only path now
Co-authored-by: Copilot <[email protected]>
- app.py: replace send_file with manual Range-aware chunked generator;
returns 206 on partial requests so browsers can seek/buffer properly.
Thumbnail route returns 202 while ffmpeg is generating.
- app_video.py: thumbnail generation moved to background gevent greenlet
(_generate_thumbnail_bg). get_thumbnail_path returns immediately with
RuntimeError('generating') if not cached; ffmpeg only ever runs once
per video (result cached in dash/thumbnails/).
- dash.html: replaced bottom card player with Bootstrap modal (modal-xl,
centered, dark bg) — video pops up in place regardless of scroll position.
- local-videos.js: rewritten to use Bootstrap modal; thumbnail loader
polls with fetch() and retries on 202 every 2s (up to 15x) instead of
setting img.src directly (which can't detect 202).
Co-authored-by: Copilot <[email protected]>
- Replace 8-point scatter chart with proper 60-point line chart
- Blue gradient fill (rgba 35%→2%), tension 0.4 smooth curve
- No legend (obvious), no X axis grid, soft Y axis gridlines
- Y axis auto-scales: max(60, sessionMax*1.2) rounded to 10s
- Ticks: '0 km/h' labels on Y, time strings on X (max 6)
- chart.update('none') for live updates (no animation stutter)
- Speed history card header now shows current speed + session max
even when the graph is collapsed
- Footer bar inside expanded graph: session avg, point count, last reading time
- makeGradient() refreshed on every update in case canvas was resized
- obd_rpm now appends ' rpm' to the displayed value
Co-authored-by: Copilot <[email protected]>
- Replace SB Admin 2 sidebar with a clean fixed sidebar (desktop)
and a sticky bottom tab navigation (mobile, ≤767px)
- Sidebar collapses to icon-only mode with state persisted in localStorage
- Logo is a functional home link on all viewports
- Mobile: full-width content, bottom nav replaces sidebar
- Dashboard: 6 OBD stat cards (status, speed, RPM, coolant, throttle, fuel)
wired to live socket events; removed all placeholder buttons/dropdowns
- GPS info section cleaned up (lat/lon display instead of X/Y paragraphs)
- DTC page: live /api/dtc wiring — MIL indicator + DTC table populated from API
- New /api/dtc route added to app.py (mil + codes)
- yaris.js: added obd_coolant_temp / obd_fuel_level / obd_throttle listeners
- yaris.css: full rewrite — layout variables, sidebar, bottom nav, OBD cards,
border-start utilities, mobile padding for bottom nav
Co-authored-by: Copilot <[email protected]>
Prefer a configured phone hotspot for outbound synchronization, preserve secure write-only credentials, and recover through home WiFi or the local Pi hotspot.
Co-authored-by: Copilot <[email protected]>
Removes the phone-relay feature entirely per the Phase 11 finding
that it hit a fundamental phone-OS limitation (a browser page can't
route WiFi and cellular traffic simultaneously, no code fix possible)
- see PLAN.md for the full test writeup.
- app.py: dropped /api/dash/pending, /api/dash/file/<path>,
/api/dash/relay/config, /api/dash/relay/heartbeat,
/api/dash/relay/stop, /api/dash/relay/mark_uploaded routes and
their now-unused imports (os, send_file, abort, safe_join).
- modules/app_upload.py: dropped list_pending_dashcam_files(),
relay_heartbeat()/relay_stop()/is_relay_active(),
RELAY_LEASE_SECONDS/_relay_lease_until, and the relay-lease check
inside _upload_dashcam_files()/get_status(). Kept the chunked
upload protocol, dashcam_hash_cache/_hash_dashcam_file() (still
used by the Pi's own background uploader), and DASHCAM_CHUNK_SIZE -
none of that was relay-specific.
- templates/dash.html: removed the 'Relay via phone' card.
- static/js/dash.js: removed all relay JS (relayOneFile,
fetchChunkWithRetry, uploadChunkWithRetry, heartbeat timers,
pending-file list rendering).
- server/app.py: removed the CORS after_request hook, added solely
for the phone's cross-origin browser calls (the Pi's own Python
requests-based client was never subject to CORS).
- readme.md: dropped the phone-relay feature bullet, updated the
project-status blurb to explain the removal + point at the planned
native Android app as the real fix for that use case.
- PLAN.md: marked Phase 12 done with a verification writeup.
Verified: py_compile on all touched Python files, node --check on
dash.js, grepped all touched files for any leftover relay/RELAY
reference (none), and smoke-tested by importing app.py + hitting it
with Flask's test client - app starts cleanly, every relay route is
gone from url_map, and both / and /dash still render HTTP 200 with
zero 'relay' occurrences in /dash's HTML.
Co-authored-by: Copilot <[email protected]>
Root-caused the slow-upload complaint to the Pi's own congested 2.4GHz
WiFi radio (confirmed via iperf3 - same degraded throughput even on
pure local LAN traffic, ruling out Cloudflare/ISP). Since the Pi 3 B
has no 5GHz radio, add a way to bypass it for the internet-bound leg:
a phone opens /dash and manually relays pending dashcam files straight
to the remote server over its own network, reusing the exact same
chunked upload protocol the Pi's client already uses.
- server/app.py: add CORS headers (after_request) so the phone's
browser can call the remote server cross-origin from a page served
by the Pi.
- app.py: new endpoints - GET /api/dash/pending (list not-yet-uploaded
files), GET /api/dash/file/<path> (Range-request-capable download,
path-traversal safe), GET /api/dash/relay/config (hands the phone
the device credentials), POST /api/dash/relay/heartbeat|stop (lease-
based pause of the Pi's own background uploader, self-heals if the
phone's tab closes uncleanly), POST /api/dash/relay/mark_uploaded.
- modules/app_upload.py: list_pending_dashcam_files(), relay lease
helpers, _upload_dashcam_files() now pauses while a relay lease is
active.
- modules/app_sql.py: new dashcam_hash_cache table so a file's sha256
is only computed once (keyed by path+size+mtime) instead of being
recomputed on every loop iteration/pending-list call - benefits the
existing background uploader too.
- templates/dash.html / static/js/dash.js: new "Relay via phone" card
- per-file and "relay all" buttons (manual trigger only, per user
preference), downloads+uploads one 40MB slice at a time so the
phone never holds a whole file in memory, retries chunks up to 3x.
- PLAN.md/readme.md/server/README.md updated (Phase 11).
Tested via a full local end-to-end simulation driving the real Pi and
server Flask apps side by side (Postgres mocked only at the db module
boundary - all HTTP/chunking/auth/CORS logic is real): verified Range
downloads (206), byte-for-byte reassembly via SHA256, path-traversal
rejection, and the relay lease correctly pausing/resuming the
background uploader. Not yet tested against a real phone/live network.