mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
feat(measurements): add Sleep Score and body-temperature deviation types
Two additive measurement_type values for the Oura coverage completion:
- SLEEP_SCORE — the headline 0-100 sleep-quality score (unit score),
kept apart from the WHOOP sleep sub-scores so two vendors' headline
numbers never share a bucket.
- BODY_TEMPERATURE_DEVIATION — a signed nightly body-temperature offset
from the personal baseline (celsius). It is a deviation, not an
absolute reading, so it cannot share the body / skin / wrist
temperature buckets.
Migration 0165 extends the enum (additive, no backfill). Register both in
the validation enum, units, plausibility ranges, category overlay, list-UI
label/icon/colour maps, chart-token allowlist, and the PR-direction switch
(both resolve to no record direction). Both are descriptive signals, not
clinical vitals, so they stay out of the doctor-report PDF. Localise the
two list labels across all six locales and regenerate the OpenAPI contract.
This commit is contained in:
1 parent
44eed87e89
commit
3cdbab908c
17 files changed
+133
-1
No files matched your search
@@ -1569,6 +1569,8 @@ paths:
|
||||
- AVERAGE_HEART_RATE
|
||||
- MAX_HEART_RATE
|
||||
- SLEEP_DISTURBANCE_COUNT
|
||||
- SLEEP_SCORE
|
||||
- BODY_TEMPERATURE_DEVIATION
|
||||
- in: query
|
||||
name: from
|
||||
schema:
|
||||
@@ -1743,6 +1745,8 @@ paths:
|
||||
- AVERAGE_HEART_RATE
|
||||
- MAX_HEART_RATE
|
||||
- SLEEP_DISTURBANCE_COUNT
|
||||
- SLEEP_SCORE
|
||||
- BODY_TEMPERATURE_DEVIATION
|
||||
value:
|
||||
type: number
|
||||
measuredAt:
|
||||
@@ -4372,6 +4376,8 @@ paths:
|
||||
- AVERAGE_HEART_RATE
|
||||
- MAX_HEART_RATE
|
||||
- SLEEP_DISTURBANCE_COUNT
|
||||
- SLEEP_SCORE
|
||||
- BODY_TEMPERATURE_DEVIATION
|
||||
description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s."
|
||||
required: true
|
||||
description: "The measurement type to read (single metric — no fan-out). Closed enum: an unknown type 422s."
|
||||
@@ -8831,6 +8837,8 @@ components:
|
||||
- AVERAGE_HEART_RATE
|
||||
- MAX_HEART_RATE
|
||||
- SLEEP_DISTURBANCE_COUNT
|
||||
- SLEEP_SCORE
|
||||
- BODY_TEMPERATURE_DEVIATION
|
||||
value:
|
||||
type: number
|
||||
unit:
|
||||
@@ -9260,6 +9268,8 @@ components:
|
||||
- AVERAGE_HEART_RATE
|
||||
- MAX_HEART_RATE
|
||||
- SLEEP_DISTURBANCE_COUNT
|
||||
- SLEEP_SCORE
|
||||
- BODY_TEMPERATURE_DEVIATION
|
||||
value:
|
||||
type: number
|
||||
unit:
|
||||
|
||||
@@ -550,6 +550,8 @@
|
||||
"typeAverageHeartRate": "Durchschnittlicher Puls",
|
||||
"typeMaxHeartRate": "Maximaler Puls",
|
||||
"typeSleepDisturbanceCount": "Schlafstörungen",
|
||||
"typeSleepScore": "Schlafscore",
|
||||
"typeBodyTemperatureDeviation": "Körpertemperaturabweichung",
|
||||
"filterBySource": "Nach Quelle filtern",
|
||||
"sourceComputed": "Berechnet",
|
||||
"sourceWhoop": "WHOOP",
|
||||
|
||||
@@ -550,6 +550,8 @@
|
||||
"typeAverageHeartRate": "Average heart rate",
|
||||
"typeMaxHeartRate": "Maximum heart rate",
|
||||
"typeSleepDisturbanceCount": "Sleep disturbances",
|
||||
"typeSleepScore": "Sleep score",
|
||||
"typeBodyTemperatureDeviation": "Body temperature deviation",
|
||||
"filterBySource": "Filter by source",
|
||||
"sourceComputed": "Computed",
|
||||
"sourceWhoop": "WHOOP",
|
||||
|
||||
@@ -550,6 +550,8 @@
|
||||
"typeAverageHeartRate": "Frecuencia cardíaca media",
|
||||
"typeMaxHeartRate": "Frecuencia cardíaca máxima",
|
||||
"typeSleepDisturbanceCount": "Interrupciones del sueño",
|
||||
"typeSleepScore": "Puntuación de sueño",
|
||||
"typeBodyTemperatureDeviation": "Desviación de temperatura corporal",
|
||||
"filterBySource": "Filtrar por fuente",
|
||||
"sourceComputed": "Calculado",
|
||||
"sourceWhoop": "WHOOP",
|
||||
|
||||
@@ -550,6 +550,8 @@
|
||||
"typeAverageHeartRate": "Fréquence cardiaque moyenne",
|
||||
"typeMaxHeartRate": "Fréquence cardiaque maximale",
|
||||
"typeSleepDisturbanceCount": "Perturbations du sommeil",
|
||||
"typeSleepScore": "Score de sommeil",
|
||||
"typeBodyTemperatureDeviation": "Écart de température corporelle",
|
||||
"filterBySource": "Filtrer par source",
|
||||
"sourceComputed": "Calculé",
|
||||
"sourceWhoop": "WHOOP",
|
||||
|
||||
@@ -550,6 +550,8 @@
|
||||
"typeAverageHeartRate": "Frequenza cardiaca media",
|
||||
"typeMaxHeartRate": "Frequenza cardiaca massima",
|
||||
"typeSleepDisturbanceCount": "Disturbi del sonno",
|
||||
"typeSleepScore": "Punteggio del sonno",
|
||||
"typeBodyTemperatureDeviation": "Deviazione della temperatura corporea",
|
||||
"filterBySource": "Filtra per fonte",
|
||||
"sourceComputed": "Calcolato",
|
||||
"sourceWhoop": "WHOOP",
|
||||
|
||||
@@ -550,6 +550,8 @@
|
||||
"typeAverageHeartRate": "Średnie tętno",
|
||||
"typeMaxHeartRate": "Maksymalne tętno",
|
||||
"typeSleepDisturbanceCount": "Zakłócenia snu",
|
||||
"typeSleepScore": "Wynik snu",
|
||||
"typeBodyTemperatureDeviation": "Odchylenie temperatury ciała",
|
||||
"filterBySource": "Filtruj według źródła",
|
||||
"sourceComputed": "Obliczone",
|
||||
"sourceWhoop": "WHOOP",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- v1.17.1 — Oura coverage completion (additive).
|
||||
--
|
||||
-- Two new `measurement_type` enum values so the Oura sync can store signals it
|
||||
-- already fetches (or now fetches) but had no home for:
|
||||
--
|
||||
-- 1. `SLEEP_SCORE` — Oura's headline 0–100 Sleep Score from the
|
||||
-- `daily_sleep` collection (distinct from the per-stage `sleep` detail).
|
||||
-- Kept apart from the WHOOP `SLEEP_PERFORMANCE` / `SLEEP_EFFICIENCY`
|
||||
-- sub-scores so two vendors' headline numbers never share a bucket.
|
||||
-- 2. `BODY_TEMPERATURE_DEVIATION` — Oura's nightly body-temperature deviation
|
||||
-- from the user's personal baseline (`daily_readiness.temperature_deviation`),
|
||||
-- a SIGNED °C offset (illness / luteal-phase / stress signal). It is NOT an
|
||||
-- absolute reading, so it cannot share the BODY_TEMPERATURE /
|
||||
-- SKIN_TEMPERATURE / WRIST_TEMPERATURE absolute-temperature buckets.
|
||||
--
|
||||
-- Purely-additive: two enum extensions, no backfill, no existing row touched,
|
||||
-- no new tables or columns. Both stored as `source = OURA`.
|
||||
|
||||
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'SLEEP_SCORE';
|
||||
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'BODY_TEMPERATURE_DEVIATION';
|
||||
@@ -774,6 +774,19 @@ enum MeasurementType {
|
||||
MAX_HEART_RATE // bpm — WHOOP `cycle.score.max_heart_rate`. The day's whole-cycle peak heart rate.
|
||||
SLEEP_DISTURBANCE_COUNT // count — WHOOP `sleep.score.stage_summary.disturbance_count`. Per-night tally of sleep disturbances.
|
||||
|
||||
// ── v1.17.1 — Oura coverage completion (additive) ──
|
||||
// Oura's headline 0–100 Sleep Score from the `daily_sleep` collection
|
||||
// (distinct from the per-stage `sleep` detail). Stored as `source = OURA`,
|
||||
// unit `score`. Kept apart from SLEEP_PERFORMANCE / SLEEP_EFFICIENCY (WHOOP
|
||||
// sub-scores) so two vendors' headline numbers never share a bucket.
|
||||
SLEEP_SCORE // score (0–100) — Oura `daily_sleep.score`. Per-night sleep quality headline.
|
||||
// Oura nightly body-temperature deviation from the user's personal baseline
|
||||
// (`daily_readiness.temperature_deviation`), in °C. NOT an absolute
|
||||
// temperature — a signed offset (illness / luteal-phase / stress signal),
|
||||
// so it cannot share the BODY_TEMPERATURE / SKIN_TEMPERATURE / WRIST_TEMPERATURE
|
||||
// absolute-reading buckets. Plausible band is small and centred on 0.
|
||||
BODY_TEMPERATURE_DEVIATION // celsius (signed deviation) — Oura `daily_readiness.temperature_deviation`.
|
||||
|
||||
@@map("measurement_type")
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,9 @@ export const MEASUREMENT_TYPE_LABEL_KEYS: Record<string, string> = {
|
||||
AVERAGE_HEART_RATE: "measurements.typeAverageHeartRate",
|
||||
MAX_HEART_RATE: "measurements.typeMaxHeartRate",
|
||||
SLEEP_DISTURBANCE_COUNT: "measurements.typeSleepDisturbanceCount",
|
||||
// ── v1.17.1 — Oura coverage completion ──
|
||||
SLEEP_SCORE: "measurements.typeSleepScore",
|
||||
BODY_TEMPERATURE_DEVIATION: "measurements.typeBodyTemperatureDeviation",
|
||||
};
|
||||
|
||||
export const MEASUREMENT_TYPE_ICONS: Record<string, LucideIcon> = {
|
||||
@@ -225,6 +228,9 @@ export const MEASUREMENT_TYPE_ICONS: Record<string, LucideIcon> = {
|
||||
AVERAGE_HEART_RATE: HeartPulse,
|
||||
MAX_HEART_RATE: Heart,
|
||||
SLEEP_DISTURBANCE_COUNT: Waves,
|
||||
// ── v1.17.1 — Oura coverage completion ──
|
||||
SLEEP_SCORE: Moon,
|
||||
BODY_TEMPERATURE_DEVIATION: Thermometer,
|
||||
};
|
||||
|
||||
export const MEASUREMENT_TYPE_COLORS: Record<string, string> = {
|
||||
@@ -326,4 +332,9 @@ export const MEASUREMENT_TYPE_COLORS: Record<string, string> = {
|
||||
AVERAGE_HEART_RATE: "bg-chart-3/20 text-chart-3",
|
||||
MAX_HEART_RATE: "bg-chart-3/20 text-chart-3",
|
||||
SLEEP_DISTURBANCE_COUNT: "bg-chart-2/20 text-chart-2",
|
||||
// ── v1.17.1 — Oura coverage completion ──
|
||||
// chart-2 (sleep family) for the Sleep Score; chart-5 (metabolic/thermal)
|
||||
// for the body-temperature deviation.
|
||||
SLEEP_SCORE: "bg-chart-2/20 text-chart-2",
|
||||
BODY_TEMPERATURE_DEVIATION: "bg-chart-5/20 text-chart-5",
|
||||
};
|
||||
@@ -90,10 +90,13 @@ const EXPECTED_TYPES = [
|
||||
"AVERAGE_HEART_RATE",
|
||||
"MAX_HEART_RATE",
|
||||
"SLEEP_DISTURBANCE_COUNT",
|
||||
// ── v1.17.1 — Oura coverage completion ──
|
||||
"SLEEP_SCORE",
|
||||
"BODY_TEMPERATURE_DEVIATION",
|
||||
] as const;
|
||||
|
||||
describe("measurementTypeEnum coverage", () => {
|
||||
it("exposes the 63 canonical measurement types", () => {
|
||||
it("exposes the 65 canonical measurement types", () => {
|
||||
expect([...measurementTypeEnum.options].sort()).toEqual(
|
||||
[...EXPECTED_TYPES].sort(),
|
||||
);
|
||||
@@ -215,6 +218,14 @@ describe("measurementTypeEnum coverage", () => {
|
||||
"AVERAGE_HEART_RATE",
|
||||
"MAX_HEART_RATE",
|
||||
"SLEEP_DISTURBANCE_COUNT",
|
||||
// v1.17.1 — Oura coverage completion. The Sleep Score is a nightly
|
||||
// derived composite and the body-temperature deviation is a signed
|
||||
// baseline offset (illness / cycle / stress signal), not a measured
|
||||
// clinical vital — both surface on the Insights / measurement-list
|
||||
// surfaces and never belong in the clinical vitals PDF. See
|
||||
// doctor-report-pdf-core.ts for the matching exclusion rationale.
|
||||
"SLEEP_SCORE",
|
||||
"BODY_TEMPERATURE_DEVIATION",
|
||||
]);
|
||||
|
||||
it("doctor-report PDF vital types cover the canonical enum minus documented exclusions", () => {
|
||||
|
||||
@@ -188,6 +188,11 @@ export const DOCTOR_REPORT_TYPE_UNIT_KEYS: Record<string, string | null> = {
|
||||
* and carry a "descriptive, not a clinical assessment" disclaimer. They stay
|
||||
* out of this table by design (paired with `PDF_VITAL_EXCLUSIONS` in
|
||||
* `measurement-type-enum-coverage.test.ts`).
|
||||
*
|
||||
* v1.17.1 — Oura coverage completion. `SLEEP_SCORE` (a nightly derived
|
||||
* composite) and `BODY_TEMPERATURE_DEVIATION` (a signed baseline offset, not an
|
||||
* absolute reading) are excluded for the same reason as the other derived /
|
||||
* lifestyle signals — they are descriptive, not measured clinical vitals.
|
||||
*/
|
||||
export const DOCTOR_REPORT_VITAL_TYPES = [
|
||||
"WEIGHT",
|
||||
|
||||
@@ -115,6 +115,10 @@ export const ALLOWED_CHART_TOKENS = [
|
||||
"metric:AVERAGE_HEART_RATE",
|
||||
"metric:MAX_HEART_RATE",
|
||||
"metric:SLEEP_DISTURBANCE_COUNT",
|
||||
// v1.17.1 — Oura coverage completion. Both are continuous nightly series, so
|
||||
// each carries a `metric:<TYPE>` token and renders through the generic chart.
|
||||
"metric:SLEEP_SCORE",
|
||||
"metric:BODY_TEMPERATURE_DEVIATION",
|
||||
] as const;
|
||||
|
||||
export type ChartToken = (typeof ALLOWED_CHART_TOKENS)[number];
|
||||
@@ -230,6 +234,10 @@ const ORPHAN_ENUMS = [
|
||||
"AVERAGE_HEART_RATE",
|
||||
"MAX_HEART_RATE",
|
||||
"SLEEP_DISTURBANCE_COUNT",
|
||||
// v1.17.1 — Oura coverage completion, same shape: strip the bare enum name
|
||||
// if the model drops it into prose.
|
||||
"SLEEP_SCORE",
|
||||
"BODY_TEMPERATURE_DEVIATION",
|
||||
] as const;
|
||||
|
||||
// `\b` boundaries keep ordinary English prose untouched — "weight"
|
||||
|
||||
@@ -60,6 +60,13 @@ const MEASUREMENT_TYPES_WITHOUT_HK_COUNTERPART = new Set<MeasurementType>([
|
||||
"AVERAGE_HEART_RATE",
|
||||
"MAX_HEART_RATE",
|
||||
"SLEEP_DISTURBANCE_COUNT",
|
||||
// v1.17.1 — Oura coverage completion. The Sleep Score and the body-temperature
|
||||
// deviation ingest server-side from the Oura API (source = OURA), never from
|
||||
// HealthKit: Apple ships no headline 0–100 sleep score and surfaces only an
|
||||
// absolute wrist temperature, not a signed baseline deviation. No HK mapping
|
||||
// by design.
|
||||
"SLEEP_SCORE",
|
||||
"BODY_TEMPERATURE_DEVIATION",
|
||||
]);
|
||||
|
||||
describe("APPLE_HEALTH_TYPE_MAP", () => {
|
||||
|
||||
@@ -188,6 +188,13 @@ export const MEASUREMENT_CATEGORIES: ReadonlyMap<
|
||||
["AVERAGE_HEART_RATE", "cardiovascular"],
|
||||
["MAX_HEART_RATE", "cardiovascular"],
|
||||
["SLEEP_DISTURBANCE_COUNT", "sleep"],
|
||||
|
||||
// ── v1.17.1 — Oura coverage completion ──
|
||||
// The Sleep Score is a per-night sleep-quality index → sleep cluster.
|
||||
["SLEEP_SCORE", "sleep"],
|
||||
// Body-temperature deviation is an overnight thermal signal that pairs with
|
||||
// skin / wrist temperature in the metabolic cluster.
|
||||
["BODY_TEMPERATURE_DEVIATION", "metabolic"],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -176,6 +176,12 @@ export function getPRDirection(
|
||||
// WALKING_HEART_RATE_AVERAGE case above. Defer to a null PR direction.
|
||||
case "AVERAGE_HEART_RATE":
|
||||
case "MAX_HEART_RATE":
|
||||
// v1.17.1 — Oura coverage. The Sleep Score is a nightly derived composite
|
||||
// (like the other sleep-quality indices), and the body-temperature
|
||||
// deviation is a homeostatic signed offset centred on 0 — neither has a
|
||||
// goal axis, so both stay null like the rest of the derived/homeostatic set.
|
||||
case "SLEEP_SCORE":
|
||||
case "BODY_TEMPERATURE_DEVIATION":
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,15 @@ export const measurementTypeEnum = z.enum([
|
||||
"AVERAGE_HEART_RATE",
|
||||
"MAX_HEART_RATE",
|
||||
"SLEEP_DISTURBANCE_COUNT",
|
||||
// ── v1.17.1 — Oura coverage completion (additive) ──
|
||||
// Oura's headline 0–100 Sleep Score (`daily_sleep.score`), distinct from the
|
||||
// WHOOP SLEEP_PERFORMANCE / SLEEP_EFFICIENCY sub-scores. The body-temperature
|
||||
// deviation is a SIGNED °C offset from the user's baseline
|
||||
// (`daily_readiness.temperature_deviation`), not an absolute reading, so it
|
||||
// never shares the BODY_TEMPERATURE / SKIN_TEMPERATURE / WRIST_TEMPERATURE
|
||||
// buckets. Both ingest server-side as `source = OURA`.
|
||||
"SLEEP_SCORE",
|
||||
"BODY_TEMPERATURE_DEVIATION",
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -312,6 +321,11 @@ const unitMap: Record<string, string> = {
|
||||
MAX_HEART_RATE: "bpm",
|
||||
// Per-night sleep disturbance tally — a plain integer count.
|
||||
SLEEP_DISTURBANCE_COUNT: "count",
|
||||
// ── v1.17.1 — Oura coverage completion ──
|
||||
// Oura's headline 0–100 Sleep Score — bare "score" like the other 0–100 scores.
|
||||
SLEEP_SCORE: "score",
|
||||
// Signed body-temperature deviation in °C (Oura nightly baseline offset).
|
||||
BODY_TEMPERATURE_DEVIATION: "celsius",
|
||||
};
|
||||
|
||||
export function getUnitForType(type: string): string {
|
||||
@@ -487,6 +501,14 @@ const VALUE_RANGES: Record<string, { min: number; max: number }> = {
|
||||
// Per-night sleep disturbance count — 0 is an undisturbed night; 200 is a
|
||||
// generous ceiling over any plausible severely-fragmented night.
|
||||
SLEEP_DISTURBANCE_COUNT: { min: 0, max: 200 },
|
||||
// ── v1.17.1 — Oura coverage completion ──
|
||||
// Sleep Score (0–100), same band as the other score classes.
|
||||
SLEEP_SCORE: { min: 0, max: 100 },
|
||||
// Body-temperature deviation (°C) — a signed nightly offset centred on 0.
|
||||
// Oura clamps its own display near ±1 °C; ±5 °C is a generous band that
|
||||
// rejects an obvious sensor glitch while keeping every real illness /
|
||||
// luteal-phase swing.
|
||||
BODY_TEMPERATURE_DEVIATION: { min: -5, max: 5 },
|
||||
};
|
||||
|
||||
export function validateMeasurementRange(
|
||||
|
||||
Reference in new issue
Block a user