Backlog: add route drawing, split server work into v4

Route drawing promoted to a full section. The key point is that road-snapped
shortest path needs a routing engine, and that is the whole decision -- OSRM,
GraphHopper or Valhalla self-hosted, or commercial with a key in the app.
Profiles matter more than usual here, since a motorcycle route and a bicycle
route between the same pins genuinely differ, which is where activity type pays
off. Also flagged that a routing engine's ETA is based on posted limits, not on
how fast the rider actually goes -- and the app already stores enough history to
do better.

Group rides, accounts and paid cloud backup moved out to a new v4 section. They
are one programme, not three: each needs a server, identity and a privacy stance,
and none is worth building alone. Recorded that live positions want a websocket
alongside the existing batched uploader rather than replacing it, since the batch
path is what guarantees no fix is lost -- and that location sharing is a consent
question, not a feature.

Renamed V3-BACKLOG.md to BACKLOG.md, since it now covers both, and made the
split explicit up front: v3 is everything buildable with no server, v4 is
everything that cannot be. That means v3 can proceed indefinitely without anyone
deciding to run infrastructure or hold other people's location data.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 10:31:10 -05:00
parent af05c5e061
commit 82754de9c4
3 changed files with 134 additions and 50 deletions

View File

@@ -13,10 +13,14 @@
--- ---
# v3 backlog # Backlog — v3 and v4
Everything known-outstanding as of v2.0.1, with enough context to pick up cold. Everything known-outstanding, with enough context to pick up cold. Nothing here is
Nothing here is committed to — it is a menu, roughly ordered by value. committed to — it is a menu, roughly ordered by value.
**v3 is everything that can be built with no server.** **v4 is everything that cannot.**
That split is the most useful thing in this document: it means v3 can proceed indefinitely
without anyone deciding to run infrastructure or hold other people's location data.
**Before planning anything: run the real-ride checklist in **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 [the real-ride checklist](port/REAL-RIDE-CHECKLIST.md).** Several items below may turn out to be non-issues, and
@@ -57,29 +61,7 @@ covering: navigation record→trips→detail→back, rotation/state retention, e
--- ---
## 2. The original v3 candidate — live group ride view ## 2. Live map on the recording screen — **decision reversed**
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. 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 Dylan has since asked for one — for visual appeal, and because **people may mount the phone
@@ -104,7 +86,7 @@ What still holds regardless:
--- ---
## 4. Activity type per ride ## 3. Activity type per ride
Promoted out of the ideas list, because it turned out to be a **positioning decision** Promoted out of the ideas list, because it turned out to be a **positioning decision**
rather than a feature. See [LAUNCH.md](LAUNCH.md). rather than a feature. See [LAUNCH.md](LAUNCH.md).
@@ -165,6 +147,59 @@ time they don't, they can fix it after.
--- ---
## 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 ## 5. Smaller items
| Item | Notes | | Item | Notes |
@@ -185,40 +220,89 @@ time they don't, they can fix it after.
Terse on purpose. Unshaped, to be consolidated later. Terse on purpose. Unshaped, to be consolidated later.
- **Live map while recording.** More visually appealing than a numbers screen. Reverses the - **Live map while recording.** More visually appealing than a numbers screen. Reverses the
v2 decision — see section 3 for the constraints that survive it. 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 - **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 match the icon. Decide on an actual visual identity and push the UI toward something
polished rather than merely clean. 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.
- **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 ### Threads running through these
Sign-up, cloud backup and group ride are one programme, not three: they all need a server, 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. identity, and a privacy stance. They have therefore been moved out to **v4** below, and
should be scoped together or not at all.
Activity type (now section 4) and theming are independent and much cheaper — either could 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 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. path to be proven while the stakes are still low.
Live map, handlebar mounting and waypoint following also cluster: all three assume a Live map, handlebar mounting and route *following* cluster: all three assume a visible
visible screen during the ride, and all three want the same map component. Route planning 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 (section 4) is the odd one out — it needs no ride in progress at all and could be built
standalone. entirely standalone.
--- ---
## 7. Things that must not regress ---
# v4 — everything that needs a server
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.
## 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. Hard-won and easy to undo by accident. Each has a comment in the code explaining why.
@@ -239,7 +323,7 @@ Hard-won and easy to undo by accident. Each has a comment in the code explaining
--- ---
## 8. Reading order for picking this up cold # Reading order for picking this up cold
1. [../README.md](../README.md) — what the app is and its current state 1. [../README.md](../README.md) — what the app is and its current state
2. [ARCHITECTURE.md](ARCHITECTURE.md) — why it is built this way 2. [ARCHITECTURE.md](ARCHITECTURE.md) — why it is built this way

View File

@@ -121,7 +121,7 @@ Everything in Stage 1, plus the parts that are genuinely work.
Everything user-facing currently says motorcycle, but the recording pipeline is entirely Everything user-facing currently says motorcycle, but the recording pipeline is entirely
activity-agnostic — it records positions and speeds, and nothing in it cares what you are activity-agnostic — it records positions and speeds, and nothing in it cares what you are
sitting on. `V3-BACKLOG.md` already carries **activity type per ride** as an idea. sitting on. `BACKLOG.md` already carries **activity type per ride** as an idea.
That matters here rather than only in the backlog, because it decides the store category, That matters here rather than only in the backlog, because it decides the store category,
the screenshots and who finds the app. Cyclists are a far larger audience than the screenshots and who finds the app. Cyclists are a far larger audience than

View File

@@ -756,7 +756,7 @@ regex including unverified items, and that lesson is written into the audit's pr
- `docs/ARCHITECTURE.md` — the durable reasoning, with a table of what changed and why, - `docs/ARCHITECTURE.md` — the durable reasoning, with a table of what changed and why,
the Float→double divergence, and the two iOS settings that are easy to get wrong the Float→double divergence, and the two iOS settings that are easy to get wrong
- `README.md` — status, the parity harness, and the two native bugs this port found - `README.md` — status, the parity harness, and the two native bugs this port found
- `docs/V3-BACKLOG.md` — carried over with a header noting the two items whose status - `docs/BACKLOG.md` — carried over with a header noting the two items whose status
changed (UI tests are no longer a gap; elevation can now be checked against Kotlin) changed (UI tests are no longer a gap; elevation can now be checked against Kotlin)
- The native repo's `README.md` now opens with a pointer here, and records both bugs - The native repo's `README.md` now opens with a pointer here, and records both bugs