Merge pull request #179 from trails-cool/fix/openspec-validation

Fix all OpenSpec validation failures
This commit is contained in:
Ullrich Schäfer 2026-04-06 21:22:22 +01:00 committed by GitHub
commit 2d7844c02b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 414 additions and 35 deletions

View file

@ -0,0 +1,52 @@
## ADDED Requirements
### Requirement: Photo upload via presigned URLs
Users SHALL be able to upload photos to an activity using presigned PUT URLs, with client-side image processing before upload.
#### Scenario: Upload photo
- **WHEN** a user selects a photo on the activity detail page
- **THEN** the client resizes the image (max 2048px long edge), strips EXIF data, converts to WebP
- **AND** requests a presigned PUT URL from `POST /api/activities/:id/photos`
- **AND** uploads directly to Garage (S3-compatible storage)
- **AND** confirms the upload via `PATCH /api/activities/:id/photos/:photoId`
#### Scenario: Size limit enforced
- **WHEN** a processed photo exceeds 5 MB
- **THEN** the presigned URL rejects the upload via content-length condition
#### Scenario: Per-activity photo limit
- **WHEN** an activity already has 20 photos
- **THEN** the server rejects new presigned URL requests
### Requirement: Photo gallery display
The activity detail page SHALL display uploaded photos in a responsive grid with lightbox viewing.
#### Scenario: Gallery grid
- **WHEN** a user views an activity with photos
- **THEN** photos are displayed in a responsive grid (1 column mobile, 2 tablet, 3 desktop) with lazy loading
#### Scenario: Lightbox view
- **WHEN** a user clicks a photo in the gallery
- **THEN** a full-size lightbox overlay opens with left/right navigation and close on escape
### Requirement: Photo deletion
Activity owners SHALL be able to delete photos with soft delete and background S3 cleanup.
#### Scenario: Delete photo
- **WHEN** a user clicks the delete icon on a photo
- **THEN** the photo status is set to 'deleted' and it disappears from the UI
- **AND** the S3 object is cleaned up on next page load or periodic cleanup
### Requirement: Photo storage infrastructure
Photos SHALL be stored in Garage (S3-compatible, self-hosted) running as a Docker container.
#### Scenario: Garage container
- **WHEN** docker compose starts
- **THEN** a Garage container is running with an `activity-photos` bucket
### Requirement: Photo privacy disclosure
Photo storage and EXIF handling SHALL be documented in the privacy manifest.
#### Scenario: Privacy page updated
- **WHEN** a user visits the privacy page
- **THEN** it documents photo storage location, EXIF stripping, and deletion behavior

View file

@ -0,0 +1,29 @@
## MODIFIED Requirements
### Requirement: Optional local monitoring stack
The dev environment SHALL support an optional monitoring profile matching the production stack.
#### Scenario: Start with monitoring
- **WHEN** a developer runs `pnpm dev:full` with `--profile monitoring`
- **THEN** Prometheus, Grafana, and Loki start alongside the app services
### Requirement: Production-aligned PostgreSQL config
The dev PostgreSQL SHALL match production configuration including pg_stat_statements.
#### Scenario: pg_stat_statements available
- **WHEN** the dev PostgreSQL container starts
- **THEN** pg_stat_statements is enabled via initialization scripts
### Requirement: Database seed script
The dev environment SHALL provide a seed script for consistent test data.
#### Scenario: Seed database
- **WHEN** a developer runs the seed script
- **THEN** test users, routes, and activities are created in the local database
### Requirement: Dev environment reset
The dev environment SHALL provide a command to tear down and recreate the local stack.
#### Scenario: Reset dev environment
- **WHEN** a developer runs `pnpm dev:reset`
- **THEN** all Docker volumes are removed, containers are recreated, and the database is re-seeded

View file

