Complete mobile app specs: fill all gaps

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

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

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

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

View file

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

View file

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

View file

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

View file

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