mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
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:
1 parent
44eed87e89
commit
fd39580ee6
16 files changed
+117
-1
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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';
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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"],
|
||||
]);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user