@ -0,0 +1,50 @@
## ADDED Requirements
### Requirement: Overnight waypoint markers
Any waypoint SHALL be toggleable as an overnight stop, creating day boundaries in the route.
#### Scenario: Toggle overnight
- **WHEN** a user toggles the overnight flag on a waypoint
- **THEN** the waypoint's `overnight: true` flag is set in the Yjs document
- **AND** the route is visually divided into days at that point
#### Scenario: Implicit day boundaries
- **WHEN** overnight stops are set
- **THEN** the first waypoint is the implicit start of Day 1 and the last waypoint is the implicit end of the final day
### Requirement: Per-day statistics
The Planner SHALL compute and display distance, ascent, and estimated duration for each day.
#### Scenario: Day stats computed
- **WHEN** a route has overnight waypoints
- **THEN** per-day distance, total ascent, and estimated duration are derived from segment boundaries and coordinates
### Requirement: Day-aware sidebar
The sidebar SHALL group waypoints by day with collapsible sections and per-day stats.
#### Scenario: Day breakdown
- **WHEN** a route has multiple days
- **THEN** waypoints are grouped under "Day 1", "Day 2", etc. with collapsible sections
- **AND** each section header shows day distance and ascent
### Requirement: Elevation chart day dividers
The elevation chart SHALL show day boundaries as dashed vertical lines.
#### Scenario: Day dividers on chart
- **WHEN** a route has multiple days
- **THEN** dashed vertical lines with "Day N" labels appear at each overnight waypoint position
### Requirement: Map day labels
The map SHALL display day summary labels at day boundary waypoints.
#### Scenario: Day labels on map
- **WHEN** a route has multiple days
- **THEN** white pill markers at day boundaries show "Day N . X km"
### Requirement: Multi-day GPX export
Day structure SHALL be preserved in GPX exports via waypoint type elements.
#### Scenario: Export multi-day route
- **WHEN** a user exports a plan with overnight waypoints
- **THEN** overnight waypoints include a `<type>overnight</type>` element in the GPX
- **AND** reimporting the GPX restores the day structure

View file

@ -0,0 +1,28 @@
## ADDED Requirements
### Requirement: Map-based route exploration
The Journal SHALL provide an explore page with a full-page map for browsing public routes by location.
#### Scenario: Browse public routes
- **WHEN** a user visits `/routes/explore`
- **THEN** a full-page Leaflet map is displayed
- **AND** public routes within the current viewport are shown as clickable polylines
#### Scenario: Route popup
- **WHEN** a user clicks a route polyline on the explore map
- **THEN** a popup shows the route name, distance, elevation gain, author, and a link to the detail page
### Requirement: Bounding box spatial query
The Journal SHALL provide an API endpoint for querying public routes by map viewport bounds.
#### Scenario: Viewport query
- **WHEN** the map viewport changes
- **THEN** the client requests routes intersecting the current bounds via PostGIS `ST_Intersects`
- **AND** results are limited to 50 routes with simplified geometries
### Requirement: Spatial index
The routes table SHALL have a GiST index on the geometry column for fast bounding box queries.
#### Scenario: Index exists
- **WHEN** the database is set up
- **THEN** a GiST index on `journal.routes.geom` supports efficient spatial queries

View file

@ -0,0 +1,38 @@
## ADDED Requirements
### Requirement: Route visibility
Routes and activities SHALL support private (default) and public visibility levels.
#### Scenario: Set route public
- **WHEN** a route owner toggles visibility to "public"
- **THEN** the route is viewable by anyone with the link
#### Scenario: Default private
- **WHEN** a new route is created
- **THEN** its visibility is "private" (owner-only)
### Requirement: Per-user route sharing
Route owners SHALL be able to share routes with specific users at view or edit permission levels.
#### Scenario: Share with user
- **WHEN** a route owner shares a route with another user at "view" level
- **THEN** the shared user sees the route in their collection and can view it
#### Scenario: Edit permission
- **WHEN** a route owner shares a route with "edit" permission
- **THEN** the shared user can update the route name, description, and GPX
### Requirement: Route forking
Logged-in users SHALL be able to fork any public route into their own collection.
#### Scenario: Fork public route
- **WHEN** a user clicks "Fork" on a public route
- **THEN** a copy of the route metadata and latest GPX is created in their collection
- **AND** a `forkedFromId` link references the original route
### Requirement: Contributor tracking
Route versions SHALL record the identity of the contributor who saved them.
#### Scenario: Planner callback contributor
- **WHEN** the Planner saves back to the Journal via JWT callback
- **THEN** the contributor's identity is recorded on the new route version

