Full UI redesign pass complete: persistent tab shell with an always-visible background map, Modern Professional Dark theme, monochrome dark map tiles, offline skeleton map, a shared GlassPanel/FloatingPill component kit, customizable HUD telemetry widgets, and the Map HUD / Plan & Route Planning / Rides History screen rebuilds. 374 tests passing, up from 316. The APK is a fresh release build (debug-signed, no release signing config exists yet). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012Xki7YAcc2TiN2PRZJ2tXr
361 lines
17 KiB
Markdown
361 lines
17 KiB
Markdown
> **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.
|
|
|
|
---
|
|
|
|
# Backlog — v3 and the ROADMAP
|
|
|
|
Everything known-outstanding, with enough context to pick up cold. Nothing here is
|
|
committed to — it is a menu, roughly ordered by value.
|
|
|
|
**v3 is now broken out into tickets: [v3/README.md](v3/README.md).** This document stays
|
|
as the reasoning; the tickets are the executable form.
|
|
|
|
**v3 is everything that can be built with no server. The ROADMAP is everything that
|
|
cannot, plus whatever bugs and features come up that need triage before the next v3-style
|
|
ticket batch.** The no-server split is still the most useful structural fact in this
|
|
document: it means v3 can proceed indefinitely without anyone deciding to run
|
|
infrastructure or hold other people's location data. The ROADMAP is where everything else
|
|
gets managed first.
|
|
|
|
**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. 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.
|
|
|
|
---
|
|
|
|
## 3. Activity type per ride
|
|
|
|
Promoted out of the ideas list, because it turned out to be a **positioning decision**
|
|
rather than a feature. See [LAUNCH.md](LAUNCH.md).
|
|
|
|
Everything user-facing says motorcycle, but the recording pipeline never did: it records
|
|
positions, speeds and altitudes, and nothing in it cares what you are sitting on. Dylan
|
|
already rides both bikes and motorcycles.
|
|
|
|
**Why it matters beyond the feature:** it decides the store category, the screenshots and
|
|
who ever finds the app. Cyclists are a far larger audience than motorcyclists and are
|
|
already used to paying for ride apps. That question is much cheaper to settle before a
|
|
store listing exists than after.
|
|
|
|
### Schema
|
|
|
|
An `activity` column on `Trip`, stored as a text enum like `TripState`:
|
|
|
|
```
|
|
motorcycle · bicycle · scooter · skateboard · running · walking · other
|
|
```
|
|
|
|
This is the **first real migration** the port will ship. The destructive fallback is gone
|
|
for good, so it needs `m.addColumn(trips, trips.activity)` with a default of `motorcycle`
|
|
for existing rows, `schemaVersion` bumped to 2, and a migration test that opens a v1
|
|
database and asserts the rides survive. Getting that path right once matters more than the
|
|
feature does — every later schema change depends on it.
|
|
|
|
### Type should drive defaults, not just labels
|
|
|
|
This is where the real value is, and it is easy to miss:
|
|
|
|
| Setting | Why it differs |
|
|
|---|---|
|
|
| Speed noise floor (1.5 km/h) | Fine for a motorcycle; wrong for walking, where real movement lives near it |
|
|
| Speed histogram bucket (10 km/h) | Useless for running — everything lands in one bucket. Wants ~1 km/h. |
|
|
| Accuracy gate (50 m) | A motorcycle at speed can tolerate looser fixes than a walker |
|
|
| Elevation smoothing window | Tuned at 15 samples for 2 Hz road speed; a slower activity covers less ground per sample |
|
|
| Map fit zoom | A 2 km walk and a 200 km ride want different defaults |
|
|
|
|
Treat these as a per-activity profile rather than scattering `if (activity == …)` through
|
|
the code.
|
|
|
|
### Do not put a picker in front of Start
|
|
|
|
The founding premise is press-and-go with gloves on. A modal asking "what are you doing?"
|
|
before recording begins would undo that.
|
|
|
|
Better: **default to the last activity used**, and make it editable on the trip detail
|
|
screen afterwards, next to rename. Most people do the same thing most days, and the one
|
|
time they don't, they can fix it after.
|
|
|
|
### Follow-ons, once the column exists
|
|
|
|
- **Filter and group the trips list** by activity
|
|
- **GPX `<type>` on `<trk>`** — Strava and Garmin read it, so an exported ride imports as
|
|
the right activity instead of defaulting to something wrong
|
|
- Per-activity totals, if a stats screen ever appears
|
|
|
|
---
|
|
|
|
## 4. Route drawing and planning
|
|
|
|
Drop pins on a map, have the **shortest path between them resolved along real roads**, and
|
|
get distance and estimated ride time back before setting off. Save it, ride it later.
|
|
|
|
This is *pre*-ride planning: a genuinely new mode alongside recording, not an extension of
|
|
it. It needs no ride in progress, no server of its own, and could be built entirely
|
|
standalone — which makes it the largest thing in v3 that carries no dependency on anything
|
|
else.
|
|
|
|
### It needs a routing engine, and that is the whole decision
|
|
|
|
Straight lines between pins are easy and reuse `haversineMeters`. **Road-snapped shortest
|
|
path is not** — it needs a routing service over OpenStreetMap data:
|
|
|
|
| Option | Trade-off |
|
|
|---|---|
|
|
| **Public OSRM demo server** | Free, zero setup, **not for production use** and rate-limited. Fine for prototyping only. |
|
|
| **Self-hosted OSRM** | Fast, well-understood. Needs a machine and a regional OSM extract (a province is a few GB). |
|
|
| **GraphHopper** | Self-hostable, good cycling and motorcycle profiles, friendlier ETAs |
|
|
| **Valhalla** | Best multi-modal profiles, heavier to run |
|
|
| **Commercial (Mapbox, Google)** | No ops, per-request billing, an API key in the app |
|
|
|
|
**Profiles matter here more than usual.** A motorcycle route and a bicycle route between
|
|
the same two pins are genuinely different, and cycling engines avoid motorways while
|
|
motorcycle riders often want the twisty road rather than the fast one. This is where
|
|
[section 3](#3-activity-type-per-ride) pays off — the activity picks the routing profile.
|
|
|
|
### Estimated time is a promise, and an easy one to get wrong
|
|
|
|
Routing engines return a duration based on posted speed limits. That is not how long *you*
|
|
take. Once there is real ride history, a far better estimate comes from the rider's own
|
|
average moving speed for that activity — which the app already stores on every `Trip`.
|
|
|
|
Show the engine's estimate first, and replace it with a personal one when there is enough
|
|
history to justify it.
|
|
|
|
### Data model
|
|
|
|
`Route` + `Waypoint`, **separate from `Trip`** — a plan is not a recording, and conflating
|
|
them would put unridden kilometres into ride totals. The resolved geometry (the polyline
|
|
the engine returns) should be cached on the `Route` so a saved plan opens offline and does
|
|
not re-bill a routing request every time it is viewed.
|
|
|
|
### Follow-ons
|
|
|
|
- Follow a planned route on the live map while riding (needs section 2)
|
|
- Compare a recorded ride against the plan afterwards — where you deviated, how the real
|
|
time compared to the estimate
|
|
- Export a plan as GPX so it loads into a dedicated sat-nav
|
|
|
|
---
|
|
|
|
## 5. 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. |
|
|
|
|
---
|
|
|
|
## 6. 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 2 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.
|
|
### 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. They have therefore been moved out to the **ROADMAP**
|
|
below, and should be scoped together or not at all.
|
|
|
|
Activity type (now section 4) and theming are independent and much cheaper — either could
|
|
ship alone, and activity type is the natural first v3 task because it forces the migration
|
|
path to be proven while the stakes are still low.
|
|
|
|
Live map, handlebar mounting and route *following* cluster: all three assume a visible
|
|
screen during the ride, and all three want the same map component. Route *planning*
|
|
(section 4) is the odd one out — it needs no ride in progress at all and could be built
|
|
entirely standalone.
|
|
|
|
---
|
|
|
|
---
|
|
|
|
# ROADMAP
|
|
|
|
Formerly "v4." Renamed because it now holds more than the server-dependent programme
|
|
below: it's also where Dylan's running list of bugs and features gets triaged before
|
|
becoming its own ticket batch, the way v3 was broken out. That triage takes priority over
|
|
the items below — nothing here is blocking, all of it has been waiting since before v3
|
|
started.
|
|
|
|
## The server-dependent programme
|
|
|
|
Split out from v3 deliberately. These three are **one programme, not three items**: each
|
|
needs a server, an identity system, and a privacy stance, and none of them is worth
|
|
building alone. Nothing in v3 depends on any of them.
|
|
|
|
**A fourth item that doesn't fit that programme but shares its precondition:**
|
|
self-hosted road-snapped routing (v3's V3-08/V3-09, gated on
|
|
[V3-17](v3/V3-17-osrm-hosting.md)). It needs a server the same way this section's three
|
|
items do, but nothing about identity, privacy, or the group-ride use case — it's routing
|
|
infrastructure, not a rider-facing programme. Filed under `docs/v3/` for now since that's
|
|
where the tickets it unblocks already live; flagged here because "v3 is everything
|
|
buildable with no server" stops being strictly true the moment V3-17 is picked up.
|
|
|
|
## Live group rides
|
|
|
|
Invite someone to a ride and see both of you on the map, live.
|
|
|
|
This was in the **v1 brief's goal statement**, so it has been the intended destination all
|
|
along — it was deferred from v2 as "needs real server work", and that is still the honest
|
|
summary.
|
|
|
|
### Already in place, from v1
|
|
|
|
- `TelemetryUploader` — batched POST, retry, offline-safe, and structurally unable to stall
|
|
recording
|
|
- The `synced` column and backlog semantics
|
|
- `trip_id` / `segment_id` carried **per point**, so a server can reconstruct rides *and*
|
|
their pauses
|
|
- `Config.deviceId` — a stable per-install id, enough to distinguish riders
|
|
|
|
### Missing
|
|
|
|
- **A server. Nothing exists.** This is the actual work.
|
|
- UI for the endpoint — still only reachable via `Config.setUploadEndpoint()`
|
|
- Auth, rider identity, group membership, invitations
|
|
- Other riders drawn on the map, which also needs the live map from v3 section 2
|
|
- **Live** delivery. The current uploader is a batched backlog drain on a 30 s timer, which
|
|
is right for archiving and useless for watching someone move. Live positions want a
|
|
websocket or similar, running *alongside* the existing uploader rather than replacing it —
|
|
the batch path is what guarantees no fix is ever lost.
|
|
|
|
### The decisions that are not technical
|
|
|
|
- **Location sharing is consent, not a feature.** Who can see you, for how long, and how
|
|
does it stop? Sharing that outlives the ride is a privacy incident waiting to happen.
|
|
- Ride invitations mean handling someone declining, leaving mid-ride, or losing signal for
|
|
twenty minutes — the map has to say "last seen 8 minutes ago", not silently freeze them
|
|
in place.
|
|
- This is the point where Rippr stops being a local-only app and starts holding other
|
|
people's location data.
|
|
|
|
## Accounts and sign-up
|
|
|
|
Prerequisite for everything else here. Note that Apple **requires in-app account deletion**
|
|
for any app offering account creation, and that a location app with accounts inherits real
|
|
obligations — see [LAUNCH.md](LAUNCH.md).
|
|
|
|
## Paid cloud backup
|
|
|
|
Ongoing storage of rides over time, and the only monetization model that justifies
|
|
recurring money — because it is the only one with recurring costs. Needs accounts first,
|
|
plus decisions on hosting, pricing, and what happens to someone's history when they stop
|
|
paying.
|
|
|
|
---
|
|
|
|
# Things that must not regress · all versions
|
|
|
|
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`.
|
|
|
|
---
|
|
|
|
# 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.
|