Complete mobile app specs: fill all gaps

Design fixes:
- D6: Cleaned up, points to separate activity-recording change
- D13: Locked in TanStack Query + Zustand + React Context
- D14: Tile hosting (OpenFreeMap default, configurable tileUrl)
- D15: Photo/media (presigned upload URLs, thumbnails)
- D16: Journal REST API implementation (api.v1.*.ts route modules)

New specs (4):
- web-push-relay: Web Push → APNs/FCM relay (Mastodon pattern)
- api-contract-package: @trails-cool/api with Zod schemas
- device-management: Connected devices list + revoke
- photo-media: Presigned uploads, thumbnails, photo display

Task updates:
- Added Phase 2: Journal REST API (16 tasks)
- Added Phase 7: Notifications (8 tasks)
- Renumbered all phases (1-7)
- 112 total tasks

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-12 22:12:54 +02:00
parent f6e46d871b
commit cd939ccf07
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
6 changed files with 352 additions and 104 deletions

View file

@ -96,14 +96,9 @@ No real-time collaboration on mobile. If the user needs full collaborative editi
- Activities: Stored locally first, synced to Journal when online
- Edit queue: Changes made offline queued and synced on reconnect (conflict resolution: last-write-wins, with a warning if the server version changed)
### D6: Activity recording with expo-location
### D6: Activity recording
Use `expo-location` for GPS tracking in the foreground/background:
- Start/stop recording from the active route view
- Live stats: distance, duration, current speed, elevation gain
- Track points stored locally, converted to GPX on stop
- Save as Journal activity linked to the route
- Export to HealthKit (iOS) / Health Connect (Android) via `expo-health`
Activity recording (GPS tracking, HealthKit) is a separate change — see `mobile-activity-recording`.
### D7: Navigation structure
@ -145,7 +140,40 @@ This aligns with the ActivityPub federation direction and matches how Mastodon c
### D13: State management
TBD — to be discussed.
Three layers, each with a clear responsibility:
- **TanStack Query** for server state — routes, activities, user profile. Provides caching, background refetch, stale-while-revalidate, cursor pagination helpers, and optimistic updates. All Journal API data flows through TanStack Query hooks.
- **Zustand** for local UI state — edit mode flags, recording state, offline queue status, selected map layer, download progress. Small, synchronous stores with no persistence (or optional persistence to AsyncStorage where needed).
- **React Context** only for auth token and server URL — these are set once at login and consumed everywhere. No complex state, no frequent updates. Avoids prop drilling for the two values every API call needs.
### D14: Tile hosting
MapLibre needs a vector tile source. Default to **OpenFreeMap** — free, OSM-based, no API key required. This matches the project's principle of using open standards and avoiding vendor lock-in.
Self-hosted instances can configure a custom tile URL in the discovery endpoint. The `/.well-known/trails-cool` response gains a `tileUrl` field (e.g., `"tileUrl": "https://tiles.example.org/{z}/{x}/{y}.pbf"`). When present, the mobile app and any future MapLibre web client use it instead of the default.
Fallback: if vector tiles fail to load (offline, misconfigured URL), fall back to raster tiles from OpenStreetMap tile servers — the same approach the web Planner already uses with Leaflet.
### D15: Photo/media handling
Routes and activities can have photos. The upload flow avoids proxying large files through the Journal server:
1. Client requests a presigned upload URL from `POST /api/v1/uploads` with filename and content type
2. Client uploads the file directly to S3/Garage using the presigned URL
3. Client confirms the upload via the API with the storage key (e.g., `POST /api/v1/routes/:id` or `PUT /api/v1/routes/:id` with the storage key in the photos array)
Photos are stored as an array of URLs on the route or activity record. Thumbnails are generated server-side on upload confirmation — the Journal creates a resized version and stores it alongside the original. The API returns both `url` and `thumbnailUrl` for each photo.
### D16: Journal REST API implementation
The REST API endpoints (`/api/v1/*`) are implemented as React Router route modules under `apps/journal/app/routes/api.v1.*.ts`. This follows the existing routing pattern — both apps use explicit `routes.ts` for registration.
Shared middleware handles:
- **Bearer token validation**: Extracts and validates the OAuth2 access token from the `Authorization` header, attaches the authenticated user to the request context
- **Zod body parsing**: Validates request bodies against schemas from `@trails-cool/api`, returns structured 400 errors on failure
- **Error formatting**: Catches exceptions and returns the standard `ApiErrorResponse` shape
Existing web routes (cookie-based auth, form actions, loaders) remain unchanged. The `/api/v1/*` routes use bearer tokens exclusively — no cookie sessions.
## Risks / Trade-offs

