mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
Add Strava as a per-user BYO-OAuth workout source, mirroring the shipped WHOOP/Oura/Polar pattern. Strava caps every new app at single-player mode, so each self-hoster registers their own app and pastes the client id/secret into Settings (falling back to a shared env app). Strava is a workout source only — it exposes no daily metrics and no training-depth analytics are derived. - Schema (migration 0233): per-user encrypted client id/secret + access/refresh tokens, plaintext athlete id, incremental cursor, and backfill marker on `User`; `STRAVA` added to `MeasurementSource`. - Token model follows Oura's reactive-refresh-on-401 with a compare-and-set on the stored refresh ciphertext, because Strava rotates its refresh token on every refresh. - Client over safeFetch: authorization-code + refresh, activity list + budget- aware per-activity detail (for calories/HR), honouring the 200/15min cap; each activity maps to one `Workout` row. Activity name/description stay in metadata as data, never surfaced or interpolated. - Dedup reuses the existing source-agnostic two-layer picker: `STRAVA` slots into the workout ladder below the device-native wearables and above MANUAL/IMPORT, so an Apple-Watch + Strava twin collapses to one canonical row. - Poll-cohort hourly sync (:17) + self-converging boot backfill, wired into the integration-sync registrar. Webhook deferred. - OAuth routes (connect/callback/credentials/disconnect/status/test) with signed state, cross-account re-auth rejection, and a surgical `www.strava.com` connect-src gated to the Strava surfaces. - Settings card + consolidated status envelope entry; env + compose whitelist; i18n across all six locales. - Garmin: no direct connector is possible (business-partner-only). Add docs/integrations/garmin.md documenting the Apple Health / Google Health Connect path and Garmin's withheld proprietary scores, cross-linked from both platform docs, plus a non-connector info note in the Connections panel. Regenerate docs/api/openapi.yaml for the new `STRAVA` MeasurementSource value.
82 lines
3.9 KiB
TypeScript
82 lines
3.9 KiB
TypeScript
/**
|
|
* Query keys — external integrations: API tokens, Telegram, Withings,
|
|
* WHOOP, Fitbit, moodLog, and the Apple Health export import job.
|
|
* Part of the centralized factory; aggregated in `./index.ts`.
|
|
*/
|
|
export const integrationKeys = {
|
|
tokens: () => ["tokens"] as const,
|
|
// v1.22.0 — MCP connector tokens (the dedicated `health:read` scope minted
|
|
// for the remote MCP endpoint). Distinct key from `tokens()` so the MCP
|
|
// settings card's mint / revoke mutations invalidate only this list.
|
|
mcpTokens: () => ["mcp-tokens"] as const,
|
|
// MCP Phase 3 — OAuth connector connections (the revocable unit for a remote
|
|
// client authorized through the OAuth bridge). Distinct from `mcpTokens()` so
|
|
// a connection revoke invalidates only the connection list.
|
|
mcpConnections: () => ["mcp-connections"] as const,
|
|
telegram: () => ["telegram"] as const,
|
|
telegramSettings: () => ["telegram", "settings"] as const,
|
|
withings: () => ["withings"] as const,
|
|
/**
|
|
* v1.4.42 W3-QUERYKEY-LONGTAIL — the per-card Withings status read.
|
|
* Shares the `["withings"]` prefix with `withings()` so a disconnect
|
|
* mutation invalidates both at once.
|
|
*/
|
|
withingsStatus: () => ["withings", "status"] as const,
|
|
whoop: () => ["whoop"] as const,
|
|
/**
|
|
* Per-card WHOOP status read. Shares the `["whoop"]` prefix with `whoop()`
|
|
* so a disconnect / credentials mutation invalidates both at once.
|
|
*/
|
|
whoopStatus: () => ["whoop", "status"] as const,
|
|
// v1.12.0 — Fitbit/Pixel integration card, mirroring the WHOOP keys.
|
|
fitbit: () => ["fitbit"] as const,
|
|
/**
|
|
* Per-card Fitbit status read. Shares the `["fitbit"]` prefix with `fitbit()`
|
|
* so a disconnect / credentials mutation invalidates both at once.
|
|
*/
|
|
fitbitStatus: () => ["fitbit", "status"] as const,
|
|
|
|
// v1.27.0 — Google Health (Fitbit + Pixel Watch + Fitbit Air) integration
|
|
// card. Reads status off the consolidated /api/integrations/status envelope
|
|
// (like Fitbit/WHOOP); the connect/disconnect/credentials/sync mutations
|
|
// invalidate this key so the card repaints. Distinct hyphenated array so it
|
|
// never collides with the classic `fitbit` key.
|
|
googleHealth: () => ["google-health"] as const,
|
|
|
|
// v1.17.0 — Nightscout CGM integration card. Self-contained status read
|
|
// (not the consolidated /api/integrations/status envelope), so the
|
|
// connect/disconnect mutations invalidate this single key.
|
|
nightscout: () => ["nightscout"] as const,
|
|
nightscoutStatus: () => ["nightscout", "status"] as const,
|
|
|
|
// v1.17.0 (F4) — Polar + Oura OAuth integration cards. Self-contained status
|
|
// reads (not the consolidated /api/integrations/status envelope), so the
|
|
// connect/disconnect mutations invalidate these single keys.
|
|
polar: () => ["polar"] as const,
|
|
polarStatus: () => ["polar", "status"] as const,
|
|
oura: () => ["oura"] as const,
|
|
ouraStatus: () => ["oura", "status"] as const,
|
|
|
|
// v1.28.x — Strava OAuth integration card. Reads status off the consolidated
|
|
// /api/integrations/status envelope (like WHOOP/Fitbit/Polar/Oura); the
|
|
// connect/disconnect/credentials mutations invalidate this key so the card
|
|
// repaints.
|
|
strava: () => ["strava"] as const,
|
|
stravaStatus: () => ["strava", "status"] as const,
|
|
|
|
moodlogStatus: () => ["moodlog-status"] as const,
|
|
integrationsStatus: () => ["integrations", "status"] as const,
|
|
|
|
/**
|
|
* v1.15.7 — in-flight Apple Health `export.zip` import. Keyed by the
|
|
* `jobId` returned from `POST /api/import/apple-health-export` so the
|
|
* Export & Import settings area can poll
|
|
* `GET /api/import/apple-health-export/[jobId]/status` with a
|
|
* `refetchInterval` while the job is running and stop on a terminal
|
|
* state. Each upload mints its own key, so a re-upload (or a second
|
|
* archive) never collides with a previous job's cached status.
|
|
*/
|
|
importJobStatus: (jobId: string) =>
|
|
["import", "apple-health", "status", jobId] as const,
|
|
};
|