View file

@ -0,0 +1,40 @@
## ADDED Requirements
### Requirement: Design token system
The Planner SHALL use CSS custom properties for colors, typography, and spacing with Tailwind config extensions.
#### Scenario: Consistent styling
- **WHEN** a component is styled
- **THEN** it uses project design tokens (warm off-whites, sage green accent, earthy tones) via Tailwind classes
#### Scenario: Typography
- **WHEN** text is rendered
- **THEN** body text uses Outfit font and statistics use Geist Mono
### Requirement: Redesigned topbar
The Planner topbar SHALL include a logo, segmented color mode toggle, participant avatars, and export controls.
#### Scenario: Topbar layout
- **WHEN** a user is in a planning session
- **THEN** the topbar shows: logo with mountain mark, color mode toggle, participant avatars with names, and Export GPX button
### Requirement: Redesigned sidebar
The Planner sidebar SHALL display a route summary header with stats, collapsible day breakdown, and waypoints nested inside days.
#### Scenario: Route summary
- **WHEN** a route is computed
- **THEN** the sidebar header shows route name, distance, ascent, and estimated duration
### Requirement: Redesigned map markers
Map waypoint markers SHALL use olive/dark numbered circles with overnight badges on stop waypoints.
#### Scenario: Marker styling
- **WHEN** waypoints are displayed on the map
- **THEN** they appear as dark numbered circles instead of default blue markers
### Requirement: Mobile responsive layout
The Planner SHALL use a bottom sheet pattern on mobile instead of a hidden sidebar.
#### Scenario: Mobile view
- **WHEN** the Planner is viewed on a mobile device
- **THEN** a bottom sheet replaces the sidebar with swipeable tabs (Days/Waypoints/Notes)

View file

@ -0,0 +1,46 @@
## ADDED Requirements
### Requirement: Per-waypoint text notes
Each waypoint SHALL support an optional plain-text note synced via Yjs.
#### Scenario: Add note to waypoint
- **WHEN** a user clicks the note area under a waypoint in the sidebar and types text
- **THEN** the note is stored in the waypoint's Y.Map as a `note` string field
- **AND** auto-saves on blur
#### Scenario: Note syncs to participants
- **WHEN** a user adds or edits a waypoint note
- **THEN** all other participants see the update in real-time via Yjs
### Requirement: Map note indicators
Waypoint markers with notes SHALL show a visual indicator on the map.
#### Scenario: Note icon on marker
- **WHEN** a waypoint has a note
- **THEN** its map marker shows a small note icon
#### Scenario: Note tooltip
- **WHEN** a user hovers or taps a marker with a note
- **THEN** the note text appears in a tooltip
### Requirement: Notes in GPX export
Waypoint notes SHALL be exported as `<desc>` elements in GPX output.
#### Scenario: Export notes
- **WHEN** a user exports a plan with waypoint notes
- **THEN** each waypoint's note appears as a `<desc>` element in the GPX file
### Requirement: Nearby POI display
When a waypoint is selected, nearby POIs from OpenStreetMap SHALL be shown on the map and in the sidebar.
#### Scenario: POI lookup
- **WHEN** a user selects a waypoint
- **THEN** nearby POIs are fetched from the Overpass API and displayed as small markers on the map and as a list in the sidebar
### Requirement: Snap waypoint to POI
Users SHALL be able to move a waypoint to a nearby POI's exact coordinates.
#### Scenario: Snap to POI
- **WHEN** a user clicks a nearby POI
- **THEN** the waypoint moves to the POI's coordinates
- **AND** the POI's name and type are added as a note prefix (e.g., "Campsite - Waldcamp Fichtelberg")

View file

@ -1,4 +1,8 @@
## MODIFIED Requirements
## Purpose
User account settings page with connected services management, profile editing, and security controls.
## Requirements
### Requirement: Connected Services section
The settings page SHALL include a "Connected Services" section for managing external integrations.

View file

@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
Activity creation, chronological feed, detail views, and route linking in the Journal app.
## Requirements
### Requirement: Create activity
The Journal SHALL allow authenticated users to create an activity by uploading a GPX trace and adding a description.

View file