View file

@ -0,0 +1,52 @@
## ADDED Requirements
### Requirement: Zod schemas as source of truth
The `@trails-cool/api` package SHALL define all API request and response shapes as Zod schemas, with TypeScript types inferred via `z.infer<>`.
#### Scenario: Server validates request body
- **WHEN** the Journal server receives a request body for a known endpoint
- **THEN** it validates the body using the corresponding Zod schema from `@trails-cool/api` and returns a structured 400 error on failure
#### Scenario: Client validates response
- **WHEN** the mobile client receives a response from a Journal instance (especially self-hosted on an older version)
- **THEN** it can optionally validate the response against the Zod schema to detect incompatibilities
### Requirement: API version constant
The package SHALL export an `API_VERSION` semver constant as the single source of truth for the current API version.
#### Scenario: Version bump in one place
- **WHEN** the API version needs to be bumped (new endpoint, new field, breaking change)
- **THEN** the version is changed in `@trails-cool/api` and both the Journal server and mobile client see it at compile time
### Requirement: Endpoint path constants
The package SHALL export typed constants for all API endpoint paths.
#### Scenario: Endpoint paths used by server and client
- **WHEN** the server registers a route or the client constructs a URL
- **THEN** both import the path from `@trails-cool/api` (e.g., `ENDPOINTS.routes.list` resolves to `"/api/v1/routes"`)
### Requirement: Request and response schemas
The package SHALL define Zod schemas for all API endpoints.
#### Scenario: Routes schemas
- **WHEN** a route-related endpoint is called
- **THEN** schemas exist for `RouteListResponse`, `RouteDetailResponse`, `CreateRouteRequest`, `UpdateRouteRequest`
#### Scenario: Activities schemas
- **WHEN** an activity-related endpoint is called
- **THEN** schemas exist for `ActivityListResponse`, `ActivityDetailResponse`, `CreateActivityRequest`
#### Scenario: Auth schemas
- **WHEN** an auth-related endpoint is called
- **THEN** schemas exist for `TokenExchangeRequest`, `TokenResponse`, `DiscoveryResponse`
#### Scenario: Upload schemas
- **WHEN** an upload-related endpoint is called
- **THEN** schemas exist for `PresignedUploadRequest`, `PresignedUploadResponse`
### Requirement: Error response schema
The package SHALL define a standard error response schema used by all endpoints.
#### Scenario: Error shape
- **WHEN** any API endpoint returns an error
- **THEN** the response matches the `ApiErrorResponse` schema: `{ error: string, code: string, fields?: Array<{ field: string, message: string }> }`

View file

@ -0,0 +1,34 @@
## ADDED Requirements
### Requirement: Connected devices list
The Journal web settings page SHALL display a list of connected devices (active OAuth2 sessions).
#### Scenario: View connected devices
- **WHEN** the user opens the account settings page on the Journal web UI
- **THEN** a "Connected Devices" section lists all active OAuth2 tokens with device name, last active timestamp, and approximate location (derived from IP)
#### Scenario: Device shows last active time
- **WHEN** a device makes an API request using its OAuth2 token
- **THEN** the Journal updates the token's last active timestamp and IP address
### Requirement: Device metadata on token
Each OAuth2 token SHALL store device name, last active timestamp, and IP address.
#### Scenario: Device name stored on token exchange
- **WHEN** the mobile app exchanges an authorization code for tokens via `POST /oauth/token`
- **THEN** the device name (sent in the request body, e.g., "iPhone 15, iOS 18") is stored alongside the token record
#### Scenario: Last active updated on API use
- **WHEN** a bearer token is used to authenticate an API request
- **THEN** the token's `lastActiveAt` and `lastActiveIp` fields are updated
### Requirement: Revoke individual device tokens
Users SHALL be able to revoke individual device sessions from the Journal web settings page.
#### Scenario: Revoke device from web
- **WHEN** the user clicks "Revoke" on a connected device in the Journal settings
- **THEN** the OAuth2 token for that device is deleted and all subsequent API requests from that device fail with 401
#### Scenario: Revoke device from API
- **WHEN** a client sends `DELETE /api/v1/auth/devices/:id` with a valid bearer token
- **THEN** the targeted device token is revoked (the requesting device can revoke other devices, not itself via this endpoint)

