Files
phaseflow/specs/garmin-integration.md
2026-01-10 17:13:18 +00:00

109 lines
2.8 KiB
Markdown

# Garmin Integration Specification
## 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