Commit graph

120 commits

Author SHA1 Message Date
Ullrich Schäfer
97ddf36621
fix(planner): elevation chart drag survives leaving the canvas; move reset-zoom
- Drag-to-zoom on the chart now tracks via window mousemove/mouseup, so
  it keeps going when the pointer leaves the chart and still completes if
  released outside it (previously mouseleave aborted the drag and an
  outside release was lost). Listeners are detached on mouseup/unmount.
- Move the "reset zoom" button from top-right (which overlapped the
  color-mode dropdown) to the bottom-right of the chart, and restyle it
  on tokens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 01:27:45 +02:00
Ullrich Schäfer
6d905c152f
Merge pull request #606 from trails-cool/feat/planner-computing-overlay
feat(planner): move "computing route" indicator from topbar to a map overlay
2026-07-16 01:16:09 +02:00
Ullrich Schäfer
24913b9f3e
Merge branch 'main' into feat/planner-colormode-select 2026-07-16 01:03:09 +02:00
Ullrich Schäfer
46018c9f89
feat(planner): move "computing route" from topbar to a map overlay
The topbar's "Route wird berechnet…" text appeared/disappeared with
routing, shifting the topbar layout. Move it onto the map as a floating
status pill (spinner + label, top-center, pointer-events-none) that
appears temporarily without affecting the topbar. Restyle the ambient
top-edge progress bar to accent tokens. Drop the now-unused `computing`
prop from Topbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 01:01:08 +02:00
Ullrich Schäfer
079ce5aba2
Merge branch 'main' into feat/planner-colormode-select 2026-07-16 00:44:42 +02:00
Ullrich Schäfer
c504d6e34b
Merge branch 'main' into feat/topbar-button-restyle 2026-07-16 00:44:25 +02:00
Ullrich Schäfer
3603624b70
fix(planner): distinct aria-label for export dropdown toggle
The restyled Export split-button's dropdown toggle got aria-label
"Export GPX", colliding with the main button's text and breaking the
E2E selector that clicked the old "▾" glyph. Give it a dedicated
exportOptions label (en + de) and target it in the E2E test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:43:30 +02:00
Ullrich Schäfer
1d3ba956cb
feat(planner): restyle Export + SaveToJournal buttons on tokens
Bring the two slotted topbar actions onto the design system so the bar
reads as one cohesive surface:

- SaveToJournalButton: primary Button primitive; "saved" uses the
  accent, error uses a new --color-danger token, and the return link
  gets secondary token styling.
- ExportButton: token split-button (secondary look, chevron icon,
  single divider) and a token dropdown menu (raised surface, soft
  shadow, muted descriptions).
