feat(sources): add Nightscout, Polar and Oura as measurement sources

Introduce the schema foundation the glucose and wearable integrations build on:
the NIGHTSCOUT, POLAR and OURA measurement sources, encrypted credential columns
for each (Nightscout carries a base URL, an API token and a private-host opt-in
flag; Polar and Oura carry OAuth tokens), and their integration keys. The
recovery source ladder now ranks WHOOP, then Oura, then Polar over the computed
proxy. Additive migration only — new enum values and nullable columns, no
backfill. Nightscout is glucose-only and stays off the wearable priority ladders.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 18:48:02 +02:00
commit 4884f019b7
7 files changed
+517 -24

No files matched your search

+330
View File
@@ -1634,6 +1634,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
- in: query
name: groupBy
schema:
@@ -2080,6 +2083,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
externalId:
type: string
maxLength: 128
@@ -3083,6 +3089,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
activeEnergy:
maxItems: 8
type: array
@@ -3096,6 +3105,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
walkingRunningDistance:
maxItems: 8
type: array
@@ -3109,6 +3121,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
flightsClimbed:
maxItems: 8
type: array
@@ -3122,6 +3137,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
sleep:
maxItems: 8
type: array
@@ -3135,6 +3153,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
weight:
maxItems: 8
type: array
@@ -3148,6 +3169,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bloodPressure:
maxItems: 8
type: array
@@ -3161,6 +3185,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
pulse:
maxItems: 8
type: array
@@ -3174,6 +3201,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyFat:
maxItems: 8
type: array
@@ -3187,6 +3217,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyTemperature:
maxItems: 8
type: array
@@ -3200,6 +3233,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
spo2:
maxItems: 8
type: array
@@ -3213,6 +3249,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
hrv:
maxItems: 8
type: array
@@ -3226,6 +3265,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
restingHeartRate:
maxItems: 8
type: array
@@ -3239,6 +3281,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
vo2Max:
maxItems: 8
type: array
@@ -3252,6 +3297,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
skinTemperature:
maxItems: 8
type: array
@@ -3265,6 +3313,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
respiratoryRate:
maxItems: 8
type: array
@@ -3278,6 +3329,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
recovery:
maxItems: 8
type: array
@@ -3291,6 +3345,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
metricPriority:
type: object
properties:
@@ -3307,6 +3364,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
activeEnergy:
maxItems: 8
type: array
@@ -3320,6 +3380,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
walkingRunningDistance:
maxItems: 8
type: array
@@ -3333,6 +3396,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
flightsClimbed:
maxItems: 8
type: array
@@ -3346,6 +3412,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
sleep:
maxItems: 8
type: array
@@ -3359,6 +3428,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
weight:
maxItems: 8
type: array
@@ -3372,6 +3444,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bloodPressure:
maxItems: 8
type: array
@@ -3385,6 +3460,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
pulse:
maxItems: 8
type: array
@@ -3398,6 +3476,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyFat:
maxItems: 8
type: array
@@ -3411,6 +3492,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyTemperature:
maxItems: 8
type: array
@@ -3424,6 +3508,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
spo2:
maxItems: 8
type: array
@@ -3437,6 +3524,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
hrv:
maxItems: 8
type: array
@@ -3450,6 +3540,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
restingHeartRate:
maxItems: 8
type: array
@@ -3463,6 +3556,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
vo2Max:
maxItems: 8
type: array
@@ -3476,6 +3572,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
skinTemperature:
maxItems: 8
type: array
@@ -3489,6 +3588,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
respiratoryRate:
maxItems: 8
type: array
@@ -3502,6 +3604,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
recovery:
maxItems: 8
type: array
@@ -3515,6 +3620,9 @@ paths:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
deviceTypePriority:
type: object
properties:
@@ -8157,6 +8265,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
notes:
anyOf:
- type: string
@@ -8583,6 +8694,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
notes:
anyOf:
- type: string
@@ -8797,6 +8911,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
- type: "null"
start:
type: string
@@ -8914,6 +9031,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
- type: "null"
start:
type: string
@@ -9120,6 +9240,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
externalId:
anyOf:
- type: string
@@ -9225,6 +9348,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
externalId:
anyOf:
- type: string
@@ -11176,6 +11302,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
activeEnergy:
maxItems: 8
type: array
@@ -11189,6 +11318,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
walkingRunningDistance:
maxItems: 8
type: array
@@ -11202,6 +11334,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
flightsClimbed:
maxItems: 8
type: array
@@ -11215,6 +11350,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
sleep:
maxItems: 8
type: array
@@ -11228,6 +11366,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
weight:
maxItems: 8
type: array
@@ -11241,6 +11382,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bloodPressure:
maxItems: 8
type: array
@@ -11254,6 +11398,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
pulse:
maxItems: 8
type: array
@@ -11267,6 +11414,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyFat:
maxItems: 8
type: array
@@ -11280,6 +11430,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyTemperature:
maxItems: 8
type: array
@@ -11293,6 +11446,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
spo2:
maxItems: 8
type: array
@@ -11306,6 +11462,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
hrv:
maxItems: 8
type: array
@@ -11319,6 +11478,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
restingHeartRate:
maxItems: 8
type: array
@@ -11332,6 +11494,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
vo2Max:
maxItems: 8
type: array
@@ -11345,6 +11510,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
skinTemperature:
maxItems: 8
type: array
@@ -11358,6 +11526,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
respiratoryRate:
maxItems: 8
type: array
@@ -11371,6 +11542,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
recovery:
maxItems: 8
type: array
@@ -11384,6 +11558,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
metricPriority:
type: object
properties:
@@ -11400,6 +11577,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
activeEnergy:
maxItems: 8
type: array
@@ -11413,6 +11593,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
walkingRunningDistance:
maxItems: 8
type: array
@@ -11426,6 +11609,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
flightsClimbed:
maxItems: 8
type: array
@@ -11439,6 +11625,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
sleep:
maxItems: 8
type: array
@@ -11452,6 +11641,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
weight:
maxItems: 8
type: array
@@ -11465,6 +11657,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bloodPressure:
maxItems: 8
type: array
@@ -11478,6 +11673,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
pulse:
maxItems: 8
type: array
@@ -11491,6 +11689,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyFat:
maxItems: 8
type: array
@@ -11504,6 +11705,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyTemperature:
maxItems: 8
type: array
@@ -11517,6 +11721,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
spo2:
maxItems: 8
type: array
@@ -11530,6 +11737,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
hrv:
maxItems: 8
type: array
@@ -11543,6 +11753,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
restingHeartRate:
maxItems: 8
type: array
@@ -11556,6 +11769,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
vo2Max:
maxItems: 8
type: array
@@ -11569,6 +11785,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
skinTemperature:
maxItems: 8
type: array
@@ -11582,6 +11801,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
respiratoryRate:
maxItems: 8
type: array
@@ -11595,6 +11817,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
recovery:
maxItems: 8
type: array
@@ -11608,6 +11833,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
additionalProperties: false
deviceTypePriority:
type: object
@@ -11670,6 +11898,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
activeEnergy:
maxItems: 8
type: array
@@ -11683,6 +11914,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
walkingRunningDistance:
maxItems: 8
type: array
@@ -11696,6 +11930,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
flightsClimbed:
maxItems: 8
type: array
@@ -11709,6 +11946,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
sleep:
maxItems: 8
type: array
@@ -11722,6 +11962,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
weight:
maxItems: 8
type: array
@@ -11735,6 +11978,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bloodPressure:
maxItems: 8
type: array
@@ -11748,6 +11994,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
pulse:
maxItems: 8
type: array
@@ -11761,6 +12010,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyFat:
maxItems: 8
type: array
@@ -11774,6 +12026,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyTemperature:
maxItems: 8
type: array
@@ -11787,6 +12042,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
spo2:
maxItems: 8
type: array
@@ -11800,6 +12058,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
hrv:
maxItems: 8
type: array
@@ -11813,6 +12074,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
restingHeartRate:
maxItems: 8
type: array
@@ -11826,6 +12090,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
vo2Max:
maxItems: 8
type: array
@@ -11839,6 +12106,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
skinTemperature:
maxItems: 8
type: array
@@ -11852,6 +12122,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
respiratoryRate:
maxItems: 8
type: array
@@ -11865,6 +12138,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
recovery:
maxItems: 8
type: array
@@ -11878,6 +12154,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
metricPriority:
type: object
properties:
@@ -11894,6 +12173,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
activeEnergy:
maxItems: 8
type: array
@@ -11907,6 +12189,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
walkingRunningDistance:
maxItems: 8
type: array
@@ -11920,6 +12205,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
flightsClimbed:
maxItems: 8
type: array
@@ -11933,6 +12221,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
sleep:
maxItems: 8
type: array
@@ -11946,6 +12237,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
weight:
maxItems: 8
type: array
@@ -11959,6 +12253,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bloodPressure:
maxItems: 8
type: array
@@ -11972,6 +12269,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
pulse:
maxItems: 8
type: array
@@ -11985,6 +12285,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyFat:
maxItems: 8
type: array
@@ -11998,6 +12301,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
bodyTemperature:
maxItems: 8
type: array
@@ -12011,6 +12317,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
spo2:
maxItems: 8
type: array
@@ -12024,6 +12333,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
hrv:
maxItems: 8
type: array
@@ -12037,6 +12349,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
restingHeartRate:
maxItems: 8
type: array
@@ -12050,6 +12365,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
vo2Max:
maxItems: 8
type: array
@@ -12063,6 +12381,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
skinTemperature:
maxItems: 8
type: array
@@ -12076,6 +12397,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
respiratoryRate:
maxItems: 8
type: array
@@ -12089,6 +12413,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
recovery:
maxItems: 8
type: array
@@ -12102,6 +12429,9 @@ components:
- COMPUTED
- WHOOP
- FITBIT
- NIGHTSCOUT
- POLAR
- OURA
additionalProperties: false
deviceTypePriority:
type: object
@@ -0,0 +1,52 @@
-- v1.17.0 — schema foundation for three new measurement sources: Nightscout
-- glucose (F1) and Polar + Oura OAuth wearables (F4).
--
-- Purely-additive: three enum-extensions on `measurement_source` plus eight
-- new nullable columns on `users` (encrypted credentials + one boolean flag).
-- No backfill, no existing row touched, no new tables. The feature waves add
-- their own connection / oauth-state tables later; this step only unblocks
-- them from colliding on the enum + the credential surface.
--
-- 1. `measurement_source` += `NIGHTSCOUT`, `POLAR`, `OURA` for the
-- server-side native ingest of each source.
-- 2. `users` += Nightscout base URL + token (encrypted) + an
-- `allow_private_host` opt-in boolean (default false) that lets a
-- self-hoster point HealthLog at a private/LAN Nightscout instance past
-- the public-host SSRF floor.
-- 3. `users` += Polar OAuth credentials (access / refresh token + user id),
-- encrypted, mirroring the WHOOP credential columns.
-- 4. `users` += Oura OAuth credentials (access / refresh token), encrypted,
-- mirroring the WHOOP credential columns.
--
-- Idempotent guards (`IF NOT EXISTS`) make reruns safe. Forward-only.
--
-- Reversibility: Postgres cannot remove an enum value, so the three sources
-- stay; with no rows carrying them they are inert. The columns drop with
-- `DROP COLUMN IF EXISTS`.
-- ── 1. measurement_source — append the three server-owned sources ──────
ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'NIGHTSCOUT';
ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'POLAR';
ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'OURA';
-- ── 2. users — Nightscout credentials + private-host opt-in ────────────
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "nightscout_url_encrypted" TEXT;
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "nightscout_token_encrypted" TEXT;
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "nightscout_allow_private_host" BOOLEAN NOT NULL DEFAULT false;
-- ── 3. users — Polar OAuth credentials (encrypted at app level) ────────
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "polar_access_token_encrypted" TEXT;
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "polar_refresh_token_encrypted" TEXT;
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "polar_user_id_encrypted" TEXT;
-- ── 4. users — Oura OAuth credentials (encrypted at app level) ─────────
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "oura_access_token_encrypted" TEXT;
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "oura_refresh_token_encrypted" TEXT;
+39
View File
@@ -109,6 +109,32 @@ model User {
fitbitClientIdEncrypted String? @map("fitbit_client_id_encrypted")
fitbitClientSecretEncrypted String? @map("fitbit_client_secret_encrypted")
// v1.17.0 — Nightscout glucose integration (F1, per-user, encrypted at app
// level). A self-hoster points HealthLog at their own Nightscout instance:
// the base URL + the API token / secret are stored encrypted. By default
// the base URL must resolve to a public host (the SSRF floor); a self-hoster
// running Nightscout on a private/LAN network can flip
// `nightscoutAllowPrivateHost` to opt that instance past the public-host
// requirement explicitly.
nightscoutUrlEncrypted String? @map("nightscout_url_encrypted")
nightscoutTokenEncrypted String? @map("nightscout_token_encrypted")
nightscoutAllowPrivateHost Boolean @default(false) @map("nightscout_allow_private_host")
// v1.17.0 — Polar OAuth credentials (F4, per-user, encrypted at app level).
// Mirrors the WHOOP credential columns: the per-app authorized-user cap on
// Polar AccessLink makes a single shared app unworkable for a multi-operator
// product, so each self-hoster registers their own Polar app and the OAuth
// tokens are stored encrypted.
polarAccessTokenEncrypted String? @map("polar_access_token_encrypted")
polarRefreshTokenEncrypted String? @map("polar_refresh_token_encrypted")
polarUserIdEncrypted String? @map("polar_user_id_encrypted")
// v1.17.0 — Oura OAuth credentials (F4, per-user, encrypted at app level).
// Mirrors the WHOOP credential columns; OAuth access/refresh tokens stored
// encrypted.
ouraAccessTokenEncrypted String? @map("oura_access_token_encrypted")
ouraRefreshTokenEncrypted String? @map("oura_refresh_token_encrypted")
// moodLog integration (per-user, encrypted)
moodLogUrlEncrypted String? @map("mood_log_url_encrypted")
moodLogApiKeyEncrypted String? @map("mood_log_api_key_encrypted")
@@ -790,6 +816,19 @@ enum MeasurementSource {
// (no client write path); rejected on batch + single POST like WITHINGS/WHOOP.
// iOS DTO mirrors this exact spelling so /api/sync/changes decodes the rows.
FITBIT
// v1.17.0 — Nightscout glucose integration (F1). Server-owned (no client
// write path); glucose rows ingest from the user's Nightscout instance.
// Rejected on batch + single POST like WITHINGS/WHOOP/FITBIT. iOS DTO
// mirrors this exact spelling so /api/sync/changes decodes the rows.
NIGHTSCOUT
// v1.17.0 — Polar OAuth integration (F4). Server-owned (no client write
// path); rejected on batch + single POST like WITHINGS/WHOOP/FITBIT. iOS
// DTO mirrors this exact spelling so /api/sync/changes decodes the rows.
POLAR
// v1.17.0 — Oura OAuth integration (F4). Server-owned (no client write
// path); rejected on batch + single POST like WITHINGS/WHOOP/FITBIT. iOS
// DTO mirrors this exact spelling so /api/sync/changes decodes the rows.
OURA
@@map("measurement_source")
}
+26 -9
View File
@@ -39,7 +39,15 @@ import { auditLog } from "@/lib/auth/audit";
import { getEvent } from "@/lib/logging/context";
import { dispatchNotification } from "@/lib/notifications/dispatcher";
export type IntegrationKey = "withings" | "whoop" | "fitbit" | "moodlog";
export type IntegrationKey =
| "withings"
| "whoop"
| "fitbit"
// v1.17.0 — Nightscout glucose (F1) + Polar / Oura OAuth (F4).
| "nightscout"
| "polar"
| "oura"
| "moodlog";
/**
* Failure kinds carried into `recordSyncFailure`.
@@ -774,6 +782,22 @@ export interface AlertInput {
* kind is a one-row table edit instead of two more arms in two
* different ternary stacks (the style-guide forbids nested ternaries).
*/
/**
* Display label per `IntegrationKey`. A one-row table edit replaces the
* nested-ternary chain the admin-alert formatter used to carry — adding
* a new integration is a single line here instead of another arm in a
* ternary stack (the style-guide forbids nested ternaries).
*/
const INTEGRATION_LABELS: Record<IntegrationKey, string> = {
withings: "Withings",
whoop: "WHOOP",
fitbit: "Fitbit",
nightscout: "Nightscout",
polar: "Polar",
oura: "Oura",
moodlog: "moodLog",
};
const FAILURE_KIND_COPY: Record<
FailureKind,
{ reason: string; action: string }
@@ -819,14 +843,7 @@ export function formatAdminAlertPayload(input: AlertInput): {
message: string;
metadata: Record<string, unknown>;
} {
const integrationLabel =
input.integration === "withings"
? "Withings"
: input.integration === "whoop"
? "WHOOP"
: input.integration === "fitbit"
? "Fitbit"
: "moodLog";
const integrationLabel = INTEGRATION_LABELS[input.integration];
const subjectLabel = input.subjectLabel ?? input.userId;
const { reason: reasonLabel, action: actionLabel } =
FAILURE_KIND_COPY[input.kind];
@@ -217,11 +217,35 @@ describe("DEFAULT_SOURCE_PRIORITY", () => {
}
});
it("ranks WHOOP native recovery above the COMPUTED proxy (v1.11)", () => {
// v1.11.0 — native-vs-derived: the device-native Recovery outranks
it("ranks device-native recovery above the COMPUTED proxy (v1.11/v1.17)", () => {
// v1.11.0 — native-vs-derived: a device-native Recovery outranks
// HealthLog's COMPUTED proxy when both exist, with the proxy as the
// fallback for users without a strap.
expect(DEFAULT_SOURCE_PRIORITY.recovery).toEqual(["WHOOP", "COMPUTED"]);
// fallback for users without a wearable. v1.17.0 — Oura readiness and
// Polar nightly recovery slot between WHOOP and the proxy.
expect(DEFAULT_SOURCE_PRIORITY.recovery).toEqual([
"WHOOP",
"OURA",
"POLAR",
"COMPUTED",
]);
// The COMPUTED proxy stays the last resort regardless of how many
// native wearables precede it.
expect(DEFAULT_SOURCE_PRIORITY.recovery.at(-1)).toBe("COMPUTED");
});
it("slots Polar + Oura below the established wearables (v1.17)", () => {
// v1.17.0 — Polar / Oura are worn wearables in the same overnight class
// as WHOOP/Fitbit; they rank below the established straps but above the
// iPhone-relayed HealthKit summary and the Withings nightly summary on
// the recovery-input ladders.
for (const key of ["sleep", "hrv", "restingHeartRate"] as const) {
const ladder = DEFAULT_SOURCE_PRIORITY[key];
expect(ladder).toContain("OURA");
expect(ladder).toContain("POLAR");
expect(ladder.indexOf("OURA")).toBeLessThan(ladder.indexOf("APPLE_HEALTH"));
expect(ladder.indexOf("POLAR")).toBeLessThan(ladder.indexOf("APPLE_HEALTH"));
expect(ladder.indexOf("WHOOP")).toBeLessThan(ladder.indexOf("OURA"));
}
});
});
+12
View File
@@ -143,6 +143,18 @@ export const measurementSourceEnum = z.enum([
// (no client write path), exactly like WHOOP. Part of this enum so the
// read/response shapes (and the iOS decoder) can decode the rows it surfaces.
"FITBIT",
// v1.17.0 — Nightscout glucose integration (F1). Server-owned (no client
// write path), exactly like WHOOP/FITBIT. Part of this enum so the
// read/response shapes (and the iOS decoder) can decode the rows it surfaces.
"NIGHTSCOUT",
// v1.17.0 — Polar OAuth integration (F4). Server-owned (no client write
// path), exactly like WHOOP/FITBIT. Part of this enum so the read/response
// shapes (and the iOS decoder) can decode the rows it surfaces.
"POLAR",
// v1.17.0 — Oura OAuth integration (F4). Server-owned (no client write
// path), exactly like WHOOP/FITBIT. Part of this enum so the read/response
// shapes (and the iOS decoder) can decode the rows it surfaces.
"OURA",
]);
/**
+30 -11
View File
@@ -226,27 +226,46 @@ export const DEFAULT_SOURCE_PRIORITY: Required<MetricPriority> = {
// the iPhone-relayed HealthKit summary or the Withings nightly summary.
// v1.12.0 — Fitbit is a wrist wearable in the same class as WHOOP but with
// a lower-fidelity nightly estimate, so it ranks just below it.
sleep: ["WHOOP", "FITBIT", "APPLE_HEALTH", "WITHINGS"],
hrv: ["WHOOP", "FITBIT", "APPLE_HEALTH", "WITHINGS"],
restingHeartRate: ["WHOOP", "FITBIT", "APPLE_HEALTH", "WITHINGS"],
// v1.17.0 — Polar + Oura are worn wearables in the same overnight-sampling
// class as WHOOP/Fitbit; they rank below the established straps but above
// the iPhone-relayed HealthKit summary and the Withings nightly summary.
sleep: ["WHOOP", "FITBIT", "OURA", "POLAR", "APPLE_HEALTH", "WITHINGS"],
hrv: ["WHOOP", "FITBIT", "OURA", "POLAR", "APPLE_HEALTH", "WITHINGS"],
restingHeartRate: [
"WHOOP",
"FITBIT",
"OURA",
"POLAR",
"APPLE_HEALTH",
"WITHINGS",
],
// A real scale beats a strap's body-measurement estimate for weight.
weight: ["WITHINGS", "APPLE_HEALTH", "MANUAL", "WHOOP", "FITBIT"],
bloodPressure: ["WITHINGS", "APPLE_HEALTH", "MANUAL"],
pulse: ["WITHINGS", "APPLE_HEALTH", "MANUAL", "FITBIT"],
pulse: ["WITHINGS", "APPLE_HEALTH", "MANUAL", "FITBIT", "POLAR", "OURA"],
bodyFat: ["WITHINGS", "APPLE_HEALTH", "MANUAL", "FITBIT"],
bodyTemperature: ["WITHINGS", "APPLE_HEALTH", "MANUAL", "FITBIT"],
// Withings ScanWatch pulse-ox is the primary SpO2 sensor; WHOOP second,
// Fitbit third.
spo2: ["WITHINGS", "WHOOP", "FITBIT", "APPLE_HEALTH", "MANUAL"],
// Fitbit third, Oura's ring fourth.
spo2: ["WITHINGS", "WHOOP", "FITBIT", "OURA", "APPLE_HEALTH", "MANUAL"],
vo2Max: ["WITHINGS", "APPLE_HEALTH", "FITBIT", "MANUAL"],
// v1.11.0 — new WHOOP-overlapping keys. ScanWatch dermal reading is the
// primary skin-temperature sensor; WHOOP's strap is second, Fitbit third.
skinTemperature: ["WITHINGS", "WHOOP", "FITBIT", "APPLE_HEALTH"],
respiratoryRate: ["WHOOP", "FITBIT", "APPLE_HEALTH", "WITHINGS"],
// primary skin-temperature sensor; WHOOP's strap is second, Fitbit third,
// Oura's ring fourth.
skinTemperature: ["WITHINGS", "WHOOP", "FITBIT", "OURA", "APPLE_HEALTH"],
respiratoryRate: [
"WHOOP",
"FITBIT",
"OURA",
"POLAR",
"APPLE_HEALTH",
"WITHINGS",
],
// v1.11.0 — native-vs-derived recovery. WHOOP's device-native Recovery
// outranks HealthLog's COMPUTED proxy when both exist; the proxy is the
// fallback for users without a strap.
recovery: ["WHOOP", "COMPUTED"],
// fallback for users without a strap. v1.17.0 — Oura readiness + Polar
// nightly recovery slot below WHOOP and above the computed proxy.
recovery: ["WHOOP", "OURA", "POLAR", "COMPUTED"],
};
/**