View file

@ -0,0 +1,41 @@
## ADDED Requirements
### Requirement: Presigned upload URL
The Journal API SHALL provide presigned upload URLs for direct-to-storage file uploads.
#### Scenario: Request upload URL
- **WHEN** a client sends `POST /api/v1/uploads` with filename and content type
- **THEN** the server returns a presigned upload URL for S3/Garage and a storage key to reference the uploaded file
#### Scenario: Upload photo directly to storage
- **WHEN** the client receives a presigned URL
- **THEN** the client uploads the file directly to S3/Garage using an HTTP PUT to the presigned URL, bypassing the Journal server
### Requirement: Photo attachment on routes and activities
Routes and activities SHALL support an array of attached photos.
#### Scenario: Attach photo to route
- **WHEN** a client creates or updates a route with storage keys in the photos array
- **THEN** the route record stores the photo references and they are returned in subsequent GET requests with `url` and `thumbnailUrl`
#### Scenario: Attach photo to activity
- **WHEN** a client creates an activity with storage keys in the photos array
- **THEN** the activity record stores the photo references and they are returned in subsequent GET requests
### Requirement: Server-side thumbnail generation
The Journal SHALL generate thumbnails when a photo upload is confirmed.
#### Scenario: Thumbnail created on upload confirmation
- **WHEN** a route or activity is saved with a new photo storage key
- **THEN** the server reads the original image from storage, generates a resized thumbnail, stores it alongside the original, and records both URLs
### Requirement: Photo display
Clients SHALL display photos in route and activity detail views.
#### Scenario: View photos in route detail
- **WHEN** a client fetches a route with attached photos
- **THEN** the response includes an array of photo objects with `url` (full size) and `thumbnailUrl` (resized)
#### Scenario: Delete photo
- **WHEN** a client updates a route or activity and removes a storage key from the photos array
- **THEN** the photo and its thumbnail are deleted from storage on save

View file

@ -0,0 +1,42 @@
## ADDED Requirements
### Requirement: Web Push API on Journal
The Journal SHALL implement the standard Web Push API (RFC 8030) for sending push notifications to subscribed clients.
#### Scenario: Subscribe to push notifications
- **WHEN** a client sends `POST /api/v1/push/subscribe` with a push subscription object (endpoint URL, p256dh key, auth secret)
- **THEN** the Journal stores the subscription and uses it to deliver future notifications for that user
#### Scenario: Receive route update notification
- **WHEN** a shared route is updated by another user
- **THEN** the Journal sends an encrypted Web Push message to all subscribers of that route's owner
#### Scenario: Unsubscribe from push notifications
- **WHEN** a client sends `DELETE /api/v1/push/unsubscribe` with the subscription endpoint URL
- **THEN** the Journal removes the subscription and stops sending notifications to it
### Requirement: Push relay service
A relay service SHALL translate Web Push messages into platform-native push notifications (APNs for iOS, FCM for Android).
#### Scenario: Relay forwards encrypted payload to APNs
- **WHEN** the relay receives a Web Push message destined for an iOS device
- **THEN** the relay forwards the encrypted payload to APNs without decrypting or reading the content
#### Scenario: Relay forwards encrypted payload to FCM
- **WHEN** the relay receives a Web Push message destined for an Android device
- **THEN** the relay forwards the encrypted payload to FCM without decrypting or reading the content
#### Scenario: End-to-end encryption
- **WHEN** a notification is sent from the Journal through the relay to the device
- **THEN** the relay cannot read the notification content — only the mobile app can decrypt it using the subscription keys
### Requirement: Self-hosted relay compatibility
Self-hosted Journal instances SHALL send push notifications through the hosted relay by default, with an option to run a private relay.
#### Scenario: Default relay for self-hosted instances
- **WHEN** a self-hosted Journal instance sends a Web Push notification
- **THEN** it sends the standard Web Push request to the trails.cool hosted relay (no Apple/Google credentials needed on the self-hosted instance)
#### Scenario: Custom relay for self-hosters
- **WHEN** a self-hosted administrator configures a custom relay URL
- **THEN** the Journal sends Web Push requests to the custom relay instead of the hosted one

