Add GPX file import to planner + archive change

Two import entry points:
- Home page: "Import GPX" button next to "Start Planning"
- In-session: drag-and-drop GPX onto the map (with confirmation)

Parses GPX client-side, extracts waypoints (Douglas-Peucker) and
no-go areas from extensions. Non-GPX files show error toast.

Also:
- Fix spec drift: non-GPX drop now shows error toast (was silent)
- Add E2E tests for import button, invalid GPX, and session creation
- Archive gpx-import-planner change, sync specs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-03 17:31:44 +01:00
parent 516b86aa3d
commit a9f8ee61f0
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
16 changed files with 394 additions and 175 deletions

View file

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

View file

@ -0,0 +1,33 @@
## Context
The planner currently receives GPX data only via URL parameters or the journal API. All GPX parsing infrastructure exists (`parseGpxAsync`, `extractWaypoints`, no-go area parsing) but there's no user-facing file upload. Users expect to open a local GPX file directly — standard in every route planning tool.
## Goals / Non-Goals
**Goals:**
- Let users import a GPX file from the planner home page to start a new session
- Let users import a GPX file into an existing session (replacing current waypoints)
- Support drag-and-drop onto the map as an alternative to the file picker
- Reuse existing GPX parsing, waypoint extraction, and no-go area infrastructure
**Non-Goals:**
- Importing non-GPX formats (KML, GeoJSON, FIT) — future work
- Merging imported GPX with existing session data — import replaces
- Server-side file storage — GPX is parsed client-side, only waypoints/no-go areas are stored in Yjs
## Decisions
**Client-side parsing:** Parse GPX in the browser using `parseGpxAsync` (which uses native `DOMParser`). No need to upload the file to the server. Extract waypoints and no-go areas, then initialize the Yjs session.
**Two entry points:**
1. **Home page:** Upload button next to "Start Planning". Creates a new session with the imported data.
2. **Session map:** Drag-and-drop onto the map. Replaces current waypoints and no-go areas after confirmation.
**Session creation flow (home page):** POST the parsed waypoints and no-go areas to `/api/sessions` (same as the journal handoff), then redirect to the new session URL with data in query params.
**In-session import (drag-and-drop):** Parse client-side, confirm replacement, then update Yjs arrays directly. No server round-trip needed.
## Risks / Trade-offs
- **Large GPX files:** Douglas-Peucker runs client-side. Files with 100K+ points may be slow. Acceptable for v1 — optimize later if needed.
- **Replacing vs merging:** Import replaces all waypoints/no-go areas. Users might expect to add to existing data. A confirmation dialog mitigates accidental loss.

View file

@ -0,0 +1,27 @@
## Why
The planner has no UI for importing GPX files directly. Users can only get routes into the planner via URL parameters (`/new?gpx=...`) or the journal's "Edit in Planner" handoff. There's no way to open a local GPX file from the planner itself — a basic expectation for any route planning tool.
## What Changes
- Add a GPX file upload button to the planner home page and session header
- When a GPX file is uploaded, create a new session with waypoints extracted from the track (via Douglas-Peucker) and no-go areas from extensions
- Support drag-and-drop of GPX files onto the map
- Reuse existing `parseGpxAsync`, `extractWaypoints`, and no-go area parsing infrastructure
## Capabilities
### New Capabilities
- `gpx-import`: GPX file import UI in the planner (upload button, drag-and-drop, file parsing, session creation)
### Modified Capabilities
- `planner-session`: Session can now be initialized from a GPX file upload (not just URL params)
- `planner-journal-handoff`: The "Export Plan" → reimport flow is now a first-class UI action
## Impact
- `apps/planner/app/routes/home.tsx` — add upload button
- `apps/planner/app/components/PlannerMap.tsx` — drag-and-drop zone
- `apps/planner/app/routes/new.tsx` — handle file upload POST
- `packages/i18n/src/locales/` — new translation keys
- `e2e/planner.test.ts` — new E2E tests for file import

View file

