mobile-app: Unified React Native + Expo app combining Planner and
Journal. OAuth2 PKCE auth, MapLibre maps, versioned REST API with
Zod schemas, configurable server URL, offline SQLite, Web Push relay
notifications. TanStack Query + Zustand state management. Jest +
Maestro testing. 76 tasks across 5 phases.
map-core-package: Extract renderer-agnostic map definitions (tiles,
color palettes, POI categories, z-index) into @trails-cool/map-core.
Pure refactor preparing for MapLibre on mobile. 27 tasks.
mobile-activity-recording: GPS recording, live stats, HealthKit/Health
Connect export. Separated from mobile-app for independent scheduling.
mobile-nearby-sync: BLE route sync between nearby devices for offline
group riding. QR waypoint sharing as simpler v1. TXQR noted as future.
journal-rest-api spec: Full API contract — endpoints, auth, pagination,
errors, discovery, versioning, BRouter proxy.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Synced new spec (chart ↔ map interaction, mobile touch, iOS safe area,
responsive header) to openspec/specs/elevation-map-interaction/.
All 4 artifacts complete. All 14 tasks complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>
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>
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>
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>
Added scenarios for character-level notes sync and cursor awareness
between participants.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- leaflet.markercluster: Dynamic import with fallback to plain layer
group. Clusters POI markers at low zoom, ungroups at zoom 15+.
- Base layer sync: baselayerchange event writes to Yjs, new
participants load the selected base layer on connect.
- Both tile overlays and base layer now persist across participants
and crash recovery.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
OverlaySync component listens to Leaflet overlayadd/overlayremove
events and writes enabled overlay IDs to Yjs routeData. On connect,
loads initial overlay state from Yjs. LayersControl.Overlay uses
checked prop to reflect the synced state.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
useYjsPoiSync: Bidirectional sync between local POI state and Yjs
routeData. When a participant enables/disables POI categories, the
change propagates to all connected clients. Initial state loaded from
Yjs on connect. Crash recovery included via existing Yjs localStorage
snapshot.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
useProfileDefaults hook observes profile changes in Yjs and merges
relevant POI categories into the enabled set. Skips initial load to
respect existing state. Cycling profiles → bike infra, hiking →
shelter + viewpoints.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Hillshading: mock tile endpoint, enable via DOM evaluate, verify
tile requests fired
- POI markers: mock Overpass API response, zoom to threshold, enable
category, verify marker rendered in marker pane
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PoiPanel: Collapsible panel with category checkboxes, count badges,
loading/error/zoom-too-low states
- PoiMarkers: L.Marker with L.DivIcon per POI, click popup with name,
hours, website, OSM link. z-index below route/waypoints.
- PoiRefresher: Listens to map moveend, refreshes POIs via usePois hook
- Wired into PlannerMap alongside existing controls
Skipped markercluster (7.3) — can add later if density is an issue.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Synced delta spec (6 requirements) to openspec/specs/multi-day-routes/.
Archived change to openspec/changes/archive/2026-04-11-multi-day-routes/.
All 4 artifacts complete. All 35 tasks complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- planner.test.ts: E2E test for overnight toggle → day breakdown in sidebar,
and GPX import with overnight metadata
- integration.test.ts: E2E test for isDayBreak preservation through GPX import
- RouteMapThumbnail: Per-day route coloring with alternating colors
- overnight.test.ts: Y.Doc-aware tests for isOvernight/setOvernight
- daybreaks-extraction.test.ts: GPX → dayBreaks index extraction tests
All 28 tasks complete.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- RouteMapThumbnail: Color route segments per day with alternating colors
when dayBreaks are present, pass through from route detail page
- overnight.test.ts: 6 tests for isOvernight/setOvernight with Y.Doc
- daybreaks-extraction.test.ts: 3 tests for GPX → dayBreaks index extraction
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- compute-days.test.ts: 8 tests covering single day, two days, three days,
ascent/descent, edge cases (first/last overnight, single waypoint, empty)
- generate.test.ts: 4 new tests for isDayBreak emission, GPX roundtrip with
isDayBreak preserved, and splitByDay multi-track output
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
generateGpx() gains a splitByDay option that splits the track into
separate <trk> elements per day, each named "Day N: Start - End".
Track points are split at the closest points to overnight waypoints.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Overnight waypoints render amber-brown (#8B6D3A) with moon icon
- Day boundary labels (white pill "Day N · X km") at overnight waypoints
- Right-click middle waypoints toggles overnight (first/last still delete)
- Wire days prop through SessionView → PlannerMap
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- routes.server.ts: Extract dayBreaks from parsed GPX waypoints on save
- routes.$id.tsx: Compute per-day stats in loader, render day breakdown
section with distance/ascent/descent per day
- ElevationChart: Dashed vertical lines at day boundaries
- i18n: Add Journal day breakdown keys (en + de)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- ElevationChart: Dashed vertical lines at day boundaries with distance labels
- SessionView: Lift useDays() to top level, pass to both ElevationChart and
SidebarTabs to avoid duplicate hook calls
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- DayBreakdown: Collapsible day sections with per-day stats
- WaypointSidebar: Overnight toggle button (moon icon), day-grouped view
when any waypoint is marked overnight, route summary in header
- SessionView: Wire useDays() hook into sidebar via SidebarTabs
- i18n: Add multiDay keys for en + de (day labels, overnight, stats)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- overnight.ts: Yjs helpers to set/clear/check overnight flag on waypoints
- compute-days.ts: Pure function splitting routes into DayStage[] at day breaks
- use-days.ts: Reactive hook mapping Yjs state into computeDays() input
- GPX generate: emit <type>overnight</type> for isDayBreak waypoints
- GPX parse: recognize <type>overnight</type> and set isDayBreak on waypoints
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The spec was Planner-only — day structure would be lost when saving to the
Journal. Added GPX roundtrip (D9), Journal storage (D10), route detail day
breakdown (D11), and shared computeDays in @trails-cool/gpx (D12).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add ## Purpose sections and convert delta headers to ## Requirements
on all 25 specs
- Add SHALL keywords to requirements missing them (gpx-import,
planner-session, planner-journal-handoff)
- Convert prose GPX format section to proper scenarios (no-go-areas)
- Create specs/ delta files for 7 changes that were missing them
(activity-photos, local-dev-stack, multi-day-routes, route-discovery,
route-sharing, visual-redesign, waypoint-notes)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove Garage from service list (commented out, not deployed)
- Replace S3_ENDPOINT/S3_BUCKET with actual Journal env vars
(JWT_SECRET, SESSION_SECRET, WAHOO_*)
- Add monitoring exporters (postgres-exporter, node-exporter, cAdvisor)
to monitoring stack scenario
- Add metrics collection requirement documenting Prometheus scrape
targets and pg_stat_statements custom queries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bugs fixed:
- FIT parser returns timestamps as Date objects, not strings — convert
to ISO 8601 before passing to generateGpx (caused str.replace crash)
- FIT parser already converts semicircles to degrees — remove redundant
conversion that produced near-zero coordinates
- Wahoo CDN URLs are pre-signed S3 URLs — remove Bearer auth header
from download requests (caused 400 "Unsupported Authorization Type")
- Filter out third-party workouts (fitness_app_id >= 1000) since Wahoo
does not share their data via the API
UX improvements:
- Individual imports use fetcher (no page refresh), button shows
"Importing..." inline
- "Import all" button imports all unimported workouts on the page
sequentially with progress indicator
- Add @vitejs/plugin-basic-ssl for local HTTPS dev (opt-in via HTTPS=1)
Also adds unit test for FIT-to-GPX conversion with real fixture file,
and updates wahoo-import spec to reflect all behaviors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Archive completed changes and sync their delta specs to main:
- wahoo-import: new wahoo-import spec, updated journal-auth and account-settings
- journal-route-previews: new route-preview spec, updated map-display and route-management
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Yjs UndoManager tracks waypoints, no-go areas, and notes. Only
local mutations (origin "local") are undoable — remote changes
from other users are not.
- Keyboard shortcuts: Ctrl/Cmd+Z (undo), Ctrl/Cmd+Shift+Z/Y (redo)
- Shortcuts suppressed in text inputs (browser-native undo there)
- Undo/redo buttons in header with disabled state
- stopCapturing on drag to isolate drag as one undo step
- All mutation sites wrapped with "local" origin
- 5 unit tests for UndoManager behavior
- Archived undo-redo change
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Route/activity list pages: map thumbnails with route drawn on OSM tiles
- Route/activity detail pages: interactive Leaflet map with zoom controls
- Server: expose GeoJSON from PostGIS via ST_AsGeoJSON (simplified for lists)
- RouteMapThumbnail component: shared, supports thumbnail and interactive modes
- Placeholder shown for routes/activities without geometry
- Archive journal-route-previews change, sync specs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents:
- Right-click to delete
- Save to Journal preserves no-go areas in GPX extensions
- Export Plan includes full planning state
- Export Route is clean track only
- GPX extensions format with trails:planning namespace
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
BRouter supports a `polygons` parameter with full vertex coordinates.
We were approximating polygons as circles (centroid + max radius),
which poorly represented elongated or concave shapes.
Now passes polygon vertices directly — no approximation, exact
no-go boundaries.
Also removes unused haversineMeters helper and updates the spec.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>