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:
@@ -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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user