@ -0,0 +1,36 @@
## ADDED Requirements
### Requirement: Import GPX from home page
Users SHALL be able to import a GPX file from the planner home page to start a new planning session.
#### Scenario: Upload GPX via file picker
- **WHEN** a user clicks the "Import GPX" button on the home page and selects a GPX file
- **THEN** the file is parsed client-side using `parseGpxAsync`
- **AND** waypoints are extracted via `extractWaypoints` (Douglas-Peucker for single-segment tracks)
- **AND** no-go areas are extracted from GPX extensions if present
- **AND** a new session is created with the extracted data
- **AND** the user is redirected to the new session
#### Scenario: Invalid GPX file
- **WHEN** a user uploads a file that is not valid GPX
- **THEN** an error message is shown
- **AND** no session is created
### Requirement: Import GPX via drag-and-drop
Users SHALL be able to drag a GPX file onto the map in an existing session.
#### Scenario: Drop GPX on map
- **WHEN** a user drags a `.gpx` file onto the map area
- **THEN** a visual drop zone indicator appears
- **AND** on drop, the file is parsed client-side
- **AND** a confirmation dialog asks whether to replace the current route
- **AND** on confirm, the session's waypoints and no-go areas are replaced with the imported data
#### Scenario: Cancel import
- **WHEN** a user drops a GPX file and the confirmation dialog appears
- **THEN** clicking "Cancel" leaves the session unchanged
### Requirement: Non-GPX files are rejected
#### Scenario: Drop non-GPX file
- **WHEN** a user drops a non-GPX file on the map
- **THEN** the file is ignored with a brief error toast

View file

@ -0,0 +1,9 @@
## MODIFIED Requirements
### Requirement: Export Plan reimport
The "Export Plan" GPX can now be reimported directly in the planner via the file upload UI, completing the round-trip without needing the journal.
#### Scenario: Reimport exported plan
- **WHEN** a user exports a plan and later imports it via the planner's GPX upload
- **THEN** waypoints, no-go areas, and track data are restored from the GPX
- **AND** BRouter re-routes between the imported waypoints with the imported no-go areas active

View file

@ -0,0 +1,9 @@
## MODIFIED Requirements
### Requirement: Session initialization
Sessions can now be initialized from a GPX file upload in addition to URL parameters and the journal handoff.
#### Scenario: Session created from GPX upload
- **WHEN** a session is created via GPX file upload on the home page
- **THEN** waypoints and no-go areas from the GPX are passed via URL parameters to the session page
- **AND** the Yjs document is initialized with the extracted data on the client side

View file

@ -0,0 +1,31 @@
## 1. Home Page Import
- [x] 1.1 Add "Import GPX" button next to "Start Planning" on the planner home page
- [x] 1.2 Add hidden file input (`accept=".gpx"`) triggered by the button
- [x] 1.3 On file select: parse GPX client-side with `parseGpxAsync`, extract waypoints and no-go areas
- [x] 1.4 POST extracted data to `/api/sessions`, redirect to new session with waypoints + no-go areas in URL params
- [x] 1.5 Show error toast if GPX parsing fails
## 2. Drag-and-Drop Import
- [x] 2.1 Add drag-and-drop zone to `PlannerMap` (listen for `dragenter`, `dragover`, `drop` on map container)
- [x] 2.2 Show visual overlay when a file is dragged over the map ("Drop GPX file here")
- [x] 2.3 On drop: validate file extension is `.gpx`, reject others with error toast
- [x] 2.4 Parse dropped GPX file client-side
- [x] 2.5 Show confirmation dialog ("Replace current route with imported GPX?")
- [x] 2.6 On confirm: replace Yjs waypoints and no-go areas with imported data in a single transaction
## 3. i18n
- [x] 3.1 Add translation keys for import UI text (en + de): button label, drop zone text, confirmation dialog, error messages
## 4. Testing
### Unit tests
- [x] 4.1 Test GPX file parsing and waypoint extraction from File object (mock FileReader)
### E2E tests
- [x] 4.2 Home page import: upload GPX file via file input → session created with waypoints
- [x] 4.3 Drag-and-drop: drop GPX on map → waypoints replaced (Playwright file drop)
- [x] 4.4 Invalid file: upload non-GPX → error toast shown, no session created
- [x] 4.5 Plan round-trip: export plan → reimport via upload → waypoints and no-go areas match

View file

