Add mobile app, map-core, nearby sync, and activity recording specs

mobile-app: Unified React Native + Expo app combining Planner and
Journal. OAuth2 PKCE auth, MapLibre maps, versioned REST API with
Zod schemas, configurable server URL, offline SQLite, Web Push relay
notifications. TanStack Query + Zustand state management. Jest +
Maestro testing. 76 tasks across 5 phases.

map-core-package: Extract renderer-agnostic map definitions (tiles,
color palettes, POI categories, z-index) into @trails-cool/map-core.
Pure refactor preparing for MapLibre on mobile. 27 tasks.

mobile-activity-recording: GPS recording, live stats, HealthKit/Health
Connect export. Separated from mobile-app for independent scheduling.

mobile-nearby-sync: BLE route sync between nearby devices for offline
group riding. QR waypoint sharing as simpler v1. TXQR noted as future.

journal-rest-api spec: Full API contract — endpoints, auth, pagination,
errors, discovery, versioning, BRouter proxy.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-12 21:52:30 +02:00
parent e288630a12
commit fff77a2ed2
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
23 changed files with 996 additions and 0 deletions

View file

@ -0,0 +1,27 @@
## MODIFIED Requirements
### Requirement: OAuth2 PKCE authorization flow
The Journal SHALL support OAuth2 authorization code flow with PKCE for mobile app token exchange, in addition to existing passkey and magic link authentication.
#### Scenario: Authorization endpoint
- **WHEN** a client requests `GET /oauth/authorize` with client_id, redirect_uri, code_challenge, and code_challenge_method
- **THEN** the Journal shows the existing login UI and, upon successful authentication, redirects to the redirect_uri with an authorization code
#### Scenario: Token exchange
- **WHEN** a client sends `POST /oauth/token` with the authorization code, code_verifier, client_id, and redirect_uri
- **THEN** the Journal validates the PKCE challenge, issues an access token and refresh token, and returns them
#### Scenario: Token refresh
- **WHEN** a client sends `POST /oauth/token` with grant_type=refresh_token and a valid refresh token
- **THEN** the Journal issues a new access token and optionally a new refresh token
#### Scenario: Invalid PKCE challenge
- **WHEN** a client sends a code_verifier that does not match the stored code_challenge
- **THEN** the Journal rejects the token exchange with a 400 error
### Requirement: OAuth2 client registration
The Journal SHALL register the mobile app as a trusted first-party OAuth2 client.
#### Scenario: Mobile app client
- **WHEN** the mobile app initiates an OAuth2 flow with client_id `trails-cool-mobile`
- **THEN** the Journal recognizes it as a trusted client and allows the `trailscool://` redirect URI scheme

View file

