Archive phase-1-mvp, sync specs to main
All 85 tasks complete. Archive change to openspec/changes/archive/ and sync 9 capability specs to openspec/specs/ for future reference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f7288c88d1
commit
e530492753
22 changed files with 1414 additions and 0 deletions
|
|
@ -0,0 +1,2 @@
|
|||
schema: spec-driven
|
||||
created: 2026-03-22
|
||||
130
openspec/changes/archive/2026-03-25-phase-1-mvp/design.md
Normal file
130
openspec/changes/archive/2026-03-25-phase-1-mvp/design.md
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
## Context
|
||||
|
||||
trails.cool is a new platform with two apps: a collaborative route Planner and
|
||||
a federated activity Journal. The full architecture is documented in
|
||||
docs/architecture.md with 19 resolved design decisions. Phase 1 builds the
|
||||
foundation — both apps with minimal features, shared packages, and deployment
|
||||
infrastructure.
|
||||
|
||||
The Planner is stateless and ephemeral — it runs collaborative editing sessions
|
||||
via Yjs and computes routes via BRouter. The Journal is stateful — it stores
|
||||
user accounts, routes, and activities in PostgreSQL with PostGIS.
|
||||
|
||||
Both apps share a TypeScript/React stack (React Router 7, Tailwind) and are
|
||||
deployed to a single Hetzner CX21 server via Docker Compose.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Working Planner with collaborative waypoint editing and BRouter route computation
|
||||
- Working Journal with user accounts, route CRUD, and activity feed
|
||||
- Seamless handoff between Journal and Planner (open route in Planner, save back)
|
||||
- Shared packages for types, UI components, map rendering, GPX parsing, i18n
|
||||
- Deployable to Hetzner via Terraform + Docker Compose
|
||||
- Germany map coverage (~750 MB RD5 segments)
|
||||
|
||||
**Non-Goals:**
|
||||
- ActivityPub federation (Phase 2)
|
||||
- Following/followers, likes, comments (Phase 2)
|
||||
- Photo attachments (Phase 2)
|
||||
- Route sharing permissions beyond basic CRUD (Phase 2)
|
||||
- Mobile app or offline support (Phase 3)
|
||||
- Multi-day route planning UI (Phase 3)
|
||||
- WASM compilation of BRouter (Phase 3)
|
||||
- Monitoring stack (Grafana/Prometheus — add when needed)
|
||||
|
||||
## Decisions
|
||||
|
||||
### D1: React Router 7 for both apps
|
||||
|
||||
Both Planner and Journal use React Router 7 (Remix stack) as their full-stack
|
||||
framework. This gives SSR for Journal (SEO, initial load), API routes, and
|
||||
loader/action patterns.
|
||||
|
||||
**Alternative considered**: Separate frameworks (e.g., Next.js for Journal, Vite
|
||||
SPA for Planner). Rejected because maintaining two frameworks doubles learning
|
||||
curve and prevents sharing server-side code patterns.
|
||||
|
||||
### D2: BRouter wrapped as HTTP proxy in Planner backend
|
||||
|
||||
The Planner's React Router 7 server proxies BRouter API calls. Clients never
|
||||
talk to BRouter directly. This allows rate limiting, request validation, and
|
||||
later caching at the proxy layer.
|
||||
|
||||
**Alternative considered**: Expose BRouter directly. Rejected because BRouter
|
||||
has no built-in auth, rate limiting, or CORS support.
|
||||
|
||||
### D3: Yjs with y-websocket for CRDT sync
|
||||
|
||||
Use y-websocket for the Planner's real-time sync. The WebSocket server runs
|
||||
as part of the Planner's Node.js process (not a separate service). Yjs documents
|
||||
are persisted to PostgreSQL for crash recovery.
|
||||
|
||||
**Alternative considered**: Separate y-websocket service. Rejected for Phase 1
|
||||
simplicity — one process is easier to deploy and debug. Can extract later.
|
||||
|
||||
### D4: PostgreSQL shared instance, separate schemas
|
||||
|
||||
One PostgreSQL instance with two schemas:
|
||||
- `planner` — Yjs session documents, session metadata
|
||||
- `journal` — users, routes, activities, media references
|
||||
|
||||
**Alternative considered**: Separate PostgreSQL instances. Rejected — unnecessary
|
||||
overhead for 100 users. Single instance is simpler to backup and manage.
|
||||
|
||||
### D5: Routing host election via Yjs awareness
|
||||
|
||||
Only one client per session talks to BRouter (the "routing host"). The host is
|
||||
the session initiator; on disconnect, the longest-connected client takes over.
|
||||
This avoids redundant BRouter API calls.
|
||||
|
||||
**Alternative considered**: Server-side route computation (Planner backend
|
||||
watches Yjs changes and computes routes). Better long-term but more complex.
|
||||
Client-side host election is simpler for Phase 1.
|
||||
|
||||
### D6: Scoped JWT for Planner-Journal callback
|
||||
|
||||
When the Journal opens a Planner session, it generates a scoped JWT token
|
||||
embedded in the callback URL. The Planner sends this JWT when saving GPX back.
|
||||
The Journal validates the JWT signature to authorize the write.
|
||||
|
||||
**Alternative considered**: Session cookies / OAuth flow. Rejected — the Planner
|
||||
is stateless and doesn't have access to the Journal's session.
|
||||
|
||||
### D7: Leaflet with plugin-based layers
|
||||
|
||||
Leaflet (not Mapbox GL) for map rendering. Leaflet is lighter, has no API key
|
||||
requirement, and has mature plugin ecosystem for OSM tiles.
|
||||
|
||||
**Alternative considered**: Mapbox GL JS. Rejected — requires API key, larger
|
||||
bundle, and commercial license for heavy usage.
|
||||
|
||||
### D8: pnpm + Turborepo for monorepo
|
||||
|
||||
pnpm workspaces for dependency management, Turborepo for build orchestration
|
||||
and caching. This is the standard monorepo toolchain for TypeScript projects.
|
||||
|
||||
**Alternative considered**: Nx. Rejected — heavier setup, more opinionated.
|
||||
Turborepo is simpler and sufficient for our needs.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
**[BRouter Java dependency]** → The Planner requires a JVM to run BRouter.
|
||||
This adds Docker image size (~200 MB) and memory usage (~128 MB heap).
|
||||
Mitigation: BRouter runs in its own container with constrained resources.
|
||||
|
||||
**[Yjs document size growth]** → Long editing sessions could grow Yjs documents.
|
||||
Mitigation: Monitor document sizes in PostgreSQL. Add compaction if needed.
|
||||
|
||||
**[Single server SPOF]** → All services on one Hetzner CX21.
|
||||
Mitigation: Acceptable for 100 users. Daily backups to Hetzner Storage Box.
|
||||
Scale to multiple servers in Phase 2 if needed.
|
||||
|
||||
**[BRouter segment freshness]** → RD5 segments are updated weekly on brouter.de.
|
||||
Stale data could cause routing on newly built roads to fail.
|
||||
Mitigation: Weekly cron job to download updated segments.
|
||||
|
||||
**[Cross-origin Planner-Journal integration]** → Planner and Journal are on
|
||||
different subdomains (planner.trails.cool vs trails.cool). Cookie sharing
|
||||
won't work.
|
||||
Mitigation: JWT-based callback (Decision D6). No cookies needed.
|
||||
43
openspec/changes/archive/2026-03-25-phase-1-mvp/proposal.md
Normal file
43
openspec/changes/archive/2026-03-25-phase-1-mvp/proposal.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
## Why
|
||||
|
||||
trails.cool needs its foundation: a working Planner for collaborative route
|
||||
editing and a Journal for managing routes and activities. Without Phase 1,
|
||||
nothing can be tested or shown to users. The architecture plan is finalized
|
||||
(see docs/architecture.md) — now we need a working MVP to validate the
|
||||
product-market fit with 100 European users, primarily in Germany.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Set up the monorepo build toolchain (Turborepo, pnpm, TypeScript, React Router 7, Tailwind)
|
||||
- Build the Planner app: real-time collaborative route editing via Yjs, BRouter integration for route computation, Leaflet map with OSM tiles, session sharing via link, GPX export
|
||||
- Build the Journal app: user accounts, route CRUD, start Planner sessions from routes via callback, GPX import/export, basic profile page, activity feed
|
||||
- Deploy BRouter as a Docker service with Germany RD5 segments (~750 MB)
|
||||
- Set up infrastructure as code (Terraform + Docker Compose) for Hetzner Cloud
|
||||
- Establish shared packages: types, UI components, map utilities, GPX parsing, i18n
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `planner-session`: Collaborative route editing sessions with Yjs CRDTs, shareable links, guest access, session lifecycle (create, join, save, close, expire)
|
||||
- `brouter-integration`: BRouter HTTP API wrapper, routing host election, route computation from waypoints, profile selection (bike/hike)
|
||||
- `map-display`: Leaflet map with OSM/OpenTopoMap/CyclOSM base layers, waypoint editing, route visualization, elevation profile display
|
||||
- `journal-auth`: User accounts with federated identity structure (@user@instance), registration, login, profile pages
|
||||
- `route-management`: Route CRUD, GPX import/export, route metadata envelope, PostGIS spatial storage, route versioning (sequential)
|
||||
- `planner-journal-handoff`: Callback-based integration between Journal and Planner — open Planner from route, save GPX back via scoped JWT token
|
||||
- `activity-feed`: Activity CRUD, activity feed (own activities), link activities to routes (1:N blueprint model)
|
||||
- `shared-packages`: Monorepo shared packages — @trails-cool/types, @trails-cool/ui, @trails-cool/map, @trails-cool/gpx, @trails-cool/i18n
|
||||
- `infrastructure`: Terraform for Hetzner Cloud, Docker Compose for services, CI/CD via GitHub Actions
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
(None — this is the initial build, no existing capabilities to modify.)
|
||||
|
||||
## Impact
|
||||
|
||||
- **New apps**: `apps/planner` and `apps/journal` (React Router 7)
|
||||
- **New packages**: `packages/types`, `packages/ui`, `packages/map`, `packages/gpx`, `packages/i18n`
|
||||
- **Infrastructure**: Hetzner CX21 server, PostgreSQL + PostGIS, Garage (S3), BRouter Docker container
|
||||
- **External dependencies**: React Router 7, Yjs, Leaflet, Fedify (stub for Phase 1), BRouter (Java), Tailwind CSS, react-i18next
|
||||
- **Data**: Germany RD5 segments from brouter.de (~750 MB), PostgreSQL schemas (planner.*, activity.*)
|
||||
- **Domains**: trails.cool (Journal), planner.trails.cool (Planner)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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=<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
|
||||
|
|
@ -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/<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, 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
117
openspec/changes/archive/2026-03-25-phase-1-mvp/tasks.md
Normal file
117
openspec/changes/archive/2026-03-25-phase-1-mvp/tasks.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
## 1. Monorepo Toolchain Setup
|
||||
|
||||
- [x] 1.1 Install pnpm and Turborepo, configure workspaces
|
||||
- [x] 1.2 Set up TypeScript config (base tsconfig, per-package extends)
|
||||
- [x] 1.3 Set up Tailwind CSS (shared config, content paths for monorepo)
|
||||
- [x] 1.4 Set up ESLint and Prettier (shared config)
|
||||
- [x] 1.5 Scaffold Planner app with React Router 7 (`apps/planner`)
|
||||
- [x] 1.6 Scaffold Journal app with React Router 7 (`apps/journal`)
|
||||
- [x] 1.7 Verify `turbo dev` starts both apps and `turbo build` succeeds
|
||||
|
||||
## 2. Shared Packages
|
||||
|
||||
- [x] 2.1 Implement `@trails-cool/types` — Route, Activity, Waypoint, RouteVersion, RouteMetadata interfaces
|
||||
- [x] 2.2 Implement `@trails-cool/gpx` — GPX parser (XML → waypoints/tracks/elevation) and GPX generator (waypoints/tracks → XML)
|
||||
- [x] 2.3 Implement `@trails-cool/map` — MapView React component (Leaflet + OSM), RouteLayer component (GeoJSON polyline), layer switcher (OSM/OpenTopoMap/CyclOSM)
|
||||
- [x] 2.4 Implement `@trails-cool/ui` — Button, Input, Card, Layout components with Tailwind styling
|
||||
- [x] 2.5 Implement `@trails-cool/i18n` — react-i18next config, English + German translation files, LanguageSwitcher component
|
||||
- [x] 2.6 Verify all packages are importable from both apps
|
||||
|
||||
## 3. Infrastructure
|
||||
|
||||
- [x] 3.1 Create Terraform config for Hetzner CX21 with Docker installed
|
||||
- [x] 3.2 Create Docker Compose for all services (Journal, Planner, BRouter, PostgreSQL+PostGIS, Garage)
|
||||
- [x] 3.3 Create BRouter Dockerfile with segment volume mount
|
||||
- [x] 3.4 Create segment download script (Germany: E5_N45, E5_N50, E10_N45, E10_N50)
|
||||
- [x] 3.5 Configure DNS for trails.cool and planner.trails.cool with TLS (Caddy)
|
||||
- [x] 3.6 Set up GitHub Actions CI pipeline (build, typecheck, lint, unit tests, e2e)
|
||||
- [x] 3.7 Set up GitHub Actions CD pipeline (build Docker images, push to ghcr.io, deploy to Hetzner)
|
||||
- [x] 3.8 Set up PostgreSQL backup cron (daily pg_dump to Hetzner Storage Box)
|
||||
|
||||
## 4. Planner — Session Management
|
||||
|
||||
- [x] 4.1 Set up Yjs with y-websocket in Planner backend (WebSocket endpoint at /sync)
|
||||
- [x] 4.2 Implement Yjs persistence to PostgreSQL (planner schema, sessions table)
|
||||
- [x] 4.3 Implement session creation endpoint (POST /api/sessions → returns session ID)
|
||||
- [x] 4.4 Implement session creation with initial GPX (parse GPX → Yjs document with waypoints)
|
||||
- [x] 4.5 Implement session join page (GET /session/:id → connect to Yjs document)
|
||||
- [x] 4.6 Implement session expiry (garbage collection cron, configurable TTL)
|
||||
- [x] 4.7 Implement manual session close (owner action, notify participants)
|
||||
|
||||
## 5. Planner — BRouter Integration
|
||||
|
||||
- [x] 5.1 Implement BRouter HTTP proxy endpoint (POST /api/route → forward to BRouter)
|
||||
- [x] 5.2 Implement rate limiting middleware (60 requests/session/hour)
|
||||
- [x] 5.3 Implement routing host election via Yjs awareness (host/participant roles)
|
||||
- [x] 5.4 Implement routing host failover (detect disconnect, elect new host by join timestamp)
|
||||
- [x] 5.5 Implement route computation trigger (host watches waypoint changes, debounce 500ms, call BRouter)
|
||||
- [x] 5.6 Implement route broadcast (host stores GeoJSON result in Y.Map, syncs to all)
|
||||
- [x] 5.7 Implement profile selection (sync profile choice via Y.Map, trigger recompute)
|
||||
|
||||
## 6. Planner — Map UI
|
||||
|
||||
- [x] 6.1 Integrate MapView component in Planner with full-screen layout and client-side Yjs connection
|
||||
- [x] 6.1a Implement user presence display (Yjs awareness, colors, names, live map cursors)
|
||||
- [x] 6.2 Implement waypoint add (click map → add to Y.Array)
|
||||
- [x] 6.3 Implement waypoint drag (move marker → update Y.Array)
|
||||
- [x] 6.4 Implement waypoint delete (right-click → remove from Y.Array)
|
||||
- [x] 6.5 Implement waypoint list sidebar (draggable reorder, synced with Y.Array)
|
||||
- [x] 6.6 Implement route polyline display (render GeoJSON from Y.Map)
|
||||
- [x] 6.7 Implement elevation profile chart (parse elevation from route GeoJSON, render chart)
|
||||
- [x] 6.8 Implement profile selector UI (dropdown, synced via Y.Map)
|
||||
- [x] 6.9 Implement GPX export button (generate GPX from current waypoints and route)
|
||||
|
||||
## 7. Journal — Auth
|
||||
|
||||
- [x] 7.1 Update DB schema: remove password_hash from users, add credentials table (WebAuthn) and magic_tokens table
|
||||
- [x] 7.2 Implement passkey registration flow (email + username → WebAuthn create → account created)
|
||||
- [x] 7.3 Implement passkey login flow (WebAuthn get → session created)
|
||||
- [x] 7.4 Implement magic link request (POST /api/auth/magic-link → send email with token)
|
||||
- [x] 7.5 Implement magic link verification (GET /auth/verify?token=... → session created)
|
||||
- [x] 7.6 Implement "Add passkey" prompt after magic link login on new device
|
||||
- [x] 7.7 Implement session middleware (validate cookie, load user in loader context)
|
||||
- [x] 7.8 Implement logout (POST /api/auth/logout, invalidate session)
|
||||
- [x] 7.9 Implement user profile page (GET /users/:username)
|
||||
- [x] 7.10 Store federated identity format (@user@domain) in user record
|
||||
|
||||
## 8. Journal — Route Management
|
||||
|
||||
- [x] 8.1 Set up PostgreSQL schema (journal.routes table with PostGIS geometry column, journal.route_versions table)
|
||||
- [x] 8.2 Implement route creation page (form: name, description, optional GPX upload)
|
||||
- [x] 8.3 Implement route detail page (map, metadata, version history)
|
||||
- [x] 8.4 Implement route edit page (update name, description)
|
||||
- [x] 8.5 Implement route deletion (with confirmation dialog)
|
||||
- [x] 8.6 Implement GPX import (parse GPX, extract geometry for PostGIS, compute stats)
|
||||
- [x] 8.7 Implement GPX export (generate GPX from stored data, download)
|
||||
- [x] 8.8 Implement route versioning (create new version on each GPX update)
|
||||
- [x] 8.9 Implement route list page (user's routes, sorted by last updated)
|
||||
- [x] 8.10 Implement route metadata computation (distance, elevation gain/loss from GPX)
|
||||
|
||||
## 9. Planner-Journal Handoff
|
||||
|
||||
- [x] 9.1 Implement JWT token generation in Journal (scoped to route_id, with expiry)
|
||||
- [x] 9.2 Implement "Edit in Planner" button on Journal route detail page (redirect with callback + token + GPX)
|
||||
- [x] 9.3 Implement callback URL handling in Planner (store callback URL in session metadata)
|
||||
- [x] 9.4 Implement "Save to Journal" button in Planner (POST GPX + JWT to callback URL)
|
||||
- [x] 9.5 Implement callback endpoint in Journal (POST /api/routes/:id/callback — validate JWT, save new version)
|
||||
- [x] 9.6 Implement "Return to Journal" link after successful save
|
||||
|
||||
## 10. Journal — Activity Feed
|
||||
|
||||
- [x] 10.1 Set up PostgreSQL schema (journal.activities table with route_id FK, gpx, stats)
|
||||
- [x] 10.2 Implement activity creation page (GPX upload, description, optional route link)
|
||||
- [x] 10.3 Implement activity detail page (map with GPS trace, stats, description)
|
||||
- [x] 10.4 Implement activity feed page (chronological list of own activities)
|
||||
- [x] 10.5 Implement "Link to Route" action (select existing route to link)
|
||||
- [x] 10.6 Implement "Create Route from Activity" action (create route from activity GPX)
|
||||
|
||||
## 11. Testing & Polish
|
||||
|
||||
- [x] 11.1 End-to-end test: Create route in Journal → Edit in Planner → Save back to Journal
|
||||
- [x] 11.2 End-to-end test: Two users collaboratively edit waypoints in Planner
|
||||
- [x] 11.3 End-to-end test: Import GPX → view route on map → export GPX
|
||||
- [x] 11.4 Test BRouter routing with Germany segments (Berlin → Munich route)
|
||||
- [x] 11.5 Test session expiry and manual close
|
||||
- [x] 11.6 Verify i18n works (English and German)
|
||||
- [x] 11.7 Basic responsive layout testing (desktop, tablet)
|
||||
- [x] 11.8 Deploy to Hetzner and verify production setup
|
||||
44
openspec/specs/activity-feed/spec.md
Normal file
44
openspec/specs/activity-feed/spec.md
Normal file
|
|
@ -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
|
||||
58
openspec/specs/brouter-integration/spec.md
Normal file
58
openspec/specs/brouter-integration/spec.md
Normal file
|
|
@ -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
|
||||
65
openspec/specs/infrastructure/spec.md
Normal file
65
openspec/specs/infrastructure/spec.md
Normal file
|
|
@ -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
|
||||
89
openspec/specs/journal-auth/spec.md
Normal file
89
openspec/specs/journal-auth/spec.md
Normal file
|
|
@ -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
|
||||
52
openspec/specs/map-display/spec.md
Normal file
52
openspec/specs/map-display/spec.md
Normal file
|
|
@ -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
|
||||
41
openspec/specs/planner-journal-handoff/spec.md
Normal file
41
openspec/specs/planner-journal-handoff/spec.md
Normal file
|
|
@ -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=<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
|
||||
81
openspec/specs/planner-session/spec.md
Normal file
81
openspec/specs/planner-session/spec.md
Normal file
|
|
@ -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/<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, 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
|
||||
75
openspec/specs/route-management/spec.md
Normal file
75
openspec/specs/route-management/spec.md
Normal file
|
|
@ -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
|
||||
56
openspec/specs/shared-packages/spec.md
Normal file
56
openspec/specs/shared-packages/spec.md
Normal file
|
|
@ -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
|
||||
Loading…
Add table
Add a link
Reference in a new issue