- New --color-danger token (#a03c3c, the no-go hue at full strength)
  for error text.

Behavior and i18n unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:42:37 +02:00
Ullrich Schäfer
b09d810301
feat(planner): restyle sidebar on tokens (waypoints, notes, days)
Migrate the whole sidebar surface onto the design system:

- SidebarTabs: token tab bar, sage active indicator.
- WaypointSidebar: token surfaces/text; sage number badges; overnight
  marker uses the Badge (stop tone) primitive; route summary + stats
  footer in Geist Mono; destructive delete signalled via bg-nogo.
- NotesPanel + DayBreakdown headers/rows on tokens.
- i18n the previously-hardcoded empty-state string (en + de).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:39:12 +02:00
Ullrich Schäfer
28f92d0d8f
feat(planner): restyle color-mode selector + chart header on tokens
The elevation chart's 10-mode color selector now uses the Select
primitive (token-styled, consistent with ProfileSelector). Chart-header
chrome (top border, title link, legend text) moved onto tokens too.

Kept as a dropdown rather than a segmented control — 10 modes don't fit
a segmented toggle. Canvas/legend data-viz colors are unchanged (task
1.5).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:32:05 +02:00
Ullrich Schäfer
a56445daf3
feat(ui): Select primitive; restyle ProfileSelector on tokens
- New Select primitive in @trails-cool/ui: token-styled native <select>
  (appearance-none + overlaid chevron for a consistent closed control),
  sm/md sizes, sibling of Input. Unit-tested + added to /dev/ui.
- ProfileSelector uses Select (size sm) with token label; the profile
  label now hides on small screens. Completes the topbar's migration
  onto the design system.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 00:12:23 +02:00
Ullrich Schäfer
4f0d4a34b3
feat(planner): redesign topbar with tokens + primitives, presentational
Extract a presentational Topbar driven by plain props, so SessionView
is the container (reads Yjs/awareness) and the topbar can render in the
/dev/ui gallery without a live session.

- New useParticipants(yjs) hook: awareness -> sorted Participant[] +
  renameLocal. Replaces ParticipantList's inline logic.
- New presentational ParticipantAvatars (Avatar + Host Badge + inline
  rename) and Topbar (token-styled shell; undo/redo via IconButton;
  connection status with a live dot). ProfileSelector/Export/Save are
  passed in as slots (their own restyle is separate).
- SessionView composes <Topbar> from real data; delete ParticipantList.
- /dev/ui gains a "Topbar - configurations" section (solo, multiplayer
  + computing, guest/connecting). Surfaced a narrow-width overflow, now
  fixed by making the participant strip the shrink/clip element.

Connection status text ("Connected"/"Verbunden") and behavior are
unchanged, so existing planner E2E selectors still match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 23:51:27 +02:00
Ullrich Schäfer
eb1ab7d9e4
fix(planner): note tooltip sizes to content (width max-content, cap 280)
Refines #594: a fixed width made short notes an oversized box and wrapped
long notes early. Use `width: max-content` capped at `max-width: 280` so
the tooltip hugs short notes on one line and grows to 280px before wrapping
long ones (instead of collapsing to the longest word, the pre-#593 bug).
Keeps the centered text.

Verified in local dev with both a short ("Zug zurück?") and a long note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:44:05 +02:00
Ullrich Schäfer
64d770ebea
fix(planner): give the note tooltip a fixed width + centered text
Follow-up to #593. With a long note the tooltip still squeezed to ~1 word
wide: an inline-block with only max-width, inside Leaflet's auto-width
tooltip, shrinks to its longest word rather than filling max-width.

Give it an explicit `width: 200` (a consistent, comfortably wide box that
wraps long notes cleanly) and `text-align: center`. Verified live in the
browser against a long repeated note.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 21:33:43 +02:00
Ullrich Schäfer
8cf483a4ee
fix(planner): waypoint-note tooltip no longer collapses to one char per line
The note tooltip's inner span was `display: block` with
`word-break: break-word` and no width, so the Leaflet tooltip sized itself
to the block's *min-content* — which under break-word is ~1 character —
and every character wrapped onto its own line (a tall vertical strip).

Switch to `display: inline-block` (sizes to content up to maxWidth, so it
stays one line when short and wraps at 220px when long) and the standard
`overflow-wrap: break-word`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 17:48:33 +02:00
Ullrich Schäfer
b45e69885d
poi-index: map-core selectors, planner.pois schema, /api/pois route + client, metrics
Replace the planner's Overpass proxy with a self-hosted POI index:
- map-core POI categories become structured tag selectors (single source of
  truth) + osmium filter / classification helpers
- planner.pois PostGIS table (centroid points, GiST + category indexes)
- /api/pois serving route (session + same-origin + rate limit, 100 cap)
- lib/pois.ts client (renamed from overpass.ts; GET, quantized bbox)
- metrics: poi_api_requests_total + DB-collected index rows/age gauges;
  drop overpass_* metrics
- remove /api/overpass proxy + dead OVERPASS_URLS on the planner service
  (Journal surface backfill still uses it via code default)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 22:38:03 +02:00
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
765c9f49a8
remove the map and ui shim packages
Both failed the deletion test in the telling direction:

- @trails-cool/ui: Button/Input/Card had zero consumers — both apps
  roll their own elements inline. The only live part was a 6-line
  styles.css (the Tailwind entry + one keyframe), which now lives in
  each app as app/styles.css.
- @trails-cool/map: MapView and RouteLayer had zero consumers; the
  package was otherwise a re-export of two map-core constants, and the
  two import sites now use @trails-cool/map-core directly. The
  "map components go in @trails-cool/map" convention had drifted long
  ago — the real map components live in apps/planner/app/components.

CLAUDE.md's repository structure and conventions updated to match
reality (including pointing shared-type guidance at the post-#515
sources: db row types, api contracts, Waypoint in types). Dockerfiles
no longer COPY the deleted package manifests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 07:47:59 +02:00
Ullrich Schäfer
3b9672e0ff
planner: give the Yjs document a typed schema seam
The routeData Y.Map's ~15 string keys (geojson, coordinates,
segmentBoundaries, road metadata, profile, colorMode, baseLayer,
overlays, poiCategories) were read and written raw at ~30 call sites,
each with its own JSON parsing and casts; parseJsonArray existed twice
and waypoint extraction four times. GPX assembly was duplicated between
SaveToJournalButton and ExportButton, so the saved plan and the
exported file could silently diverge.

- new lib/route-data.ts owns the routeData (+ noGoAreas) schema:
  typed read/write, JSON encoding internal, ColorMode moves here
  (re-exported from ColoredRoute for existing importers)
- new lib/gpx-export.ts owns GPX assembly: buildRouteGpx /
  buildPlanGpx / buildDayGpxFiles / hasDayBreaks; multi-day splitting
  becomes a pure, tested function
- waypoint-ymap.ts gains extractWaypoints / extractWaypointData; the
  four hand-rolled copies (use-routing, use-waypoint-manager,
  WaypointSidebar, use-days) now share it, and WaypointSidebar's
  moveWaypoint reuses the round-trip helpers instead of re-listing
  every waypoint field
- all hooks/components consume the seam; no raw routeData key strings
  remain outside route-data.ts

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 01:35:04 +02:00
Ullrich Schäfer
985ec54023
chore(ts): replace 7 \as unknown as\ shims with proper types
Most of the existing coercions are legitimate (Drizzle raw-SQL row
shapes, linkedom DOMParser interop, fit-file-parser's loose \`any\`
callback API) — leaving those is honest. The ones cleaned up here
were shims around APIs that have real types we just hadn't wired:

- **\`window.__leafletMap\`** (4 sites: MapHelpers.tsx, SessionView.tsx ×3)
  Added \`Window\` declaration in \`apps/planner/app/types/global.d.ts\`.
  Callers now use plain \`window.__leafletMap\` typed as
  \`L.Map | undefined\`.

- **\`L.markerClusterGroup\`** (PoiPanel.tsx) — leaflet.markercluster
  augments the global \`L\` namespace at runtime but ships no types.
  Added a \`declare module \"leaflet\"\` block in the same global.d.ts
  with a minimal signature for what we actually use.

- **\`L.DomEvent.preventDefault / .stop\`** taking a Leaflet event
  rather than a native one (PlannerMap.tsx, RouteInteraction.tsx,
  NoGoAreaLayer.tsx). Leaflet events carry the native event under
  \`.originalEvent\`; using that drops the cast entirely.

- **\`_creds as unknown as OAuthCredentials\`** orphan in
  wahoo/webhook.ts — \`_creds\` was unused inside the callback (the
  void-cast was a no-op preserving the type for documentation).
  Replaced with \`async ()\`, dropped the unused import.

Net: 26 \`as unknown as\` / \`as any\` sites → 19, all remaining ones
gated by external lib interop or Drizzle raw-SQL.

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:02:25 +02:00
Ullrich Schäfer
0917de6080
fix(planner): keep journal callback token off the client (#2 Phase A)
The Save-to-Journal flow had the browser fetch the journal with a
\`Bearer \${callbackToken}\` header. The JWT was visible in DevTools,
exfiltratable via any XSS or browser extension, and the planner's
\`loader\` shipped it down to the client as part of the page payload.

Now:

- **New action**: \`POST /api/save-to-journal\` (\`routes/api.save-to-journal.ts\`).
  Body: \`{ sessionId, gpx }\`. The action loads \`callbackUrl\` +
  \`callbackToken\` from \`planner.sessions\` (set at /new time when the
  user came from the journal), POSTs to the journal server-to-server
  with the Bearer, and forwards the response.

- **\`SaveToJournalButton\`**: drops the \`callbackUrl\` + \`callbackToken\`
  props. Takes \`sessionId\` only and POSTs to the planner action.

- **\`session.\$id.tsx\` loader**: stops returning \`callbackUrl\` /
  \`callbackToken\` to the client. Returns a single \`hasJournalCallback\`
  boolean so the button still knows whether to render.

- **\`SessionView\`**: same prop simplification.

Trust model is unchanged: the same \`sessionId\` that grants Yjs
membership grants save authority. Knowing the URL = ability to act.
The action only adds a server-side hop so the JWT never reaches
browser JS.

Phase B (jti single-use enforcement on the journal side) follows in
a separate PR — needs a journal DB column + verifier change.

Full repo: pnpm typecheck / lint / test all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 00:29:24 +02:00
Ullrich Schäfer
9e2ca5595e
Centralize waypoint Yjs serialization in waypointFromYMap/waypointToYMap
Introduce waypoint-ymap.ts with typed helpers so all Yjs↔Waypoint
conversions go through one place. New Waypoint fields now only need
to be added once rather than in every consumer.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 21:02:27 +02:00
Ullrich Schäfer
02f8a8be44
Propagate waypoint notes through session init, save-to-journal, and Journal display
- use-yjs: seed note from initialWaypoints on session open
- api.sessions: include note in initialWaypoints type
- SaveToJournalButton: include note in GPX waypoints on save
- Journal routes.: map and display waypoint notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 20:52:12 +02:00
Ullrich Schäfer
aef67aab7a
Include waypoint note in GPX export and extractWaypoints type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 07:38:55 +02:00
Ullrich Schäfer
c2abb64ee0
Add per-waypoint notes and nearby POI snap to Planner
- `note` field on Waypoint type, stored in Yjs Y.Map and exported as
  `<desc>` inside `<wpt>` in GPX
- Inline textarea note editing in WaypointSidebar with auto-resize,
  character counter (500 max), save-on-blur, Escape cancel
- Note indicator dot on map markers; note tooltip on hover
- `useNearbyPois` hook: fetches POIs within 500m of selected waypoint
  via Overpass proxy, 500ms debounce, AbortController, 60s rate-limit
  suppression
- NearbyPoiMarkers component: renders POI markers on map for selected
  waypoint with snap-to-POI on click
- Nearby section in WaypointSidebar: list with snap buttons, spinner,
  empty/rate-limited states, "Show more" toggle (5 → all)
- Unit tests for fetchNearbyPois bbox geometry and snap-to-POI Yjs
  transaction behaviour

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 23:58:21 +02:00
Ullrich Schäfer
861701e881
Show POI details (phone, website, opening hours) on Journal route detail page
Waypoints snapped to OSM POIs in the Planner now carry their metadata all
the way through to the Journal:

- Extend Waypoint type with osmId and poiTags fields
- Extract osmId/poiTags from Yjs Y.Map in ExportButton and SaveToJournalButton
- Encode POI metadata as <trails:poi> extensions in GPX <wpt> elements
- Parse <trails:poi> extensions back in the GPX parser
- Display phone, website, opening hours, address on Journal route detail
- E2E test for the full roundtrip; seed endpoint now defaults to public visibility

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 23:32:38 +02:00
Ullrich Schäfer
34529a9432
Split ElevationChart + PlannerMap into deep modules; add visual regression testing
ElevationChart (1013 lines) split into:
- elevation-chart-draw.ts — pure drawElevationChart(ctx, w, h, params) function
- use-elevation-data.ts — useElevationData(routeData) hook
- ElevationChart.tsx — ~300 lines, interaction + render only

PlannerMap (869 lines) split into:
- MapHelpers.tsx — 7 Leaflet sub-components (MapExposer, RouteFitter, MapClickHandler,
  CursorTracker, NoGoAreaButton, OverlaySync, PoiRefresher)
- use-waypoint-manager.ts — all waypoint CRUD + route data sync
- use-gpx-drop.ts — GPX drag-and-drop hook
- PlannerMap.tsx — ~200 lines, orchestration only

Add Vitest browser visual regression tests for drawElevationChart via
@vitest/browser + Playwright (toMatchScreenshot). Tests cover plain, grade,
elevation, surface color modes plus hover and drag-select states.

Add update-visual-snapshots.yml workflow: triggered by workflow_dispatch or
the `update-snapshots` PR label; commits snapshots back to the branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:41:23 +02:00
Ullrich Schäfer
ed7f6ce153
Session-bind /api/route and /api/overpass
Today both proxies are effectively open to anyone who can set an
Origin header for trails.cool — a third party can use us as a free
BRouter/Overpass relay. Require a live planner session on every call
so abuse traffic costs the scraper a session row (observable,
revocable) before they can issue a single query.

Server:
- New `requireSession(id)` helper — returns the session row or a 401
  Response. Reused by both route handlers.
- `/api/route`: `sessionId` in body is now required and verified;
  rate-limit key always falls back to the session id.
- `/api/overpass`: new `X-Trails-Session` header, verified. Header
  keeps the session out of the request body so the body-keyed cache
  is unaffected.

Client plumbing:
- `useRouting(yjs, sessionId)` — sessionId goes into the /api/route
  body.
- `usePois(sessionId)` → `queryPois(..., sessionId)` → `X-Trails-Session`
  on the proxy call.
- `PlannerMap` + `YjsDebugPanel` gain a `sessionId` prop from
  `SessionView`.

Journal server-to-server:
- Demo-bot and `/api/v1/routes/compute` now POST `/api/sessions` to
  mint a throwaway planner session, then cite it on the forwarded
  `/api/route` call. Planner's `expire-sessions` cron cleans these up
  (7d window) so nothing needs explicit teardown.

Tests:
- 5 unit tests for `requireSession` covering missing / empty /
  non-string / unknown-session / valid-session cases.
- Two integration E2E tests document the 401 for missing session on
  each proxy.
- Pre-existing `/api/route` integration tests updated to mint a
  session first.

Caveat: existing browser tabs lose their /api/route ability until
reload (the old JS doesn't know to send sessionId). Acceptable for
an anonymous planner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 22:18:45 +02:00
Ullrich Schäfer
60b94b5789
Extract @trails-cool/map-core package from Planner
New renderer-agnostic package with zero dependencies:
- Tile configs (base layers, overlay layers)
- Color palettes (surface, highway, smoothness, tracktype, cycleway,
  bikeroute, elevation, maxspeed) — 8 color maps + 3 color functions
- POI category definitions (9 categories with Overpass queries, icons,
  colors, profile mappings)
- Z-index layering constants
- Snap distance constant

Updated 15 consuming files to import from @trails-cool/map-core.
Deleted poi-categories.ts and z-index.ts from the Planner (fully moved).
packages/map re-exports tile configs from map-core for backwards compat.

All 117 tests pass, no user-facing changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 22:28:14 +02:00
Ullrich Schäfer
7c0a45abe5
Hide computing route text on mobile to prevent layout reflow
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:39:55 +02:00
Ullrich Schäfer
650fa0f36a
Fix mobile header layout: hide secondary items on small screens
- Participant list: hidden on mobile (sm:block)
- Undo/redo buttons: hidden on mobile (sm:flex)
- Connection status: hidden on mobile (sm:inline)
- Header: no-wrap, tighter padding on mobile
- Username input: text-base on mobile to prevent iOS auto-zoom

Keeps profile selector and export button visible on all sizes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:39:19 +02:00
Ullrich Schäfer
ca48284252
Fix iOS safe area: header respects notch, chart respects home indicator
- viewport-fit=cover enables safe area insets in Safari
- Header: pt-[max(0.5rem,env(safe-area-inset-top))] clears the notch
- Elevation chart: pb-[max(0.5rem,env(safe-area-inset-bottom))] clears
  the home indicator bar

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:34:25 +02:00
Ullrich Schäfer
66885c82f2
Add mobile touch support for elevation chart interaction
- Single touch + drag: scrub the highlight point along the chart,
  map dot follows in real-time
- Tap: pan map to that point (10px threshold for movement)
- Two-finger touch: range select — area between fingers highlighted,
  on release zooms map to fit that route section
- touch-none CSS + preventDefault prevents page scrolling on chart

Spec updated with mobile touch interaction scenarios.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:26:14 +02:00
Ullrich Schäfer
3345ef127e
Add bidirectional elevation chart ↔ map interaction
Three new interactions:

1. Route hover → chart: Invisible interactive polyline (weight 20,
   opacity 0) detects mouse hover on the route, computes cumulative
   distance, and highlights the corresponding position on the chart.

2. Chart click → map pan: Clicking the elevation chart pans the map
   to center on that point along the route. Uses 5px threshold to
   distinguish from drag.

3. Chart drag-select → map zoom: Dragging a range on the chart shows
   a blue selection overlay, then zooms the map to fit the route
   coordinates in that range. "Reset zoom" button appears to restore
   the full route view.

State flow avoids feedback loops: external highlights (from map hover)
set the chart crosshair without re-emitting onHover back to the map.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 19:23:37 +02:00
Ullrich Schäfer
3787821571
Improve bike route legend: human-readable names, non-judgmental colors
- Legend: International / National / Regional / Local / None
- Hover: shows full names instead of icn/ncn/rcn/lcn
- Palette: purple (intl) → blue (national) → teal (regional) →
  emerald (local) → gray (none). No red/green implied quality.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:13:44 +02:00
Ullrich Schäfer
ffb5348396
Link elevation chart title to OSM wiki for each color mode
Clicking the chart title (e.g. "Road Type Profile") opens the
corresponding OSM wiki page. Plain, elevation, and grade modes
remain as plain text since they're not OSM tag-based.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:13:44 +02:00
Ullrich Schäfer
6ec95216e1
Add smoothness, track type, cycleway, and bike route color modes
Four new route visualization modes extracted from BRouter WayTags:

- Smoothness: excellent (green) → impassable (dark red)
- Track Type: grade1 (green, best) → grade5 (red, worst)
- Cycleway: track/lane/shared_lane/no with direction awareness
- Bike Route: icn/ncn/rcn/lcn priority from route_bicycle_* tags

Each mode includes map polyline coloring, elevation chart coloring,
inline legend, hover label, i18n (EN+DE), and mock fixture data.

Dockerfile patched to also expose tracktype in BRouter WayTags
(smoothness, cycleway, and route_bicycle already present).

Dropdown order: plain, elevation, grade, surface, road type, speed
limit, smoothness, track type, cycleway, bike route.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:13:44 +02:00
Ullrich Schäfer
768fa9df44
Reorder color mode dropdown: plain, elevation, grade, surface, road type, speed limit
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:13:44 +02:00
Ullrich Schäfer
61d34821df
Re-add speed limit color mode with BRouter profile patch
The dummyUsage trick needs a separate assign per tag:
  assign dummyUsage2 = maxspeed=
(not appended to the existing smoothness= line)

- Dockerfile: Patch all BRouter profiles to include maxspeed in
  WayTags output via separate assign dummyUsage2
- brouter.ts: Extract maxspeed with direction handling
  (maxspeed:forward/backward + reversedirection awareness)
- ColoredRoute: maxspeed color mode (green ≤30, yellow ≤50,
  orange ≤70, red ≤100, dark red 100+)
- ElevationChart: maxspeed chart rendering, legend, hover "X km/h"
- PlannerMap: maxspeeds state + Yjs read + prop passing
- i18n: EN "Speed Limit" / DE "Tempolimit"
- Mock fixtures: maxspeed data for E2E tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:13:44 +02:00
Ullrich Schäfer
cec613d989
Fix E2E select locator, remove maxspeed mode (data unavailable)
E2E fixes:
- Color mode select locator uses option[value='highway'] filter
  instead of fragile page.locator("select").last()

Removed maxspeed color mode:
- BRouter does not expose maxspeed in WayTags output despite the tag
  existing in its lookup table. The dummyUsage profile trick only
  affects cost calculation, not tiledesc output.
- Can revisit if BRouter adds maxspeed to WayTags in a future version
  or if we fork/patch the BRouter profiles more deeply.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:13:43 +02:00
Ullrich Schäfer
4bcd6137de
Fix E2E select locator and add speed limit color mode
E2E fixes:
- Color mode select locator now uses option[value='highway'] filter
  instead of fragile page.locator("select").last()

Speed limit color mode:
- Extract maxspeed tags from BRouter tiledesc (with forward/backward
  direction handling following brouter-web's pattern)
- Color route and chart by speed limit: green ≤30, yellow ≤50,
  orange ≤70, red ≤100, dark red 100+ km/h
- Legend with speed thresholds, hover shows "X km/h"
- i18n: EN "Speed Limit" / DE "Tempolimit"
- Mock fixtures include maxspeed data

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:13:43 +02:00
Ullrich Schäfer
1e2f6beded
Add road type color mode to route visualization
Extract highway=* tags from BRouter tiledesc messages alongside surface
data and add a new "Road Type" color mode that colors the route polyline
and elevation chart by OSM highway classification (cycleway, residential,
path, etc.). Includes color palette, legend, hover labels, i18n (EN+DE),
unit tests for tag extraction, and E2E tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 11:13:43 +02:00
Ullrich Schäfer
08070cdd90
Sync notes through GPX, Journal, and Planner roundtrip
Full notes lifecycle:
- GPX: description field in GpxData, <metadata><desc> in generate/parse
- Export: Plan GPX and Save to Journal include notes as description
- Journal: updateRoute extracts description from GPX, stores on route
- Reimport: Edit in Planner passes notes via URL params → Yjs Y.Text
- Drop import: GPX with <desc> restores notes in session

Spec updated: session-notes gains GPX export, Journal sync, and
reimport requirements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 03:35:40 +02:00
Ullrich Schäfer
442f911fda
Improve surface legend and hover text
- Legend shows actual surface types present in the route (up to 6)
  with colored swatches instead of generic text
- Hover label shows surface type name (e.g. "34m · 12.3km · asphalt")

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 03:21:40 +02:00
Ullrich Schäfer
7bddc131cd
Color elevation chart by surface type when in surface mode
The chart was falling through to plain blue when surface mode was
selected. Now reads surface data from Yjs routeData and draws
segments colored by road surface type, matching the route colors
on the map.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 03:21:39 +02:00
Ullrich Schäfer
858c7e9118
Show min/max elevation in meters in the elevation legend
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 03:21:39 +02:00
Ullrich Schäfer
ec584c8b88
Add inline color legends next to chart title for all modes
Legends sit between the title and the mode selector dropdown:
- Grade: colored swatches with percentage thresholds
- Elevation: gradient bar from low (green) to high (red)
- Surface: text hint about surface coloring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 03:21:39 +02:00
Ullrich Schäfer
67439df0ee
Add color legend below grade profile chart
Shows grade thresholds with color swatches: <3% green, <6% yellow,
<10% orange, <15% red, 15%+ dark red. Only visible in grade mode.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 03:21:39 +02:00
Ullrich Schäfer
da1a5248d6
Move color mode selector to elevation chart, add grade profile
- Moved color mode dropdown from header bar to inline with the
  elevation chart title — closer to the visualization it controls
- Removed ColorModeToggle component from SessionView header
- Added "Grade" color mode: colors route and elevation chart by
  steepness (green <3%, yellow <6%, orange <10%, red <15%, dark red 15%+)
- Grade hover label shows percentage (e.g. "+4.2%")
- Fixed useTranslation namespace in ElevationChart ("planner")
- i18n: Added grade keys for en + de

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 03:21:39 +02:00
Ullrich Schäfer
205e2bbbe0
Fix notes not loading on session reload
- Initialize CodeMirror with existing Y.Text content (doc: yjs.notes.toString())
  so text is visible immediately, not just after the next remote change
- Use dedicated Y.UndoManager for notes instead of sharing with waypoints
  undo manager (different tracked origins caused conflicts)
- Use yUndoManagerKeymap for proper Yjs-aware undo/redo

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 03:00:51 +02:00