> **Carried forward from the native repo** (`~/dojo/rippr/docs/v3/BACKLOG.md`) when the > Flutter port completed. Two items below have changed status since it was written: > > - **Compose UI tests** — no longer a gap. The port has 15 widget tests plus 4 > integration tests; see `docs/port/PARITY-AUDIT.md`. > - **Elevation gain accuracy** — the algorithm is now proven bit-identical across Kotlin > and Dart (`tool/parity/run.sh`), so any future tuning can be checked against the > original rather than guessed at. The instruction below still stands: **do not tune it > blind.** > > Everything else carries over unchanged, including the v3 ideas and the > "must not regress" list. --- # v3 backlog Everything known-outstanding as of v2.0.1, with enough context to pick up cold. Nothing here is committed to — it is a menu, roughly ordered by value. **Before planning anything: run the real-ride checklist in [the real-ride checklist](port/REAL-RIDE-CHECKLIST.md).** Several items below may turn out to be non-issues, and others may appear that nobody has thought of. --- ## 1. Carried over from v2 — the honest debt ### Elevation gain accuracy · *needs real data first* ~30 m of phantom gain per ten stationary minutes against synthetic ±8 m uniform noise. Real GPS altitude error is *correlated* rather than uniform, so the true behaviour is unknown. The current implementation is a 15-sample moving average plus reversal hysteresis (see [ARCHITECTURE.md](ARCHITECTURE.md)). A naive version reported 1498 m over a parked bike, so the guard rails matter. **Do not tune this blind.** Record a flat ride, check whether the reported gain is plausible, and only then adjust. If it needs work, options are a longer smoothing window, a larger threshold, or using barometric pressure where available (much more accurate than GPS altitude, and most phones have the sensor). ### Compose UI tests · *the largest coverage gap* Zero UI tests across six screens. Everything was verified by manual screenshot. Worth covering: navigation record→trips→detail→back, rotation/state retention, empty states, `NotFound`, chart degradation below two points, selection mode enabling Merge only at two. ### Unmeasured, and probably should be - **Battery drain** over a multi-hour ride — never measured, and it is the thing most likely to make the app unusable in practice - **Map memory across repeated navigation** — the osmdroid lifecycle is a known hazard and the wiring was never leak-tested - **GPX import into Strava/Garmin** — validated against an XML parser, but schema validity does not guarantee a consumer accepts it --- ## 2. The original v3 candidate — live group ride view Deferred from v2 as "needs real server work". This was in the **v1** brief's goal statement, so it has been the intended destination all along. Already in place: - `TelemetryUploader` — batched POST, retry, offline-safe, cannot stall recording - `synced` column and backlog semantics - `trip_id` / `segment_id` per point, so a server can reconstruct rides and pauses - `Config.deviceId` — stable per-install id to distinguish riders Missing: - **A server.** Nothing exists. This is the actual work. - **UI for the endpoint** — currently only reachable via `Config.setUploadEndpoint()` - Other riders' positions on a map, and a live map at all (see below) - Auth, rider identity, group membership **Worth deciding early:** this is the point where Rippr stops being a local-only app. That brings hosting, privacy, and location-sharing consent into scope. --- ## 3. Live map on the recording screen — **decision reversed** v2 deliberately shipped no live map, on the reasoning that the phone rides in a pocket. Dylan has since asked for one — for visual appeal, and because **people may mount the phone on the handlebars** to watch the route live. Treat the v2 stance as superseded. **The handlebar case changes the premise, not just the feature.** v1 and v2 were both built around "start it, pocket it, stop it". A mounted phone is a different product with different constraints, and it is worth deciding explicitly whether that becomes a first-class mode: - **Screen on for the whole ride** — battery goes from "a background service" to "a service plus a lit screen plus continuous map rendering". Measure before committing. - **Sunlight legibility** — the current dark theme is chosen for glanceability, but daylight behind a visor is a different problem. - **Glove-sized targets** — already partly handled (72dp buttons); a map needs the same care. - **Keep-screen-awake** handling, and what happens on a call or notification. What still holds regardless: - **`TrackingService` must never reference a map.** Rendering belongs to the Compose lifecycle of a visible screen, not the service. - **No tile fetch or redraw while backgrounded**, even in mounted mode. --- ## 4. Smaller items | Item | Notes | |---|---| | **Trip splitting** | Merge exists; split does not. The natural counterpart. | | **SAF export** | Dropped in T16 as unnecessary — share sheet covers it. Add if a real need appears. | | **Auto-pause** | Detect a stop and pause automatically. Rejected in v2 as unreliable in traffic; revisit only with real ride data showing it would help. | | **Distance units** | Metric only, hardcoded. Trivial to add a preference. | | **Settings screen** | None exists. `Config` has endpoint, deviceId, mapEnabled — the map toggle currently lives on trip detail because one switch did not justify a screen. | | **Offline tile pre-download** | osmdroid caches what it renders; a mountain ride with no signal shows blank tiles. Respect OSM's usage policy — no bulk prefetch of their public servers. | | **Notification live stats** | Show distance/duration in the ongoing notification, readable without unlocking. | | **Crash reporting** | None. A recorder that dies mid-ride currently leaves no trace beyond logcat. | --- ## 5. Ideas Terse on purpose. Unshaped, to be consolidated later. - **Live map while recording.** More visually appealing than a numbers screen. Reverses the v2 decision — see section 3 for the constraints that survive it. - **Pick a real theme.** The current look is functional dark + safety orange, chosen to match the icon. Decide on an actual visual identity and push the UI toward something polished rather than merely clean. - **User sign-up and accounts.** Register people, give their data somewhere to live. Prerequisite for anything cloud-side, and pairs with the group-ride server in section 2. - **Activity type per ride.** Motorcycle, bicycle, skateboard, running, other. The app is not inherently motorcycle-only — the recording pipeline is activity-agnostic already. Note: adds a column to `Trip`, so it needs a real `Migration` (the destructive fallback is gone). Type could also drive sensible defaults — speed noise floor, map zoom, elevation smoothing. - **Paid cloud backup.** Ongoing storage of rides over time. Needs accounts first, plus a decision on hosting, pricing, and what happens to data when someone stops paying. - **Waypoint route planning.** Drop a series of pins on the map to "draw" a route, get distance and estimates back, and save it to ride later. This is *pre*-ride planning — a genuinely new mode alongside recording, not an extension of it. Needs its own entity (`Route` + `Waypoint`), separate from `Trip`, since a plan is not a recording. Straight-line pin-to-pin distance is easy and reuses `Geo.haversineMeters`; snapping to actual roads needs a routing service (OSRM, GraphHopper, Valhalla — self-hostable) and is a much larger step. Natural follow-ons: follow a planned route on the live map, and compare a recorded ride against the plan afterwards. ### Threads running through these Sign-up, cloud backup and group ride are one programme, not three: they all need a server, identity, and a privacy stance. Worth scoping together rather than separately. Activity type and theming are independent and much cheaper — either could ship alone. Live map, handlebar mounting and waypoint following also cluster: all three assume a visible screen during the ride, and all three want the same map component. Route planning is the odd one out — it needs no ride in progress at all and could be built entirely standalone. --- ## 6. Things that must not regress Hard-won and easy to undo by accident. Each has a comment in the code explaining why. 1. **The unbounded `Channel` + single batched writer.** Do not write to the database from the location callback. 2. **Points stamped with `tripId`/`segmentId` at creation.** Refactoring this into a write-time lookup breaks the pause guarantee silently. 3. **Recording state derived from the database.** Never reintroduce an in-memory flag. 4. **`fallbackToDestructiveMigration()` stays removed.** Any schema change ships a `Migration` against `app/schemas/com.rippr.data.AppDatabase/2.json`. 5. **Decimation is render-only.** It must never reach storage or export. 6. **`RipprTheme`'s `Surface`.** It sets `LocalContentColor`; without it, text without an explicit colour renders black-on-black and disappears. 7. **The map zoom clamp.** `zoomToBoundingBox` ignores `maxZoomLevel`; a short ride will render an empty grid without it. 8. **Instrumented tests use in-memory databases.** One previously wiped the real device database in `setUp`. --- ## 7. Reading order for picking this up cold 1. [../README.md](../README.md) — what the app is and its current state 2. [ARCHITECTURE.md](ARCHITECTURE.md) — why it is built this way 3. `~/dojo/rippr/docs/v2/PROGRESS.md` (native repo) — every bug found during v2 and how 4. [the real-ride checklist](port/REAL-RIDE-CHECKLIST.md) — **especially "What the emulator cannot verify"** 5. `~/dojo/rippr/docs/DEVELOPMENT.md` (native repo) — when you actually need to build something The v2 planning approach worked well and is worth repeating: one document per task with goal, context, design, acceptance criteria and risks, written *before* implementing, plus a running progress log recording what actually went wrong. Several bugs were caught precisely because the risk had been written down first — and one (the osmdroid lifecycle) was written down and then walked into anyway, which is its own lesson.