mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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.
92 lines
4.4 KiB
TypeScript
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,
|
|
};
|