diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index ef3b07d43..5a859419a 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -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 diff --git a/prisma/migrations/0160_v1170_new_measurement_sources/migration.sql b/prisma/migrations/0160_v1170_new_measurement_sources/migration.sql new file mode 100644 index 000000000..8931b5dfb --- /dev/null +++ b/prisma/migrations/0160_v1170_new_measurement_sources/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3a07aacde..313ef0ff4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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") } diff --git a/src/lib/integrations/status.ts b/src/lib/integrations/status.ts index 126baab0c..ef0fd09c9 100644 --- a/src/lib/integrations/status.ts +++ b/src/lib/integrations/status.ts @@ -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 = { + 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; } { - 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]; diff --git a/src/lib/validations/__tests__/source-priority.test.ts b/src/lib/validations/__tests__/source-priority.test.ts index 856822b0c..b286cb052 100644 --- a/src/lib/validations/__tests__/source-priority.test.ts +++ b/src/lib/validations/__tests__/source-priority.test.ts @@ -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")); + } }); }); diff --git a/src/lib/validations/measurement.ts b/src/lib/validations/measurement.ts index 041e6259f..a10432006 100644 --- a/src/lib/validations/measurement.ts +++ b/src/lib/validations/measurement.ts @@ -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", ]); /** diff --git a/src/lib/validations/source-priority.ts b/src/lib/validations/source-priority.ts index 87895c498..866472632 100644 --- a/src/lib/validations/source-priority.ts +++ b/src/lib/validations/source-priority.ts @@ -226,27 +226,46 @@ export const DEFAULT_SOURCE_PRIORITY: Required = { // 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"], }; /**