Files
phaseflow/specs/garmin-integration.md
Petru Paler 6a8d55c0b9 Document spec gaps: auth, phase scaling, observability, testing
Address 21 previously undefined behaviors across specs:

- Authentication: Replace email/password with OIDC (Pocket-ID)
- Cycle tracking: Add fixed-luteal phase scaling formula with examples
- Calendar: Document period logging behavior (preserve predictions)
- Garmin: Clarify connection is required (no phase-only mode)
- Dashboard: Add UI states, dark mode, onboarding, accessibility
- Notifications: Document timezone batching approach
- New specs: observability.md (health, metrics, logging)
- New specs: testing.md (unit + integration strategy)
- Main spec: Add backup/recovery, known limitations, API updates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 07:49:56 +00:00

111 lines
3.1 KiB
Markdown

# Garmin Integration Specification
> **Required**: Garmin connection is mandatory. PhaseFlow cannot function without biometric data from Garmin. The app's core value proposition is combining cycle phases with real physiological data—phase-only mode is not supported.
## Job to Be Done
When I connect my Garmin account, I want the app to automatically sync my biometrics daily, so that training decisions are based on real physiological data.
## OAuth Flow
### Initial Token Bootstrap (Manual)
Garmin's Connect API uses OAuth 1.0a → OAuth 2.0 exchange.
**Process:**
1. User runs `scripts/garmin_auth.py` locally
2. Script obtains OAuth 1.0a request token
3. User authorizes in browser
4. Script exchanges for OAuth 2.0 access token
5. User pastes tokens into Settings > Garmin page
**Tokens Stored:**
- `oauth1` - OAuth 1.0a credentials (JSON, encrypted)
- `oauth2` - OAuth 2.0 access token (JSON, encrypted)
- `expires_at` - Token expiration timestamp
### Token Storage
Tokens encrypted with AES-256-GCM using `ENCRYPTION_KEY` env var.
**Functions (`src/lib/encryption.ts`):**
- `encrypt(plaintext: string): string` - Returns base64 ciphertext
- `decrypt(ciphertext: string): string` - Returns plaintext
## API Endpoints
### GET `/api/garmin/status`
Returns current Garmin connection status.
**Response:**
```json
{
"connected": true,
"daysUntilExpiry": 85,
"lastSync": "2024-01-10T07:00:00Z"
}
```
### POST `/api/garmin/tokens`
Updates stored Garmin tokens.
**Request Body:**
```json
{
"oauth1": { ... },
"oauth2": { ... },
"expires_at": "2024-04-10T00:00:00Z"
}
```
## Garmin Data Fetching
### Daily Sync (`/api/cron/garmin-sync`)
Runs via cron job at 6:00 AM user's timezone.
**Endpoints Called:**
1. `/usersummary-service/stats/bodyBattery/dates/{date}` - Body Battery
2. `/hrv-service/hrv/{date}` - HRV Status
3. `/fitnessstats-service/activity` - Intensity Minutes
**Data Extracted:**
- `bodyBatteryCurrent` - Latest BB value
- `bodyBatteryYesterdayLow` - Yesterday's minimum BB
- `hrvStatus` - "Balanced" or "Unbalanced"
- `weekIntensityMinutes` - 7-day rolling sum
### Garmin Client (`src/lib/garmin.ts`)
**Functions:**
- `fetchGarminData(endpoint, { oauth2Token })` - Generic API caller
- `isTokenExpired(tokens)` - Check if refresh needed
- `daysUntilExpiry(tokens)` - Days until token expires
## Token Expiration Handling
Garmin OAuth 2.0 tokens expire after ~90 days.
**Warning Triggers:**
- 14 days before: Yellow warning in Settings
- 7 days before: Email notification
- 0 days: Red alert, manual refresh required
## Success Criteria
1. Tokens stored encrypted at rest
2. Daily sync completes before 7 AM notification
3. Token expiration warnings sent 14 and 7 days before
4. Failed syncs logged with actionable error messages
## Acceptance Tests
- [ ] Encrypt/decrypt round-trips correctly
- [ ] `/api/garmin/status` returns accurate days until expiry
- [ ] `/api/garmin/tokens` validates token structure
- [ ] Cron sync fetches all three Garmin endpoints
- [ ] Expired token triggers appropriate error handling
- [ ] Token expiration warning at 14-day threshold