mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
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:
1 parent
9f8f4167e4
commit
524626d2bd
7 files changed
+517
-24
No files matched your search
@@ -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"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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"],
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in new issue
Block a user