diff --git a/openspec/changes/phase-1-mvp/.openspec.yaml b/openspec/changes/archive/2026-03-25-phase-1-mvp/.openspec.yaml similarity index 100% rename from openspec/changes/phase-1-mvp/.openspec.yaml rename to openspec/changes/archive/2026-03-25-phase-1-mvp/.openspec.yaml diff --git a/openspec/changes/phase-1-mvp/design.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/design.md similarity index 100% rename from openspec/changes/phase-1-mvp/design.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/design.md diff --git a/openspec/changes/phase-1-mvp/proposal.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/proposal.md similarity index 100% rename from openspec/changes/phase-1-mvp/proposal.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/proposal.md diff --git a/openspec/changes/phase-1-mvp/specs/activity-feed/spec.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/specs/activity-feed/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp/specs/activity-feed/spec.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/specs/activity-feed/spec.md diff --git a/openspec/changes/phase-1-mvp/specs/brouter-integration/spec.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/specs/brouter-integration/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp/specs/brouter-integration/spec.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/specs/brouter-integration/spec.md diff --git a/openspec/changes/phase-1-mvp/specs/infrastructure/spec.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/specs/infrastructure/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp/specs/infrastructure/spec.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/specs/infrastructure/spec.md diff --git a/openspec/changes/phase-1-mvp/specs/journal-auth/spec.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/specs/journal-auth/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp/specs/journal-auth/spec.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/specs/journal-auth/spec.md diff --git a/openspec/changes/phase-1-mvp/specs/map-display/spec.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/specs/map-display/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp/specs/map-display/spec.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/specs/map-display/spec.md diff --git a/openspec/changes/phase-1-mvp/specs/planner-journal-handoff/spec.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/specs/planner-journal-handoff/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp/specs/planner-journal-handoff/spec.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/specs/planner-journal-handoff/spec.md diff --git a/openspec/changes/phase-1-mvp/specs/planner-session/spec.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/specs/planner-session/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp/specs/planner-session/spec.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/specs/planner-session/spec.md diff --git a/openspec/changes/phase-1-mvp/specs/route-management/spec.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/specs/route-management/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp/specs/route-management/spec.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/specs/route-management/spec.md diff --git a/openspec/changes/phase-1-mvp/specs/shared-packages/spec.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/specs/shared-packages/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp/specs/shared-packages/spec.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/specs/shared-packages/spec.md diff --git a/openspec/changes/phase-1-mvp/tasks.md b/openspec/changes/archive/2026-03-25-phase-1-mvp/tasks.md similarity index 100% rename from openspec/changes/phase-1-mvp/tasks.md rename to openspec/changes/archive/2026-03-25-phase-1-mvp/tasks.md diff --git a/openspec/specs/activity-feed/spec.md b/openspec/specs/activity-feed/spec.md new file mode 100644 index 0000000..aed90e4 --- /dev/null +++ b/openspec/specs/activity-feed/spec.md @@ -0,0 +1,44 @@ +## ADDED Requirements + +### Requirement: Create activity +The Journal SHALL allow authenticated users to create an activity by uploading a GPX trace and adding a description. + +#### Scenario: Create activity with GPX +- **WHEN** a user uploads a GPX file and enters a description +- **THEN** an activity is created with the GPS trace, computed distance and duration, and stored in the database + +#### Scenario: Create activity linked to route +- **WHEN** a user creates an activity and selects an existing route +- **THEN** the activity is linked to that route (route_id foreign key) + +### Requirement: Activity feed +The Journal SHALL display a chronological feed of the authenticated user's activities. + +#### Scenario: View own activity feed +- **WHEN** a logged-in user navigates to their feed +- **THEN** they see their activities in reverse chronological order with name, date, distance, and duration + +### Requirement: Activity detail page +The Journal SHALL display an activity detail page with map, stats, and description. + +#### Scenario: View activity detail +- **WHEN** a user navigates to an activity URL +- **THEN** they see the activity name, description, a map with the GPS trace, distance, duration, and elevation stats + +### Requirement: Link activity to route +Users SHALL be able to link an existing activity to a route, or create a route from an activity trace. + +#### Scenario: Link activity to existing route +- **WHEN** a user selects "Link to Route" on an activity and chooses a route +- **THEN** the activity's route_id is set to the selected route + +#### Scenario: Create route from activity +- **WHEN** a user selects "Create Route from Activity" +- **THEN** a new route is created using the activity's GPX trace + +### Requirement: Activity without route +Activities SHALL be allowed to exist without a linked route. + +#### Scenario: Standalone activity +- **WHEN** a user imports a GPX activity without linking to a route +- **THEN** the activity is stored with route_id as null diff --git a/openspec/specs/brouter-integration/spec.md b/openspec/specs/brouter-integration/spec.md new file mode 100644 index 0000000..4c477ac --- /dev/null +++ b/openspec/specs/brouter-integration/spec.md @@ -0,0 +1,58 @@ +## ADDED Requirements + +### Requirement: Route computation from waypoints +The Planner SHALL compute a route between ordered waypoints by calling the BRouter HTTP API and returning the result as GeoJSON. + +#### Scenario: Compute route with two waypoints +- **WHEN** the routing host submits two waypoints (start, end) with profile "trekking" +- **THEN** the BRouter API returns a GeoJSON route within 2 seconds + +#### Scenario: Compute route with via points +- **WHEN** the routing host submits three or more waypoints +- **THEN** the BRouter API returns a route passing through all waypoints in order + +### Requirement: Routing host election +The Planner SHALL elect one participant per session as the "routing host" who is responsible for sending waypoint changes to BRouter. Only the host SHALL make BRouter API calls. + +#### Scenario: Initial host assignment +- **WHEN** a session is created +- **THEN** the session creator is assigned as the routing host via Yjs awareness state + +#### Scenario: Host failover +- **WHEN** the current routing host disconnects +- **THEN** the longest-connected remaining participant becomes the new host within 5 seconds + +### Requirement: Route broadcast +The routing host SHALL store computed route results in the Yjs document so that all participants receive route updates automatically. + +#### Scenario: Route update propagation +- **WHEN** the routing host receives a new route from BRouter +- **THEN** the route GeoJSON is stored in a Y.Map field and all participants see the updated route on their maps + +### Requirement: Profile selection +The Planner SHALL support selecting a routing profile that determines how BRouter computes the route. + +#### Scenario: Switch routing profile +- **WHEN** a user changes the routing profile from "trekking" to "shortest" +- **THEN** the profile change syncs via Yjs and the routing host recomputes the route + +### Requirement: BRouter API proxy +The Planner backend SHALL proxy all BRouter API calls. Clients SHALL NOT communicate with BRouter directly. + +#### Scenario: Proxied route request +- **WHEN** the routing host client requests a route computation +- **THEN** the Planner backend forwards the request to BRouter, applies rate limiting, and returns the response + +### Requirement: Rate limiting +The Planner backend SHALL rate limit BRouter API calls to prevent abuse. + +#### Scenario: Rate limit exceeded +- **WHEN** a session exceeds 60 route computations per hour +- **THEN** subsequent requests receive a 429 response with a Retry-After header + +### Requirement: BRouter Docker deployment +BRouter SHALL run as a separate Docker container with Germany RD5 segments mounted as a volume. + +#### Scenario: BRouter container starts +- **WHEN** the Docker Compose stack starts +- **THEN** the BRouter container is reachable at its internal HTTP port and can compute routes using the mounted RD5 segments diff --git a/openspec/specs/infrastructure/spec.md b/openspec/specs/infrastructure/spec.md new file mode 100644 index 0000000..69342ba --- /dev/null +++ b/openspec/specs/infrastructure/spec.md @@ -0,0 +1,65 @@ +## ADDED Requirements + +### Requirement: Terraform Hetzner provisioning +Infrastructure SHALL be provisioned on Hetzner Cloud using Terraform with the Hetzner provider. + +#### Scenario: Provision server +- **WHEN** `terraform apply` is run +- **THEN** a Hetzner CX21 server (2 vCPU, 4 GB RAM, 40 GB SSD) is created with Docker installed + +### Requirement: Docker Compose deployment +All services SHALL be deployed via Docker Compose on the Hetzner server. + +#### Scenario: Start all services +- **WHEN** `docker compose up -d` is run on the server +- **THEN** the Journal, Planner, BRouter, PostgreSQL, and Garage containers start and are reachable + +### Requirement: Service configuration +Each service SHALL be configured via environment variables defined in Docker Compose. + +#### Scenario: Journal configuration +- **WHEN** the Journal container starts +- **THEN** it reads DOMAIN, DATABASE_URL, PLANNER_URL, S3_ENDPOINT, and S3_BUCKET from environment variables + +#### Scenario: Planner configuration +- **WHEN** the Planner container starts +- **THEN** it reads BROUTER_URL and DATABASE_URL from environment variables + +### Requirement: PostgreSQL with PostGIS +The database SHALL be PostgreSQL with the PostGIS extension for spatial queries. + +#### Scenario: PostGIS available +- **WHEN** the PostgreSQL container starts +- **THEN** the PostGIS extension is available and can be enabled with `CREATE EXTENSION postgis` + +### Requirement: BRouter segment management +The infrastructure SHALL support downloading and updating Germany RD5 segments from brouter.de. + +#### Scenario: Download segments +- **WHEN** the segment download script is run +- **THEN** Germany RD5 files (E5_N45, E5_N50, E10_N45, E10_N50) are downloaded to the segments volume + +#### Scenario: Weekly segment update +- **WHEN** the weekly cron job runs +- **THEN** RD5 segments are updated from brouter.de and the BRouter container is restarted + +### Requirement: CI/CD pipeline +GitHub Actions SHALL build and deploy both apps on push to main. + +#### Scenario: Push triggers deployment +- **WHEN** code is pushed to the main branch +- **THEN** GitHub Actions builds Docker images, pushes to ghcr.io/trails-cool/, and deploys to the Hetzner server + +### Requirement: Backup strategy +The infrastructure SHALL include daily backups of the PostgreSQL database. + +#### Scenario: Daily backup +- **WHEN** the daily backup cron runs +- **THEN** a PostgreSQL dump is uploaded to the Hetzner Storage Box + +### Requirement: Domain and TLS +The infrastructure SHALL configure DNS and TLS for trails.cool and planner.trails.cool. + +#### Scenario: HTTPS access +- **WHEN** a user navigates to https://trails.cool +- **THEN** the connection is secured with a valid TLS certificate diff --git a/openspec/specs/journal-auth/spec.md b/openspec/specs/journal-auth/spec.md new file mode 100644 index 0000000..2a6d055 --- /dev/null +++ b/openspec/specs/journal-auth/spec.md @@ -0,0 +1,89 @@ +## ADDED Requirements + +### Requirement: Passkey registration +The Journal SHALL allow new users to register using a passkey (WebAuthn). No password is required. + +#### Scenario: Successful passkey registration +- **WHEN** a user enters an email and username and creates a passkey via the browser prompt +- **THEN** a new user account is created, the passkey credential is stored, and the user is logged in + +#### Scenario: Duplicate email +- **WHEN** a user submits an email that is already registered +- **THEN** the system displays an error indicating the email is already in use + +#### Scenario: Duplicate username +- **WHEN** a user submits a username that is already taken +- **THEN** the system displays an error indicating the username is not available + +### Requirement: Passkey login +The Journal SHALL allow returning users to log in using a stored passkey. + +#### Scenario: Successful passkey login +- **WHEN** a user clicks "Sign in" and selects a passkey from the browser prompt +- **THEN** the user is authenticated and redirected to their activity feed + +#### Scenario: No passkey available +- **WHEN** a user has no passkey on the current device +- **THEN** the system offers magic link login as a fallback + +### Requirement: Magic link login (fallback) +The Journal SHALL allow users to log in via a magic link sent to their email. This serves as a fallback for devices without passkey support or for logging in on a new device. + +#### Scenario: Request magic link +- **WHEN** a user enters their email and clicks "Send magic link" +- **THEN** an email with a single-use login link is sent to their address + +#### Scenario: Valid magic link +- **WHEN** a user clicks a valid, non-expired magic link +- **THEN** the user is logged in and the link is invalidated + +#### Scenario: Expired magic link +- **WHEN** a user clicks a magic link older than 15 minutes +- **THEN** the system displays an error and prompts them to request a new link + +#### Scenario: Rate limiting +- **WHEN** a user requests more than 5 magic links in 10 minutes +- **THEN** subsequent requests are rejected with a rate limit message + +### Requirement: Add passkey from new device +The Journal SHALL allow logged-in users to register additional passkeys for new devices. + +#### Scenario: Add passkey after magic link login +- **WHEN** a user logs in via magic link on a new device +- **THEN** the system prompts them to register a passkey for that device + +### Requirement: User profile page +Each user SHALL have a public profile page displaying their username and routes. + +#### Scenario: View own profile +- **WHEN** a logged-in user navigates to their profile +- **THEN** they see their username, bio, and a list of their routes + +#### Scenario: View other user's profile +- **WHEN** a user navigates to another user's profile URL +- **THEN** they see that user's username, bio, and public routes + +### Requirement: Federated identity structure +User accounts SHALL follow the federated identity pattern (`@user@instance`) to prepare for ActivityPub federation in Phase 2. + +#### Scenario: Username format +- **WHEN** a user registers with username "alice" on trails.cool +- **THEN** their full identity is stored as `@alice@trails.cool` + +### Requirement: Session management +The Journal SHALL maintain authenticated sessions using secure HTTP-only cookies. + +#### Scenario: Session persistence +- **WHEN** a logged-in user closes and reopens their browser +- **THEN** they remain logged in if the session has not expired + +#### Scenario: Logout +- **WHEN** a user clicks "Log out" +- **THEN** their session is invalidated and they are redirected to the login page + +### Requirement: No passwords +The Journal SHALL NOT support password-based authentication. All authentication is via passkeys or magic links. + +#### Scenario: No password field +- **WHEN** a user views the registration or login page +- **THEN** there is no password field diff --git a/openspec/specs/map-display/spec.md b/openspec/specs/map-display/spec.md new file mode 100644 index 0000000..7975af0 --- /dev/null +++ b/openspec/specs/map-display/spec.md @@ -0,0 +1,52 @@ +## ADDED Requirements + +### Requirement: Map rendering with OSM tiles +The Planner and Journal SHALL render interactive maps using Leaflet with OpenStreetMap tiles as the default base layer. + +#### Scenario: Default map view +- **WHEN** a user opens the Planner or a route view in the Journal +- **THEN** an interactive map is displayed with OpenStreetMap tiles centered on the route or a default location (Germany) + +### Requirement: Base layer switching +The map SHALL support switching between multiple base tile layers. + +#### Scenario: Switch to OpenTopoMap +- **WHEN** a user selects "OpenTopoMap" from the layer switcher +- **THEN** the map tiles change to topographic tiles from OpenTopoMap + +#### Scenario: Available base layers +- **WHEN** a user opens the layer switcher +- **THEN** the options include OpenStreetMap, OpenTopoMap, and CyclOSM + +### Requirement: Waypoint editing on map +The Planner map SHALL allow users to add, move, and delete waypoints by interacting with the map. + +#### Scenario: Add waypoint by clicking +- **WHEN** a user clicks on the map +- **THEN** a new waypoint is added at the clicked location and synced via Yjs + +#### Scenario: Move waypoint by dragging +- **WHEN** a user drags an existing waypoint marker +- **THEN** the waypoint coordinates update and sync via Yjs + +#### Scenario: Delete waypoint +- **WHEN** a user right-clicks a waypoint and selects "Delete" +- **THEN** the waypoint is removed and the change syncs via Yjs + +### Requirement: Route visualization +The map SHALL display the computed route as a polyline on the map. + +#### Scenario: Display route +- **WHEN** BRouter returns a route GeoJSON +- **THEN** the route is rendered as a colored polyline on the map + +#### Scenario: Route updates on waypoint change +- **WHEN** a waypoint is added, moved, or deleted +- **THEN** the route polyline updates after BRouter recomputes the route + +### Requirement: Elevation profile display +The Planner SHALL display an elevation profile chart for the current route. + +#### Scenario: Show elevation profile +- **WHEN** a route is computed +- **THEN** an elevation profile chart is displayed below the map showing distance vs elevation with total ascent/descent statistics diff --git a/openspec/specs/planner-journal-handoff/spec.md b/openspec/specs/planner-journal-handoff/spec.md new file mode 100644 index 0000000..960df69 --- /dev/null +++ b/openspec/specs/planner-journal-handoff/spec.md @@ -0,0 +1,41 @@ +## ADDED Requirements + +### Requirement: Open Planner from Journal +The Journal SHALL allow a route owner to open a route in the Planner for collaborative editing. + +#### 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=&token=` 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 diff --git a/openspec/specs/planner-session/spec.md b/openspec/specs/planner-session/spec.md new file mode 100644 index 0000000..9e20ba9 --- /dev/null +++ b/openspec/specs/planner-session/spec.md @@ -0,0 +1,81 @@ +## ADDED 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). + +#### 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/` + +#### Scenario: Create session from Journal callback +- **WHEN** the Journal opens `planner.trails.cool/new?callback=&token=&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/` +- **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, max: 30 days). + +#### 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: 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 diff --git a/openspec/specs/route-management/spec.md b/openspec/specs/route-management/spec.md new file mode 100644 index 0000000..c224ae5 --- /dev/null +++ b/openspec/specs/route-management/spec.md @@ -0,0 +1,75 @@ +## ADDED Requirements + +### Requirement: Create route +The Journal SHALL allow authenticated users to create a new route with a name and optional description. + +#### Scenario: Create empty route +- **WHEN** a user clicks "New Route" and enters a name +- **THEN** a new route record is created in PostgreSQL and the user is redirected to the route detail page + +### Requirement: View route +The Journal SHALL display route details including map, metadata, and elevation stats. + +#### Scenario: View route detail +- **WHEN** a user navigates to a route's URL +- **THEN** they see the route name, description, a map with the route polyline, distance, and elevation gain/loss + +### Requirement: Update route +The Journal SHALL allow the route owner to update the route name, description, and GPX. + +#### Scenario: Update route metadata +- **WHEN** a route owner edits the route name or description and saves +- **THEN** the route record is updated and a new version is created + +### Requirement: Delete route +The Journal SHALL allow the route owner to delete a route. + +#### Scenario: Delete route with confirmation +- **WHEN** a route owner clicks "Delete" and confirms +- **THEN** the route and all its versions are permanently deleted + +### Requirement: GPX import +The Journal SHALL allow users to create or update a route by uploading a GPX file. + +#### Scenario: Import GPX as new route +- **WHEN** a user uploads a GPX file on the "Import" page +- **THEN** a new route is created with waypoints and track parsed from the GPX, and route geometry is stored in PostGIS + +#### Scenario: Import GPX to existing route +- **WHEN** a route owner uploads a GPX file on an existing route's page +- **THEN** the route GPX is replaced and a new version is created + +### Requirement: GPX export +The Journal SHALL allow users to download any route as a GPX file. + +#### Scenario: Export route as GPX +- **WHEN** a user clicks "Export GPX" on a route detail page +- **THEN** a GPX file is downloaded containing the route track and waypoints + +### Requirement: Route versioning +The Journal SHALL store sequential versions of each route. Each GPX update creates a new version. + +#### Scenario: View version history +- **WHEN** a route owner views the route detail page +- **THEN** they see a list of versions with version number, date, and contributor + +### Requirement: Route list +The Journal SHALL display a list of the authenticated user's routes. + +#### Scenario: View my routes +- **WHEN** a logged-in user navigates to their route list +- **THEN** they see all their routes with name, distance, and last updated date + +### Requirement: PostGIS spatial storage +Route geometries SHALL be stored as PostGIS LineString geometries extracted from the GPX. + +#### Scenario: Spatial data stored on import +- **WHEN** a GPX file is imported or a route is saved from the Planner +- **THEN** the route geometry is extracted and stored as a PostGIS LineString for future spatial queries + +### Requirement: Route metadata envelope +Routes SHALL be stored with a metadata envelope containing computed statistics (distance, elevation gain/loss), routing profile, contributor list, and tags. + +#### Scenario: Metadata computed on save +- **WHEN** a route GPX is saved +- **THEN** distance and elevation statistics are computed from the GPX and stored in the metadata diff --git a/openspec/specs/shared-packages/spec.md b/openspec/specs/shared-packages/spec.md new file mode 100644 index 0000000..e40a8e1 --- /dev/null +++ b/openspec/specs/shared-packages/spec.md @@ -0,0 +1,56 @@ +## ADDED Requirements + +### Requirement: Shared types package +The `@trails-cool/types` package SHALL export TypeScript interfaces for Route, Activity, Waypoint, RouteVersion, and RouteMetadata used by both apps. + +#### Scenario: Import types in Planner +- **WHEN** the Planner app imports `@trails-cool/types` +- **THEN** it has access to the Waypoint, Route, and RouteMetadata interfaces + +#### Scenario: Import types in Journal +- **WHEN** the Journal app imports `@trails-cool/types` +- **THEN** it has access to Route, Activity, RouteVersion, and RouteMetadata interfaces + +### Requirement: GPX parsing package +The `@trails-cool/gpx` package SHALL parse GPX XML into structured data (waypoints, tracks, elevation) and generate GPX XML from structured data. + +#### Scenario: Parse GPX to waypoints +- **WHEN** the gpx package parses a valid GPX file +- **THEN** it returns an array of Waypoint objects with lat, lon, and optional name + +#### Scenario: Generate GPX from waypoints +- **WHEN** the gpx package is given an array of waypoints and a track +- **THEN** it generates a valid GPX XML string + +#### Scenario: Extract elevation data +- **WHEN** the gpx package parses a GPX file with elevation data +- **THEN** it returns elevation gain, loss, and a profile array of distance/elevation pairs + +### Requirement: Map rendering package +The `@trails-cool/map` package SHALL provide React components for rendering Leaflet maps with configurable base layers and route overlays. + +#### Scenario: Render map component +- **WHEN** the map package's MapView component is rendered with a center and zoom +- **THEN** a Leaflet map is displayed with the default OSM tile layer + +#### Scenario: Display route on map +- **WHEN** the map package's RouteLayer component receives GeoJSON +- **THEN** it renders a polyline on the map + +### Requirement: UI component package +The `@trails-cool/ui` package SHALL provide shared React components (buttons, layout, form elements) styled with Tailwind CSS. + +#### Scenario: Use Button component +- **WHEN** an app renders the Button component from `@trails-cool/ui` +- **THEN** a styled button is displayed consistent with the trails.cool design + +### Requirement: i18n package +The `@trails-cool/i18n` package SHALL provide react-i18next configuration and translation strings starting with English and German. + +#### Scenario: Display German translation +- **WHEN** a user's browser locale is set to German +- **THEN** UI strings are displayed in German + +#### Scenario: Fallback to English +- **WHEN** a user's browser locale is not supported +- **THEN** UI strings fall back to English