- Add INTEGRATION_SECRET to journal service in docker-compose.yml with :? guard so a missing value fails loudly at compose-up time - Add INTEGRATION_SECRET to E2E test step in ci.yml via GitHub secret (unit tests already set their own value in the test file) - Archive openspec/changes/komoot-import → archive/2026-05-23-komoot-import - Sync delta specs: new openspec/specs/komoot-import/spec.md, updated openspec/specs/route-management/spec.md Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.8 KiB
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.
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. Access depends on the route's visibility: public routes are viewable by anyone including unauthenticated visitors, unlisted routes are viewable by anyone who has the URL, and private routes are viewable only by the owner.
Scenario: Owner views own route
- WHEN a logged-in user navigates to a route they own at any visibility
- THEN they see the route name, description, a map with the route polyline, distance, and elevation gain/loss
Scenario: Anyone views a public route
- WHEN any visitor (including unauthenticated) navigates to a
publicroute's URL - THEN they see the full route detail page as above
Scenario: Anyone with the URL views an unlisted route
- WHEN any visitor navigates directly to an
unlistedroute's URL - THEN they see the full route detail page as above
Scenario: Non-owner is blocked from a private route
- WHEN a visitor who is not the owner requests a
privateroute URL - THEN the server responds with HTTP 404 (not 403), so the existence of the private route is not leaked
Scenario: Public and unlisted route pages emit social-share metadata
- WHEN a visitor loads a
publicorunlistedroute detail page - THEN the response emits Open Graph and Twitter Card meta tags (
og:title,og:description,og:type="article",og:site_name,twitter:card="summary")
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 data includes geometry for rendering
Route and activity loaders SHALL return GeoJSON geometry when available.
Scenario: Route list returns simplified geometry
- WHEN the routes list loader runs
- THEN each route includes a
geojsonfield containing the geometry as a GeoJSON string - AND the geometry is simplified server-side via
ST_Simplify()for list page performance
Scenario: Route detail returns full geometry
- WHEN the route detail loader runs and the route has geometry
- THEN the route includes a
geojsonfield with the full-resolution GeoJSON geometry
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
Requirement: Route visibility
The Journal SHALL persist a visibility value on every route and SHALL allow the owner to change it.
Scenario: New routes default to private
- WHEN a route is created without an explicit visibility
- THEN the route row is persisted with
visibility = 'private'
Scenario: Owner changes a route's visibility
- WHEN a route owner selects a different visibility (
private,unlisted,public) in the edit flow and saves - THEN the stored visibility is updated and subsequent access checks use the new value immediately
Scenario: Non-owner cannot change visibility
- WHEN a request to update visibility arrives from a user who is not the route owner
- THEN the server rejects it with HTTP 403 or 404 (matching the current update-route behaviour), and the stored value is unchanged
Requirement: Route listings respect visibility
Any listing that exposes routes beyond the owner's own dashboard SHALL only include routes with visibility = 'public'.
Scenario: Public profile lists only public routes
- WHEN a visitor loads
/users/:username - THEN the rendered list of routes includes only the user's
publicroutes;unlistedandprivateroutes are omitted
Scenario: Owner's own routes list is unchanged
- WHEN a logged-in user views their own routes list at
/routes - THEN the list includes all of their own routes regardless of visibility
Requirement: Route creation via import
The system SHALL support creating routes via import from external services, in addition to manual creation and GPX upload.
Scenario: Route created from import
- WHEN a Komoot tour is imported
- THEN a route is created with name, distance, elevation, GPX geometry, and a source field indicating "komoot"
Scenario: Route links to activity
- WHEN a tour is imported as an activity
- THEN the activity is linked to the created route
Requirement: Synthetic route flag
The Journal SHALL persist a synthetic boolean on every route so automated / demo content can be distinguished from user-created content.
Scenario: User-created routes default to non-synthetic
- WHEN a route is created through any user-facing flow (New Route, GPX import, Planner handoff)
- THEN the route row is persisted with
synthetic = false
Scenario: Bot inserts flag their rows as synthetic
- WHEN the demo-activity-bot inserts a route
- THEN the row is persisted with
synthetic = true
Scenario: Synthetic flag is not user-editable
- WHEN a route owner edits a route via any user-facing action
- THEN the stored
syntheticvalue is not changed by the edit