Add the Rippr Flutter port: source, history bundle, and installable APK

The port runs on Android and iOS and is feature-complete; the native Android app
is superseded but kept, since it is still the only version that has recorded real
rides.

rippr-flutter-1.0-debug.apk is package com.rippr.port, deliberately different
from the native com.rippr so both install side by side. Recording the same ride
on both at once is the strongest available check that the port is faithful.

Added INSTALL.md covering both platforms. Android is a one-line adb install; iOS
has no APK equivalent and must be built and signed through Xcode with a free
Apple ID, which gives a 7-day profile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 22:37:45 -05:00
parent 64f5dff30b
commit 0bc42b2e5a
98 changed files with 8417 additions and 24 deletions

View File

@@ -0,0 +1,196 @@
> **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.