feat(sources): add Nightscout, Polar, Oura schema foundation

Extend MeasurementSource with NIGHTSCOUT, POLAR, OURA and add the
per-user encrypted credential columns each source needs: a Nightscout
base URL + token plus a private-host opt-in flag, and Polar/Oura OAuth
tokens mirroring the WHOOP columns. Migration 0160 is additive only —
new enum values, nullable columns, one boolean default false.

Register the three sources across the read enum, the integration-status
key set, and the source-priority ladders.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 18:47:14 +02:00
1 parent 9f8f4167e4
commit 524626d2bd
7 files changed
+517 -24

No files matched your search

+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"],
};
/**