@ -0,0 +1,124 @@
## ADDED Requirements
### Requirement: REST API namespace
The Journal SHALL expose a versioned REST API under `/api/v1/` for external clients.
#### Scenario: API base path
- **WHEN** a client sends a request to `/api/v1/*`
- **THEN** the Journal processes it as an API request with JSON responses and bearer token auth
#### Scenario: Non-API routes unaffected
- **WHEN** a browser accesses the Journal's existing web routes
- **THEN** they continue to work via React Router loaders/actions with cookie sessions
### Requirement: Instance discovery
The Journal SHALL expose a discovery endpoint for clients to identify the instance.
#### Scenario: Discovery endpoint
- **WHEN** a client fetches `GET /.well-known/trails-cool`
- **THEN** the response includes `apiVersion` (semver), instance name, and API base URL
#### Scenario: API version compatibility
- **WHEN** the client's minimum required API version exceeds the server's `apiVersion`
- **THEN** the client blocks with an upgrade prompt (offline data still accessible)
### Requirement: Shared API contract package
The API contract SHALL be defined in `@trails-cool/api` using Zod schemas, shared between server and clients.
#### Scenario: Type-safe requests
- **WHEN** the Journal server receives a request body
- **THEN** it validates the body using the Zod schema from `@trails-cool/api`
#### Scenario: Version in one place
- **WHEN** the API version needs to be bumped
- **THEN** it is changed in `@trails-cool/api` — both server and clients see it at compile time
### Requirement: Authentication
The API SHALL use OAuth2 bearer tokens for authentication.
#### Scenario: Authenticated request
- **WHEN** a client sends a request with `Authorization: Bearer <token>`
- **THEN** the server validates the token and processes the request as the authenticated user
#### Scenario: Unauthenticated request
- **WHEN** a client sends a request without a valid bearer token to a protected endpoint
- **THEN** the server responds with 401 Unauthorized
#### Scenario: Token refresh
- **WHEN** an access token expires
- **THEN** the client exchanges its refresh token at `POST /api/v1/auth/token` for a new access token
### Requirement: Routes endpoints
The API SHALL provide CRUD endpoints for routes.
#### Scenario: List routes
- **WHEN** `GET /api/v1/routes?cursor=<cursor>`
- **THEN** returns paginated route list with id, name, distance, elevationGain, thumbnail geojson, updatedAt, and `nextCursor`
#### Scenario: Get route detail
- **WHEN** `GET /api/v1/routes/:id`
- **THEN** returns full route with metadata, GPX, waypoints, dayBreaks, day stats, geojson, and version history
#### Scenario: Update route
- **WHEN** `PUT /api/v1/routes/:id` with GPX body
- **THEN** creates a new version, updates stats and dayBreaks, returns updated route
#### Scenario: Create route
- **WHEN** `POST /api/v1/routes` with name and optional GPX
- **THEN** creates a new route, returns the route with id
#### Scenario: Delete route
- **WHEN** `DELETE /api/v1/routes/:id`
- **THEN** deletes the route and all versions, returns 204
### Requirement: Activities endpoints
The API SHALL provide CRUD endpoints for activities.
#### Scenario: List activities
- **WHEN** `GET /api/v1/activities?cursor=<cursor>`
- **THEN** returns paginated activity list with id, name, routeId, distance, duration, startedAt, and `nextCursor`
#### Scenario: Get activity detail
- **WHEN** `GET /api/v1/activities/:id`
- **THEN** returns full activity with stats, GPX, linked route info, geojson
#### Scenario: Create activity
- **WHEN** `POST /api/v1/activities` with name, GPX, optional routeId
- **THEN** creates activity, computes stats from GPX, returns activity with id
#### Scenario: Delete activity
- **WHEN** `DELETE /api/v1/activities/:id`
- **THEN** deletes the activity, returns 204
### Requirement: Route computation proxy
The API SHALL proxy BRouter route computation requests.
#### Scenario: Compute route
- **WHEN** `POST /api/v1/routes/compute` with waypoints array and profile
- **THEN** the Journal forwards to its BRouter instance and returns the enriched route (geojson, coordinates, segmentBoundaries, surfaces, highways, etc.)
### Requirement: Cursor-based pagination
All list endpoints SHALL use cursor-based pagination.
#### Scenario: First page
- **WHEN** a list endpoint is called without a cursor
- **THEN** returns the first page of results with `nextCursor` (null if no more results)
#### Scenario: Next page
- **WHEN** a list endpoint is called with `?cursor=<nextCursor>`
- **THEN** returns the next page of results
### Requirement: Error responses
The API SHALL return structured error responses.
#### Scenario: Validation error
- **WHEN** a request body fails Zod validation
- **THEN** returns 400 with `{ error: "Validation failed", code: "VALIDATION_ERROR", fields: [...] }`
#### Scenario: Not found
- **WHEN** a resource doesn't exist
- **THEN** returns 404 with `{ error: "Not found", code: "NOT_FOUND" }`
#### Scenario: Server error
- **WHEN** an unexpected error occurs
- **THEN** returns 500 with `{ error: "Internal server error", code: "INTERNAL_ERROR" }` (no stack traces)

View file

@ -0,0 +1,45 @@
## ADDED Requirements
### Requirement: Expo app scaffold
The system SHALL provide a React Native + Expo managed workflow app at `apps/mobile/` in the monorepo, sharing workspace packages.
#### Scenario: App boots on iOS and Android
- **WHEN** the app is launched on iOS or Android
- **THEN** the Expo managed app loads, displays the tab navigation, and renders the Map tab by default
#### Scenario: Monorepo integration
- **WHEN** the mobile app is built
- **THEN** it resolves `@trails-cool/types`, `@trails-cool/gpx`, and `@trails-cool/i18n` from pnpm workspace dependencies
### Requirement: Tab navigation
The system SHALL provide a bottom tab bar with four tabs: Map, Routes, Activities, and Profile.
#### Scenario: Tab switching
- **WHEN** the user taps a tab (Map, Routes, Activities, or Profile)
- **THEN** the corresponding screen is displayed and the active tab is visually highlighted
#### Scenario: Tab state preservation
- **WHEN** the user switches between tabs
- **THEN** each tab preserves its scroll position and navigation stack
### Requirement: Authentication flow
The system SHALL require Journal account authentication before accessing protected screens.
#### Scenario: First launch
- **WHEN** the user opens the app for the first time
- **THEN** a login screen is shown with the option to enter their Journal instance URL and authenticate
#### Scenario: Session persistence
- **WHEN** the user has previously authenticated and reopens the app
- **THEN** stored tokens are loaded from SecureStore and the user is logged in automatically
### Requirement: Deep linking
The system SHALL handle deep links for opening routes and activities.
#### Scenario: Open route via deep link
- **WHEN** the app receives a deep link like `trailscool://routes/:id`
- **THEN** the app navigates to the route detail screen for that route
#### Scenario: Open Planner for collaborative editing
- **WHEN** the user taps "Edit in Planner" on a route
- **THEN** the system opens the Planner web URL in the device browser with the appropriate session callback

