mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +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
@@ -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