@ -0,0 +1,36 @@
## ADDED Requirements
### Requirement: Import GPX from home page
Users SHALL be able to import a GPX file from the planner home page to start a new planning session.
#### Scenario: Upload GPX via file picker
- **WHEN** a user clicks the "Import GPX" button on the home page and selects a GPX file
- **THEN** the file is parsed client-side using `parseGpxAsync`
- **AND** waypoints are extracted via `extractWaypoints` (Douglas-Peucker for single-segment tracks)
- **AND** no-go areas are extracted from GPX extensions if present
- **AND** a new session is created with the extracted data
- **AND** the user is redirected to the new session
#### Scenario: Invalid GPX file
- **WHEN** a user uploads a file that is not valid GPX
- **THEN** an error message is shown
- **AND** no session is created
### Requirement: Import GPX via drag-and-drop
Users SHALL be able to drag a GPX file onto the map in an existing session.
#### Scenario: Drop GPX on map
- **WHEN** a user drags a `.gpx` file onto the map area
- **THEN** a visual drop zone indicator appears
- **AND** on drop, the file is parsed client-side
- **AND** a confirmation dialog asks whether to replace the current route
- **AND** on confirm, the session's waypoints and no-go areas are replaced with the imported data
#### Scenario: Cancel import
- **WHEN** a user drops a GPX file and the confirmation dialog appears
- **THEN** clicking "Cancel" leaves the session unchanged
### Requirement: Non-GPX files are rejected
#### Scenario: Drop non-GPX file
- **WHEN** a user drops a non-GPX file on the map
- **THEN** the file is ignored with a brief error toast

View file

@ -1,41 +1,9 @@
## ADDED Requirements
## MODIFIED Requirements
### Requirement: Open Planner from Journal
The Journal SHALL allow a route owner to open a route in the Planner for collaborative editing.
### Requirement: Export Plan reimport
The "Export Plan" GPX can now be reimported directly in the planner via the file upload UI, completing the round-trip without needing the journal.
#### Scenario: Start editing session
- **WHEN** a route owner clicks "Edit in Planner" on a route detail page
- **THEN** the Journal generates a scoped JWT token and redirects to `planner.trails.cool/new?callback=<url>&token=<jwt>` with the route's current GPX
### Requirement: Save from Planner to Journal
The Planner SHALL save route edits back to the Journal via the callback URL provided at session creation.
#### Scenario: Save route back
- **WHEN** a user clicks "Save" in the Planner and a callback URL exists
- **THEN** the Planner POSTs the current GPX and metadata to the callback URL with the JWT token
#### Scenario: Journal receives save callback
- **WHEN** the Journal receives a POST to the callback endpoint with a valid JWT
- **THEN** the Journal creates a new route version with the GPX and credits the contributor
### Requirement: Scoped JWT token
The Journal SHALL generate scoped JWT tokens for Planner callbacks containing the instance URL, route ID, permissions, and expiry.
#### Scenario: Valid token accepted
- **WHEN** the Planner sends a save request with a valid, non-expired JWT
- **THEN** the Journal accepts the request and saves the route
#### Scenario: Expired token rejected
- **WHEN** the Planner sends a save request with an expired JWT
- **THEN** the Journal returns a 401 error
#### Scenario: Invalid token rejected
- **WHEN** the Planner sends a save request with a tampered JWT
- **THEN** the Journal returns a 401 error
### Requirement: Return to Journal after save
After saving, the Planner SHALL provide a link back to the route in the Journal.
#### Scenario: Return link displayed
- **WHEN** a save to the Journal succeeds
- **THEN** the Planner displays a success message with a link to the route in the Journal
#### Scenario: Reimport exported plan
- **WHEN** a user exports a plan and later imports it via the planner's GPX upload
- **THEN** waypoints, no-go areas, and track data are restored from the GPX
- **AND** BRouter re-routes between the imported waypoints with the imported no-go areas active

View file