View file

@ -0,0 +1,49 @@
## ADDED Requirements
### Requirement: Journal authentication client
The system SHALL provide an API client that handles OAuth2 PKCE authentication with a Journal instance.
#### Scenario: Login flow
- **WHEN** the user enters their Journal instance URL and taps "Sign in"
- **THEN** the app opens the Journal's `/oauth/authorize` endpoint in an in-app browser, receives the auth code on redirect, and exchanges it for access + refresh tokens
#### Scenario: Token storage
- **WHEN** tokens are received from the Journal
- **THEN** the access token and refresh token are stored in Expo SecureStore
#### Scenario: Token refresh
- **WHEN** an API request fails with a 401
- **THEN** the client automatically uses the refresh token to obtain a new access token and retries the request
### Requirement: Routes API client
The system SHALL provide methods for listing, fetching, creating, and updating routes on the Journal.
#### Scenario: List user routes
- **WHEN** the Routes tab is opened
- **THEN** the client fetches the user's routes from `GET /api/routes` and returns them as typed Route objects
#### Scenario: Fetch route detail
- **WHEN** a route is selected
- **THEN** the client fetches the route with GPX data from `GET /api/routes/:id` including waypoints and geometry
#### Scenario: Save route
- **WHEN** the user saves an edited route
- **THEN** the client sends the updated GPX to `PUT /api/routes/:id` and returns the new version
### Requirement: Activities API client
The system SHALL provide methods for listing, fetching, and creating activities on the Journal.
#### Scenario: List user activities
- **WHEN** the Activities tab is opened
- **THEN** the client fetches the user's activities from `GET /api/activities`
#### Scenario: Create activity
- **WHEN** a recording is saved
- **THEN** the client sends the activity data (GPX, distance, duration, routeId) to `POST /api/activities`
### Requirement: Network error handling
The system SHALL handle network failures gracefully across all API calls.
#### Scenario: Request timeout or network error
- **WHEN** an API request fails due to network issues
- **THEN** the client returns a typed error and the UI shows an appropriate message with a retry option

View file

@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: Download routes for offline
The system SHALL allow users to download route data (GPX, waypoints, metadata) for offline access.
#### Scenario: Download a route
- **WHEN** the user taps "Download for offline" on a route
- **THEN** the route GPX, parsed waypoints, and metadata are stored in the local SQLite database
#### Scenario: View offline route
- **WHEN** the user views a downloaded route without network connectivity
- **THEN** the route is loaded from the local database and displayed on the map with cached tiles
#### Scenario: Delete offline data
- **WHEN** the user removes a route from offline storage
- **THEN** all cached data for that route (GPX, tiles, metadata) is deleted from the device
### Requirement: Map tile caching
The system SHALL cache map tiles for downloaded route regions so maps are viewable offline.
#### Scenario: Tile download for route region
- **WHEN** a route is downloaded for offline use
- **THEN** map tiles covering the route bounding box at relevant zoom levels are cached locally
#### Scenario: Storage budget
- **WHEN** the total offline storage exceeds the configured budget (shown on Profile tab)
- **THEN** the system warns the user and suggests removing older offline routes
### Requirement: Offline edit queue
The system SHALL queue route edits made offline and sync them when connectivity returns.
#### Scenario: Queue edits while offline
- **WHEN** the user edits a route without network connectivity
- **THEN** the changes are saved to a local edit queue and a sync-pending indicator is shown
#### Scenario: Sync on reconnect
- **WHEN** the device regains network connectivity
- **THEN** queued edits are sent to the Journal API in order and the sync indicator clears
#### Scenario: Conflict on sync
- **WHEN** the server version of a route changed while the user was offline
- **THEN** the system warns the user and applies last-write-wins, preserving the local version

View file

@ -0,0 +1,49 @@
## ADDED Requirements
### Requirement: Map-based waypoint editing
The system SHALL allow users to add, move, and delete waypoints on a native map view.
#### Scenario: Add waypoint
- **WHEN** the user long-presses on the map
- **THEN** a new waypoint is added at that location and the route is recomputed through it
#### Scenario: Move waypoint
- **WHEN** the user drags an existing waypoint marker
- **THEN** the waypoint position updates and the route segments connected to it are recomputed
#### Scenario: Delete waypoint
- **WHEN** the user taps a waypoint and confirms deletion
- **THEN** the waypoint is removed and the route is recomputed without it
### Requirement: Overnight stops and POI snap
The system SHALL support marking waypoints as overnight stops and snapping to nearby POIs.
#### Scenario: Toggle overnight stop
- **WHEN** the user taps a waypoint and toggles "Overnight stop"
- **THEN** the waypoint is marked with an overnight icon and day segments update accordingly
#### Scenario: POI snap suggestion
- **WHEN** the user adds a waypoint near a known POI (campsite, shelter, etc.)
- **THEN** the system suggests snapping to the POI location and applying its name
### Requirement: BRouter routing
The system SHALL compute route segments between waypoints via the BRouter routing engine.
#### Scenario: Route computation
- **WHEN** two or more waypoints exist
- **THEN** the system requests a route from BRouter (proxied through the Journal API) and displays the resulting polyline on the map
#### Scenario: Routing error
- **WHEN** BRouter fails to compute a route between waypoints
- **THEN** the system shows an error and falls back to displaying a straight line between waypoints
### Requirement: Save route to Journal
The system SHALL save edited routes back to the Journal via its API.
#### Scenario: Save after editing
- **WHEN** the user taps "Save" after editing a route
- **THEN** the updated GPX is generated from current waypoints and route geometry, and saved to the Journal API as a new version
#### Scenario: Unsaved changes warning
- **WHEN** the user navigates away from the editor with unsaved changes
- **THEN** the system prompts the user to save or discard changes

View file

@ -0,0 +1,26 @@
## ADDED Requirements
### Requirement: Unit and component testing with Jest
The mobile app SHALL use Jest + jest-expo + React Native Testing Library for unit and component tests.
#### Scenario: Unit tests run
- **WHEN** `pnpm test` is run in the mobile workspace
- **THEN** Jest executes all `*.test.ts(x)` files with jest-expo preset
#### Scenario: Component rendering tests
- **WHEN** a component test renders a screen with React Native Testing Library
- **THEN** the test can query by accessibility label, text, and testID without a device
### Requirement: E2E testing with Maestro
The mobile app SHALL use Maestro for end-to-end flow testing on real/emulated devices.
#### Scenario: Maestro flow execution
- **WHEN** a Maestro YAML flow is run against a development build
- **THEN** the flow interacts with the app via the accessibility layer (tap, scroll, assert text)
#### Scenario: E2E in CI
- **WHEN** a PR is opened
- **THEN** Maestro E2E flows run against an EAS preview build
### Requirement: No Vitest for mobile
Vitest SHALL NOT be used for the mobile app. React Native Testing Library has incomplete Vitest support — Jest with jest-expo is the stable, Expo-recommended choice.

View file

@ -0,0 +1,16 @@
## MODIFIED Requirements
### Requirement: Deep link from mobile to Planner
The system SHALL allow the mobile app to open the Planner web app for full collaborative editing of a route.
#### Scenario: Open in Planner from mobile
- **WHEN** the user taps "Edit in Planner" on a route in the mobile app
- **THEN** the device browser opens the Planner with the route loaded and a JWT callback URL for saving back to the Journal
#### Scenario: Return to mobile after Planner save
- **WHEN** the user saves a route in the Planner web session that was opened from the mobile app
- **THEN** the Planner triggers a deep link back to the mobile app, which refreshes the route to show the updated version
#### Scenario: Planner session without save
- **WHEN** the user returns to the mobile app without saving in the Planner
- **THEN** the mobile app shows the route unchanged (no stale data)

View file

@ -0,0 +1,30 @@
## MODIFIED Requirements
### Requirement: React Native compatibility for types package
The `@trails-cool/types` package SHALL work in React Native without modification.
#### Scenario: Types import in mobile app
- **WHEN** the mobile app imports interfaces from `@trails-cool/types`
- **THEN** all types resolve correctly with no DOM or Node.js API dependencies
### Requirement: React Native compatibility for gpx package
The `@trails-cool/gpx` package SHALL work in React Native by using a platform-appropriate XML parser.
#### Scenario: GPX parsing on mobile
- **WHEN** the mobile app calls `parseGpx()` with a GPX string
- **THEN** the package uses the React Native runtime's DOMParser (or a polyfill) instead of `linkedom`, and returns the same typed output as on Node.js
#### Scenario: GPX generation on mobile
- **WHEN** the mobile app calls `generateGpx()` with route data
- **THEN** the package produces valid GPX XML without relying on Node.js APIs
### Requirement: React Native compatibility for i18n package
The `@trails-cool/i18n` package SHALL work in React Native with the same translation files.
#### Scenario: i18n initialization on mobile
- **WHEN** the mobile app initializes i18n using `@trails-cool/i18n`
- **THEN** translations load correctly and `useTranslation()` works in React Native components
#### Scenario: Language switching
- **WHEN** the user changes language in the mobile app's Profile tab
- **THEN** all strings update to the selected language using the shared translation files