Trips starting/ending on speed-only rows violated the trips.*GPSX NOT NULL
constraint. Use the first/last GPS-bearing row for start/end coordinates, and
skip a trip that never got any GPS fix (no location to record).
connect_obd() set _car_ever_connected without a global declaration, so the
module-level flag was never set and was_car_ever_connected() always returned
False — local trip building has been silently blocked since BUG-10 landed.
Also persist the flag to the settings table so a drive followed by a reboot
still builds trips, and clean up dead code in was_car_ever_connected().
- _log_loop: when GPS has no fix but OBD speed > 1, write speed-only rows
(NULL coordinates) instead of dropping the motion
- insert_tracking: accept None gps_x/gps_y -> NULL GPSX/GPSY
- app_trip: keep NULL-coord rows through normalize; _segment returns 0 for
missing coords; route_points drops them (no position to draw)
- tests: speed-only rows build a trip with max speed + zero distance
- docs: FEAT-12 + readme note; server already filters NULL-GPS safely
- Settings: when the Pi is on the phone's hotspot (mode phone), show the
Pi's hotspot IP (http://<ip>) with an LTE-only hint instead of the AP URL
- BUG-15 documents the phone dual-role routing loop (host hotspot + connect
to AP simultaneously breaks the uplink)
- yaris_uplink configure() keyfile: dns=8.8.8.8;1.1.1.1 + ignore-auto-dns=true
so the Pi doesn't rely on the phone's DHCP DNS, which failed to resolve
yaris.ceaf.dev (NameResolutionError) and broke uploads on the hotspot
- test asserts the DNS lines are written to the keyfile
adb-pushed files are invisible to the app due to scoped storage, so add a
'Create 120 MB test file' button in the Downloads tab that writes the file
through the app's own uid (visible in its contribution set).
setProgressAsync (fire-and-forget) was dropping/coalescing updates before
they were persisted. Switch to awaited suspend setProgress() in both
UploadWorker and DownloadWorker so each progress write lands and the
LiveData observer actually sees pct advance.
- Hide upload button when file is in uploadedNames (already on server)
- UploadWorker still guards via /status already_uploaded check
- Indeterminate progress bar during Hashing/Checking phases
- Upload label always shows percentage
- Pi route_points: emit per-point odometer (raw OBD value or baseline+prior+within-trip distance)
- Pi get_trip_route: SELECT ODOMETER, compute prior-trips distance + baseline
- Pi list_trips: fallback now starts from baseline (was 0)
- Server get_trip_route: per-point odometer (raw or simulated) with haversine accumulation
- Server list_trips: odo_start/end from baseline + cumulative (was hardcoded None)
- history.js: Odometer line in replay scrubber below speed
- Both history templates: trip-replay-odometer element
- Header now has Test connection, Sync now, Save settings in a row
- remote-config-message alert moved below header for inline feedback
- Removed buttons from Remote server credentials card
- yaris_uplink.py: PRIMARY_INTERFACE=wlan0, AP_INTERFACE=wlan1, AP_CONNECTION=Hotspot-local
- install_netwatch.sh: WLAN=wlan0, WLAN_FB=wlan1, AP on wlan1
- _ap_client_count() reads the AP interface instead of hardcoded wlan0
UploadWorker now promotes to foreground + reports phase immediately:
- 'Hashing…' while computing SHA256
- 'Checking server…' during resume status check
- 'Uploading…' with per-chunk percentage
Downloads tab renders the phase label for uploads
- CredentialStore tracks uploaded filenames in prefs
- UploadWorker success marks file as uploaded
- Downloads tab shows green CloudDone icon next to files already on server
- Upload button skipped for files already on server (dedup)
- DownloadWorker.createForegroundNotification now passes FOREGROUND_SERVICE_TYPE_DATA_SYNC
(was defaulting to 'none' → InvalidForegroundServiceTypeException crash on upload)
- Dashcam file rows show CloudDone (green, on server) / CloudQueue (grey, not uploaded)
- CredentialStore now uses getSharedPreferences instead of EncryptedSharedPreferences
- Still sandboxed by Android app isolation (other apps can't read it)
- Mobile bottom nav: white → dark (#1c1c1e) bg, white icons/text
- Mobile topbar: white → dark bg on small screens, white text/controls
- Mobile logo color: dark → white for dark bg readability
- Desktop topbar remains white (contrasts with dark sidebar)
- Settings page: SSID + password fields in In-car access card
- GET /api/hotspot reads current SSID (password never returned)
- POST /api/hotspot updates via nmcli (Hotspot-local profile on wlan0)
- deploy/yaris-hotspot.sudoers for privileged nmcli access
- Password is write-only with placeholder hint
- LaunchedEffect auto-refreshes on tab open (no more manual refresh needed)
- Imported Videos card matches USB Dashcam compact style
- Removed bulky Refresh button from imported videos
- Shared YarisHeader composable: bold YARIS + red accent bar + subtitle
- Dark theme: #1c1c1e background, #2a2a2c surface (matching web UI sidebar)
- Red accent #e63946 matches web logo underline
- Applied to Dashcam, Downloads, Settings screens
- DashcamScreen: Download + Delete as IconButton with Icons.Filled
- DownloadsScreen: CloudUpload + Delete as IconButton
- No text labels, just clean Material icons
- Sidebar bg: navy (#1a1a2e) → dark gray (#1c1c1e)
- Logo: replaced fa-car icon + text with styled YARIS wordmark
using Racing Sans One (free Google Font, OFL licensed)
- Red accent underline (#e63946) on logo
- Applied to both server (server/templates/base.html) and
Pi (templates/base.html) + mobile topbar
- Settings tab now has credential fields + network test (Test Pi, Test Server)
- Removed standalone Test tab
- Bottom nav: Dashcam, Downloads, Settings
- Deleting files show CircularProgressIndicator instead of delete button
- Active download card redesigned: spinner + filename + %, rounded progress bar
- AnimatedVisibility for progress section
- Empty state with icon
- Track _deletingNames in ViewModel for reactive UI
Nominatim was slow (rate-limited, 1 req/sec, ~4s timeouts) and unreliable
from Docker/Cloudflare (blocked or throttled). It was the root cause of
location labels failing to appear during trip scrubbing.
- server/app.py: _geocode_cached now uses reverse_geocoder exclusively
(offline, instant, city/province level). Removed Nominatim HTTP calls,
ThreadPoolExecutor, concurrent.futures import.
- app.py (Pi): same — reverse_geocoder imported at module level,
Nominatim + urllib.request removed.
- Batch endpoint simplified to sequential loop with lru_cache + dedup.
No network dependency at all — geocoding is instant and always works.
reverse_geocoder provides city/province labels (e.g. 'Montreal, Quebec')
which is the right granularity for trip scrubbing.
JS preload(): replaced N individual GET /api/geocode (concurrency 2) with
a single POST /api/geocode/batch — one HTTP roundtrip instead of up to
N/2 sequential ones. schedLoc debounce reduced 600→200ms.
Server geocode_batch(): ThreadPoolExecutor(max_workers=4) parallelises
Nominatim/reverse_geocoder calls instead of sequential loop. Deduplicates
coords before submitting futures. lru_cache absorbs duplicates within batch.
Pi geocode_batch_route(): deduplicate coords, removed stale 'if label:'
guard (label always non-empty after BUG-13 fix).
Root cause: server _geocode_cached() returned None → /api/geocode
returned HTTP 204 with no body. JS frontend treated 204 as success,
r.json() on empty body threw → showed raw-coordinate fallback.
Server fixes:
- _geocode_cached() now always returns a non-empty string (coordinate
fallback when both Nominatim and reverse_geocoder fail)
- /api/geocode always returns HTTP 200 with {label: '...'} (never 204)
- /api/geocode/batch always includes every coord key in result dict
- Nominatim call: jsonv2 + accept-language=en (aligned with Pi)
- reverse_geocoder: mode=1 (aligned with Pi)
Pi alignment:
- _geocode_cached() now checks for empty reverse_geocoder label before
returning, falling through to coordinate string (same guard as server)
- Trips 18517, 18516 show (x,y) instead of address/street name
- Likely regression in geocoding pipeline or cache lookup
- Plan: rework server-side geocoding, backport to Pi if simple
- All 10 FEATs now in strict numerical order under single Feature ideas section
- Added summary table (ID, Status, Created, Title, Phase) for quick scanning
- Created dates sourced from git history (ef3a951 through bc29a01)
- Removed orphan Ideas that landed section (FEAT-06/07 merged into main flow)
- Downloads now go to Movies/yaris-obd/ (visible in file managers)
- New Downloads tab: list downloaded files, tap to play (system player),
delete button
- FileProvider added for video playback intent
- Downloaded files auto-refresh after each download
- _portal_device_id() reads from session, falls back to DEVICE_NAME env var
- GET/POST /api/v1/portal/select-device for selector data + set
- Dropdown in server base.html topbar, JS populates from API, page reloads on change
- All portal pages (overview, trips, dashcam, DTC, device settings) now use selected device
- Server: dtc_events table (migrate_phase27_dtc.sql), db.py helpers
(upsert_dtc_snapshot, list_dtc_events, delete_dtc_event)
- Server API: POST /api/v1/dtc/batch (device auth), GET/DELETE /api/v1/portal/dtc
- Server portal: /dtc page with code, description, MIL, status, timestamps, delete
- Server nav: DTC history in sidebar + mobile bottom nav
- Pi: _upload_dtc_snapshot() in app_upload.py — uploads when snapshot changes,
skips when OBD disconnected or unchanged
- DashcamScreen: LazyColumn of imported Pi videos with thumbnails (Coil),
file size, date, upload status, converting progress
- DashcamViewModel: Retrofit-backed video list + download to phone storage
+ delete local copy
- PiApi extended: listVideos, getVideoFile (streaming), getThumbnail, deleteVideo
- DTOs: VideoListResponse, VideoFile, ConvertProgress
- Bottom nav: Dashcam tab + Test tab
- Verified: video list loads from real Pi over yaris 911 hotspot
- Simplified to single OkHttpClient — Android OS routes correctly:
10.42.0.1 → WiFi (Pi hotspot subnet)
yaris.ceaf.dev → cellular (WiFi has no internet)
- Removed NetworkBinder-based per-socket binding (EPERM on Pixel,
OS-routed approach works better for this use case)
- Verified on real Pixel: Pi GPS + server health both return 200
simultaneously while connected to "yaris 911" hotspot
- NetworkBinder functions were suspend but never actually suspended
(ConnectivityManager calls are synchronous) — made them plain fun
- MainActivity.discoverNetworks() now calls them directly, no coroutine
- Wrapped in try/catch so network discovery failure doesn't crash Activity
- Removed unused awaitNetwork() extension + coroutine imports