From cd939ccf07c86b56a60233d892dc9d4b64829689 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 12 Apr 2026 22:12:54 +0200 Subject: [PATCH] Complete mobile app specs: fill all gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- openspec/changes/mobile-app/design.md | 44 +++- .../specs/api-contract-package/spec.md | 52 ++++ .../specs/device-management/spec.md | 34 +++ .../mobile-app/specs/photo-media/spec.md | 41 +++ .../mobile-app/specs/web-push-relay/spec.md | 42 +++ openspec/changes/mobile-app/tasks.md | 243 +++++++++++------- 6 files changed, 352 insertions(+), 104 deletions(-) create mode 100644 openspec/changes/mobile-app/specs/api-contract-package/spec.md create mode 100644 openspec/changes/mobile-app/specs/device-management/spec.md create mode 100644 openspec/changes/mobile-app/specs/photo-media/spec.md create mode 100644 openspec/changes/mobile-app/specs/web-push-relay/spec.md diff --git a/openspec/changes/mobile-app/design.md b/openspec/changes/mobile-app/design.md index 1a5234e..458ed09 100644 --- a/openspec/changes/mobile-app/design.md +++ b/openspec/changes/mobile-app/design.md @@ -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 diff --git a/openspec/changes/mobile-app/specs/api-contract-package/spec.md b/openspec/changes/mobile-app/specs/api-contract-package/spec.md new file mode 100644 index 0000000..1e6a026 --- /dev/null +++ b/openspec/changes/mobile-app/specs/api-contract-package/spec.md @@ -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 }> }` diff --git a/openspec/changes/mobile-app/specs/device-management/spec.md b/openspec/changes/mobile-app/specs/device-management/spec.md new file mode 100644 index 0000000..1982b35 --- /dev/null +++ b/openspec/changes/mobile-app/specs/device-management/spec.md @@ -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) diff --git a/openspec/changes/mobile-app/specs/photo-media/spec.md b/openspec/changes/mobile-app/specs/photo-media/spec.md new file mode 100644 index 0000000..d5ef953 --- /dev/null +++ b/openspec/changes/mobile-app/specs/photo-media/spec.md @@ -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 diff --git a/openspec/changes/mobile-app/specs/web-push-relay/spec.md b/openspec/changes/mobile-app/specs/web-push-relay/spec.md new file mode 100644 index 0000000..36ffafa --- /dev/null +++ b/openspec/changes/mobile-app/specs/web-push-relay/spec.md @@ -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 diff --git a/openspec/changes/mobile-app/tasks.md b/openspec/changes/mobile-app/tasks.md index 3219026..ef99988 100644 --- a/openspec/changes/mobile-app/tasks.md +++ b/openspec/changes/mobile-app/tasks.md @@ -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