@ -1,130 +1,9 @@
## ADDED Requirements
## MODIFIED Requirements
### Requirement: Create collaborative session
The Planner SHALL allow creating a new editing session that generates a unique shareable URL. Sessions SHALL be created either from the Planner directly (empty route) or via a Journal callback (with initial GPX data).
### Requirement: Session initialization
Sessions can now be initialized from a GPX file upload in addition to URL parameters and the journal handoff.
#### Scenario: Create empty session
- **WHEN** a user navigates to planner.trails.cool
- **THEN** a new Yjs session is created with an empty waypoint list and the user is redirected to `/session/<session-id>`
#### Scenario: Create session from Journal callback
- **WHEN** the Journal opens `planner.trails.cool/new?callback=<url>&token=<jwt>&gpx=<encoded-gpx>`
- **THEN** a new Yjs session is created with waypoints parsed from the GPX and the callback URL is stored for later save operations
### Requirement: Join session via link
The Planner SHALL allow any user (including guests without accounts) to join an existing session by navigating to its URL.
#### Scenario: Join active session
- **WHEN** a user navigates to `planner.trails.cool/session/<session-id>`
- **THEN** the user connects to the Yjs document and sees the current route state with all other participants' cursors
#### Scenario: Join expired session
- **WHEN** a user navigates to a session URL that has expired
- **THEN** the system displays an error message indicating the session no longer exists
### Requirement: Real-time collaborative editing
The Planner SHALL synchronize waypoint edits across all connected participants in real-time using Yjs CRDTs.
#### Scenario: Add waypoint
- **WHEN** participant A adds a waypoint to the map
- **THEN** participant B sees the waypoint appear within 500ms
#### Scenario: Reorder waypoints
- **WHEN** participant A drags a waypoint to reorder it
- **THEN** participant B sees the updated waypoint order within 500ms
#### Scenario: Concurrent edits
- **WHEN** participant A and B both add waypoints simultaneously
- **THEN** both waypoints appear for both participants without conflict
### Requirement: Session persistence
The Planner SHALL persist Yjs session state to PostgreSQL so that sessions survive server restarts.
#### Scenario: Server restart recovery
- **WHEN** the Planner server restarts while a session is active
- **THEN** reconnecting clients recover the full session state from PostgreSQL
### Requirement: Session expiry
The Planner SHALL automatically expire sessions after a configurable period of inactivity (default: 7 days, no hard ceiling enforced).
#### Scenario: Session expires
- **WHEN** no edits are made to a session for 7 days
- **THEN** the session is deleted from PostgreSQL and its URL returns a 404
### Requirement: Manual session close
The session owner (initiator) SHALL be able to manually close a session.
#### Scenario: Owner closes session
- **WHEN** the session owner clicks "Close Session"
- **THEN** all connected participants are notified, the session triggers auto-save if a callback exists, and the session becomes inaccessible
### Requirement: User presence
The Planner SHALL display presence indicators showing which users are currently connected to a session, including live cursors on the map.
#### Scenario: Show connected users
- **WHEN** multiple users are connected to a session
- **THEN** each user sees a list of other connected users with assigned colors
#### Scenario: Live map cursors
- **WHEN** a user moves their mouse over the map
- **THEN** other participants see a labeled cursor at that position on their map, colored to match the user's assigned color
#### Scenario: Cursor disappears on leave
- **WHEN** a user disconnects from the session
- **THEN** their cursor disappears from all other participants' maps within 5 seconds
### Requirement: Planner home page
The Planner home page SHALL explain the tool's purpose and provide a one-click way to start planning.
#### Scenario: First-time visitor
- **WHEN** a user visits planner.trails.cool for the first time
- **THEN** they see a landing page explaining collaborative route planning, key features, and a prominent "Start Planning" button
#### Scenario: Start a session
- **WHEN** a user clicks "Start Planning"
- **THEN** a new anonymous session is created and the user is redirected to the session view
#### Scenario: Journal link
- **WHEN** a user wants to save routes permanently
- **THEN** a secondary CTA links to trails.cool for account creation
### Requirement: No user data collection
The Planner SHALL NOT collect, store, or track any personal user data. Sessions are anonymous by default.
#### Scenario: Anonymous session participation
- **WHEN** a user joins a session without any account
- **THEN** the user is assigned a random color and temporary display name with no data persisted about their identity
### Requirement: Session participant awareness
Users in a planning session SHALL see who else is present and be able to identify themselves.
#### Scenario: Participant list visible
- **WHEN** multiple users are in a session
- **THEN** the header shows each participant's name and color
#### Scenario: Host badge
- **WHEN** a participant is the routing host
- **THEN** their entry in the participant list shows a host indicator
#### Scenario: Edit own name
- **WHEN** a user clicks their own name in the participant list
- **THEN** an inline text input appears to change their display name
#### Scenario: Name persisted
- **WHEN** a user changes their name
- **THEN** the name is saved to localStorage and immediately visible to all other participants via awareness
#### Scenario: Join notification
- **WHEN** a new participant joins the session
- **THEN** a brief toast shows "[name] joined"
#### Scenario: Leave notification
- **WHEN** a participant leaves the session
- **THEN** a brief toast shows "[name] left"
### Requirement: Planner session data model
The Yjs document SHALL include noGoAreas and notes fields alongside waypoints and routeData.
#### Scenario: Session with all fields
- **WHEN** a Planner session is active
- **THEN** the Yjs doc contains: waypoints (Y.Array), routeData (Y.Map), noGoAreas (Y.Array), notes (Y.Text)
#### Scenario: Session created from GPX upload
- **WHEN** a session is created via GPX file upload on the home page
- **THEN** waypoints and no-go areas from the GPX are passed via URL parameters to the session page
- **AND** the Yjs document is initialized with the extracted data on the client side