mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
feat(fitbit): add the Fitbit connection data model and FITBIT measurement source
Lay the data-model foundation for the Fitbit/Pixel integration over the Google Health API, mirroring the WHOOP provider. No OAuth, sync, or UI yet. - Prisma: FitbitConnection (1:1 per user, encrypted access/refresh tokens) and FitbitOAuthState models, two encrypted BYO-credential columns on User, and FITBIT appended to the MeasurementSource enum. Migration 0117_v1120_fitbit_integration is additive only — one enum value plus two tables and two nullable columns, no new MeasurementType (every launch metric maps onto an existing type). - Wire the two new encrypted columns and the FitbitConnection token columns into scripts/rotate-encryption-key.ts. - FITBIT touchpoints: the read measurementSourceEnum (not the writable set), the DEFAULT_SOURCE_PRIORITY and workout-source ladders, the sources-section label map, the IntegrationKey union plus its display name, and a fitbit()/fitbitStatus() query-key pair. The source-rank SQL and read-tier collapse pick FITBIT up from the ladders with no code change. - Add the FITBIT source label to all six locale bundles and regenerate the OpenAPI contract so the source enum carries FITBIT for the iOS decoder.
This commit is contained in:
1 parent
045da70133
commit
86b2774567
18 files changed
+368
-20
No files matched your search
@@ -0,0 +1,44 @@
|
||||
# v1.12.0 — server → iOS: new `FITBIT` MeasurementSource heads-up
|
||||
|
||||
**Date:** 2026-06-04
|
||||
**From:** server
|
||||
**Status:** landing in v1.12.0 (data-model foundation merged; sync/UI follow in
|
||||
later waves of the same release)
|
||||
|
||||
## What is changing
|
||||
|
||||
v1.12.0 adds a Fitbit/Pixel integration over the Google Health API. Like WHOOP
|
||||
and Withings, it is a **server-owned, read-only ingest** — there is no client
|
||||
write path; the batch + single-POST surfaces reject it. The rows surface through
|
||||
the same read/response shapes the iOS app already decodes (`/api/sync/changes`,
|
||||
the measurement list, charts).
|
||||
|
||||
The one contract item for iOS in this wave:
|
||||
|
||||
- **New `MeasurementSource` enum value: `FITBIT`** (exact spelling, uppercase).
|
||||
Add the `FITBIT` case to the iOS DTO's `MeasurementSource` decoding so
|
||||
`/api/sync/changes` and the measurement reads decode Fitbit rows. This is the
|
||||
same contract that gated `WHOOP` and `APPLE_HEALTH`.
|
||||
|
||||
> **Mismatched spelling = silent client-side decode drop.** A row tagged
|
||||
> `source: "FITBIT"` that the DTO cannot decode is silently skipped, so the
|
||||
> measurement count diverges from the server with no error surfaced. Pin the
|
||||
> exact spelling.
|
||||
|
||||
## What is NOT changing
|
||||
|
||||
- **No new `MeasurementType` values.** Every Fitbit launch metric maps onto an
|
||||
existing type (`WEIGHT`, `BODY_FAT`, `OXYGEN_SATURATION`,
|
||||
`HEART_RATE_VARIABILITY`, `RESTING_HEART_RATE`, `RESPIRATORY_RATE`, `PULSE`,
|
||||
`WRIST_TEMPERATURE`, `ACTIVITY_STEPS`, `WALKING_RUNNING_DISTANCE`,
|
||||
`ACTIVE_ENERGY_BURNED`, `FLIGHTS_CLIMBED`, `VO2_MAX`, `SLEEP_DURATION`, plus
|
||||
`Workout` rows). No DTO `MeasurementType` change is needed.
|
||||
- **Blood glucose / blood pressure / body temperature are deferred.** Those
|
||||
slots exist but are NOT in the launch sync set (gated on the Google Health
|
||||
API roadmap). No action for iOS now.
|
||||
|
||||
## Confirmation requested
|
||||
|
||||
Confirm the iOS DTO carries the `FITBIT` case ahead of the v1.12.0 deploy. A
|
||||
follow-up `v1.12.0-server-to-ios-fitbit-LIVE.md` will land once the enum is live
|
||||
on apps01 (verified via `/api/version`).
|
||||
@@ -1189,6 +1189,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
externalId:
|
||||
type: string
|
||||
maxLength: 128
|
||||
@@ -1801,6 +1802,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1813,6 +1815,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1825,6 +1828,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1837,6 +1841,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1849,6 +1854,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1861,6 +1867,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1873,6 +1880,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1885,6 +1893,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1897,6 +1906,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1909,6 +1919,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1921,6 +1932,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1933,6 +1945,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1945,6 +1958,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1957,6 +1971,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
skinTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1969,6 +1984,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
respiratoryRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1981,6 +1997,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
recovery:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -1993,6 +2010,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
metricPriority:
|
||||
type: object
|
||||
properties:
|
||||
@@ -2008,6 +2026,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2020,6 +2039,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2032,6 +2052,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2044,6 +2065,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2056,6 +2078,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2068,6 +2091,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2080,6 +2104,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2092,6 +2117,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2104,6 +2130,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2116,6 +2143,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2128,6 +2156,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2140,6 +2169,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2152,6 +2182,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2164,6 +2195,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
skinTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2176,6 +2208,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
respiratoryRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2188,6 +2221,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
recovery:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -2200,6 +2234,7 @@ paths:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
deviceTypePriority:
|
||||
type: object
|
||||
properties:
|
||||
@@ -4778,6 +4813,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
notes:
|
||||
anyOf:
|
||||
- type: string
|
||||
@@ -5093,6 +5129,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
notes:
|
||||
anyOf:
|
||||
- type: string
|
||||
@@ -5252,6 +5289,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
- type: "null"
|
||||
start:
|
||||
type: string
|
||||
@@ -5366,6 +5404,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
- type: "null"
|
||||
start:
|
||||
type: string
|
||||
@@ -5569,6 +5608,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
externalId:
|
||||
anyOf:
|
||||
- type: string
|
||||
@@ -5673,6 +5713,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
externalId:
|
||||
anyOf:
|
||||
- type: string
|
||||
@@ -6936,6 +6977,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6948,6 +6990,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6960,6 +7003,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6972,6 +7016,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6984,6 +7029,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -6996,6 +7042,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7008,6 +7055,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7020,6 +7068,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7032,6 +7081,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7044,6 +7094,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7056,6 +7107,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7068,6 +7120,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7080,6 +7133,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7092,6 +7146,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
skinTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7104,6 +7159,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
respiratoryRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7116,6 +7172,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
recovery:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7128,6 +7185,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
metricPriority:
|
||||
type: object
|
||||
properties:
|
||||
@@ -7143,6 +7201,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7155,6 +7214,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7167,6 +7227,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7179,6 +7240,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7191,6 +7253,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7203,6 +7266,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7215,6 +7279,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7227,6 +7292,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7239,6 +7305,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7251,6 +7318,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7263,6 +7331,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7275,6 +7344,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7287,6 +7357,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7299,6 +7370,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
skinTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7311,6 +7383,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
respiratoryRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7323,6 +7396,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
recovery:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7335,6 +7409,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
additionalProperties: false
|
||||
deviceTypePriority:
|
||||
type: object
|
||||
@@ -7396,6 +7471,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7408,6 +7484,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7420,6 +7497,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7432,6 +7510,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7444,6 +7523,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7456,6 +7536,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7468,6 +7549,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7480,6 +7562,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7492,6 +7575,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7504,6 +7588,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7516,6 +7601,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7528,6 +7614,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7540,6 +7627,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7552,6 +7640,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
skinTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7564,6 +7653,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
respiratoryRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7576,6 +7666,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
recovery:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7588,6 +7679,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
metricPriority:
|
||||
type: object
|
||||
properties:
|
||||
@@ -7603,6 +7695,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
activeEnergy:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7615,6 +7708,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
walkingRunningDistance:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7627,6 +7721,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
flightsClimbed:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7639,6 +7734,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
sleep:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7651,6 +7747,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
weight:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7663,6 +7760,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bloodPressure:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7675,6 +7773,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
pulse:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7687,6 +7786,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyFat:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7699,6 +7799,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
bodyTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7711,6 +7812,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
spo2:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7723,6 +7825,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
hrv:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7735,6 +7838,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
restingHeartRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7747,6 +7851,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
vo2Max:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7759,6 +7864,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
skinTemperature:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7771,6 +7877,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
respiratoryRate:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7783,6 +7890,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
recovery:
|
||||
maxItems: 8
|
||||
type: array
|
||||
@@ -7795,6 +7903,7 @@ components:
|
||||
- APPLE_HEALTH
|
||||
- COMPUTED
|
||||
- WHOOP
|
||||
- FITBIT
|
||||
additionalProperties: false
|
||||
deviceTypePriority:
|
||||
type: object
|
||||
|
||||
@@ -3130,6 +3130,7 @@
|
||||
"MANUAL": "Manuelle Eingabe",
|
||||
"IMPORT": "Import",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Berechnet"
|
||||
},
|
||||
"deviceLabels": {
|
||||
|
||||
@@ -3130,6 +3130,7 @@
|
||||
"MANUAL": "Manual entry",
|
||||
"IMPORT": "Import",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Computed"
|
||||
},
|
||||
"deviceLabels": {
|
||||
|
||||
@@ -3130,6 +3130,7 @@
|
||||
"MANUAL": "Entrada manual",
|
||||
"IMPORT": "Importar",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Calculado"
|
||||
},
|
||||
"deviceLabels": {
|
||||
|
||||
@@ -3130,6 +3130,7 @@
|
||||
"MANUAL": "Saisie manuelle",
|
||||
"IMPORT": "Importer",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Calculé"
|
||||
},
|
||||
"deviceLabels": {
|
||||
|
||||
@@ -3130,6 +3130,7 @@
|
||||
"MANUAL": "Inserimento manuale",
|
||||
"IMPORT": "Importa",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Calcolato"
|
||||
},
|
||||
"deviceLabels": {
|
||||
|
||||
@@ -3130,6 +3130,7 @@
|
||||
"MANUAL": "Wpis ręczny",
|
||||
"IMPORT": "Importuj",
|
||||
"WHOOP": "WHOOP",
|
||||
"FITBIT": "Fitbit",
|
||||
"COMPUTED": "Obliczone"
|
||||
},
|
||||
"deviceLabels": {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
-- v1.12.0 — Fitbit/Pixel integration via the Google Health API (schema + data
|
||||
-- layer).
|
||||
--
|
||||
-- Purely-additive: one enum-extension, two new tables, and two new nullable
|
||||
-- columns on `users`. No backfill, no existing row touched. Unlike the WHOOP
|
||||
-- migration (0111) this adds NO new `measurement_type` values — every Fitbit
|
||||
-- launch metric maps onto an existing type.
|
||||
--
|
||||
-- 1. `measurement_source` += `FITBIT` for the server-side native ingest.
|
||||
-- 2. `fitbit_connections` — per-user encrypted token row (1:1 with users),
|
||||
-- mirroring `whoop_connections` minus `max_heart_rate`, plus
|
||||
-- `fitbit_user_id` and `backfill_completed_at`.
|
||||
-- 3. `fitbit_oauth_states` — short-lived `(nonce → userId)` CSRF ledger,
|
||||
-- identical in shape to `whoop_oauth_states`.
|
||||
-- 4. `users` += `fitbit_client_id_encrypted` / `fitbit_client_secret_encrypted`
|
||||
-- — per-user BYO-keys (Google's Testing-mode per-app authorized-user cap
|
||||
-- makes a single shared client unworkable for a multi-operator product).
|
||||
--
|
||||
-- Idempotent guards (`IF NOT EXISTS`) make reruns safe. Forward-only.
|
||||
--
|
||||
-- Reversibility: Postgres cannot remove an enum value, so `FITBIT` stays; with
|
||||
-- no rows carrying it it is inert. The two tables drop with `DROP TABLE IF
|
||||
-- EXISTS`, the two columns with `DROP COLUMN IF EXISTS`.
|
||||
|
||||
-- ── 1. measurement_source — append the Fitbit server-owned source ──────
|
||||
ALTER TYPE "measurement_source" ADD VALUE IF NOT EXISTS 'FITBIT';
|
||||
|
||||
-- ── 2. fitbit_connections ─────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS "fitbit_connections" (
|
||||
"id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"fitbit_user_id" TEXT NOT NULL,
|
||||
"access_token" TEXT NOT NULL,
|
||||
"refresh_token" TEXT NOT NULL,
|
||||
"token_expires_at" TIMESTAMP(3) NOT NULL,
|
||||
"scope" TEXT,
|
||||
"last_synced_at" TIMESTAMP(3),
|
||||
"backfill_completed_at" TIMESTAMP(3),
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "fitbit_connections_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "fitbit_connections_user_id_key"
|
||||
ON "fitbit_connections" ("user_id");
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "fitbit_connections"
|
||||
ADD CONSTRAINT "fitbit_connections_user_id_fkey"
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- ── 3. fitbit_oauth_states ────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS "fitbit_oauth_states" (
|
||||
"nonce" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"expires_at" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "fitbit_oauth_states_pkey" PRIMARY KEY ("nonce")
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "fitbit_oauth_states_expires_at_idx"
|
||||
ON "fitbit_oauth_states" ("expires_at");
|
||||
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE "fitbit_oauth_states"
|
||||
ADD CONSTRAINT "fitbit_oauth_states_user_id_fkey"
|
||||
FOREIGN KEY ("user_id") REFERENCES "users"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
|
||||
-- ── 4. users — per-user Fitbit BYO-key columns (encrypted at app level) ─
|
||||
ALTER TABLE "users"
|
||||
ADD COLUMN IF NOT EXISTS "fitbit_client_id_encrypted" TEXT;
|
||||
ALTER TABLE "users"
|
||||
ADD COLUMN IF NOT EXISTS "fitbit_client_secret_encrypted" TEXT;
|
||||
@@ -66,6 +66,14 @@ model User {
|
||||
whoopClientIdEncrypted String? @map("whoop_client_id_encrypted")
|
||||
whoopClientSecretEncrypted String? @map("whoop_client_secret_encrypted")
|
||||
|
||||
// v1.12.0 — Fitbit/Pixel OAuth credentials (per-user BYO-keys, encrypted at
|
||||
// app level) over the Google Health API. The per-app authorized-user cap in
|
||||
// Google's Testing mode makes a single shared client unworkable for a
|
||||
// multi-operator product, so each self-hoster registers their own Google
|
||||
// Cloud OAuth client and pastes the client id/secret into Settings.
|
||||
fitbitClientIdEncrypted String? @map("fitbit_client_id_encrypted")
|
||||
fitbitClientSecretEncrypted String? @map("fitbit_client_secret_encrypted")
|
||||
|
||||
// moodLog integration (per-user, encrypted)
|
||||
moodLogUrlEncrypted String? @map("mood_log_url_encrypted")
|
||||
moodLogApiKeyEncrypted String? @map("mood_log_api_key_encrypted")
|
||||
@@ -340,6 +348,10 @@ model User {
|
||||
// OAuth-state ledger (1:N) relations.
|
||||
whoopConnection WhoopConnection?
|
||||
whoopOAuthStates WhoopOAuthState[]
|
||||
// v1.12.0 — Fitbit/Pixel integration. Mirrors the WHOOP connection (1:1) +
|
||||
// OAuth-state ledger (1:N) relations.
|
||||
fitbitConnection FitbitConnection?
|
||||
fitbitOAuthStates FitbitOAuthState[]
|
||||
moodEntries MoodEntry[]
|
||||
notificationChannels NotificationChannel[]
|
||||
pushSubscriptions PushSubscription[]
|
||||
@@ -672,6 +684,10 @@ enum MeasurementSource {
|
||||
// (no client write path) as `source = WHOOP` rows. iOS DTO mirrors this
|
||||
// exact spelling so the `/api/sync/changes` delta feed decodes the rows.
|
||||
WHOOP
|
||||
// v1.12.0 — Fitbit/Pixel integration via the Google Health API. Server-owned
|
||||
// (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
|
||||
|
||||
@@map("measurement_source")
|
||||
}
|
||||
@@ -1827,6 +1843,47 @@ model WhoopOAuthState {
|
||||
@@map("whoop_oauth_states")
|
||||
}
|
||||
|
||||
/// v1.12.0 — Fitbit/Pixel connection over the Google Health API. Mirrors
|
||||
/// `WhoopConnection` 1:1 per user. Tokens are encrypted at the app level
|
||||
/// before persistence. Unlike WHOOP, Google refresh tokens do not rotate per
|
||||
/// use, so `refreshToken` is overwritten only when a token response carries a
|
||||
/// fresh one.
|
||||
model FitbitConnection {
|
||||
id String @id @default(cuid())
|
||||
userId String @unique @map("user_id")
|
||||
fitbitUserId String @map("fitbit_user_id") // Google/Fitbit external id
|
||||
accessToken String @map("access_token") // encrypted at app level
|
||||
refreshToken String @map("refresh_token") // encrypted at app level
|
||||
tokenExpiresAt DateTime @map("token_expires_at")
|
||||
/// Space-separated OAuth scope string granted by the Google authorisation
|
||||
/// flow. Null = legacy connection that predates scope tracking.
|
||||
scope String? @map("scope")
|
||||
lastSyncedAt DateTime? @map("last_synced_at")
|
||||
/// Set once the self-converging history backfill walks every collection to
|
||||
/// completion. Null = backfill still in flight (or never started).
|
||||
backfillCompletedAt DateTime? @map("backfill_completed_at")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("fitbit_connections")
|
||||
}
|
||||
|
||||
/// v1.12.0 — short-lived `(nonce → userId)` ledger backing the Fitbit OAuth
|
||||
/// state check, identical in shape + lifecycle to `WhoopOAuthState`.
|
||||
model FitbitOAuthState {
|
||||
nonce String @id
|
||||
userId String @map("user_id")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
expiresAt DateTime @map("expires_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([expiresAt])
|
||||
@@map("fitbit_oauth_states")
|
||||
}
|
||||
|
||||
// ─── Mood Entries (moodLog) ───────────────────────────────────
|
||||
|
||||
model MoodEntry {
|
||||
|
||||
@@ -102,6 +102,8 @@ async function main() {
|
||||
withingsClientSecretEncrypted: true,
|
||||
whoopClientIdEncrypted: true,
|
||||
whoopClientSecretEncrypted: true,
|
||||
fitbitClientIdEncrypted: true,
|
||||
fitbitClientSecretEncrypted: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -119,6 +121,8 @@ async function main() {
|
||||
"withingsClientSecretEncrypted",
|
||||
"whoopClientIdEncrypted",
|
||||
"whoopClientSecretEncrypted",
|
||||
"fitbitClientIdEncrypted",
|
||||
"fitbitClientSecretEncrypted",
|
||||
];
|
||||
for (const field of userFields) {
|
||||
const r = await rotateField(
|
||||
@@ -176,6 +180,26 @@ async function main() {
|
||||
results.push(r);
|
||||
}
|
||||
|
||||
// ───── FitbitConnection table (accessToken / refreshToken) ─────
|
||||
const fitbit = await prisma.fitbitConnection.findMany({
|
||||
select: { id: true, accessToken: true, refreshToken: true },
|
||||
});
|
||||
for (const field of ["accessToken", "refreshToken"] as const) {
|
||||
const r = await rotateField(
|
||||
"FitbitConnection",
|
||||
field,
|
||||
fitbit,
|
||||
(f) => f[field],
|
||||
async (id, ciphertext) => {
|
||||
await prisma.fitbitConnection.update({
|
||||
where: { id },
|
||||
data: { [field]: ciphertext } as Record<string, string>,
|
||||
});
|
||||
},
|
||||
);
|
||||
results.push(r);
|
||||
}
|
||||
|
||||
// ───── AppSettings table (singleton typically) ─────
|
||||
const settings = await prisma.appSettings.findMany({
|
||||
select: {
|
||||
|
||||
@@ -94,6 +94,8 @@ const SOURCE_LABEL_KEYS: Record<string, string> = {
|
||||
// `recovery` ladder for native-vs-derived ordering).
|
||||
WHOOP: "settings.sections.sources.sourceLabels.WHOOP",
|
||||
COMPUTED: "settings.sections.sources.sourceLabels.COMPUTED",
|
||||
// v1.12.0 — Fitbit/Pixel native source.
|
||||
FITBIT: "settings.sections.sources.sourceLabels.FITBIT",
|
||||
};
|
||||
|
||||
const DEVICE_TYPE_LABEL_KEYS: Record<DeviceType, string> = {
|
||||
|
||||
@@ -39,7 +39,7 @@ import { auditLog } from "@/lib/auth/audit";
|
||||
import { getEvent } from "@/lib/logging/context";
|
||||
import { dispatchNotification } from "@/lib/notifications/dispatcher";
|
||||
|
||||
export type IntegrationKey = "withings" | "whoop" | "moodlog";
|
||||
export type IntegrationKey = "withings" | "whoop" | "fitbit" | "moodlog";
|
||||
|
||||
/**
|
||||
* Failure kinds carried into `recordSyncFailure`.
|
||||
@@ -824,7 +824,9 @@ export function formatAdminAlertPayload(input: AlertInput): {
|
||||
? "Withings"
|
||||
: input.integration === "whoop"
|
||||
? "WHOOP"
|
||||
: "moodLog";
|
||||
: input.integration === "fitbit"
|
||||
? "Fitbit"
|
||||
: "moodLog";
|
||||
const subjectLabel = input.subjectLabel ?? input.userId;
|
||||
const { reason: reasonLabel, action: actionLabel } =
|
||||
FAILURE_KIND_COPY[input.kind];
|
||||
|
||||
@@ -374,6 +374,13 @@ export const queryKeys = {
|
||||
* 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.4.32 — workout list + detail caches. `workouts()` is the
|
||||
// root key invalidated by the batch-ingest mutation; the recent +
|
||||
|
||||
@@ -94,6 +94,10 @@ export const DEFAULT_WORKOUT_SOURCE_PRIORITY: readonly MeasurementSource[] = [
|
||||
// session. The read-time picker clusters a WHOOP run and an Apple-Health
|
||||
// run by (activityType, startedAt ± window) and keeps the ladder winner.
|
||||
"WHOOP",
|
||||
// v1.12.0 — Fitbit/Pixel exercise logs rank just below WHOOP: a wrist
|
||||
// wearable in the same class, the next-best when no watch logged the same
|
||||
// session.
|
||||
"FITBIT",
|
||||
"WITHINGS",
|
||||
"MANUAL",
|
||||
"IMPORT",
|
||||
|
||||
@@ -102,7 +102,7 @@ describe("sourcePrioritySchema", () => {
|
||||
|
||||
it("rejects an unknown source string", () => {
|
||||
const result = sourcePrioritySchema.safeParse({
|
||||
weight: ["FITBIT", "WITHINGS"],
|
||||
weight: ["GARMIN", "WITHINGS"],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
@@ -129,6 +129,10 @@ export const measurementSourceEnum = z.enum([
|
||||
// client write path). Part of this enum so the read/response shapes (and
|
||||
// the iOS decoder) can decode the rows it surfaces.
|
||||
"WHOOP",
|
||||
// v1.12.0 — Fitbit/Pixel integration via the Google Health API. Server-owned
|
||||
// (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",
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -214,29 +214,35 @@ export type DeviceTypePriority = z.infer<typeof deviceTypePrioritySchema>;
|
||||
* receives the same reading via Withings' Health Mate iOS app).
|
||||
*/
|
||||
export const DEFAULT_SOURCE_PRIORITY: Required<MetricPriority> = {
|
||||
steps: ["APPLE_HEALTH", "WITHINGS", "MANUAL"],
|
||||
activeEnergy: ["APPLE_HEALTH", "WITHINGS", "MANUAL"],
|
||||
walkingRunningDistance: ["APPLE_HEALTH", "WITHINGS", "MANUAL"],
|
||||
flightsClimbed: ["APPLE_HEALTH", "WITHINGS", "MANUAL"],
|
||||
// v1.12.0 — Fitbit/Pixel rides below Apple Health for cumulative metrics:
|
||||
// HealthKit still aggregates the broadest device set, and a Fitbit-only
|
||||
// self-hoster has no Apple stream to compete with anyway.
|
||||
steps: ["APPLE_HEALTH", "WITHINGS", "FITBIT", "MANUAL"],
|
||||
activeEnergy: ["APPLE_HEALTH", "WITHINGS", "FITBIT", "MANUAL"],
|
||||
walkingRunningDistance: ["APPLE_HEALTH", "WITHINGS", "FITBIT", "MANUAL"],
|
||||
flightsClimbed: ["APPLE_HEALTH", "WITHINGS", "FITBIT", "MANUAL"],
|
||||
// v1.11.0 — WHOOP leads the recovery-input ladders (sleep / HRV / RHR):
|
||||
// a worn-all-night strap has higher-resolution overnight sampling than
|
||||
// the iPhone-relayed HealthKit summary or the Withings nightly summary.
|
||||
sleep: ["WHOOP", "APPLE_HEALTH", "WITHINGS"],
|
||||
hrv: ["WHOOP", "APPLE_HEALTH", "WITHINGS"],
|
||||
restingHeartRate: ["WHOOP", "APPLE_HEALTH", "WITHINGS"],
|
||||
// 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"],
|
||||
// A real scale beats a strap's body-measurement estimate for weight.
|
||||
weight: ["WITHINGS", "APPLE_HEALTH", "MANUAL", "WHOOP"],
|
||||
weight: ["WITHINGS", "APPLE_HEALTH", "MANUAL", "WHOOP", "FITBIT"],
|
||||
bloodPressure: ["WITHINGS", "APPLE_HEALTH", "MANUAL"],
|
||||
pulse: ["WITHINGS", "APPLE_HEALTH", "MANUAL"],
|
||||
bodyFat: ["WITHINGS", "APPLE_HEALTH", "MANUAL"],
|
||||
bodyTemperature: ["WITHINGS", "APPLE_HEALTH", "MANUAL"],
|
||||
// Withings ScanWatch pulse-ox is the primary SpO2 sensor; WHOOP second.
|
||||
spo2: ["WITHINGS", "WHOOP", "APPLE_HEALTH", "MANUAL"],
|
||||
vo2Max: ["WITHINGS", "APPLE_HEALTH", "MANUAL"],
|
||||
pulse: ["WITHINGS", "APPLE_HEALTH", "MANUAL", "FITBIT"],
|
||||
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"],
|
||||
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.
|
||||
skinTemperature: ["WITHINGS", "WHOOP", "APPLE_HEALTH"],
|
||||
respiratoryRate: ["WHOOP", "APPLE_HEALTH", "WITHINGS"],
|
||||
// primary skin-temperature sensor; WHOOP's strap is second, Fitbit third.
|
||||
skinTemperature: ["WITHINGS", "WHOOP", "FITBIT", "APPLE_HEALTH"],
|
||||
respiratoryRate: ["WHOOP", "FITBIT", "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.
|
||||
|
||||
Reference in new issue
Block a user