fix: pre-road audit (Phase 30) — upload-loop OSError, stats lock scope, server int guards

This commit is contained in:
Mathieu Moran
2026-08-15 22:25:17 -04:00
parent 386920b258
commit c6b1384b34
4 changed files with 62 additions and 4 deletions
+45
View File
@@ -2365,6 +2365,51 @@ Open items from the drive — fill in as reported:
---
### Phase 30: Pre-road code audit & hardening (round 2) — ✅ DONE (2026-08-15)
**Completed 2026-08-15.** Read-only audit of every Pi module, the server, the deploy
scripts/units/sudoers, SQL migrations, and the Android build config, focused on "is this
ready for the road". 99 tests pass; all Python compiles; every referenced template exists;
sudoers entries were verified against the exact commands the app invokes (`mount -o r[ow] *`
correctly matches `mount -o ro /dev/sda1 /mnt/dashcam` because sudoers `*` spans whitespace).
**Tracking-path safety**: none of the fixes below touch the core tracking path
(`app_gps._poll_loop` → `app_sql._log_loop`/`insert_tracking`, `app_obd.obd_speed`,
`app_trip.build_trip_summaries`/`refresh_trips`). The only `app_sql` change is in
`get_overview_stats()` (the `/api/stats` + status-page reader, not the logger). Verified
after the edits: compile clean + 99/99 tests, including the trip-building and trip-SQL suites.
#### Bugs found and fixed
| File | Severity | Bug | Fix |
|---|---|---|---|
| `modules/app_sql.py` | MEDIUM | `get_overview_stats()` ran a `SUM(DISTANCE_KM)` query on the shared SQLite connection *outside* `_conn_lock` — a race with the logging thread using the same connection | Moved the query inside the existing `with _conn_lock:` block |
| `modules/app_upload.py` | MEDIUM | `_upload_loop()` only caught `requests.RequestException`/`CellularError`; an `OSError` (dashcam file deleted between listing and hashing, disk full) escaped and permanently killed the upload thread | Added `OSError` to the retry/backoff `except` clause |
| `server/app.py` | LOW | `portal_trips_compat()` called `int(request.args.get("limit"))` without try/except — `?limit=abc` → 500 | Wrapped in `try/except (ValueError, TypeError)` → default 200 |
| `server/app.py` | LOW | `portal_select_device()` called `int(device_id)` without try/except — non-numeric id → 500 | Validate + return 400 on non-numeric id |
#### Audit false positives (verified NOT bugs)
- `modules/app_account.py` `_load_config()` **does** end in `return {}` (the initial read
omitted the final no-newline line; confirmed via `cat -A`/`od -c`). The file is CRLF —
cosmetic only.
- `server/app.py` `portal_dashcam_stream()` Range parsing: a leading `-` (e.g. `bytes=-5`)
always parses to an empty `start_s` and is rejected by `int("")` → 416, so no negative
seek can ever reach `f.seek()`. Already safe.
- OBD unit conversions: verified the installed `obd==0.7.3` + pint handle
`.to("kph")`, `.to("percent")`, and `.to("degC")` correctly.
#### Known / deferred (documented, not changed this phase)
- `modules/app_settings.py` `update_settings()` requires the full payload and reports omitted
booleans as "Must be true or false." — strict but safe (the web UI always sends every field).
- `modules/app_dash.py`: `dashcam_storage_usage()` clobbers its result on unmount failure, and
`import_dashcam()` releases `_device_lock` while copying so a concurrent delete could remount
read-write mid-copy. Low-likelihood; left for a follow-up.
- `app.py` `/api/local-video/convert` reports `"started"` for a `.mp4` (a no-op). Cosmetic; left
untouched to avoid editing the CRLF `app.py` for a UX-only nit.
- `android/gradle/libs.versions.toml`: `compose-compiler = "1.5.15"` is dead config (the build
uses the `kotlin.compose` plugin). Harmless.
---
### Deployment automation — fresh-OS installer + hardware→software guide (2026-08-15)
Added a one-shot fresh-install entrypoint for the deploy folder and fixed three
deployment gaps that would have broken a from-scratch install: