Break up route-features into focused specs, add new changes

Archive the monolithic route-features spec and replace with 9 focused
OpenSpec changes: multi-day-routes, waypoint-notes (with POI snapping),
undo-redo, local-dev-stack, route-sharing, route-discovery,
activity-photos, osm-overlays, plus the existing changelog and
komoot-import.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-03-29 09:53:41 +02:00
parent 9c7891402f
commit 0a330e4466
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
46 changed files with 2968 additions and 0 deletions

View file

@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-03-28

View file

@ -0,0 +1,211 @@
## Context
The Planner stores waypoints as a Yjs `Y.Array<Y.Map<unknown>>` where each
Y.Map holds `lat`, `lon`, and optionally `name`. Routes are computed
segment-by-segment between consecutive waypoints via BRouter, producing an
`EnrichedRoute` with `coordinates`, `segmentBoundaries`, `totalLength`,
`totalAscend`, and `totalTime`. The visual-redesign change already defines the
UI treatment for multi-day routes (sidebar day breakdown, elevation chart
dividers, map day labels) but explicitly defers the data model and logic.
This design covers the data model, computation, and integration decisions.
## Decisions
### D1: Day-break waypoints via `overnight` flag
A waypoint becomes a day boundary by setting `overnight: true` on its Y.Map.
The first waypoint of the route is the implicit start of Day 1, the last
waypoint is the implicit end of the final day, and every waypoint with
`overnight: true` marks the end of one day and start of the next.
```
Waypoints: [Berlin, Zossen, Dessau(overnight), Halle, Erfurt]
|------- Day 1 -------||------- Day 2 ------|
```
This is the simplest possible model: a single boolean on existing data. No new
Yjs types, no separate array, no ordering concerns. It composes naturally with
waypoint reordering, insertion, and deletion -- if an overnight waypoint is
removed, the two days merge automatically.
### D2: Day computation as a pure utility
A `computeDays()` function takes the waypoints array and the `EnrichedRoute`
and returns an array of day objects:
```typescript
interface DayStage {
dayNumber: number;
startWaypointIndex: number;
endWaypointIndex: number;
startName: string;
endName: string;
distance: number; // meters
ascent: number; // meters
descent: number; // meters
estimatedTime: number; // seconds
coordStartIndex: number;
coordEndIndex: number;
}
function computeDays(
waypoints: Array<{ lat: number; lon: number; name?: string; overnight?: boolean }>,
route: EnrichedRoute,
): DayStage[];
```
The function walks `segmentBoundaries` to map waypoint indices to coordinate
ranges, then accumulates distance and elevation per day by iterating
coordinates within each range. If no waypoints have `overnight: true`, it
returns a single day covering the entire route.
This is a pure function with no Yjs dependency -- it takes plain data and
returns plain data. This makes it easy to test and reuse.
### D3: Yjs state -- minimal addition
The only change to Yjs state is adding an `overnight` key to waypoint Y.Maps:
```typescript
// Setting overnight on a waypoint
const waypointMap = yjs.waypoints.get(index);
waypointMap.set("overnight", true);
// Clearing overnight
waypointMap.delete("overnight");
```
No new Y.Array or Y.Map types are introduced. The `overnight` key is optional
-- existing waypoints without it are treated as regular (non-overnight)
waypoints. This is fully backwards-compatible: sessions created before this
feature work identically, and clients that don't understand `overnight` simply
ignore it.
The existing crash-recovery logic (periodic localStorage save of Y.Doc state)
preserves overnight flags automatically since they are part of the Y.Doc.
### D4: Sidebar day breakdown
The `WaypointSidebar` component gains a day-grouped view when any waypoint has
`overnight: true`. The layout follows visual-redesign D4:
```
ACTIVE ROUTE
Berlin -> Erfurt 343 km ^868m 2 days
DAY 1 - Berlin -> Dessau [v]
^340m 120 km ~4h 30m
1. Berlin Alexanderplatz
2. Zossen
3. Juterbog
4. Dessau [OVERNIGHT]
> DAY 2 - Dessau -> Erfurt 223 km [>]
(collapsed days show summary only)
```
**Behavior:**
- Day 1 is expanded by default; other days are collapsed
- Clicking a day header toggles expand/collapse
- Per-day stats (distance, ascent, estimated time) shown in day header
- Overnight waypoints display an amber badge
- Waypoints within each day are numbered sequentially (1, 2, 3... restarting
per day would be confusing -- use global numbering)
- When no waypoints have `overnight: true`, the sidebar shows the flat list
as it does today (no "Day 1" wrapper for single-day routes)
### D5: Elevation chart day dividers
The `ElevationChart` canvas drawing is extended to render day boundaries as
vertical dashed lines. For each day boundary (overnight waypoint), the
corresponding distance along the route is computed from `segmentBoundaries`
and `coordinates`, and a dashed vertical line is drawn at that x-position.
```
Day 1 Day 2 Day 3
___/\__/\___ : __/\_____ : __/\___
/ \ : / \ : / \
/_______________\:/___________\:/________\
0 km 120 km 120 km 250 km 250 km 343 km
```
Each divider has a "Day N" label at the top of the chart area. The dashed line
uses a muted color (`--text-lo` / `#9A9484`) to avoid competing with the
elevation profile. This follows visual-redesign D6.
### D6: Map day labels
White pill-shaped labels are placed on the route at each day boundary, showing
"Day 1 . 120 km". These use Leaflet DivIcon markers positioned at the
coordinate of the overnight waypoint.
Styling follows visual-redesign D5:
- White background with subtle shadow (`--shadow-sm`)
- Text in `--text-hi` with distance in `--font-mono`
- Positioned slightly offset from the route line to avoid overlap with the
route itself
Day labels are only shown when there are 2+ days. They update reactively when
overnight flags change (same Yjs observe pattern as existing markers).
### D7: GPX export with day-break metadata
Day-break waypoints are exported with a `<type>overnight</type>` element inside
the `<wpt>` tag. This is valid GPX 1.1 (the `<type>` element is a standard
child of `<wpt>`).
```xml
<wpt lat="51.8365" lon="12.2428">
<name>Dessau</name>
<type>overnight</type>
</wpt>
```
Additionally, the track can optionally be split into multiple `<trk>` elements
(one per day), each with a `<name>` like "Day 1: Berlin - Dessau". This gives
GPS devices and other tools a natural per-day breakdown. The single-track export
remains the default; multi-track is an option in the export dialog.
On GPX import (future), the parser should recognize `<type>overnight</type>`
waypoints and restore the overnight flags. This is not in scope for this change
but the format is designed to support it.
### D8: Overnight toggle UX
Two interaction paths to toggle a waypoint as overnight:
1. **Sidebar**: Each waypoint row in the sidebar gains an overnight toggle
button (crescent moon icon). Clicking it sets/clears `overnight` on the
waypoint's Y.Map. The button uses amber styling (`--stop`, `--stop-bg`)
when active.
2. **Map context menu**: Right-clicking (long-press on mobile) a waypoint
marker on the map shows a context menu with "Mark as overnight stop" /
"Remove overnight stop". This reuses the same Y.Map mutation.
Visual feedback follows visual-redesign tokens:
- Overnight waypoint markers on the map use amber-brown (`--stop`: `#8B6D3A`)
instead of the default olive (`--accent`: `#4A6B40`)
- Sidebar overnight waypoints have a subtle amber background (`--stop-bg`)
- The "OVERNIGHT" badge uses `--stop` text on `--stop-bg` background with
`--stop-border` border
## Risks / Trade-offs
- **Segment boundary alignment**: The day computation relies on
`segmentBoundaries` from `EnrichedRoute` to map waypoint indices to
coordinate ranges. If the segment merge logic changes, day computation
breaks. Mitigate with thorough unit tests on `computeDays`.
- **Large routes**: A route with 50+ waypoints and many overnight stops could
make the sidebar unwieldy. Collapsible sections mitigate this. We can add
virtual scrolling later if needed.
- **Yjs backwards compatibility**: Adding `overnight` to Y.Maps is safe, but
older clients that don't understand it will silently ignore overnight flags.
In a collaborative session, one user could see day breakdown while another
does not. This is acceptable for now since all clients will be updated
together.
- **GPX round-trip**: The `<type>overnight</type>` convention is not a standard
GPX extension namespace. Other tools will ignore it, which is fine. The data
is not lost, just not interpreted.