@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
Route computation between waypoints via the BRouter HTTP API, including routing host election, result broadcasting via Yjs, profile selection, and rate-limited proxying.
## Requirements
### Requirement: Route computation from waypoints
The Planner SHALL compute a route between ordered waypoints by calling the BRouter HTTP API with tiledesc enabled and returning the result as an EnrichedRoute, preserving per-point elevation, surface data, and segment boundary indices.

View file

@ -1,3 +1,7 @@
## Purpose
Browser crash resilience via localStorage-based Yjs state persistence and recovery on reconnect.
## Requirements
### Requirement: localStorage crash recovery

View file

@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
GPX file import in the Planner via home page file picker and in-session drag-and-drop, with client-side parsing and waypoint extraction.
## Requirements
### Requirement: Import GPX from home page
Users SHALL be able to import a GPX file from the planner home page to start a new planning session.
@ -30,7 +34,9 @@ Users SHALL be able to drag a GPX file onto the map in an existing session.
- **WHEN** a user drops a GPX file and the confirmation dialog appears
- **THEN** clicking "Cancel" leaves the session unchanged
### Requirement: Non-GPX files are rejected
### Requirement: Non-GPX file rejection
The Planner SHALL reject non-GPX files dropped on the map.
#### Scenario: Drop non-GPX file
- **WHEN** a user drops a non-GPX file on the map
- **THEN** the file is ignored with a brief error toast

View file

@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
Server provisioning on Hetzner, Docker Compose deployment, CI/CD pipelines, database and BRouter management, TLS, Sentry, Grafana, and monitoring stack for the flagship instance.
## Requirements
### Requirement: Terraform Hetzner provisioning
Infrastructure SHALL be provisioned on Hetzner Cloud using Terraform with the Hetzner provider.

View file

@ -1,4 +1,8 @@
## MODIFIED Requirements
## Purpose
Authentication for the Journal app, including OAuth token storage for external services in the sync_connections table.
## Requirements
### Requirement: Store external service tokens
The journal auth system SHALL store OAuth tokens for external services alongside user credentials.

View file

@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
One-command local development setup with PostgreSQL, BRouter, automatic schema migration, and segment downloading.
## Requirements
### Requirement: One-command dev startup
The project SHALL provide a single command that starts all services needed for local development.

View file

@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
Interactive map rendering with Leaflet and OSM tiles, waypoint editing, route visualization, elevation profiles, multiplayer cursors, and polygon drawing for both Planner and Journal apps.
## 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.

View file

@ -1,3 +1,7 @@
## Purpose
Polygon-based route avoidance areas drawn on the map, synced via Yjs, sent to BRouter as constraints, and persisted in GPX extensions.
## Requirements
### Requirement: Draw no-go areas
@ -35,22 +39,16 @@ No-go areas SHALL be preserved when saving to the journal or exporting a plan.
- **AND** the file is compatible with any GPX-consuming application
### Requirement: GPX extensions format
No-go areas are stored in GPX using a custom XML namespace:
No-go areas SHALL be stored in GPX using the `trails:planning` custom XML namespace with point-based polygon representation.
```xml
<gpx xmlns:trails="https://trails.cool/gpx/1">
<extensions>
<trails:planning>
<trails:nogo>
<trails:point lat="52.5" lon="13.3"/>
<trails:point lat="52.4" lon="13.4"/>
<trails:point lat="52.3" lon="13.2"/>
</trails:nogo>
</trails:planning>
</extensions>
</gpx>
```
#### Scenario: Valid no-go area in GPX
- **WHEN** a GPX file contains a `<trails:nogo>` element with 3+ `<trails:point>` child elements
- **THEN** the parser creates a no-go area polygon from the point coordinates
- Each `<trails:nogo>` element contains 3+ `<trails:point>` elements
- Parser accepts both namespaced (`trails:nogo`) and non-namespaced (`nogo`) elements
- Areas with fewer than 3 points are rejected on parse
#### Scenario: Namespace-agnostic parsing
- **WHEN** a GPX file contains non-namespaced `<nogo>` elements instead of `<trails:nogo>`
- **THEN** the parser accepts them identically
#### Scenario: Invalid no-go area rejected
- **WHEN** a GPX file contains a no-go area with fewer than 3 points
- **THEN** the parser rejects it

