feat(oura): import workouts and intraday heart rate

Workouts from /v2/usercollection/workout land as Workout rows keyed on
the Oura id, with the activity mapped onto the canonical sport set and
the raw label, intensity and recording source kept in metadata. Heart
rate from /v2/usercollection/heartrate lands as PULSE, streamed page by
page and written in bounded batches, first write wins per instant. The
hourly window for heart rate is hours, not days: six by default,
reaching back to the newest reading after a gap, at most 72.

Both collections sit behind their own scopes (workout, heartrate). A
grant from before they were requested answers 401 naming the scope; that
is recorded as a scope note on the card, never as a failure or a
reconnect. Collection refusals carry Retry-After on a 429.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 19:35:47 +02:00
1 parent 9a3523a498
commit 7585994697
24 files changed
+1312 -179

No files matched your search

@@ -62,6 +62,7 @@ const WRITERS: Record<string, Disposition> = {
"lib/fitbit/sync-core.ts": { kind: "gate" },
"lib/google-health/sync-core.ts": { kind: "gate" },
"lib/nightscout/sync.ts": { kind: "gate" },
"lib/oura/sync-heart-rate.ts": { kind: "gate" },
// The shared identity seam. Polar, Oura, WHOOP, the Withings sleep leg,
// the Apple export importer and the batch endpoint all write through it,
// so the gate here is what makes a new provider fail closed.
@@ -61,6 +61,8 @@ const WRITE_SITES: Record<string, string> = {
"refolds the touched days and re-warms the touched types at the end of the upsert",
"lib/nightscout/sync.ts":
"refolds the touched days and re-warms the touched types at the end of the sync",
"lib/oura/sync-heart-rate.ts":
"collects the days each heart-rate page touched; refolds them and re-warms the type once per window",
"lib/withings/sync.ts":
"refolds the touched days and re-warms the touched types at the end of the sync",
"lib/withings/sync-activity.ts":
@@ -116,6 +116,15 @@ const PROVIDER_INGESTS: ProviderIngest[] = [
writeFile: "lib/import/health-connect/import.ts",
metadataKey: "healthConnectExerciseType",
},
{
provider: "Oura",
mapper: "mapOuraSportType",
mapperFile: "lib/oura/sport-map.ts",
captureFile: "lib/oura/sync-workouts.ts",
captureField: "w.activity",
writeFile: "lib/oura/sync-workouts.ts",
metadataKey: "ouraActivity",
},
{
provider: "Fitbit",
mapper: "mapFitbitSportType",