V3-06: live stats in the notification

RideNotificationController seam over flutter_local_notifications; RideNotificationCoordinator wires TripRepository.watchActiveTrip() to it and routes Pause/Resume actions back into RecordingEngine. Instantiated eagerly at app root rather than screen-owned, since a pocketed ride has no visible widget tree. Documents an unresolved risk: geolocator's own foreground-service notification cannot be suppressed, so two notifications may be visible until verified on a device.
This commit is contained in:
2026-08-17 15:04:34 -05:00
parent fbdb55258c
commit 5013b7002f
11 changed files with 471 additions and 13 deletions

View File

@@ -22,7 +22,7 @@ backup are v4 — see [../BACKLOG.md](../BACKLOG.md).
| [V3-03](V3-03-units.md) | Distance and speed units | S | V3-02 | Done |
| [V3-04](V3-04-live-map.md) | Live map on the recording screen | M | — | Done |
| [V3-05](V3-05-mounted-mode.md) | Mounted (handlebar) mode | M | V3-04 | Done |
| [V3-06](V3-06-notification-stats.md) | Live stats in the notification | S | — | Not started |
| [V3-06](V3-06-notification-stats.md) | Live stats in the notification | S | — | Done |
| [V3-07](V3-07-route-drawing.md) | Route drawing (pins, straight lines) | M | — | Not started |
| [V3-08](V3-08-road-routing.md) | Road-snapped routing and ETA | L | V3-07, V3-01 | Not started |
| [V3-09](V3-09-route-following.md) | Follow a planned route | M | V3-04, V3-08 | Not started |

View File

@@ -1,6 +1,6 @@
# V3-06 — Live stats in the notification
**Phase** Live map · **Depends on** nothing · **Size** S · **Status** Not started
**Phase** Live map · **Depends on** nothing · **Size** S · **Status** Done
## Goal
Distance and duration readable from the notification shade without unlocking.
@@ -50,3 +50,45 @@ raise a silent minimal one. More moving parts, but it also **restores Pause/Resu
## Out of scope
iOS. There is no equivalent live notification surface; a Live Activity is a much larger
piece of work and belongs in its own ticket.
## Outcome
Shipped as designed (option B), with one honestly-unresolved risk carried forward.
`RideNotificationController` is a seam over `flutter_local_notifications`
(`FakeRideNotificationController` for tests), the same shape as `LocationSource` and
`WakelockController`. `RideNotificationCoordinator` owns the wiring: it subscribes to
`TripRepository.watchActiveTrip()` (the same stream `activeTripProvider` exposes, updated
once per writer flush) and calls `show`/`cancel`; it subscribes to the controller's action
stream and routes `pause`/`resume` back into `RecordingEngine`, guarded so a Resume can't
fire against a trip that isn't actually paused. `rideNotificationText(Trip, {unit})` is
pure and unit-tested directly — distance/elapsed formatting, the `Paused ·` prefix, and
unit-system handling.
**Not screen-owned, deliberately.** Unlike the live map (V3-04) and mounted mode (V3-05),
which are fed from `RecordScreen`'s widget tree, the coordinator is instantiated eagerly
from `main.dart`'s `RipprApp.build` via a bare `ref.watch(rideNotificationCoordinatorProvider)`
— a pocketed ride has no visible widget tree, but the notification and Pause/Resume both
still have to work.
**Unresolved risk, flagged rather than papered over:** the ticket's own risk section names
"two notification sources fighting" as the obvious failure mode, and it is real.
geolocator's `ForegroundNotificationConfig` is what satisfies Android's foreground-service
requirement and cannot be suppressed; `flutter_local_notifications` raises a second,
independent notification. There is no documented way to merge or guarantee only one is
visible — the geolocator notification was made minimal and silent
(`geolocator_location_source.dart`) on the assumption that an `ongoing: true` notification
on the same-ish surface might collapse or de-prioritise it, but that assumption is
unverified without a device. This is exactly the kind of claim the project's testing
philosophy refuses to accept on faith — see the real-ride checklist (V3-13) and this
ticket's own "Integration on a device" test, neither of which could run here.
6 new tests, all in `test/ride_notification_test.dart`: three for `rideNotificationText`
(recording, paused-prefix, unit system), three for the coordinator using a real
`RecordingEngine` + in-memory `TripRepository` (not a mock) so pause/resume are checked in
the database per the project's standing rule, not by trusting the notification state.
`flutter analyze` clean; full suite green (237 tests, up from 231).
Not attempted: the device-only acceptance criteria (text updates without unlocking,
Pause/Resume from the shade, the notification resisting swipe-away, and the two-source
visibility question above) — all require a real Android device, per the ticket's own Tests
section.