Commit Graph
4 Commits
Author SHA1 Message Date
Marc-André Bombeck 72b1d1ef8a feat(oura): ingest the vO2_max and daily_stress collections
The Oura v2 client fetched sleep, readiness, activity, daily sleep, and
daily SpO2 but skipped the dedicated vO2_max collection and daily_stress.
An Oura user got no VO2max from any path even though the enum, the
cardio-fitness page, fitness-age, norms, and the LOINC code already exist.

Add fetchVo2Max + mapVo2Max (vo2_max -> VO2_MAX, mL/(kg·min)) and
fetchDailyStress + mapDailyStress. Oura exposes no headline stress score,
only minutes in high stress vs high recovery, so derive a bounded 0-100
STRESS_SCORE from the high-stress share of the day's charged time. Both
collections wire into the existing reactive-refresh sync tail; no
migration, no schema change, no iOS-contract impact.
2026-06-20 12:10:04 +02:00
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00
Marc-André Bombeck c5e91c4cb6 feat(oura): pull the real sleep timeline, SpO2, Sleep Score and more
Widen the Oura sync from a thin slice to the signals the v2 API actually
exposes, all server-authoritative:

  - Real measured sleep timeline. mapSleep now reads the per-stage
    sleep_phase_5_min hypnogram and emits one timed SLEEP_DURATION row per
    contiguous 5-minute run, each stamped at its own end instant from
    bedtime_start. The per-night reader reconstructs the true clock-time
    timeline (measured, not synthesised) instead of the degraded
    all-on-bedtime_end shape. Falls back to the four stage totals when no
    hypnogram is present.
  - daily_spo2 → OXYGEN_SATURATION and daily_sleep → SLEEP_SCORE, filling
    two ladder slots that nothing wrote before.
  - daily_readiness temperature_deviation → BODY_TEMPERATURE_DEVIATION, and
    the eight readiness contributors are parsed (no longer dropped at the
    JSON boundary) for the Insights surface.
  - daily_activity equivalent_walking_distance → WALKING_RUNNING_DISTANCE,
    so an Oura-only day no longer contributes zero distance.

Every sleep row now carries a record-scoped externalId
(sleep:<record-id>:<tag>) so a nap and the main sleep on one day stay
distinct instead of overwriting each other on a shared day key.
2026-06-15 00:09:45 +02:00
Marc-André Bombeck 88139fb981 feat(integrations): add Oura Cloud v2 OAuth sync
Connect an Oura account over the Cloud v2 API: an authorization-code OAuth
flow (shared app from OURA_CLIENT_ID / OURA_CLIENT_SECRET) on the same signed
state machinery as Polar. Unlike Polar, Oura issues refresh tokens; the sync
refreshes reactively on a 401, rotating and re-encrypting both tokens, and a
403 stays a hard reauth rather than a refresh.

An hourly poll-cohort job maps daily readiness to RECOVERY_SCORE, sleep to
per-stage durations plus efficiency / HRV / resting heart rate / respiratory
rate, and daily activity to steps and active energy — all tagged source OURA,
idempotent on (userId, type, source, externalId). Readiness flows through the
canonical recovery ladder above the computed proxy. Settings gains a connect /
disconnect card. Tokens are never logged or returned; every egress goes
through safeFetch.
2026-06-14 20:10:35 +02:00