View file

@ -0,0 +1,72 @@
## Why
Long routes -- multi-day bike tours, thru-hikes, extended backpacking trips --
are flat lists of waypoints with no structure. Users planning a 5-day ride from
Berlin to Prague have no way to mark where each day ends, see per-day distance
and climbing, or reason about daily effort. They resort to external spreadsheets
or mental arithmetic to divide the route into manageable stages.
The Planner already computes total distance and elevation across the full route.
Adding day structure is a matter of marking overnight stops on existing waypoints
and deriving per-day stats from the segment data we already have.
## What Changes
- **Day-break waypoints**: Any waypoint can be toggled as an overnight stop.
This adds an `overnight: true` flag to the waypoint's Y.Map in the existing
Yjs waypoints array. First and last waypoints are implicit day boundaries.
- **Per-day stats**: Distance, total ascent, and estimated duration computed per
day by splitting the route at overnight waypoints. Derived from the existing
`segmentBoundaries` and `coordinates` in the enriched route data.
- **Sidebar day breakdown**: Waypoints grouped by day with collapsible sections,
per-day stats, and overnight toggle. Day 1 expanded by default.
- **Elevation chart day dividers**: Dashed vertical lines at day boundaries with
"Day N" labels.
- **Map day labels**: White pill markers on the route at day boundaries showing
"Day 1 . 120 km".
- **GPX export**: Day-break waypoints exported with a `<type>overnight</type>`
element so the structure survives round-trips.
All state lives in Yjs. No database changes are needed -- the Planner remains
stateless. The visual design is already specified in the `visual-redesign`
change (D4 sidebar, D5 map markers, D6 elevation chart); this spec covers
the data model, computation logic, and integration wiring.
## Capabilities
### New Capabilities
- `multi-day-routes`: Overnight waypoint markers, per-day stats computation,
day-aware sidebar/chart/map display, multi-day GPX export
### Modified Capabilities
- `planner-session`: Waypoints gain an `overnight` property in Yjs state
- `map-display`: Day boundary labels on route, overnight marker styling
- `gpx-export`: Day-break metadata in exported GPX waypoints
## Non-Goals
- **Automatic day splitting**: No algorithm to suggest where to stop. Users
decide manually. This avoids opinionated defaults and keeps the logic simple.
- **Accommodation search**: No POI lookup for campsites or hotels. Out of scope.
- **Per-day routing profiles**: All days use the same BRouter profile. Supporting
different profiles per day would require rearchitecting the routing pipeline.
- **Journal integration**: Saving multi-day routes to the Journal is a separate
concern (route-features change). This spec is Planner-only.
## Impact
- **Yjs state**: `overnight` boolean added to waypoint Y.Map entries (additive,
backwards-compatible -- existing sessions without it behave as single-day)
- **Shared types**: `Waypoint.isDayBreak` already exists in `@trails-cool/types`
but is unused. This change activates it.
- **New utility**: `compute-days.ts` -- pure function that splits route data at
overnight waypoints and returns per-day stats
- **Sidebar**: `WaypointSidebar.tsx` gains day-grouped view with collapsible
sections and overnight toggle buttons
- **ElevationChart**: Canvas drawing extended with vertical day dividers
- **Map**: New day-label layer and overnight marker variant
- **GPX**: `generateGpx` extended to emit `<type>overnight</type>` on day-break
waypoints
- **i18n**: New keys for day labels, overnight toggle, per-day stats (en + de)

