Commit graph

5 commits

Author SHA1 Message Date
Ullrich Schäfer
3a1c34317d
route-surface-breakdown (Phase 1): Planner-path surface/waytype bars
Synchronous path + rendering for the surface/waytype breakdown:
- map-core `computeSurfaceBreakdown(coords, surfaces, highways)` → distance-
  weighted metres per surface + waytype category (unit-tested);
- `SurfaceBreakdownSchema` in @trails-cool/api;
- nullable `surfaceBreakdown` jsonb on routes + activities;
- Planner `SaveToJournalButton` computes it from the BRouter waytags already in
  routeData and sends it; `api.save-to-journal` forwards it; the journal route
  callback validates + persists (journal is the authoritative validator);
- `SurfaceBreakdown` component (stacked bars per dimension, map-core palettes,
  legend category · % · km largest-first, unknown → "other", hidden when empty)
  on route + activity detail; journal gains a @trails-cool/map-core dep;
- i18n journal.surface.* (en + de).

Phase 2 (async Overpass backfill + SSE for imports/uploads, and the e2e that
seeds a breakdown) follows in a separate PR.

Tests: computeSurfaceBreakdown unit (map-core 36); SurfaceBreakdown component
(jsdom, journal 326). typecheck + lint green; verified in the browser.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:46:49 +02:00
Ullrich Schäfer
7a1dca378f
journal: branded ownership loading for routes and activities
Ownership was checked ad hoc: some handlers loaded-and-compared
ownerId, some lib mutators enforced it in WHERE clauses and silently
no-op'd for non-owners, and nothing tied the two together. Two real
authorization bugs hid in the gaps: linkActivityToRoute ignored its
ownerId parameter entirely (any logged-in user could relink any
activity), and createRouteFromActivity loaded any activity without an
ownership check (a non-owner could copy a private activity's GPX into
their own route). PUT /api/v1/routes/:id also returned ok:true for
non-owners without updating anything.

ownership.server.ts is now the single enforcement point:

- loadOwnedRoute / loadOwnedActivity (non-throwing, for callers with
  their own error vocabulary) and requireOwnedRoute /
  requireOwnedActivity (throwing data() 404/403 for web handlers; 404
  by default so guessed ids don't leak existence)
- the returned entities carry an Owned<> brand; mutators (updateRoute,
  deleteRoute, deleteActivity, updateActivityVisibility,
  linkActivityToRoute, createRouteFromActivity) now require an
  OwnedRef, so skipping the check is a compile error
- vouchOwnership is the explicit, greppable escape hatch for the one
  non-session authorization path (the Planner JWT callback)
- WHERE ownerId clauses stay in the mutators as defense in depth

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 02:35:00 +02:00
Ullrich Schäfer
78b8b8f55f
Atomic GPX save: validate + persist row + geometry in one transaction
Eliminates the silent-failure pattern where a route/activity row could
be committed with gpx IS NOT NULL but geom IS NULL if the PostGIS write
failed after the row insert.

- New gpx-save.server.ts owns all GPX validation (GpxValidationError,
  validateGpx) and PostGIS geometry writes (writeGeom, tx-aware)
- createRoute, updateRoute, createActivity, createRouteFromActivity all
  wrapped in db.transaction() covering row + geom + version snapshot
- demo-bot uses createRoute/createActivity instead of raw inserts;
  errors propagate loudly
- Callback endpoint returns 400 for GpxValidationError instead of 401
- ADR-0006 documents the invariant for future explorers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 15:02:56 +02:00
Ullrich Schäfer
a5d682d671 Fix CORS on Planner→Journal callback endpoint
The Save to Journal button makes a cross-origin POST from
planner.trails.cool to trails.cool. The browser's preflight OPTIONS
request was returning HTML (no CORS headers), blocking the save flow.

Add CORS headers to all responses and handle OPTIONS preflight in a
loader. Origin is restricted to PLANNER_URL.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 01:16:32 +00:00
Ullrich Schäfer
40e541fcce Implement Planner-Journal handoff (Group 9)
JWT-based handoff between Journal and Planner:

Journal side:
- JWT token generation scoped to route_id with 7-day expiry (jose)
- "Edit in Planner" button on route detail page — generates JWT,
  redirects to Planner /new with callback URL, token, and GPX
- Callback endpoint (POST /api/routes/:id/callback) validates JWT
  and creates new route version from received GPX

Planner side:
- /new route accepts callback, token, returnUrl, gpx params
- Creates session with callback metadata, initializes with GPX
- "Save to Journal" button POSTs GPX with Bearer token to callback
- "Return to Journal" link shown after successful save

All 6 Group 9 tasks complete.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 20:22:58 +00:00