Files
HealthLog/src/lib/query-keys/integrations.ts
T
Marc-André Bombeck 996d2519d1 feat(import): read Health Connect exports
Upload the ZIP the Android Health Connect app writes and import its
`health_connect_export.db` in the background, the same way an Apple
Health export is imported: streamed to disk with a SHA-256, capped at
1 GiB, one import per account, a job row of kind `health_connect` that
the shared reconcile cron and staging sweep now look up on its own
queue.

The worker extracts the database (4 GiB ceiling, zip-bomb checks) and
opens it read-only with `immutable=1`, `trusted_schema=OFF` and
`query_only`. A view in place of a record table refuses the file,
columns are found by name, and schema versions below 9 are refused.
Records are read in batches of 500 and aggregated in SQLite: steps,
energy and distance as one total per day from the app the person's
Health Connect priority list ranks first, heart rate per minute for 90
days and as hourly means before that, sleep stages with the unknown
stage counted only in sessions without real stages, workouts, cycle
days, water and micronutrients. Apps whose integration the account has
connected are left out, and a reading already stored under another
source is skipped. Rows carry source HEALTH_CONNECT, `hc:<uuid>` or the
`stats:` ids, so a second import of the same export writes nothing.

Both staged files are removed on every way out; the job result holds
counts per type and per app only. Settings gain a Health Connect card
with the export steps, progress and the outcome, in all seven
languages, and the import is documented in
docs/integrations/health-connect.md.
2026-10-08 09:54:42 +02:00

92 lines
4.4 KiB
TypeScript

/**
* Query keys — external integrations: API tokens, Telegram, Withings,
* WHOOP, Fitbit, 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. Reads status off the
// consolidated /api/integrations/status envelope (like Fitbit/WHOOP); this
// key stays for the card's other reads and invalidations.
nightscout: () => ["nightscout"] 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,
integrationsStatus: () => ["integrations", "status"] as const,
// Apple Health live-sync freshness comes from the existing HealthKit config
// endpoint. It is intentionally separate from the OAuth integration ledger:
// a received-data timestamp proves ingest activity, not a web connection.
healthKitStatus: () => ["integrations", "healthkit"] 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,
/**
* v1.42 (#972) — the account's latest Health Connect export import. One key
* per account rather than per job: the status route answers the newest
* job, so the card shows the last outcome after a reload as well.
*/
healthConnectImportStatus: () =>
["import", "health-connect", "status"] as const,
};