View file

@ -0,0 +1,40 @@
## 1. Data Model & Computation
- [ ] 1.1 Add `overnight` property support to Yjs waypoint Y.Maps: helper functions `setOvernight(yjs, index, value)` and `isOvernight(yMap)` in a new `apps/planner/app/lib/overnight.ts`
- [ ] 1.2 Create `apps/planner/app/lib/compute-days.ts` with `computeDays()` pure function: takes waypoints array + `EnrichedRoute`, returns `DayStage[]` (day number, waypoint range, coord range, distance, ascent, descent, estimated time)
- [ ] 1.3 Create `useDays()` hook in `apps/planner/app/lib/use-days.ts`: observes Yjs waypoints + routeData, calls `computeDays()`, returns reactive `DayStage[]`
## 2. Sidebar Day Breakdown
- [ ] 2.1 Create `DayBreakdown` component: renders collapsible day sections with per-day stats (distance, ascent, estimated time), Day 1 expanded by default
- [ ] 2.2 Add overnight toggle button (moon icon) to each waypoint row in `WaypointSidebar` — amber styling when active, calls `setOvernight()`
- [ ] 2.3 Integrate `DayBreakdown` into `WaypointSidebar`: show day-grouped view when any waypoint has overnight, flat list otherwise
- [ ] 2.4 Add route summary header to sidebar: total distance, ascent, number of days (e.g. "Berlin -> Erfurt 343 km ^868m 2 days")
## 3. Map Integration
- [ ] 3.1 Add overnight marker variant to `PlannerMap`: amber-brown circle (`--stop` token) for overnight waypoints, replacing default olive marker
- [ ] 3.2 Add day label DivIcon markers on route at day boundaries: white pill with "Day N . X km" text, positioned at overnight waypoint coordinates
- [ ] 3.3 Add right-click context menu on waypoint markers with "Mark as overnight stop" / "Remove overnight stop" option
## 4. Elevation Chart
- [ ] 4.1 Add day divider rendering to `ElevationChart`: dashed vertical lines at overnight waypoint distances with "Day N" labels at top
- [ ] 4.2 Show per-day distance ranges on x-axis labels (e.g. "120 km" at each day boundary)
## 5. GPX Export
- [ ] 5.1 Extend `generateGpx` in `@trails-cool/gpx` to emit `<type>overnight</type>` for waypoints with `isDayBreak: true`
- [ ] 5.2 Add multi-track export option: split track into one `<trk>` per day, each named "Day N: Start - End"
## 6. i18n
- [ ] 6.1 Add translation keys for en + de: day labels ("Day 1", "Tag 1"), overnight toggle ("Mark as overnight stop" / "Als Ubernachtung markieren"), per-day stats, route summary
## 7. Testing
- [ ] 7.1 Unit tests for `computeDays()`: single day (no overnight), two days, three days, empty route, single waypoint, overnight on first/last waypoint edge cases
- [ ] 7.2 Unit tests for `overnight.ts` helpers: set/clear/check overnight on Y.Map
- [ ] 7.3 Unit tests for GPX export with overnight waypoints: verify `<type>overnight</type>` output, multi-track splitting
- [ ] 7.4 E2E test: add waypoints, toggle overnight on one, verify sidebar shows day breakdown with correct stats
- [ ] 7.5 E2E test: export GPX with day breaks, verify downloaded file contains overnight metadata