View file

@ -19,131 +19,182 @@
### 1.3 Shared Package Compatibility
- [ ] 1.2.1 Audit `@trails-cool/types` for DOM/Node.js dependencies — confirm it's pure TypeScript interfaces
- [ ] 1.2.2 Refactor `@trails-cool/gpx` to use a platform-agnostic XML parser: use `linkedom` on Node.js and `DOMParser` on React Native/browser
- [ ] 1.2.3 Write unit tests for `parseGpx()` and `generateGpx()` running in a jsdom-free environment to verify no DOM dependency
- [ ] 1.2.4 Verify `@trails-cool/i18n` initializes in React Native — add an `initMobile()` export if the current init assumes a browser environment
- [ ] 1.2.5 Add `apps/mobile` to Turborepo pipeline (`turbo.json`) for build and typecheck
- [ ] 1.3.1 Audit `@trails-cool/types` for DOM/Node.js dependencies — confirm it's pure TypeScript interfaces
- [ ] 1.3.2 Refactor `@trails-cool/gpx` to use a platform-agnostic XML parser: use `linkedom` on Node.js and `DOMParser` on React Native/browser
- [ ] 1.3.3 Write unit tests for `parseGpx()` and `generateGpx()` running in a jsdom-free environment to verify no DOM dependency
- [ ] 1.3.4 Verify `@trails-cool/i18n` initializes in React Native — add an `initMobile()` export if the current init assumes a browser environment
- [ ] 1.3.5 Add `apps/mobile` to Turborepo pipeline (`turbo.json`) for build and typecheck
### 1.3 Journal Auth (OAuth2 PKCE)
### 1.4 Journal Auth (OAuth2 PKCE)
- [ ] 1.3.1 Add `journal.oauth_clients` table with client_id, redirect_uri, and trusted flag
- [ ] 1.3.2 Add `journal.oauth_codes` table (code, userId, clientId, codeChallenge, codeChallengeMethod, expiresAt)
- [ ] 1.3.3 Add `journal.oauth_tokens` table (accessToken, refreshToken, userId, clientId, expiresAt)
- [ ] 1.3.4 Implement `GET /oauth/authorize` endpoint — show login UI, generate auth code on success, redirect to client
- [ ] 1.3.5 Implement `POST /oauth/token` endpoint — validate PKCE code_verifier, issue access + refresh tokens
- [ ] 1.3.6 Implement refresh token grant in `POST /oauth/token`
- [ ] 1.3.7 Add middleware to validate OAuth2 bearer tokens on existing API routes
- [ ] 1.3.8 Seed `trails-cool-mobile` as a trusted first-party OAuth2 client with `trailscool://` redirect URI
- [ ] 1.3.9 Write unit tests for PKCE challenge validation, token issuance, and token refresh
- [ ] 1.4.1 Add `journal.oauth_clients` table with client_id, redirect_uri, and trusted flag
- [ ] 1.4.2 Add `journal.oauth_codes` table (code, userId, clientId, codeChallenge, codeChallengeMethod, expiresAt)
- [ ] 1.4.3 Add `journal.oauth_tokens` table (accessToken, refreshToken, userId, clientId, expiresAt)
- [ ] 1.4.4 Implement `GET /oauth/authorize` endpoint — show login UI, generate auth code on success, redirect to client
- [ ] 1.4.5 Implement `POST /oauth/token` endpoint — validate PKCE code_verifier, issue access + refresh tokens
- [ ] 1.4.6 Implement refresh token grant in `POST /oauth/token`
- [ ] 1.4.7 Add middleware to validate OAuth2 bearer tokens on existing API routes
- [ ] 1.4.8 Seed `trails-cool-mobile` as a trusted first-party OAuth2 client with `trailscool://` redirect URI
- [ ] 1.4.9 Write unit tests for PKCE challenge validation, token issuance, and token refresh
### 1.4 Server Configuration
### 1.5 Server Configuration
- [ ] 1.4.1 Add server URL input on login screen with "Connect to a different server" toggle (defaults to `https://trails.cool`)
- [ ] 1.4.2 Add `GET /.well-known/trails-cool` discovery endpoint on the Journal — returns instance name, version, API base URL
- [ ] 1.4.3 Validate entered server URL by fetching discovery endpoint before proceeding to login
- [ ] 1.4.4 Check `apiVersion` semver from discovery against app's required minimum — block with upgrade prompt if server is too old
- [ ] 1.4.5 Persist server URL in Expo SecureStore, use as base for all API calls and OAuth2 flow
- [ ] 1.4.6 Re-check API version on app foreground (after background) — show banner if version mismatch detected
- [ ] 1.4.5 Add "Switch server" option on Profile tab — logs out, clears local data, returns to login
- [ ] 1.5.1 Add server URL input on login screen with "Connect to a different server" toggle (defaults to `https://trails.cool`)
- [ ] 1.5.2 Add `GET /.well-known/trails-cool` discovery endpoint on the Journal — returns instance name, version, API base URL
- [ ] 1.5.3 Validate entered server URL by fetching discovery endpoint before proceeding to login
- [ ] 1.5.4 Check `apiVersion` semver from discovery against app's required minimum — block with upgrade prompt if server is too old
- [ ] 1.5.5 Persist server URL in Expo SecureStore, use as base for all API calls and OAuth2 flow
- [ ] 1.5.6 Re-check API version on app foreground (after background) — show banner if version mismatch detected
- [ ] 1.5.7 Add "Switch server" option on Profile tab — logs out, clears local data, returns to login
### 1.5 Journal API Client (Mobile)
### 1.6 Journal API Client (Mobile)
- [ ] 1.4.1 Create `apps/mobile/src/lib/api-client.ts` with base HTTP client, auth header injection, and 401 auto-refresh logic
- [ ] 1.4.2 Create `apps/mobile/src/lib/auth.ts` with OAuth2 PKCE login flow using `expo-auth-session` and token storage via `expo-secure-store`
- [ ] 1.4.3 Implement routes API methods: listRoutes, getRoute, updateRoute, createRoute
- [ ] 1.4.4 Implement activities API methods: listActivities, getActivity, createActivity
- [ ] 1.4.5 Add typed error handling for network failures, 401, and server errors
- [ ] 1.4.6 Write unit tests for API client with mocked fetch responses
- [ ] 1.6.1 Create `apps/mobile/src/lib/api-client.ts` with base HTTP client, auth header injection, and 401 auto-refresh logic
- [ ] 1.6.2 Create `apps/mobile/src/lib/auth.ts` with OAuth2 PKCE login flow using `expo-auth-session` and token storage via `expo-secure-store`
- [ ] 1.6.3 Implement routes API methods: listRoutes, getRoute, updateRoute, createRoute
- [ ] 1.6.4 Implement activities API methods: listActivities, getActivity, createActivity
- [ ] 1.6.5 Add typed error handling for network failures, 401, and server errors
- [ ] 1.6.6 Write unit tests for API client with mocked fetch responses
## Phase 2: Routes
## Phase 2: Journal REST API
### 2.1 Route List
### 2.1 Auth & Discovery Endpoints
- [ ] 2.1.1 Build Routes tab screen with paginated route list fetched from Journal API
- [ ] 2.1.2 Display route cards with name, distance, elevation, and thumbnail map preview
- [ ] 2.1.3 Add pull-to-refresh and loading/error states
- [ ] 2.1.4 Add i18n keys for route list strings (en + de)
- [ ] 2.1.1 Implement bearer token auth middleware for `/api/v1/*` routes
- [ ] 2.1.2 Implement `GET /.well-known/trails-cool` discovery endpoint (apiVersion, instanceName, tileUrl, apiBaseUrl)
- [ ] 2.1.3 Implement `POST /api/v1/auth/token` (OAuth2 code-to-token exchange, refresh grant)
### 2.2 Route Detail
### 2.2 Routes Endpoints
- [ ] 2.2.1 Build route detail screen with `react-native-maps` showing the route polyline and waypoint markers
- [ ] 2.2.2 Display route metadata: name, distance, elevation gain, number of days, waypoint list
- [ ] 2.2.3 Add "Edit", "Download Offline", and "Edit in Planner" action buttons
- [ ] 2.2.4 Style map markers and polyline colors to match the web Planner
- [ ] 2.2.1 Implement `GET /api/v1/routes` with cursor-based pagination
- [ ] 2.2.2 Implement `GET /api/v1/routes/:id` with full route detail (GPX, waypoints, versions)
- [ ] 2.2.3 Implement `POST /api/v1/routes` to create a new route
- [ ] 2.2.4 Implement `PUT /api/v1/routes/:id` to update a route (new version)
- [ ] 2.2.5 Implement `DELETE /api/v1/routes/:id`
### 2.3 Route Editing
### 2.3 Activities Endpoints
- [ ] 2.3.1 Implement add-waypoint via long-press on map, inserting at the nearest route segment
- [ ] 2.3.2 Implement drag-to-move for waypoint markers
- [ ] 2.3.3 Implement waypoint deletion with confirmation
- [ ] 2.3.4 Add overnight stop toggle in waypoint detail sheet
- [ ] 2.3.5 Add POI snap suggestions when adding waypoints near known POIs
- [ ] 2.3.6 Integrate BRouter routing via Journal API proxy — recompute route segments on waypoint changes
- [ ] 2.3.7 Implement save: generate GPX from current waypoints + geometry, PUT to Journal API
- [ ] 2.3.8 Add unsaved-changes guard when navigating away from the editor
- [ ] 2.3.1 Implement `GET /api/v1/activities` with cursor-based pagination
- [ ] 2.3.2 Implement `GET /api/v1/activities/:id` with full activity detail
- [ ] 2.3.3 Implement `POST /api/v1/activities` to create a new activity
- [ ] 2.3.4 Implement `DELETE /api/v1/activities/:id`
## Phase 3: Testing
### 2.4 Supporting Endpoints
### 3.1 Unit & Component Tests
- [ ] 2.4.1 Implement `POST /api/v1/routes/compute` BRouter proxy endpoint
- [ ] 2.4.2 Implement `POST /api/v1/uploads` presigned URL endpoint
- [ ] 3.1.1 Set up Jest + jest-expo + React Native Testing Library in `apps/mobile/`
- [ ] 3.1.2 Write unit tests for API client (mocked fetch, auth refresh, error handling)
- [ ] 3.1.3 Write component tests for route list, route detail, and route editor screens
- [ ] 3.1.4 Write unit tests for offline SQLite storage layer
- [ ] 3.1.5 Add test script to Turborepo pipeline
### 2.5 Device Management
### 3.2 E2E Tests (Maestro)
- [ ] 2.5.1 Store device name and last active timestamp on OAuth2 token use
- [ ] 2.5.2 Implement `GET /api/v1/auth/devices` — list connected devices for the authenticated user
- [ ] 2.5.3 Implement `DELETE /api/v1/auth/devices/:id` — revoke a device token
- [ ] 3.2.1 Install Maestro CLI and create `apps/mobile/.maestro/` test directory
- [ ] 3.2.2 Write Maestro flow: login → see route list → open route detail
- [ ] 3.2.3 Write Maestro flow: edit route → add waypoint → save
- [ ] 3.2.4 Write Maestro flow: download route for offline → toggle airplane mode → view cached route
- [ ] 3.2.5 Configure Maestro CI integration with EAS Build (run E2E on preview builds)
### 2.6 Validation & Testing
## Phase 4: Offline
- [ ] 2.6.1 Add Zod validation middleware using schemas from `@trails-cool/api`
- [ ] 2.6.2 Unit tests for all API endpoints
### 4.1 Route Download
## Phase 3: Routes
- [ ] 4.1.1 Set up Expo SQLite database with tables for offline routes, waypoints, and edit queue
- [ ] 4.1.2 Implement route download: fetch GPX + metadata from API, store in SQLite
- [ ] 4.1.3 Build download progress UI with cancel support
- [ ] 4.1.4 Implement offline route loading — detect network state and load from SQLite when offline
### 3.1 Route List
### 4.2 Tile Cache
- [ ] 3.1.1 Build Routes tab screen with paginated route list fetched from Journal API
- [ ] 3.1.2 Display route cards with name, distance, elevation, and thumbnail map preview
- [ ] 3.1.3 Add pull-to-refresh and loading/error states
- [ ] 3.1.4 Add i18n keys for route list strings (en + de)
- [ ] 4.2.1 Implement tile download manager: given a route bounding box, download tiles at zoom levels 10-15
- [ ] 4.2.2 Store tiles in the file system via `expo-file-system` with a lookup index in SQLite
- [ ] 4.2.3 Configure `react-native-maps` to use cached tiles when offline
- [ ] 4.2.4 Add storage budget display and management on Profile tab (total cached size, delete individual routes)
### 3.2 Route Detail
### 4.3 Offline Edit Queue
- [ ] 3.2.1 Build route detail screen with `react-native-maps` showing the route polyline and waypoint markers
- [ ] 3.2.2 Display route metadata: name, distance, elevation gain, number of days, waypoint list
- [ ] 3.2.3 Add "Edit", "Download Offline", and "Edit in Planner" action buttons
- [ ] 3.2.4 Style map markers and polyline colors to match the web Planner
- [ ] 4.3.1 Implement edit queue in SQLite: store pending route edits with timestamps
- [ ] 4.3.2 Add sync-pending indicator on routes with queued edits
- [ ] 4.3.3 Implement sync-on-reconnect: process queued edits in order via Journal API
- [ ] 4.3.4 Handle sync conflicts: warn user if server version changed, apply last-write-wins
### 3.3 Route Editing
## Phase 5: Polish
- [ ] 3.3.1 Implement add-waypoint via long-press on map, inserting at the nearest route segment
- [ ] 3.3.2 Implement drag-to-move for waypoint markers
- [ ] 3.3.3 Implement waypoint deletion with confirmation
- [ ] 3.3.4 Add overnight stop toggle in waypoint detail sheet
- [ ] 3.3.5 Add POI snap suggestions when adding waypoints near known POIs
- [ ] 3.3.6 Integrate BRouter routing via Journal API proxy — recompute route segments on waypoint changes
- [ ] 3.3.7 Implement save: generate GPX from current waypoints + geometry, PUT to Journal API
- [ ] 3.3.8 Add unsaved-changes guard when navigating away from the editor
### 5.1 Deep Links
## Phase 4: Testing
- [ ] 5.1.1 Configure `trailscool://` URL scheme in `app.config.ts` for iOS and Android
- [ ] 5.1.2 Implement deep link handler: parse `trailscool://routes/:id` and `trailscool://activities/:id`, navigate to detail screens
- [ ] 5.1.3 Implement "Edit in Planner" flow: open Planner URL in browser with JWT callback, handle return deep link
- [ ] 5.1.4 Add universal links / App Links for `https://trails.cool/routes/:id` (optional, requires associated domains)
### 4.1 Unit & Component Tests
### 5.2 Push Notifications
- [ ] 4.1.1 Set up Jest + jest-expo + React Native Testing Library in `apps/mobile/`
- [ ] 4.1.2 Write unit tests for API client (mocked fetch, auth refresh, error handling)
- [ ] 4.1.3 Write component tests for route list, route detail, and route editor screens
- [ ] 4.1.4 Write unit tests for offline SQLite storage layer
- [ ] 4.1.5 Add test script to Turborepo pipeline
- [ ] 5.2.1 Set up `expo-notifications` with push token registration
- [ ] 5.2.2 Add Journal API endpoint to register device push tokens
- [ ] 5.2.3 Send push notifications for route updates on shared routes (Journal server-side)
- [ ] 5.2.4 Handle notification taps — deep link to the relevant route or activity
### 4.2 E2E Tests (Maestro)
### 5.3 App Store Preparation
- [ ] 4.2.1 Install Maestro CLI and create `apps/mobile/.maestro/` test directory
- [ ] 4.2.2 Write Maestro flow: login → see route list → open route detail
- [ ] 4.2.3 Write Maestro flow: edit route → add waypoint → save
- [ ] 4.2.4 Write Maestro flow: download route for offline → toggle airplane mode → view cached route
- [ ] 4.2.5 Configure Maestro CI integration with EAS Build (run E2E on preview builds)
- [ ] 5.3.1 Write App Store and Play Store descriptions (en + de) with screenshots
- [ ] 5.3.2 Add privacy nutrition labels for iOS (location, health, network) and Android data safety section
- [ ] 5.3.3 Configure EAS Submit for iOS App Store and Google Play
- [ ] 5.3.4 Set up over-the-air updates via EAS Update for non-native JS changes
- [ ] 5.3.5 Test full app flow end-to-end on physical iOS and Android devices
## Phase 5: Offline
### 5.1 Route Download
- [ ] 5.1.1 Set up Expo SQLite database with tables for offline routes, waypoints, and edit queue
- [ ] 5.1.2 Implement route download: fetch GPX + metadata from API, store in SQLite
- [ ] 5.1.3 Build download progress UI with cancel support
- [ ] 5.1.4 Implement offline route loading — detect network state and load from SQLite when offline
### 5.2 Tile Cache
- [ ] 5.2.1 Implement tile download manager: given a route bounding box, download tiles at zoom levels 10-15
- [ ] 5.2.2 Store tiles in the file system via `expo-file-system` with a lookup index in SQLite
- [ ] 5.2.3 Configure `react-native-maps` to use cached tiles when offline
- [ ] 5.2.4 Add storage budget display and management on Profile tab (total cached size, delete individual routes)
### 5.3 Offline Edit Queue
- [ ] 5.3.1 Implement edit queue in SQLite: store pending route edits with timestamps
- [ ] 5.3.2 Add sync-pending indicator on routes with queued edits
- [ ] 5.3.3 Implement sync-on-reconnect: process queued edits in order via Journal API
- [ ] 5.3.4 Handle sync conflicts: warn user if server version changed, apply last-write-wins
## Phase 6: Polish
### 6.1 Deep Links
- [ ] 6.1.1 Configure `trailscool://` URL scheme in `app.config.ts` for iOS and Android
- [ ] 6.1.2 Implement deep link handler: parse `trailscool://routes/:id` and `trailscool://activities/:id`, navigate to detail screens
- [ ] 6.1.3 Implement "Edit in Planner" flow: open Planner URL in browser with JWT callback, handle return deep link
- [ ] 6.1.4 Add universal links / App Links for `https://trails.cool/routes/:id` (optional, requires associated domains)
### 6.2 App Store Preparation
- [ ] 6.2.1 Write App Store and Play Store descriptions (en + de) with screenshots
- [ ] 6.2.2 Add privacy nutrition labels for iOS (location, health, network) and Android data safety section
- [ ] 6.2.3 Configure EAS Submit for iOS App Store and Google Play
- [ ] 6.2.4 Set up over-the-air updates via EAS Update for non-native JS changes
- [ ] 6.2.5 Test full app flow end-to-end on physical iOS and Android devices
## Phase 7: Notifications
### 7.1 Journal Push Endpoints
- [ ] 7.1.1 Implement `POST /api/v1/push/subscribe` — store Web Push subscription for the authenticated user
- [ ] 7.1.2 Implement `DELETE /api/v1/push/unsubscribe` — remove a push subscription
- [ ] 7.1.3 Send Web Push notification when a shared route is updated
### 7.2 Push Relay Service
- [ ] 7.2.1 Build Web Push to APNs relay service (small Go or Node service)
- [ ] 7.2.2 Build Web Push to FCM relay service
- [ ] 7.2.3 Deploy relay service with Docker Compose alongside existing infrastructure
### 7.3 Mobile Push Integration
- [ ] 7.3.1 Register push token on mobile app startup via `expo-notifications`
- [ ] 7.3.2 Handle incoming push notifications — display and deep link on tap