View file

@ -1,3 +1,7 @@
## Purpose
Health endpoints, Prometheus metrics, structured logging, Grafana dashboards, and alerting for both apps and the flagship instance.
## Requirements
### Requirement: Health endpoints

View file

@ -1,7 +1,11 @@
## MODIFIED Requirements
## Purpose
Round-trip GPX exchange between Planner and Journal, including JWT-scoped callbacks for saving routes and GPX reimport in the Planner.
## Requirements
### Requirement: Export Plan reimport
The "Export Plan" GPX can now be reimported directly in the planner via the file upload UI, completing the round-trip without needing the journal.
The Planner SHALL support reimporting an exported plan GPX via the file upload UI, completing the round-trip without needing the journal.
#### Scenario: Reimport exported plan
- **WHEN** a user exports a plan and later imports it via the planner's GPX upload

View file

@ -1,7 +1,11 @@
## MODIFIED Requirements
## Purpose
### Requirement: Session initialization
Sessions can now be initialized from a GPX file upload in addition to URL parameters and the journal handoff.
Collaborative planning session management with Yjs CRDT synchronization, supporting initialization from URL parameters, journal handoff, and GPX file upload.
## Requirements
### Requirement: Session initialization from GPX
The Planner SHALL support initializing sessions from a GPX file upload in addition to URL parameters and the journal handoff.
#### Scenario: Session created from GPX upload
- **WHEN** a session is created via GPX file upload on the home page

View file

@ -1,3 +1,7 @@
## Purpose
Request rate limiting for Planner session creation and BRouter API calls to prevent abuse.
## Requirements
### Requirement: Session creation rate limit

View file

@ -1,3 +1,7 @@
## Purpose
Multi-mode route visualization (plain, elevation gradient, surface type) with session-synced color mode selection.
## Requirements
### Requirement: Route color modes

View file

@ -1,3 +1,7 @@
## Purpose
Ghost marker drag interaction for reshaping routes by inserting new waypoints mid-segment.
## Requirements
### Requirement: Ghost marker on route hover

View file

@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
Route CRUD operations, GPX import/export, sequential versioning, PostGIS spatial storage, and route metadata in the Journal app.
## Requirements
### Requirement: Create route
The Journal SHALL allow authenticated users to create a new route with a name and optional description.

View file

@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
Map thumbnails on route and activity list pages, and interactive maps with zoom and layer switching on detail pages.
## Requirements
### Requirement: Route map preview on list pages
Route and activity list pages SHALL show a small map thumbnail for each item that has geometry.

View file

@ -1,3 +1,7 @@
## Purpose
Waypoint insertion by clicking the ghost marker on the route polyline, splitting a segment into two.
## Requirements
### Requirement: Insert waypoint by clicking on route

View file

@ -1,3 +1,7 @@
## Purpose
SOPS-encrypted secrets stored in the repository, split into app and infra files, decryptable with a single age key at deploy time.
## Requirements
### Requirement: Encrypted secrets in repository

View file

@ -1,3 +1,7 @@
## Purpose
Security headers, scanner path blocking, secret scanning, dependency auditing, non-root containers, and vulnerability disclosure policy.
## Requirements
### Requirement: Security response headers

View file

@ -1,3 +1,7 @@
## Purpose
Collaborative text notes in Planner sessions, synced in real-time via Yjs.
## Requirements
### Requirement: Collaborative session notes

View file

@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
Shared TypeScript packages (@trails-cool/types, gpx, map, ui, i18n) used by both Planner and Journal apps.
## 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.

View file

@ -1,3 +1,7 @@
## Purpose
Transactional email sending with HTML and plain-text templates for magic links and welcome messages, using SMTP in production and console logging in dev.
## Requirements
### Requirement: Email sending interface

View file

@ -1,4 +1,8 @@
## ADDED Requirements
## Purpose
Provider-agnostic activity sync framework with Wahoo as the first provider, supporting OAuth connection, webhook-based automatic sync, manual import, and FIT-to-GPX conversion.
## Requirements
### Requirement: Provider-agnostic sync framework
The system SHALL provide a common interface for external activity sync providers.