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:
parent
f6e46d871b
commit
cd939ccf07
6 changed files with 352 additions and 104 deletions
|
|
@ -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 }> }`
|
||||
34
openspec/changes/mobile-app/specs/device-management/spec.md
Normal file
34
openspec/changes/mobile-app/specs/device-management/spec.md
Normal 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)
|
||||
41
openspec/changes/mobile-app/specs/photo-media/spec.md
Normal file
41
openspec/changes/mobile-app/specs/photo-media/spec.md
Normal 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
|
||||
42
openspec/changes/mobile-app/specs/web-push-relay/spec.md
Normal file
42
openspec/changes/mobile-app/specs/web-push-relay/spec.md
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue