openspec: revise route-surface-breakdown — add async Overpass backfill + SSE
Builds on the merged A-only proposal. Broadens the capability: - breakdown now on routes AND activities; - Path 1 (sync) unchanged: BRouter waytags → breakdown at Planner save; - Path 2 (async): a `surface-backfill` pg-boss job map-matches geometry to OSM via Overpass for imports/uploads/older rows, stores the breakdown, and pushes a `surface_breakdown` SSE event so an open detail page fills in live; - privacy note: backfill sends geometry to Overpass → route via the proxy / self-hostable Overpass; documented in the manifest. Validates with `openspec validate --strict`. Specs only; no code. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
a81134332b
commit
e1e3dc5c81
4 changed files with 167 additions and 105 deletions
|
|
@ -1,46 +1,57 @@
|
|||
## Why
|
||||
|
||||
"How much of this is gravel?" is a top-three question for anyone choosing a
|
||||
route — Komoot's surface/waytype breakdown bars are one of its most-used
|
||||
features, and `@trails-cool/map-core` already ships the surface/highway colour
|
||||
palettes. We also already *compute* accurate per-segment surface and highway
|
||||
tags from BRouter while planning (`route-merge` → `EnrichedRoute.surfaces[]`),
|
||||
then **throw them away on save**. This change captures that data and shows it.
|
||||
"How much of this is gravel?" is a top-three question for choosing a route, and
|
||||
Komoot's surface/waytype bars are one of its most-used features.
|
||||
`@trails-cool/map-core` already ships the surface/highway colour palettes, and
|
||||
the Planner already *computes* accurate per-segment surface/highway from BRouter
|
||||
(`route-merge` → `EnrichedRoute.surfaces[]`) — then **discards it on save**.
|
||||
This change captures that data, shows it as proportion bars, and **backfills
|
||||
everything else** (imports, uploads, older rows) asynchronously so the bars
|
||||
aren't limited to Planner routes.
|
||||
|
||||
## What Changes
|
||||
|
||||
- On **route detail**, show surface and waytype **proportion bars** (e.g.
|
||||
"62% asphalt · 28% gravel · 10% path"), coloured with the `map-core`
|
||||
`SURFACE_COLORS` / `HIGHWAY_COLORS` palettes, with per-segment km/percent.
|
||||
- **Persist the breakdown for Planner-created routes**: compute a
|
||||
distance-weighted surface + waytype distribution from the BRouter waytags at
|
||||
save time, send it in the planner→journal handoff, and store it on the route.
|
||||
- Degrade gracefully: routes without breakdown data (older routes, or activities
|
||||
/ manually-imported GPX that never had waytags) simply don't show the bars.
|
||||
- On **route and activity detail**, show surface and waytype **proportion bars**
|
||||
(e.g. "62% asphalt · 28% gravel · 10% path"), coloured with the `map-core`
|
||||
`SURFACE_COLORS` / `HIGHWAY_COLORS` palettes, with per-category km/percent.
|
||||
- **Path 1 — synchronous (Planner routes):** compute a distance-weighted
|
||||
surface + waytype distribution from the BRouter waytags at save time, send it
|
||||
in the planner→journal handoff, store it. Accurate, free, no external call.
|
||||
- **Path 2 — async backfill (everything else):** for a route/activity that has
|
||||
geometry but no breakdown (imports, manual uploads, pre-existing rows), a
|
||||
background job map-matches the geometry to OSM ways via Overpass, computes the
|
||||
breakdown, stores it, and **pushes an SSE event** so an open detail page fills
|
||||
the bars in live.
|
||||
- Degrade gracefully: no geometry / not-yet-backfilled → no bars (or a
|
||||
"computing…" state while a backfill is in flight).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `route-surface-breakdown`: the surface/waytype proportion bars on route
|
||||
detail, how the distribution is derived from BRouter waytags, persisted at
|
||||
save, and rendered.
|
||||
- `route-surface-breakdown`: the surface/waytype proportion bars on route &
|
||||
activity detail — derived synchronously from BRouter waytags (Planner) or
|
||||
asynchronously via an Overpass backfill job with an SSE live-update.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
<!-- `planner-journal-handoff` gains one optional field in its save payload;
|
||||
noted in design, but the breakdown is a new capability, not a change to the
|
||||
handoff's existing requirements. -->
|
||||
<!-- `planner-journal-handoff` gains one optional payload field; `background-jobs`
|
||||
and `sse-broker` gain a new job + event, but those are additive uses of
|
||||
existing transports, owned by this new capability. -->
|
||||
|
||||
## Impact
|
||||
|
||||
- **Schema**: a nullable `surface_breakdown` jsonb column on `journal.routes`
|
||||
(additive — `pnpm db:push`, no data migration).
|
||||
- **Planner**: `api.save-to-journal` computes the distance-weighted breakdown
|
||||
from the session `EnrichedRoute` (surfaces/highways + coordinates) and adds it
|
||||
to the POST body.
|
||||
- **Journal**: the route callback (`api.routes.$id.callback`) accepts + stores
|
||||
it; the route detail loader exposes it; a `SurfaceBreakdown` component renders
|
||||
the bars (reusing `map-core` palettes + `stats.ts` distance formatting).
|
||||
- **No new external dependency** — BRouter already returns the tags; no Overpass
|
||||
call. Imported activities/uploads are out of scope for v1 (see design).
|
||||
- **Schema**: nullable `surface_breakdown` jsonb on `journal.routes` **and**
|
||||
`journal.activities` (additive — `pnpm db:push`).
|
||||
- **Planner**: `api.save-to-journal` computes + sends the breakdown.
|
||||
- **Journal**:
|
||||
- route callback stores the synchronous breakdown;
|
||||
- a typed `surface-backfill` pg-boss job (Overpass map-match → breakdown →
|
||||
store → SSE), enqueued after imports/uploads and available as a sweep;
|
||||
- a journal-side Overpass way/surface client (reusing the existing
|
||||
`/api/overpass` proxy pattern);
|
||||
- an SSE `surface_breakdown` event + a detail-page subscription;
|
||||
- a `SurfaceBreakdown` component (reuses `map-core` palettes + `stats.ts`).
|
||||
- **i18n**: `journal.surface.*` (en + de).
|
||||
- **Privacy**: the backfill sends route geometry to Overpass — see design for
|
||||
the proxy/self-host + visibility considerations.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue