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>
124 lines
5 KiB
Markdown
124 lines
5 KiB
Markdown
## 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)
|