The route-surface-breakdown change is fully implemented (18/18 tasks, all artifacts done). Archive it via `openspec archive` (CLI 1.6.0): - Moves the change to openspec/changes/archive/2026-07-13-route-surface-breakdown/ - Promotes its 5 delta requirements into a new capability spec at openspec/specs/route-surface-breakdown/spec.md (surface/waytype breakdown from BRouter waytags, async Overpass backfill, live SSE update, proportion bars, localized labels) Purpose paragraph filled in (the CLI leaves a TBD placeholder). Both the spec and archived change pass `openspec validate --strict`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
3.1 KiB
3.1 KiB
1. Shared derivation + shape
- 1.1
computeSurfaceBreakdown(coordinates, surfaces, highways)in@trails-cool/map-core→{ surface, highway }metres; unit-tested. - 1.2
SurfaceBreakdownSchema(Zod) in@trails-cool/api.
2. Schema
- 2.1 Nullable
surfaceBreakdownjsonb onjournal.routesandjournal.activities; pushed to dev + e2e DBs.
3. Path 1 — synchronous (Planner routes)
- 3.1
SaveToJournalButtoncomputes the breakdown fromreadComputedRouteand sends it;api.save-to-journalforwards it (journal is the authoritative validator — planner has no@trails-cool/apidep). - 3.2 Journal route callback validates the optional field with
SurfaceBreakdownSchemaand persists viaupdateRoute(overwrite on re-save).
4. Render
- 4.1 Route + activity detail loaders expose
surfaceBreakdown. - 4.2
SurfaceBreakdowncomponent (stacked bar per dimension,map-corepalettes, legend category · % · km largest-first, unknown → "other", hidden when empty); mounted onroutes.$id.tsxandactivities.$id.tsx. - 4.3 i18n
journal.surface.*(surface/waytype labels, common categories, "other") en + de.
5. Path 2 — async backfill + SSE (PHASE 2 — this PR)
- 5.1 Journal-side Overpass client (
overpass-ways.server.ts):way[highway]in bbox +out tags geom, configurableOVERPASS_URLSupstreams, timeout + oversized-bbox guard. - 5.2 Map-matcher (
surface-match.server.ts, pure): nearest OSM way per segment → surface/highway; unmatched → unknown. Unit-tested. - 5.3 Typed
surface-backfillpg-boss job: load geom → skip if breakdown exists → Overpass → match →computeSurfaceBreakdown→ store. Idempotent, retry-safe, best-effort; registered inserver.ts. - 5.4 Enqueued from
createActivity(covers Komoot/Garmin imports + manual uploads) and owner-on-open in the route + activity detail loaders (covers non-Planner routes + pre-existing rows), deduped viasingletonKey. (A dedicated sweep CLI is unneeded — on-open covers existing rows lazily.) - 5.5 Emits a
surface_breakdownSSE event ({ kind, id }) to the owner; detail pages subscribe (useSurfaceBackfillUpdates) andrevalidate()when it matches.
6. Privacy
- 6.1 Documented the backfill's Overpass bbox lookup in the privacy manifest (DE + EN);
OVERPASS_URLScan point at a self-hosted instance.
7. Tests & checks
- 7.1 Unit:
computeSurfaceBreakdownweighting + unknown bucket;matchSurfacesnearest-way + unknown bucket. - 7.2 Component (jsdom): bars/proportions, largest-first, hidden when empty/null.
- 7.3 Job test (mocked Overpass/db/events): stores + emits; skips when breakdown exists or no ways. (A full live e2e is intentionally skipped — external Overpass + SSE timing are flaky; rendering is covered by the component test + browser, the job by its mocked test.)
- 7.4 typecheck + lint + unit (map-core 36, journal 333) green; Phase 1 verified in the browser.