feat(measurements): add ANS_CHARGE and CARDIO_LOAD measurement types

Polar Nightly Recharge and Training Load Pro carry two native signals with
no existing home: the HRV-based autonomic-nervous-system charge (distinct
from the 1-6 recovery band that maps to RECOVERY_SCORE) and the device-native
cardio-load strain figure (distinct from WHOOP DAY_STRAIN and the COMPUTED
STRAIN_SCORE). Add both to the canonical enum (migration 0166) and wire them
through the validation enum, canonical units, plausibility ranges, category
cluster, chart-token allowlist, PR-direction resolver, and the measurement-
list label/icon/colour maps, with labels across all six locales. Both are
device-derived composites, so they read no PR direction and stay out of the
clinical vitals PDF, matching the WHOOP-native score classes.

Regenerate the OpenAPI contract for the two new enum members.
This commit is contained in:
Marc-André Bombeck committed 2026-06-15 00:07:30 +02:00
1 parent 44eed87e89
commit fd39580ee6
16 files changed
+117 -1

No files matched your search

+10
View File
@@ -1569,6 +1569,8 @@ paths:
- AVERAGE_HEART_RATE
- MAX_HEART_RATE
- SLEEP_DISTURBANCE_COUNT
- ANS_CHARGE
- CARDIO_LOAD
- in: query
name: from
schema:
@@ -1743,6 +1745,8 @@ paths:
- AVERAGE_HEART_RATE
- MAX_HEART_RATE
- SLEEP_DISTURBANCE_COUNT
- ANS_CHARGE
- CARDIO_LOAD
value:
type: number
measuredAt:
@@ -4372,6 +4376,8 @@ paths:
- AVERAGE_HEART_RATE
- MAX_HEART_RATE
- SLEEP_DISTURBANCE_COUNT
- ANS_CHARGE
- CARDIO_LOAD
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
- ANS_CHARGE
- CARDIO_LOAD
value:
type: number
unit:
@@ -9260,6 +9268,8 @@ components:
- AVERAGE_HEART_RATE
- MAX_HEART_RATE
- SLEEP_DISTURBANCE_COUNT
- ANS_CHARGE
- CARDIO_LOAD
value:
type: number
unit:
+2
View File
@@ -550,6 +550,8 @@
"typeAverageHeartRate": "Durchschnittlicher Puls",
"typeMaxHeartRate": "Maximaler Puls",
"typeSleepDisturbanceCount": "Schlafstörungen",
"typeAnsCharge": "ANS-Ladung",
"typeCardioLoad": "Kardio-Belastung",
"filterBySource": "Nach Quelle filtern",
"sourceComputed": "Berechnet",
"sourceWhoop": "WHOOP",
+2
View File
@@ -550,6 +550,8 @@
"typeAverageHeartRate": "Average heart rate",
"typeMaxHeartRate": "Maximum heart rate",
"typeSleepDisturbanceCount": "Sleep disturbances",
"typeAnsCharge": "ANS charge",
"typeCardioLoad": "Cardio load",
"filterBySource": "Filter by source",
"sourceComputed": "Computed",
"sourceWhoop": "WHOOP",
+2
View File
@@ -550,6 +550,8 @@
"typeAverageHeartRate": "Frecuencia cardíaca media",
"typeMaxHeartRate": "Frecuencia cardíaca máxima",
"typeSleepDisturbanceCount": "Interrupciones del sueño",
"typeAnsCharge": "Carga del SNA",
"typeCardioLoad": "Carga cardio",
"filterBySource": "Filtrar por fuente",
"sourceComputed": "Calculado",
"sourceWhoop": "WHOOP",
+2
View File
@@ -550,6 +550,8 @@
"typeAverageHeartRate": "Fréquence cardiaque moyenne",
"typeMaxHeartRate": "Fréquence cardiaque maximale",
"typeSleepDisturbanceCount": "Perturbations du sommeil",
"typeAnsCharge": "Charge SNA",
"typeCardioLoad": "Charge cardio",
"filterBySource": "Filtrer par source",
"sourceComputed": "Calculé",
"sourceWhoop": "WHOOP",
+2
View File
@@ -550,6 +550,8 @@
"typeAverageHeartRate": "Frequenza cardiaca media",
"typeMaxHeartRate": "Frequenza cardiaca massima",
"typeSleepDisturbanceCount": "Disturbi del sonno",
"typeAnsCharge": "Carica SNA",
"typeCardioLoad": "Carico cardio",
"filterBySource": "Filtra per fonte",
"sourceComputed": "Calcolato",
"sourceWhoop": "WHOOP",
+2
View File
@@ -550,6 +550,8 @@
"typeAverageHeartRate": "Średnie tętno",
"typeMaxHeartRate": "Maksymalne tętno",
"typeSleepDisturbanceCount": "Zakłócenia snu",
"typeAnsCharge": "Ładunek ANS",
"typeCardioLoad": "Obciążenie kardio",
"filterBySource": "Filtruj według źródła",
"sourceComputed": "Obliczone",
"sourceWhoop": "WHOOP",
@@ -0,0 +1,20 @@
-- v1.17.1 — Polar pull-completeness: two native Polar signals get their own
-- MeasurementType so they stop being read off the wire and dropped at the mapper.
--
-- 1. `ANS_CHARGE` — Polar Nightly Recharge `ans_charge`, the HRV-based
-- autonomic-nervous-system charge. The primary differentiated recovery
-- component, distinct from the 1–6 recovery band already mapped to
-- RECOVERY_SCORE.
-- 2. `CARDIO_LOAD` — Polar Training Load Pro `cardio_load`, the device-native
-- cardiovascular-strain figure (Polar's WHOOP-day-strain analogue). Kept
-- distinct from DAY_STRAIN (WHOOP 0–21) and the COMPUTED STRAIN_SCORE so
-- each native value keeps its own bucket and scale.
--
-- Purely additive: two enum-extensions on `measurement_type`. No backfill, no
-- column change, no existing row touched. Idempotent (`IF NOT EXISTS`); reruns
-- are safe. Forward-only — Postgres cannot remove an enum value, and with no
-- rows carrying them before the app ships they are inert until the Polar sync
-- writes them.
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'ANS_CHARGE';
ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'CARDIO_LOAD';
+11
View File
@@ -774,6 +774,17 @@ 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 — Polar Nightly Recharge + Training Load Pro coverage (additive) ──
// Two native Polar signals with no existing home. ANS_CHARGE is the HRV-based
// autonomic-nervous-system charge component of Nightly Recharge — Polar's most
// differentiated recovery signal, distinct from the 1–6 recovery band that
// already maps to RECOVERY_SCORE. CARDIO_LOAD is Polar's Training Load Pro
// cardiovascular-strain figure (its WHOOP-day-strain analogue); kept distinct
// from DAY_STRAIN (WHOOP's 0–21 scale) and the COMPUTED STRAIN_SCORE proxy so
// each device-native value keeps its own bucket and native scale.
ANS_CHARGE // score — Polar Nightly Recharge `ans_charge`. The HRV-based autonomic-nervous-system charge (typically ~-10..+10 around the personal baseline; higher = more recovered). Descriptive, not a clinical assessment.
CARDIO_LOAD // score — Polar Training Load Pro `cardio_load`. The session/day cardiovascular load (TRIMP-lineage strain). Distinct from WHOOP DAY_STRAIN (0–21) and the COMPUTED STRAIN_SCORE so the device-native value never shares a bucket.
@@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 — Polar Nightly Recharge + Training Load Pro components ──
ANS_CHARGE: "measurements.typeAnsCharge",
CARDIO_LOAD: "measurements.typeCardioLoad",
};
export const MEASUREMENT_TYPE_ICONS: Record<string, LucideIcon> = {
@@ -225,6 +228,11 @@ export const MEASUREMENT_TYPE_ICONS: Record<string, LucideIcon> = {
AVERAGE_HEART_RATE: HeartPulse,
MAX_HEART_RATE: Heart,
SLEEP_DISTURBANCE_COUNT: Waves,
// ── v1.17.1 — Polar Nightly Recharge + Training Load Pro components ──
// HeartPulse reads as the autonomic-charge signal; Gauge carries the
// cardio-load strain figure (same family as DAY_STRAIN).
ANS_CHARGE: HeartPulse,
CARDIO_LOAD: Gauge,
};
export const MEASUREMENT_TYPE_COLORS: Record<string, string> = {
@@ -326,4 +334,8 @@ 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 — Polar Nightly Recharge + Training Load Pro components ──
// chart-1 (strain/score family) for both, matching DAY_STRAIN.
ANS_CHARGE: "bg-chart-1/20 text-chart-1",
CARDIO_LOAD: "bg-chart-1/20 text-chart-1",
};
@@ -90,10 +90,13 @@ const EXPECTED_TYPES = [
"AVERAGE_HEART_RATE",
"MAX_HEART_RATE",
"SLEEP_DISTURBANCE_COUNT",
// ── v1.17.1 — Polar Nightly Recharge + Training Load Pro components ──
"ANS_CHARGE",
"CARDIO_LOAD",
] 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,13 @@ describe("measurementTypeEnum coverage", () => {
"AVERAGE_HEART_RATE",
"MAX_HEART_RATE",
"SLEEP_DISTURBANCE_COUNT",
// v1.17.1 — Polar Nightly Recharge + Training Load Pro components. ANS
// charge and Cardio Load are device-derived recovery / strain composites,
// not measured clinical vitals; they surface on their own Insights cluster
// with a "descriptive, not clinical" framing and never belong in the
// clinical vitals PDF. See doctor-report-pdf-core.ts.
"ANS_CHARGE",
"CARDIO_LOAD",
]);
it("doctor-report PDF vital types cover the canonical enum minus documented exclusions", () => {
+5
View File
@@ -115,6 +115,11 @@ export const ALLOWED_CHART_TOKENS = [
"metric:AVERAGE_HEART_RATE",
"metric:MAX_HEART_RATE",
"metric:SLEEP_DISTURBANCE_COUNT",
// v1.17.1 — Polar-native recovery / strain components. Continuous daily
// series, so each carries a `metric:<TYPE>` token and renders through the
// generic chart renderer alongside the WHOOP-native score classes.
"metric:ANS_CHARGE",
"metric:CARDIO_LOAD",
] as const;
export type ChartToken = (typeof ALLOWED_CHART_TOKENS)[number];
@@ -60,6 +60,12 @@ const MEASUREMENT_TYPES_WITHOUT_HK_COUNTERPART = new Set<MeasurementType>([
"AVERAGE_HEART_RATE",
"MAX_HEART_RATE",
"SLEEP_DISTURBANCE_COUNT",
// v1.17.1 — Polar Nightly Recharge + Training Load Pro components. These
// ingest server-side from the Polar AccessLink API (source = POLAR), never
// from HealthKit: Apple ships no identifier for Polar's autonomic charge or
// its device-native cardio-load strain figure. No HK mapping by design.
"ANS_CHARGE",
"CARDIO_LOAD",
]);
describe("APPLE_HEALTH_TYPE_MAP", () => {
+6
View File
@@ -188,6 +188,12 @@ export const MEASUREMENT_CATEGORIES: ReadonlyMap<
["AVERAGE_HEART_RATE", "cardiovascular"],
["MAX_HEART_RATE", "cardiovascular"],
["SLEEP_DISTURBANCE_COUNT", "sleep"],
// ── v1.17.1 — Polar Nightly Recharge + Training Load Pro components ──
// ANS charge + Cardio Load are device-native recovery / strain composites,
// so they join the scores cluster alongside DAY_STRAIN.
["ANS_CHARGE", "scores"],
["CARDIO_LOAD", "scores"],
]);
/**
+7
View File
@@ -176,6 +176,13 @@ 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 — Polar-native recovery / strain components. ANS_CHARGE is a
// baseline-relative autonomic deviation (homeostatic, not a goal axis) and
// CARDIO_LOAD is a device-derived cardiovascular-load composite — neither a
// higher nor a lower value is an achievement, so they stay null like the
// other device-derived scores (HRV_RMSSD / DAY_STRAIN).
case "ANS_CHARGE":
case "CARDIO_LOAD":
return null;
}
}
+17
View File
@@ -100,6 +100,13 @@ export const measurementTypeEnum = z.enum([
"AVERAGE_HEART_RATE",
"MAX_HEART_RATE",
"SLEEP_DISTURBANCE_COUNT",
// v1.17.1 — Polar Nightly Recharge + Training Load Pro components ingested
// server-side as `source = POLAR`. ANS_CHARGE is the HRV-based autonomic
// charge (distinct from the 1–6 recovery band that maps to RECOVERY_SCORE);
// CARDIO_LOAD is Polar's device-native cardiovascular-strain figure (distinct
// from WHOOP DAY_STRAIN and the COMPUTED STRAIN_SCORE).
"ANS_CHARGE",
"CARDIO_LOAD",
]);
/**
@@ -312,6 +319,9 @@ 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 — Polar Nightly Recharge + Training Load Pro components ──
ANS_CHARGE: "score",
CARDIO_LOAD: "score",
};
export function getUnitForType(type: string): string {
@@ -487,6 +497,13 @@ 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 — Polar Nightly Recharge + Training Load Pro components ──
// ANS charge is a baseline-relative autonomic deviation that can run
// negative; a generous symmetric ±100 band covers any plausible reading.
// Cardio Load is a non-negative cumulative-strain figure; 1000 is a generous
// ceiling over any plausible single-day training load.
ANS_CHARGE: { min: -100, max: 100 },
CARDIO_LOAD: { min: 0, max: 1000 },
};
export function validateMeasurementRange(