diff --git a/PLAN.md b/PLAN.md
index e3b919f..8e7f5d2 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -1194,6 +1194,53 @@ including two UX polish passes and multiple bug-fix rounds.
- Synthetic test trip in DB: 50 points, Old Port → Plateau Montreal, 3 km, 8 min, 43.5 km/h max.
- All 63 tests pass. Verified on Pi at 390×844 (mobile) and 1280×800 (desktop).
+### Phase 23.5: UI polish, dashcam improvements, odometer, WiFi upgrade — ✅ DONE
+**Completed 2026-08-10.** A focused multi-session polish and hardware upgrade pass on top of Phase 23.
+
+**Dashcam & video:**
+- Async dashcam import — import now fires in a background thread, returns `{status:"started"}` immediately, and the UI polls `/api/dash/import/status` every 3 s. Eliminates mobile timeout and `_device_lock` blocking all routes during copy.
+- Per-file Convert button on MOV/non-MP4 files. `/api/local-video/convert` POST with `force=True` bypasses the auto-convert toggle.
+- Real ffmpeg progress via `-progress pipe:1` — reads `out_time_ms` lines in a thread, computes `pct = out_time_s / duration_s * 100`. Old `elapsed/duration` estimate was wrong for RPi3 (encodes slower than realtime).
+- Bitrate set to `-b:v 4M -b:a 128k` (~100 MB per 3-min clip); previous default was unusably small.
+- `.converting.mp4` temp files excluded from file list and upload queue.
+- Thumbnail patching during conversion — `refreshConvertingOnly()` updates badge text in-place without a full re-render; full render only when file count changes. Fixes thumbnail-reload flicker every 10 s.
+- Mobile converting badge: spinner + % shown under thumbnail on mobile (`dash-thumb-meta`, `dash-thumb-converting`).
+- Mobile checkbox column now visible in dashcam file list.
+- Download button on Pi video files (`/api/local-video/file?download=1`, `as_attachment=True`).
+
+**Odometer & trips:**
+- `_log_loop` passes `app_obd.odometer` to `insert_tracking()` — OBD odometer now stored in every tracking row.
+- `START_ODOMETER` / `END_ODOMETER` columns added to trips table (auto-migrated).
+- `build_trip_summaries()` reads odometer from first/last tracking row of each trip; `list_trips()` falls back to cumulative distance sum for legacy trips with NULL odometer.
+- Overview dashboard: `odometer_km` (MAX from tracking) and `video_count` now included in `/api/stats`.
+- Trip history table: new "Odometer" column showing `start → end km`.
+
+**Settings:**
+- Separate upload toggles: "Upload trips & GPS logs" and "Upload dashcam videos (WiFi only)" are now independent switches. `TRIPS_UPLOAD_SETTING_KEY` added to `app_upload.py`.
+
+**Map & location:**
+- Recenter (⊕) button on dashboard GPS card and history route map.
+- Offline reverse geocoding via `reverse_geocoder` Python library (~20 MB KD-tree, no internet needed). Exposed as `/api/geocode?lat=&lon=` — browser never calls any external service.
+- When internet is available (phone hotspot), `/api/geocode` tries Nominatim first for street-level accuracy, falls back to `reverse_geocoder` for city/region. Results cached with `functools.lru_cache(512)` keyed at 4-decimal precision (~11 m).
+- Dashboard GPS card footer: location name shown below lat/lon, updated on first fix and after moving >50 m.
+- Trip history scrubber: location name shown in timeline panel, debounced 600 ms while scrubbing.
+- GPS coords parsed as `float` before `toFixed()` — was crashing silently on string input from socket.
+
+**Trip history UX:**
+- Imperial units removed from `formatDistance` and `formatSpeed` (km / km/h only).
+- Replay button hidden on mobile (`d-none d-sm-table-cell`); tapping anywhere on the trip row triggers replay instead (`data-trip-id` on `
`, `cursor: pointer`).
+- `data_table` macro supports optional `class` on column headers.
+
+**WiFi upgrade — TP-Link Archer T3U (RTL8812BU):**
+- RTL8812BU DKMS driver built and installed on Pi (`morrownr/88x2bu-20210702`, kernel 6.12.96+rpt-rpi-v8).
+- `INTERFACE = "wlan1"` in `yaris_uplink.py`; `install_netwatch.sh` rebinds home-wifi, Hotspot, phone-hotspot to wlan1.
+- `resolve_interface()` checks `/sys/class/net/wlan1` at startup — falls back to `wlan0` automatically if dongle is absent or driver fails. `INTERFACE` is module-level so all functions use the correct iface.
+- `restore_fallback()` extended: if wlan1 primary fails, also tries `home-wifi-fb` and Hotspot on wlan0.
+- `install_netwatch.sh` creates `home-wifi-fb` profile on wlan0 with `autoconnect=no` and lower priority — NM uses it only when explicitly activated by `restore_fallback()`.
+- `dhcp-client-id=yaris-obd` in phone-hotspot keyfile — phone DHCP assigns a consistent lease by client-id, giving the Pi a stable IP on the phone hotspot network.
+- 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
diff --git a/readme.md b/readme.md
index 29faa40..1e7a666 100644
--- a/readme.md
+++ b/readme.md
@@ -28,7 +28,7 @@ i have a dream
Dashboard — mobile (390 px) and desktop (1280 px):
-
+
## Features
@@ -65,8 +65,13 @@ Dashboard — mobile (390 px) and desktop (1280 px):
storage pressure, upload backlog, temperature/power faults, and GPS loss while moving. Optional
ntfy delivery is available but remains disabled until configured
- 🛣️ **Trip history and route replay** — detects motion from OBD wheel speed with sustained GPS
- displacement as a fallback, summarizes distance and speed in metric/imperial units, and replays
- each route as a line on the offline map with a timestamped play/pause timeline and moving marker
+ displacement as a fallback, summarizes distance and speed in metric units (km / km/h), and replays
+ each route as a line on the offline map with a timestamped play/pause timeline, moving marker, and
+ live reverse-geocoded location name during scrubbing. Tap any trip row on mobile to load the route
+- 🗺️ **Offline reverse geocoding** — a `/api/geocode` route on the Pi uses the `reverse_geocoder`
+ library (20 MB world dataset, no internet needed) for city/region, and upgrades to Nominatim
+ street-level when an internet uplink is available. Results are LRU-cached in-process. The browser
+ never calls any external geocoding service
- 🖥️ **Offline web UI** built with Flask, Jinja, Bootstrap 5, Socket.IO, Chart.js, and Leaflet,
all served locally with a shared responsive page shell
- 🔌 **Headless Pi deployment** as a self-restarting `systemd` service with a 10-second BCM2835
@@ -79,6 +84,7 @@ Dashboard — mobile (390 px) and desktop (1280 px):
| Raspberry Pi 3 Model B Rev 1.2 | Web UI host, logger, uploader, and WiFi hotspot |
| Waveshare SIM868 HAT | GNSS (GPS) + optional GSM/GPRS uplink, unused Bluetooth |
| USB OBD-II adapter | PL2303-based, ELM327-compatible (originally Bluetooth, switched to USB) |
+| TP-Link Archer T3U (RTL8812BU) | USB WiFi dongle — AC1300 dual-band, replaces onboard wlan0 for hotspot + uplink |
| USB dashcam | Any brand — detected as generic USB mass storage |
| 2013 Toyota Yaris | The patient |
@@ -103,12 +109,24 @@ For the remote server (receives trip logs + dashcam uploads), see [server/README
## Project status
-Phases 1–13, 15–23 are complete. The full responsive web UI redesign (Phase 23) is done:
-desktop sidebar with collapse, mobile bottom tab bar, 6 live OBD stat cards, Overview stats
-card (trips, distance, drive time, top speed, video count, active DTC), merged GPS+coords card,
-reworked speed history chart (gradient, rolling window, session stats, auto-expand on desktop),
-collapsible dashcam sections with mobile-friendly icon-only action buttons, compact trip history
-with short mobile date format and icon-only Replay button, and live-wired DTC page.
+Phases 1–13, 15–23, and 23.5 are complete. The full responsive web UI redesign (Phase 23) is done,
+followed by a major polish and hardware upgrade pass (Phase 23.5):
+
+- **Async dashcam import** with progress polling — no more mobile timeouts
+- **Per-file Convert button** with real ffmpeg progress (reads `out_time_ms` via `-progress pipe:1`)
+- **Odometer tracking** from OBD in every trip row and the overview dashboard
+- **Separate upload toggles** for trips/logs vs dashcam videos
+- **Download button** on Pi video files
+- **Offline reverse geocoding** — `/api/geocode` route on the Pi uses `reverse_geocoder` (20 MB,
+ no internet needed); falls back to Nominatim for street-level accuracy when online. Browser never
+ calls any external service
+- **Live location name** on dashboard GPS card (updates every ~50 m) and trip scrubber timeline
+- **Recenter button** on dashboard and history maps
+- **Imperial units removed** — metric only (km / km/h)
+- **Mobile trip replay** — tap any trip row to load the route; Replay button kept on desktop only
+- **TP-Link Archer T3U WiFi dongle** — RTL8812BU DKMS driver installed; all WiFi traffic moved to
+ `wlan1` (AC1300, dual-band). `wlan0` onboard stays silent as automatic fallback if dongle is
+ absent. Consistent phone-hotspot IP via `dhcp-client-id`
Phase 14 is partially complete: the Pi's hardware watchdog is deployed and reboot-verified.
Read-only-root storage hardening is deferred until persistent storage is moved off the SD root