From fff77a2ed2d4613cb5985017c228b30564da6911 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 12 Apr 2026 21:52:30 +0200 Subject: [PATCH] Add mobile app, map-core, nearby sync, and activity recording specs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../changes/map-core-package/.openspec.yaml | 2 + openspec/changes/map-core-package/design.md | 47 ++++++ openspec/changes/map-core-package/proposal.md | 28 ++++ .../map-core-package/specs/map-core/spec.md | 23 +++ openspec/changes/map-core-package/tasks.md | 45 +++++ .../mobile-activity-recording/.openspec.yaml | 2 + .../mobile-activity-recording/proposal.md | 22 +++ .../specs/mobile-activity-recording/spec.md | 45 +++++ openspec/changes/mobile-app/.openspec.yaml | 2 + openspec/changes/mobile-app/design.md | 156 ++++++++++++++++++ openspec/changes/mobile-app/proposal.md | 35 ++++ .../mobile-app/specs/journal-auth/spec.md | 27 +++ .../mobile-app/specs/journal-rest-api/spec.md | 124 ++++++++++++++ .../mobile-app/specs/mobile-app-shell/spec.md | 45 +++++ .../specs/mobile-journal-client/spec.md | 49 ++++++ .../mobile-app/specs/mobile-offline/spec.md | 42 +++++ .../specs/mobile-route-editor/spec.md | 49 ++++++ .../mobile-app/specs/mobile-testing/spec.md | 26 +++ .../specs/planner-journal-handoff/spec.md | 16 ++ .../mobile-app/specs/shared-packages/spec.md | 30 ++++ openspec/changes/mobile-app/tasks.md | 149 +++++++++++++++++ .../changes/mobile-nearby-sync/.openspec.yaml | 2 + .../changes/mobile-nearby-sync/proposal.md | 30 ++++ 23 files changed, 996 insertions(+) create mode 100644 openspec/changes/map-core-package/.openspec.yaml create mode 100644 openspec/changes/map-core-package/design.md create mode 100644 openspec/changes/map-core-package/proposal.md create mode 100644 openspec/changes/map-core-package/specs/map-core/spec.md create mode 100644 openspec/changes/map-core-package/tasks.md create mode 100644 openspec/changes/mobile-activity-recording/.openspec.yaml create mode 100644 openspec/changes/mobile-activity-recording/proposal.md create mode 100644 openspec/changes/mobile-activity-recording/specs/mobile-activity-recording/spec.md create mode 100644 openspec/changes/mobile-app/.openspec.yaml create mode 100644 openspec/changes/mobile-app/design.md create mode 100644 openspec/changes/mobile-app/proposal.md create mode 100644 openspec/changes/mobile-app/specs/journal-auth/spec.md create mode 100644 openspec/changes/mobile-app/specs/journal-rest-api/spec.md create mode 100644 openspec/changes/mobile-app/specs/mobile-app-shell/spec.md create mode 100644 openspec/changes/mobile-app/specs/mobile-journal-client/spec.md create mode 100644 openspec/changes/mobile-app/specs/mobile-offline/spec.md create mode 100644 openspec/changes/mobile-app/specs/mobile-route-editor/spec.md create mode 100644 openspec/changes/mobile-app/specs/mobile-testing/spec.md create mode 100644 openspec/changes/mobile-app/specs/planner-journal-handoff/spec.md create mode 100644 openspec/changes/mobile-app/specs/shared-packages/spec.md create mode 100644 openspec/changes/mobile-app/tasks.md create mode 100644 openspec/changes/mobile-nearby-sync/.openspec.yaml create mode 100644 openspec/changes/mobile-nearby-sync/proposal.md diff --git a/openspec/changes/map-core-package/.openspec.yaml b/openspec/changes/map-core-package/.openspec.yaml new file mode 100644 index 0000000..33d01f7 --- /dev/null +++ b/openspec/changes/map-core-package/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-12 diff --git a/openspec/changes/map-core-package/design.md b/openspec/changes/map-core-package/design.md new file mode 100644 index 0000000..047338c --- /dev/null +++ b/openspec/changes/map-core-package/design.md @@ -0,0 +1,47 @@ +## Context + +The Planner's map configuration is spread across multiple files: +- `packages/map/src/layers.ts` — base tile layer URLs and overlay tile configs +- `apps/planner/app/components/ColoredRoute.tsx` — 7 color palettes (surface, highway, smoothness, tracktype, cycleway, bikeroute, grade) + elevation color function +- `apps/planner/app/lib/poi-categories.ts` — 9 POI categories with Overpass queries, icons, colors, profile mappings +- `apps/planner/app/lib/z-index.ts` — marker z-index layering constants +- `apps/planner/app/lib/poi-snap.ts` — snap distance threshold + +All of this is renderer-specific (Leaflet) and app-specific (Planner). A mobile app using MapLibre would need to duplicate all of it. + +## Decisions + +### D1: Package structure + +``` +packages/map-core/ + src/ + index.ts — re-exports everything + tiles.ts — base layers + overlay layers (URLs, attribution, zoom) + colors/ + surface.ts — SURFACE_COLORS, DEFAULT_SURFACE_COLOR + highway.ts — HIGHWAY_COLORS, DEFAULT_HIGHWAY_COLOR + smoothness.ts — SMOOTHNESS_COLORS, DEFAULT_SMOOTHNESS_COLOR + tracktype.ts — TRACKTYPE_COLORS, DEFAULT_TRACKTYPE_COLOR + cycleway.ts — CYCLEWAY_COLORS, DEFAULT_CYCLEWAY_COLOR + bikeroute.ts — BIKEROUTE_COLORS, DEFAULT_BIKEROUTE_COLOR + elevation.ts — elevationColor(), routeGradeColor() + maxspeed.ts — maxspeedColor() + index.ts — re-exports all color maps + poi.ts — PoiCategory type + all category configs + z-index.ts — Z_WAYPOINT, Z_POI_MARKER, etc. + snap.ts — SNAP_DISTANCE_METERS +``` + +### D2: No rendering code + +`map-core` contains zero rendering code — no React, no Leaflet, no MapLibre. Just TypeScript constants, types, and pure functions. This ensures it works in any environment (web, mobile, Node.js, tests). + +### D3: Backwards-compatible migration + +The existing `packages/map/` package re-exports `baseLayers` and `overlayLayers` from `map-core` so existing imports continue to work. Planner components are updated to import directly from `@trails-cool/map-core` where possible. No breaking changes to public APIs. + +## Risks / Trade-offs + +- **More packages in the monorepo** — one more thing to maintain. Mitigated by the package being pure data with no dependencies. +- **Import path changes** — Planner components need updated imports. Straightforward find-and-replace. diff --git a/openspec/changes/map-core-package/proposal.md b/openspec/changes/map-core-package/proposal.md new file mode 100644 index 0000000..5dbcf74 --- /dev/null +++ b/openspec/changes/map-core-package/proposal.md @@ -0,0 +1,28 @@ +## Why + +Map configuration is scattered across Planner components: tile URLs in `packages/map/src/layers.ts`, color palettes in `ColoredRoute.tsx`, POI categories in `poi-categories.ts`, z-index constants in `z-index.ts`, and overlay configs hardcoded in `PlannerMap.tsx`. This makes it impossible to share map definitions with a future mobile app (MapLibre) or migrate the web to a different renderer without duplicating everything. + +## What Changes + +- **New `packages/map-core/` package**: Renderer-agnostic map definitions — tile sources, overlay configs, route color palettes, POI category configs, z-index layering constants. Pure TypeScript data, no rendering code, no DOM dependencies. +- **Refactor `packages/map/`**: Import tile configs from `map-core` instead of defining its own +- **Refactor Planner components**: Import color palettes, POI categories, and z-index constants from `map-core` instead of defining them locally + +## Capabilities + +### New Capabilities +- `map-core`: Shared renderer-agnostic map definitions (tiles, colors, overlays, POI categories, z-index) + +### Modified Capabilities +- `map-display`: Tile layer configs sourced from `map-core` instead of locally defined +- `route-coloring`: Color palettes and grade functions sourced from `map-core` +- `osm-poi-overlays`: POI category definitions sourced from `map-core` + +## Impact + +- New package: `packages/map-core/` added to pnpm workspace and Turborepo +- `packages/map/src/layers.ts` → thin re-export from `map-core` +- `apps/planner/app/components/ColoredRoute.tsx` → imports palettes from `map-core` +- `apps/planner/app/lib/poi-categories.ts` → moves to `map-core` +- `apps/planner/app/lib/z-index.ts` → moves to `map-core` +- No user-facing changes — pure refactor diff --git a/openspec/changes/map-core-package/specs/map-core/spec.md b/openspec/changes/map-core-package/specs/map-core/spec.md new file mode 100644 index 0000000..23d1b4b --- /dev/null +++ b/openspec/changes/map-core-package/specs/map-core/spec.md @@ -0,0 +1,23 @@ +## ADDED Requirements + +### Requirement: Renderer-agnostic map definitions +The `@trails-cool/map-core` package SHALL provide map configuration data usable by any rendering engine. + +#### Scenario: Tile source definitions +- **WHEN** a renderer needs tile layer URLs +- **THEN** `map-core` provides base layer and overlay layer configs with URL templates, attribution, and zoom limits + +#### Scenario: Route color palettes +- **WHEN** a renderer needs to color a route by surface, highway, grade, or other mode +- **THEN** `map-core` provides the color mapping and color functions for all supported modes + +#### Scenario: POI category definitions +- **WHEN** a renderer needs POI icons, colors, and Overpass queries +- **THEN** `map-core` provides the full category configuration + +### Requirement: No rendering dependencies +The package SHALL have zero dependencies on any rendering library (Leaflet, MapLibre, React, DOM). + +#### Scenario: Import in any environment +- **WHEN** `map-core` is imported in Node.js, a browser, or React Native +- **THEN** it works without errors — no DOM APIs, no rendering side effects diff --git a/openspec/changes/map-core-package/tasks.md b/openspec/changes/map-core-package/tasks.md new file mode 100644 index 0000000..e61baca --- /dev/null +++ b/openspec/changes/map-core-package/tasks.md @@ -0,0 +1,45 @@ +## 1. Package Setup + +- [ ] 1.1 Create `packages/map-core/` with `package.json`, `tsconfig.json`, add to pnpm workspace and Turborepo pipeline +- [ ] 1.2 Create `src/index.ts` with re-exports for all modules + +## 2. Tile Definitions + +- [ ] 2.1 Move `baseLayers` and `TileLayerConfig` from `packages/map/src/layers.ts` to `packages/map-core/src/tiles.ts` +- [ ] 2.2 Move `overlayLayers` and `OverlayLayerConfig` to `packages/map-core/src/tiles.ts` +- [ ] 2.3 Update `packages/map/src/layers.ts` to re-export from `@trails-cool/map-core` +- [ ] 2.4 Update `packages/map/src/index.ts` exports + +## 3. Color Palettes + +- [ ] 3.1 Extract `SURFACE_COLORS`, `DEFAULT_SURFACE_COLOR` from `ColoredRoute.tsx` to `map-core/src/colors/surface.ts` +- [ ] 3.2 Extract `HIGHWAY_COLORS`, `DEFAULT_HIGHWAY_COLOR` to `map-core/src/colors/highway.ts` +- [ ] 3.3 Extract `SMOOTHNESS_COLORS`, `DEFAULT_SMOOTHNESS_COLOR` to `map-core/src/colors/smoothness.ts` +- [ ] 3.4 Extract `TRACKTYPE_COLORS`, `DEFAULT_TRACKTYPE_COLOR` to `map-core/src/colors/tracktype.ts` +- [ ] 3.5 Extract `CYCLEWAY_COLORS`, `DEFAULT_CYCLEWAY_COLOR` to `map-core/src/colors/cycleway.ts` +- [ ] 3.6 Extract `BIKEROUTE_COLORS`, `DEFAULT_BIKEROUTE_COLOR` to `map-core/src/colors/bikeroute.ts` +- [ ] 3.7 Extract `elevationColor()`, `routeGradeColor()` to `map-core/src/colors/elevation.ts` +- [ ] 3.8 Extract `maxspeedColor()` to `map-core/src/colors/maxspeed.ts` +- [ ] 3.9 Create `map-core/src/colors/index.ts` re-exporting all color maps and functions +- [ ] 3.10 Update `ColoredRoute.tsx` to import from `@trails-cool/map-core` +- [ ] 3.11 Update `ElevationChart.tsx` to import from `@trails-cool/map-core` + +## 4. POI Categories + +- [ ] 4.1 Move `PoiCategory` type and `poiCategories` array from `poi-categories.ts` to `map-core/src/poi.ts` +- [ ] 4.2 Move `getCategoriesForProfile()` and `profileOverlayDefaults` to `map-core/src/poi.ts` +- [ ] 4.3 Update Planner imports: `PoiPanel.tsx`, `use-pois.ts`, `use-profile-defaults.ts`, `poi-categories.test.ts` +- [ ] 4.4 Move POI snap distance constant to `map-core/src/snap.ts` + +## 5. Z-Index Constants + +- [ ] 5.1 Move all z-index constants from `z-index.ts` to `map-core/src/z-index.ts` +- [ ] 5.2 Update Planner imports: `PlannerMap.tsx`, `PoiPanel.tsx`, `RouteInteraction.tsx` + +## 6. Verification + +- [ ] 6.1 Run `pnpm typecheck` — all packages compile +- [ ] 6.2 Run `pnpm lint` — no import errors +- [ ] 6.3 Run `pnpm test` — all existing tests pass (update import paths in test files) +- [ ] 6.4 Run `pnpm test:e2e` — no regressions +- [ ] 6.5 Verify `map-core` has zero dependencies in its `package.json` diff --git a/openspec/changes/mobile-activity-recording/.openspec.yaml b/openspec/changes/mobile-activity-recording/.openspec.yaml new file mode 100644 index 0000000..33d01f7 --- /dev/null +++ b/openspec/changes/mobile-activity-recording/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-12 diff --git a/openspec/changes/mobile-activity-recording/proposal.md b/openspec/changes/mobile-activity-recording/proposal.md new file mode 100644 index 0000000..b21b636 --- /dev/null +++ b/openspec/changes/mobile-activity-recording/proposal.md @@ -0,0 +1,22 @@ +## Why + +The mobile app (when built) will show routes and allow editing, but won't record activities. Users want to track their rides/hikes with GPS, see live stats, and save recordings as Journal activities. This is a separate feature that depends on the mobile app foundation being in place first. + +## What Changes + +- GPS track recording via expo-location (foreground + background) +- Live stats overlay (distance, duration, speed, elevation gain) +- Save recorded track as GPX → Journal activity linked to the active route +- HealthKit (iOS) / Health Connect (Android) export +- Discard/pause/resume recording flow + +## Capabilities + +### New Capabilities +- `mobile-activity-recording`: GPS recording, live stats, save as activity, health platform export + +## Impact + +- Depends on `mobile-app` change being implemented first +- New dependencies: expo-location, expo-health +- Background location permissions needed (App Store privacy review) diff --git a/openspec/changes/mobile-activity-recording/specs/mobile-activity-recording/spec.md b/openspec/changes/mobile-activity-recording/specs/mobile-activity-recording/spec.md new file mode 100644 index 0000000..c7e47c7 --- /dev/null +++ b/openspec/changes/mobile-activity-recording/specs/mobile-activity-recording/spec.md @@ -0,0 +1,45 @@ +## ADDED Requirements + +### Requirement: GPS track recording +The system SHALL record GPS tracks using the device's location services, supporting foreground and background recording. + +#### Scenario: Start recording +- **WHEN** the user taps "Start Recording" from a route's map view +- **THEN** GPS tracking begins, the live stats overlay is shown, and track points are stored locally + +#### Scenario: Background recording +- **WHEN** the user switches to another app or locks the screen during recording +- **THEN** GPS tracking continues in the background and track points are still recorded + +#### Scenario: Stop recording +- **WHEN** the user taps "Stop Recording" +- **THEN** recording ends, track points are converted to GPX, and the user is prompted to save + +### Requirement: Live stats display +The system SHALL show real-time statistics during an active recording. + +#### Scenario: Stats overlay +- **WHEN** a recording is in progress +- **THEN** the screen displays: elapsed duration, distance covered, current speed, and elevation gain + +### Requirement: Save as Journal activity +The system SHALL save completed recordings as activities on the user's Journal, linked to the active route. + +#### Scenario: Save recording +- **WHEN** the user confirms saving after stopping a recording +- **THEN** the GPX track is uploaded to the Journal API as a new activity linked to the route, with distance, duration, and startedAt + +#### Scenario: Discard recording +- **WHEN** the user discards a recording +- **THEN** the track data is deleted locally and no activity is created + +### Requirement: HealthKit and Health Connect export +The system SHALL export recorded activities to Apple HealthKit (iOS) and Health Connect (Android). + +#### Scenario: Export to health platform +- **WHEN** the user saves a recording and has granted health permissions +- **THEN** the activity (distance, duration, route, elevation) is written to HealthKit or Health Connect + +#### Scenario: Health permissions not granted +- **WHEN** the user has not granted health permissions +- **THEN** the activity is saved to Journal without health export, and the user can grant permissions later from Profile diff --git a/openspec/changes/mobile-app/.openspec.yaml b/openspec/changes/mobile-app/.openspec.yaml new file mode 100644 index 0000000..33d01f7 --- /dev/null +++ b/openspec/changes/mobile-app/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-12 diff --git a/openspec/changes/mobile-app/design.md b/openspec/changes/mobile-app/design.md new file mode 100644 index 0000000..1a5234e --- /dev/null +++ b/openspec/changes/mobile-app/design.md @@ -0,0 +1,156 @@ +## Context + +trails.cool is a two-app web platform: Planner (stateless, collaborative, Yjs) and Journal (accounts, PostgreSQL, ActivityPub). The Planner uses Leaflet for maps, Yjs for real-time sync, and BRouter for routing. The Journal stores routes, activities, and user data. Shared packages (`types`, `gpx`, `i18n`) are pure TypeScript. + +The mobile app combines both apps into a single React Native + Expo experience, communicating with the Journal as its backend. The user is an experienced React Native/Expo developer. + +## Goals / Non-Goals + +**Goals:** +- Unified mobile app (plan + journal in one) +- Authenticate with existing Journal account +- View and edit routes on the go +- Record GPS activities +- Work offline for viewing routes +- Reuse shared TypeScript packages + +**Non-Goals:** +- Full collaborative Yjs editing on mobile (too complex for v1 — use direct API) +- Self-hosting the mobile app (App Store distribution only) +- Replacing the web Planner (mobile editing is simplified) +- ActivityPub federation from mobile (handled by Journal server) +- Offline route computation (BRouter requires server) + +## Decisions + +### D1: Expo managed workflow with monorepo integration + +Place the app at `apps/mobile/` in the existing pnpm monorepo. Use Expo managed workflow (no native code ejection) with EAS Build for CI/CD. Share packages via pnpm workspace — `@trails-cool/types`, `@trails-cool/gpx`, `@trails-cool/i18n` imported directly. + +The `gpx` package uses `linkedom` for Node.js XML parsing. On mobile, use the built-in `DOMParser` (available in React Native's JSC/Hermes via a polyfill or the existing browser code path in `parseGpx`). + +### D2: Authentication via OAuth2 PKCE + +The Journal currently uses passkeys + magic links. Neither works well in a native app context. Add an OAuth2 authorization code flow with PKCE to the Journal: + +- Mobile app opens Journal's `/oauth/authorize` in an in-app browser +- User authenticates via existing passkey/magic link +- Journal redirects back with auth code +- Mobile app exchanges code for access + refresh tokens +- Tokens stored in Expo SecureStore + +This keeps the auth UI on the web (no need to implement passkey in native code) while giving the mobile app long-lived tokens. + +The mobile app sends a device name (e.g., model + OS version) during token exchange. The Journal stores this alongside the token, enabling a "Connected Devices" list on the Journal web settings page where users can see active sessions and revoke individual devices. This is primarily relevant for the hosted version — self-hosted instances with a single user would rarely use it, but the data is stored regardless. + +### D2b: Configurable server URL + +The mobile app defaults to `https://trails.cool` but allows connecting to any self-hosted Journal instance. On the login screen, a "Connect to a different server" option lets the user enter a custom URL (e.g., `https://trails.example.org`). The app validates the URL by fetching `/.well-known/trails-cool` (or a similar discovery endpoint) to confirm it's a compatible instance. + +The server URL is stored persistently and used as the base for all API calls and the OAuth2 login flow. Users can switch instances from the Profile tab — this logs them out and clears local data. + +The `/.well-known/trails-cool` discovery endpoint returns an `apiVersion` semver string (e.g., `"1.0.0"`). The mobile app declares a `requiredApiVersion` (minimum semver it needs). On connect and on app foreground, the app checks: + +- Server `apiVersion` satisfies app's required range → normal operation. +- Server `apiVersion` too low → **Block**: "This server needs to be updated. Please ask the administrator to upgrade." The app still allows read-only access to cached offline data. + +The API follows semver: patch versions for fixes, minor versions for additive changes (new endpoints, new optional fields), major versions only for breaking changes. Since the API is backwards compatible by design, a newer server always works with an older app — no "server too new" check needed. + +### D2c: Shared API contract package + +A new `packages/api/` workspace package (`@trails-cool/api`) serves as the single source of truth for the REST API: + +- `API_VERSION` constant (semver string) +- TypeScript types for all request/response shapes (e.g., `RouteListResponse`, `CreateActivityRequest`) +- Endpoint path constants (e.g., `ENDPOINTS.routes.list = "/api/v1/routes"`) +- Error response type + +The package uses Zod schemas as the source of truth — TypeScript types are inferred via `z.infer<>`. The Journal server uses schemas to validate incoming request bodies. The mobile client can optionally validate responses (useful for self-hosted instances on older versions). Both sides share the same schemas — runtime validation and type safety from one definition. + +### D3: MapLibre GL for mobile maps + +Leaflet is web-only. Use `react-native-maplibre-gl` for mobile — OSM vector tiles, GPU-accelerated, free, consistent with the web's OSM data source. Requires an Expo config plugin but is well-supported. + +The web Planner stays on Leaflet for now, but map logic should be structured for future convergence: + +- **`@trails-cool/map-core`** (new package): Renderer-agnostic definitions — tile source URLs, overlay configs, color palettes (route coloring, POI categories), z-index layering order. Pure data, no rendering code. +- **`@trails-cool/map`** (existing): Leaflet-specific components for web, imports from `map-core` +- **`apps/mobile/`**: MapLibre-specific components for mobile, imports from `map-core` + +This way, switching the web to MapLibre later means replacing `@trails-cool/map` internals while `map-core` stays unchanged. The mobile app validates that `map-core` abstractions work before the web migration. + +### D4: Simplified route editing (no Yjs) + +Mobile route editing talks directly to the Journal API: +1. Fetch route GPX → parse waypoints +2. User adds/moves/deletes waypoints on the map +3. Compute route via BRouter (through a proxy endpoint on the Journal, or direct to the Planner's BRouter instance) +4. Save updated GPX back to Journal API + +No real-time collaboration on mobile. If the user needs full collaborative editing, deep-link to the Planner web app. + +### D5: Offline with SQLite + tile cache + +- Routes: Download GPX + parsed waypoints into Expo SQLite +- Map tiles: Use `react-native-maps`' built-in tile caching, or download tile packages for specific regions +- 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 + +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` + +### D7: Navigation structure + +Tab bar with 4 tabs: +- **Map**: Current route on map, quick edit, start recording +- **Routes**: List of user's routes from Journal +- **Activities**: List of recorded activities +- **Profile**: Account settings, offline data management, sync status + +### D8: BRouter routing via Journal API proxy + +The mobile app computes routes through the Journal API: `POST /api/v1/routes/compute`. The Journal forwards the request to its BRouter instance and returns the enriched route. This avoids exposing BRouter publicly and works with self-hosted instances where the BRouter container isn't publicly accessible. + +### D9: Photo uploads via presigned URLs + +Route and activity photos are stored in S3/Garage (existing infrastructure). The API returns presigned upload URLs — the mobile app uploads directly to storage, then confirms the upload via the API. This avoids proxying large files through the Journal server. + +### D10: Cursor-based pagination + +All list endpoints (routes, activities) use cursor-based pagination. The response includes a `nextCursor` field — the client passes it back to get the next page. This handles feeds that change (new items, deletions) without skipping or duplicating entries. + +### D11: Expo monorepo support + +Use Expo's built-in `experiments.monorepo: true` in `app.config.ts` to resolve pnpm workspace packages in Metro. This handles symlink resolution and shared package discovery automatically. + +### D12: Push notifications via Web Push relay + +Following Mastodon's proven architecture for federated push notifications: + +- **Journal instances implement standard Web Push API** (RFC 8030) — no Apple/Google dependencies on the server side +- **trails.cool hosts a relay service** that translates Web Push → APNs (iOS) and FCM (Android). Self-hosted instances send standard Web Push to this relay. +- **Notifications are end-to-end encrypted** — the relay forwards encrypted payloads without reading them +- **Self-hosted admins need zero configuration** — no Apple Developer account, no Firebase project. Standard Web Push just works. +- **The relay is open-source** — self-hosters who want full independence can run their own relay with their own APNs/FCM credentials + +Flow: `Journal instance → Web Push (encrypted) → trails.cool relay → APNs/FCM → phone` + +This aligns with the ActivityPub federation direction and matches how Mastodon clients (Toot!, Ice Cubes, etc.) handle push for thousands of self-hosted instances through a single relay. + +### D13: State management + +TBD — to be discussed. + +## Risks / Trade-offs + +- **BRouter access from mobile**: The BRouter instance runs in Docker alongside the Planner. The mobile app needs a route computation endpoint. Options: (a) proxy through Journal API, (b) expose BRouter publicly with auth, (c) use a public BRouter instance. Option (a) is safest. +- **Map tile licensing**: Google Maps requires an API key and has usage-based pricing. Apple Maps is free on iOS. Consider MapLibre with OpenStreetMap tiles for a free, consistent cross-platform solution. +- **React Native Maps vs MapLibre**: `react-native-maps` is more mature but ties to Google/Apple. `react-native-maplibre-gl` uses vector tiles and is more consistent with the web's OSM-based approach. Worth evaluating. +- **Offline storage size**: Map tiles for a region can be hundreds of MB. Need a download manager with progress and storage budget. +- **App Store review**: GPS background tracking + health data access need careful privacy descriptions. diff --git a/openspec/changes/mobile-app/proposal.md b/openspec/changes/mobile-app/proposal.md new file mode 100644 index 0000000..22c60dc --- /dev/null +++ b/openspec/changes/mobile-app/proposal.md @@ -0,0 +1,35 @@ +## Why + +trails.cool is currently web-only: the Planner runs in a desktop browser and the Journal is a separate web app. On the road — at a campsite, at a trailhead, at a crossroads — users need their phone. The mobile browser experience works but is limited: no offline access, no HealthKit/GPS integration, no push notifications for shared route updates, and the two-app split (Planner + Journal) makes no sense on a phone where you want a single app that does both. A native mobile app (React Native + Expo) would unify planning and journaling, enable on-the-go route editing, and open the door to activity recording (GPS tracking) and health platform integration. + +## What Changes + +- **Unified mobile app**: A single React Native + Expo app that combines Planner and Journal functionality. Users authenticate with their Journal account and can browse routes, edit them, and record activities. +- **Shared packages**: Reuse `@trails-cool/types`, `@trails-cool/gpx`, `@trails-cool/i18n` in the mobile app. These are already pure TypeScript with no DOM dependencies. +- **Mobile route editing**: Simplified map-based route editing (add/move/delete waypoints, toggle overnight stops). Not full collaborative Yjs — instead, direct API edits to the Journal. +- **Offline route access**: Download route GPX + map tiles for offline use. View routes and waypoint details without connectivity. +- **Journal API client**: The mobile app talks to the Journal's existing API (auth, routes, activities). No new backend — the Journal is the source of truth. +- **Testing**: Jest + React Native Testing Library for unit/component tests, Maestro for E2E flows + +## Capabilities + +### New Capabilities +- `mobile-app-shell`: Expo app scaffold, navigation (tab bar: Map, Routes, Activities, Profile), authentication flow with Journal account +- `mobile-route-editor`: Map-based route editing on mobile — add/move/delete waypoints, overnight stops, POI snap. Direct saves to Journal API (no Yjs). +- `mobile-offline`: Download routes + map tile regions for offline viewing. Offline queue for edits synced when back online. +- `mobile-testing`: Jest + RNTL for unit/component tests, Maestro YAML flows for E2E +- `mobile-journal-client`: API client for Journal endpoints — auth, routes CRUD, activities CRUD, route versions + +### Modified Capabilities +- `journal-auth`: Add OAuth2/PKCE flow for mobile app token exchange (existing passkey/magic-link auth doesn't work natively on mobile) +- `planner-journal-handoff`: Mobile app can open Planner web sessions via deep link for full collaborative editing when needed +- `shared-packages`: Verify and adapt `@trails-cool/types`, `@trails-cool/gpx`, `@trails-cool/i18n` for React Native compatibility (no DOM, no Node.js APIs) + +## Impact + +- **New repo/workspace**: `apps/mobile/` in the monorepo, React Native + Expo managed workflow +- **Shared packages**: `packages/types`, `packages/gpx`, `packages/i18n` need to work in React Native (linkedom dependency in gpx is Node-only — need browser DOMParser or a RN-compatible XML parser) +- **Journal API**: Existing routes/activities/auth endpoints used as-is. May need an OAuth2 token endpoint for mobile auth. +- **Maps**: React Native Maps (Google Maps/Apple Maps) instead of Leaflet (web-only). New map components, not shared with web. +- **Dependencies**: expo, react-native-maps, expo-location, expo-health, expo-file-system, expo-sqlite (offline cache) +- **App Store**: iOS App Store + Google Play distribution via EAS Build diff --git a/openspec/changes/mobile-app/specs/journal-auth/spec.md b/openspec/changes/mobile-app/specs/journal-auth/spec.md new file mode 100644 index 0000000..3f36324 --- /dev/null +++ b/openspec/changes/mobile-app/specs/journal-auth/spec.md @@ -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 diff --git a/openspec/changes/mobile-app/specs/journal-rest-api/spec.md b/openspec/changes/mobile-app/specs/journal-rest-api/spec.md new file mode 100644 index 0000000..c4fa8a6 --- /dev/null +++ b/openspec/changes/mobile-app/specs/journal-rest-api/spec.md @@ -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 ` +- **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=` +- **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=` +- **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=` +- **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) diff --git a/openspec/changes/mobile-app/specs/mobile-app-shell/spec.md b/openspec/changes/mobile-app/specs/mobile-app-shell/spec.md new file mode 100644 index 0000000..e5a9f37 --- /dev/null +++ b/openspec/changes/mobile-app/specs/mobile-app-shell/spec.md @@ -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 diff --git a/openspec/changes/mobile-app/specs/mobile-journal-client/spec.md b/openspec/changes/mobile-app/specs/mobile-journal-client/spec.md new file mode 100644 index 0000000..960f284 --- /dev/null +++ b/openspec/changes/mobile-app/specs/mobile-journal-client/spec.md @@ -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 diff --git a/openspec/changes/mobile-app/specs/mobile-offline/spec.md b/openspec/changes/mobile-app/specs/mobile-offline/spec.md new file mode 100644 index 0000000..52a6b1f --- /dev/null +++ b/openspec/changes/mobile-app/specs/mobile-offline/spec.md @@ -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 diff --git a/openspec/changes/mobile-app/specs/mobile-route-editor/spec.md b/openspec/changes/mobile-app/specs/mobile-route-editor/spec.md new file mode 100644 index 0000000..918e781 --- /dev/null +++ b/openspec/changes/mobile-app/specs/mobile-route-editor/spec.md @@ -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 diff --git a/openspec/changes/mobile-app/specs/mobile-testing/spec.md b/openspec/changes/mobile-app/specs/mobile-testing/spec.md new file mode 100644 index 0000000..9274b77 --- /dev/null +++ b/openspec/changes/mobile-app/specs/mobile-testing/spec.md @@ -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. diff --git a/openspec/changes/mobile-app/specs/planner-journal-handoff/spec.md b/openspec/changes/mobile-app/specs/planner-journal-handoff/spec.md new file mode 100644 index 0000000..7e209fd --- /dev/null +++ b/openspec/changes/mobile-app/specs/planner-journal-handoff/spec.md @@ -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) diff --git a/openspec/changes/mobile-app/specs/shared-packages/spec.md b/openspec/changes/mobile-app/specs/shared-packages/spec.md new file mode 100644 index 0000000..f9aa096 --- /dev/null +++ b/openspec/changes/mobile-app/specs/shared-packages/spec.md @@ -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 diff --git a/openspec/changes/mobile-app/tasks.md b/openspec/changes/mobile-app/tasks.md new file mode 100644 index 0000000..3219026 --- /dev/null +++ b/openspec/changes/mobile-app/tasks.md @@ -0,0 +1,149 @@ +## Phase 1: Foundation + +### 1.1 App Scaffold + +- [ ] 1.1.1 Initialize Expo managed project at `apps/mobile/` with TypeScript template +- [ ] 1.1.2 Configure pnpm workspace to include `apps/mobile` and resolve shared packages (`@trails-cool/types`, `@trails-cool/gpx`, `@trails-cool/i18n`) +- [ ] 1.1.3 Set up Expo Router with bottom tab navigation (Map, Routes, Activities, Profile) and placeholder screens +- [ ] 1.1.4 Add app icon, splash screen, and `app.config.ts` with bundle identifiers for iOS and Android +- [ ] 1.1.5 Configure EAS Build for development and preview profiles + +### 1.2 API Contract Package + +- [ ] 1.2.1 Create `packages/api/` with `API_VERSION` constant, endpoint path constants, and TypeScript types for all request/response shapes +- [ ] 1.2.2 Define route types: `RouteListResponse`, `RouteDetailResponse`, `CreateRouteRequest`, `UpdateRouteRequest` +- [ ] 1.2.3 Define activity types: `ActivityListResponse`, `ActivityDetailResponse`, `CreateActivityRequest` +- [ ] 1.2.4 Define auth types: `TokenExchangeRequest`, `TokenResponse`, `DiscoveryResponse` +- [ ] 1.2.5 Define shared error type: `ApiErrorResponse` with code, message, and optional field errors +- [ ] 1.2.6 Export everything from package index, add to pnpm workspace and Turborepo pipeline + +### 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 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 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 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 + +## Phase 2: Routes + +### 2.1 Route List + +- [ ] 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.2 Route Detail + +- [ ] 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.3 Route Editing + +- [ ] 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 + +## Phase 3: Testing + +### 3.1 Unit & Component Tests + +- [ ] 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 + +### 3.2 E2E Tests (Maestro) + +- [ ] 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) + +## Phase 4: Offline + +### 4.1 Route Download + +- [ ] 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 + +### 4.2 Tile Cache + +- [ ] 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) + +### 4.3 Offline Edit Queue + +- [ ] 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 + +## Phase 5: Polish + +### 5.1 Deep Links + +- [ ] 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) + +### 5.2 Push Notifications + +- [ ] 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 + +### 5.3 App Store Preparation + +- [ ] 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 diff --git a/openspec/changes/mobile-nearby-sync/.openspec.yaml b/openspec/changes/mobile-nearby-sync/.openspec.yaml new file mode 100644 index 0000000..33d01f7 --- /dev/null +++ b/openspec/changes/mobile-nearby-sync/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-04-12 diff --git a/openspec/changes/mobile-nearby-sync/proposal.md b/openspec/changes/mobile-nearby-sync/proposal.md new file mode 100644 index 0000000..b24e00f --- /dev/null +++ b/openspec/changes/mobile-nearby-sync/proposal.md @@ -0,0 +1,30 @@ +## Why + +When bikepacking or hiking in a group, one person may update the route at the last café with WiFi. Once the group is in the middle of nowhere with no signal, the others are stuck with the old route. There's no way to share route updates between nearby devices without internet. + +## What Changes + +- **BLE route sync**: Devices discover each other via Bluetooth LE and transfer route data (waypoints + GPX geometry) directly. Uses react-native-ble-plx for cross-platform iOS ↔ Android compatibility. +- **QR waypoint sharing** (v1, simpler): Generate a QR code with compressed waypoint coordinates. The receiver scans it and gets the waypoints immediately — full route geometry recomputes when signal returns. +- **Sync protocol**: A lightweight protocol over BLE that handles discovery ("who nearby has this route?"), versioning ("is your copy newer than mine?"), and transfer (chunked GPX data). + +## Capabilities + +### New Capabilities +- `mobile-nearby-sync`: BLE-based route sync between nearby devices, QR waypoint sharing fallback + +## Non-Goals +- Real-time collaborative editing over BLE (too complex — this is one-shot sync) +- Syncing activities or other data (routes only for v1) +- Mesh networking (direct device-to-device only, no relay through third devices) + +## Impact + +- Depends on `mobile-app` change being implemented first +- New dependency: react-native-ble-plx (requires Expo dev build) +- BLE background advertising needs careful battery management +- iOS requires Bluetooth usage description in Info.plist +- Android requires BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE permissions + +## Future ideas +- TXQR (animated QR codes via camera) as an alternative transfer method — ~13KB/s demonstrated, no pairing needed, but requires porting the fountain code protocol to TypeScript