diff --git a/CHANGELOG.md b/CHANGELOG.md index f5436773f..2cc569fa9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ - `GET /api/biomarkers/{id}/series` returns one lab marker's line, the same points and window the timeline draws for it, under the Labs module. The iOS app can show a marker's chart with the timeline switched off. (#1342) +- Resting energy from Apple Health is stored as a metric of its own, + `BASAL_ENERGY_BURNED` in kcal, next to active energy and never added to + it. It used to be skipped. It is summed per day, charted, listed with your + measurements, included in backups and exports, and readable over MCP. It + feeds no score and no insight. (#1340) ### Fixed diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index a83cd903e..0e0af558f 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -4610,6 +4610,7 @@ paths: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED - in: query name: from schema: @@ -5531,6 +5532,7 @@ paths: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED description: The metric. required: true description: The metric. @@ -5660,6 +5662,7 @@ paths: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED description: The metric. required: true description: The metric. @@ -28533,7 +28536,7 @@ components: type: number const: 2 leaves: - maxItems: 110 + maxItems: 111 type: array items: type: string @@ -28595,7 +28598,7 @@ components: type: number const: 2 leaves: - maxItems: 110 + maxItems: 111 type: array items: type: string @@ -29474,6 +29477,7 @@ components: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED value: type: number measuredAt: @@ -31207,6 +31211,7 @@ components: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED biomarkerId: type: string minLength: 1 @@ -31706,7 +31711,7 @@ components: type: number const: 2 leaves: - maxItems: 110 + maxItems: 111 type: array items: type: string @@ -32908,6 +32913,7 @@ components: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED description: DB-stored measurement category. v1.4.23 added 7 Apple Health values (HRV, resting HR, active energy, flights, walking/running distance, VO2 max, body temperature). InsightsSettingsPutRequest: @@ -42213,6 +42219,7 @@ components: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED value: type: number valueMin: @@ -42980,6 +42987,7 @@ components: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED - type: "null" biomarkerName: anyOf: @@ -45577,6 +45585,7 @@ components: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED dateKey: type: string pattern: ^\d{4}-\d{2}-\d{2}$ @@ -46776,6 +46785,7 @@ components: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED - type: "null" intervalDays: anyOf: @@ -47001,6 +47011,7 @@ components: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED metricSlot: type: - string @@ -53312,6 +53323,7 @@ components: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED date: type: string format: date @@ -53591,6 +53603,7 @@ components: - STRESS_LEVEL - TRAINING_READINESS - ACUTE_TRAINING_LOAD + - BASAL_ENERGY_BURNED from: type: string format: date diff --git a/docs/integrations/data-import.md b/docs/integrations/data-import.md index af378a9d3..65d4c11b7 100644 --- a/docs/integrations/data-import.md +++ b/docs/integrations/data-import.md @@ -194,6 +194,7 @@ hand-authored. | `WALKING_ASYMMETRY` | percent (0–100) | | `WALKING_DOUBLE_SUPPORT` | percent (0–100) | | `ACTIVE_ENERGY_BURNED` | kcal | +| `BASAL_ENERGY_BURNED` | kcal | | `ENERGY_EXPENDITURE_KJ` | kJ | | `VO2_MAX` | mL/(kg·min) | | `PULSE_WAVE_VELOCITY` | m/s | diff --git a/messages/de.json b/messages/de.json index 679099eef..27a85f03f 100644 --- a/messages/de.json +++ b/messages/de.json @@ -819,6 +819,7 @@ "typeHeartRateVariability": "Herzfrequenzvariabilität", "typeRestingHeartRate": "Ruheherzfrequenz", "typeActiveEnergyBurned": "Aktive Energie", + "typeBasalEnergyBurned": "Ruheenergie", "typeFlightsClimbed": "Treppen", "typeWalkingRunningDistance": "Geh- und Laufstrecke", "typeVo2Max": "VO₂max", diff --git a/messages/en.json b/messages/en.json index 9b216d13c..db0a99616 100644 --- a/messages/en.json +++ b/messages/en.json @@ -819,6 +819,7 @@ "typeHeartRateVariability": "Heart-rate variability", "typeRestingHeartRate": "Resting heart rate", "typeActiveEnergyBurned": "Active energy", + "typeBasalEnergyBurned": "Resting energy", "typeFlightsClimbed": "Flights climbed", "typeWalkingRunningDistance": "Walking + running distance", "typeVo2Max": "VO₂ max", diff --git a/messages/es.json b/messages/es.json index 11642bb05..fa997d617 100644 --- a/messages/es.json +++ b/messages/es.json @@ -819,6 +819,7 @@ "typeHeartRateVariability": "Variabilidad de la frecuencia cardiaca", "typeRestingHeartRate": "Frecuencia cardíaca en reposo", "typeActiveEnergyBurned": "Energía activa", + "typeBasalEnergyBurned": "Energía en reposo", "typeFlightsClimbed": "Escaleras", "typeWalkingRunningDistance": "Distancia a pie y corriendo", "typeVo2Max": "VO₂ máx", diff --git a/messages/fr.json b/messages/fr.json index e1fc6d686..f6dfc8411 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -819,6 +819,7 @@ "typeHeartRateVariability": "Variabilité de la fréquence cardiaque", "typeRestingHeartRate": "Fréquence cardiaque au repos", "typeActiveEnergyBurned": "Énergie active", + "typeBasalEnergyBurned": "Énergie au repos", "typeFlightsClimbed": "Étages", "typeWalkingRunningDistance": "Distance à pied et en course", "typeVo2Max": "VO₂ max", diff --git a/messages/it.json b/messages/it.json index 0bc50c2c7..3eaba8330 100644 --- a/messages/it.json +++ b/messages/it.json @@ -819,6 +819,7 @@ "typeHeartRateVariability": "Variabilità della frequenza cardiaca", "typeRestingHeartRate": "Frequenza cardiaca a riposo", "typeActiveEnergyBurned": "Energia attiva", + "typeBasalEnergyBurned": "Energia a riposo", "typeFlightsClimbed": "Piani saliti", "typeWalkingRunningDistance": "Distanza a piedi e di corsa", "typeVo2Max": "VO₂ max", diff --git a/messages/ko.json b/messages/ko.json index 665a02dd8..3c9c8da33 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -819,6 +819,7 @@ "typeHeartRateVariability": "심박변이도", "typeRestingHeartRate": "안정 시 심박수", "typeActiveEnergyBurned": "활동 에너지", + "typeBasalEnergyBurned": "휴식 에너지", "typeFlightsClimbed": "오른 층수", "typeWalkingRunningDistance": "걷기 + 달리기 거리", "typeVo2Max": "VO₂ max", diff --git a/messages/nl.json b/messages/nl.json index c029834f3..2baaa034f 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -819,6 +819,7 @@ "typeHeartRateVariability": "Hartslagvariabiliteit", "typeRestingHeartRate": "Rusthartslag", "typeActiveEnergyBurned": "Actieve energie", + "typeBasalEnergyBurned": "Rustenergie", "typeFlightsClimbed": "Beklommen verdiepingen", "typeWalkingRunningDistance": "Afstand wandelen + hardlopen", "typeVo2Max": "VO₂max", diff --git a/messages/pl.json b/messages/pl.json index 1bd886c92..ca998a9eb 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -819,6 +819,7 @@ "typeHeartRateVariability": "Zmienność rytmu serca", "typeRestingHeartRate": "Tętno spoczynkowe", "typeActiveEnergyBurned": "Aktywna energia", + "typeBasalEnergyBurned": "Energia spoczynkowa", "typeFlightsClimbed": "Pokonane piętra", "typeWalkingRunningDistance": "Dystans marszu i biegu", "typeVo2Max": "VO₂ max", diff --git a/messages/pt-BR.json b/messages/pt-BR.json index 7b673743d..2c0220883 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -819,6 +819,7 @@ "typeHeartRateVariability": "Variabilidade da frequência cardíaca", "typeRestingHeartRate": "Frequência cardíaca em repouso", "typeActiveEnergyBurned": "Energia ativa", + "typeBasalEnergyBurned": "Energia em repouso", "typeFlightsClimbed": "Andares subidos", "typeWalkingRunningDistance": "Distância caminhada + corrida", "typeVo2Max": "VO2 máx", diff --git a/messages/ru.json b/messages/ru.json index 8a0760e91..8e1669b48 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -819,6 +819,7 @@ "typeHeartRateVariability": "Вариабельность сердечного ритма", "typeRestingHeartRate": "ЧСС в покое", "typeActiveEnergyBurned": "Активная энергия", + "typeBasalEnergyBurned": "Энергия в покое", "typeFlightsClimbed": "Этажи", "typeWalkingRunningDistance": "Расстояние ходьбы + бега", "typeVo2Max": "VO₂ max", diff --git a/prisma/migrations/0493_basal_energy_burned/migration.sql b/prisma/migrations/0493_basal_energy_burned/migration.sql new file mode 100644 index 000000000..93b642ba3 --- /dev/null +++ b/prisma/migrations/0493_basal_energy_burned/migration.sql @@ -0,0 +1,10 @@ +-- Extend `measurement_type` with resting energy, which sat in the deferred +-- list until now: +-- +-- BASAL_ENERGY_BURNED <- HKQuantityTypeIdentifierBasalEnergyBurned (kcal) +-- +-- Purely additive: no existing row carries the value. IF NOT EXISTS keeps a +-- replay on a partially applied database harmless. Enum values cannot be +-- dropped in PostgreSQL, so there is no down path. + +ALTER TYPE "measurement_type" ADD VALUE IF NOT EXISTS 'BASAL_ENERGY_BURNED'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 950f86b99..de35a4906 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1807,6 +1807,9 @@ enum MeasurementType { STRESS_LEVEL // score (0–100) — Garmin's all-day average stress level. Device-native and kept apart from the COMPUTED STRESS_SCORE, whose readers are single-source. TRAINING_READINESS // score (0–100) — Garmin Training Readiness. Not in the Personal API; written by a bridge through the ingest token (source EXTERNAL). ACUTE_TRAINING_LOAD // load (unit-less) — Garmin acute training load. Not in the Personal API; written by a bridge through the ingest token (source EXTERNAL). + // v1.47.1 — resting energy (#1340). Its own type beside ACTIVE_ENERGY_BURNED, + // never folded into it and never summed with it into a total. + BASAL_ENERGY_BURNED // kcal — HealthKit `basalEnergyBurned`, the energy the body uses at rest; cumulative, daily SUM. @@map("measurement_type") } diff --git a/src/__tests__/doctor-report-control-gating-guard.test.ts b/src/__tests__/doctor-report-control-gating-guard.test.ts index 0845b7580..52bf07926 100644 --- a/src/__tests__/doctor-report-control-gating-guard.test.ts +++ b/src/__tests__/doctor-report-control-gating-guard.test.ts @@ -70,8 +70,8 @@ describe("report scope gating — structural guard", () => { it("reads a plausible catalogue", () => { // Sanity floor: a silently degraded catalogue would pass everything below // vacuously. - expect(ALL_LEAF_IDS.length).toBe(110); - expect(Object.keys(MEASUREMENT_LEAF_GROUP)).toHaveLength(90); + expect(ALL_LEAF_IDS.length).toBe(111); + expect(Object.keys(MEASUREMENT_LEAF_GROUP)).toHaveLength(91); expect(Object.keys(STRUCTURED_LEAF_GROUP)).toHaveLength(20); }); diff --git a/src/components/day/use-day-value-format.ts b/src/components/day/use-day-value-format.ts index 4600ce951..87e53ab95 100644 --- a/src/components/day/use-day-value-format.ts +++ b/src/components/day/use-day-value-format.ts @@ -17,6 +17,7 @@ const WHOLE_NUMBER_TYPES: ReadonlySet = new Set([ "ACTIVITY_STEPS", "FLIGHTS_CLIMBED", "ACTIVE_ENERGY_BURNED", + "BASAL_ENERGY_BURNED", "TIME_IN_DAYLIGHT", "BLOOD_PRESSURE_SYS", "BLOOD_PRESSURE_DIA", diff --git a/src/components/measurements/measurement-list-meta.ts b/src/components/measurements/measurement-list-meta.ts index c42d2733a..979843697 100644 --- a/src/components/measurements/measurement-list-meta.ts +++ b/src/components/measurements/measurement-list-meta.ts @@ -55,6 +55,7 @@ export const MEASUREMENT_TYPE_LABEL_KEYS: Record = { HEART_RATE_VARIABILITY: "measurements.typeHeartRateVariability", RESTING_HEART_RATE: "measurements.typeRestingHeartRate", ACTIVE_ENERGY_BURNED: "measurements.typeActiveEnergyBurned", + BASAL_ENERGY_BURNED: "measurements.typeBasalEnergyBurned", FLIGHTS_CLIMBED: "measurements.typeFlightsClimbed", WALKING_RUNNING_DISTANCE: "measurements.typeWalkingRunningDistance", VO2_MAX: "measurements.typeVo2Max", @@ -165,6 +166,7 @@ export const MEASUREMENT_TYPE_ICONS: Record = { HEART_RATE_VARIABILITY: HeartPulse, RESTING_HEART_RATE: Heart, ACTIVE_ENERGY_BURNED: Flame, + BASAL_ENERGY_BURNED: Flame, FLIGHTS_CLIMBED: TrendingUp, WALKING_RUNNING_DISTANCE: Footprints, VO2_MAX: Gauge, diff --git a/src/components/measurements/measurement-type-colors.ts b/src/components/measurements/measurement-type-colors.ts index cf0bdd8a1..cd317846c 100644 --- a/src/components/measurements/measurement-type-colors.ts +++ b/src/components/measurements/measurement-type-colors.ts @@ -20,6 +20,7 @@ export const MEASUREMENT_TYPE_COLORS: Record = { HEART_RATE_VARIABILITY: "bg-chart-5/10 text-chart-5 dark:bg-chart-5/20", RESTING_HEART_RATE: "bg-chart-3/10 text-chart-3 dark:bg-chart-3/20", ACTIVE_ENERGY_BURNED: "bg-chart-4/10 text-chart-4 dark:bg-chart-4/20", + BASAL_ENERGY_BURNED: "bg-chart-4/10 text-chart-4 dark:bg-chart-4/20", FLIGHTS_CLIMBED: "bg-chart-2/10 text-chart-2 dark:bg-chart-2/20", WALKING_RUNNING_DISTANCE: "bg-chart-2/10 text-chart-2 dark:bg-chart-2/20", VO2_MAX: "bg-chart-1/10 text-chart-1 dark:bg-chart-1/20", diff --git a/src/components/settings/__tests__/report-selection-panel.test.tsx b/src/components/settings/__tests__/report-selection-panel.test.tsx index a91463cec..d9eb39e9f 100644 --- a/src/components/settings/__tests__/report-selection-panel.test.tsx +++ b/src/components/settings/__tests__/report-selection-panel.test.tsx @@ -195,7 +195,7 @@ describe(" — the first run", () => { // ALLERGIES, ILLNESS_EPISODES, VISITS, SURGICAL_HISTORY, IMMUNIZATIONS. expect(groupChip(html, "history")).toBe("0/5"); expect(groupChip(html, "cardio")).toBe("0/16"); - expect(groupChip(html, "activity")).toBe("0/12"); + expect(groupChip(html, "activity")).toBe("0/13"); expect(groupChip(html, "sleepRecovery")).toBe("0/19"); expect(groupChip(html, "mobility")).toBe("0/11"); expect(groupChip(html, "environment")).toBe("0/7"); diff --git a/src/lib/__tests__/measurement-type-enum-coverage.test.ts b/src/lib/__tests__/measurement-type-enum-coverage.test.ts index e219696ce..f65401533 100644 --- a/src/lib/__tests__/measurement-type-enum-coverage.test.ts +++ b/src/lib/__tests__/measurement-type-enum-coverage.test.ts @@ -129,10 +129,12 @@ const EXPECTED_TYPES = [ "STRESS_LEVEL", "TRAINING_READINESS", "ACUTE_TRAINING_LOAD", + // ── resting energy (#1340) ── + "BASAL_ENERGY_BURNED", ] as const; describe("measurementTypeEnum coverage", () => { - it("exposes the 86 canonical measurement types", () => { + it("exposes the 91 canonical measurement types", () => { expect([...measurementTypeEnum.options].sort()).toEqual( [...EXPECTED_TYPES].sort(), ); diff --git a/src/lib/import/health-connect/mapper.ts b/src/lib/import/health-connect/mapper.ts index 8334942ca..05a218d55 100644 --- a/src/lib/import/health-connect/mapper.ts +++ b/src/lib/import/health-connect/mapper.ts @@ -254,6 +254,14 @@ export interface DailySumSpec { factor: number; } +/* + * `basal_metabolic_rate_record_table` is deliberately not summed here. A + * BasalMetabolicRateRecord is a rate (power) at one instant, written when an + * app recomputes it, not an interval of energy spent; summing it per day would + * count every recomputation as another day of resting energy, and spreading + * one rate over 24 hours would invent a figure the record does not hold. + * BASAL_ENERGY_BURNED stays an Apple Health day total. + */ export const DAILY_SUM_SPECS: readonly DailySumSpec[] = [ { table: "steps_record_table", diff --git a/src/lib/insights/chart-tokens.ts b/src/lib/insights/chart-tokens.ts index 5b28c6a11..f35d6d054 100644 --- a/src/lib/insights/chart-tokens.ts +++ b/src/lib/insights/chart-tokens.ts @@ -39,6 +39,7 @@ export const ALLOWED_CHART_TOKENS = [ "metric:HEART_RATE_VARIABILITY", "metric:RESTING_HEART_RATE", "metric:ACTIVE_ENERGY_BURNED", + "metric:BASAL_ENERGY_BURNED", "metric:FLIGHTS_CLIMBED", "metric:WALKING_RUNNING_DISTANCE", "metric:VO2_MAX", diff --git a/src/lib/measurements/__tests__/apple-health-mapping.test.ts b/src/lib/measurements/__tests__/apple-health-mapping.test.ts index 4107d841b..5e858fd51 100644 --- a/src/lib/measurements/__tests__/apple-health-mapping.test.ts +++ b/src/lib/measurements/__tests__/apple-health-mapping.test.ts @@ -783,6 +783,8 @@ describe("dailyStatsExternalId (v1.4.30 — R-A Option A handoff lock)", () => { [ "HKQuantityTypeIdentifierStepCount", "HKQuantityTypeIdentifierActiveEnergyBurned", + // Resting energy (#1340). + "HKQuantityTypeIdentifierBasalEnergyBurned", "HKQuantityTypeIdentifierFlightsClimbed", "HKQuantityTypeIdentifierDistanceWalkingRunning", "HKQuantityTypeIdentifierTimeInDaylight", diff --git a/src/lib/measurements/__tests__/healthkit-accepted-types.test.ts b/src/lib/measurements/__tests__/healthkit-accepted-types.test.ts index dce617d84..a24ccf39c 100644 --- a/src/lib/measurements/__tests__/healthkit-accepted-types.test.ts +++ b/src/lib/measurements/__tests__/healthkit-accepted-types.test.ts @@ -22,7 +22,9 @@ describe("acceptedHealthKitTypes", () => { expect(HK_QUANTITY_TYPE_DEFERRED.has(id), id).toBe(false); } expect(accepted).not.toContain("HKQuantityTypeIdentifierHeight"); - expect(accepted).not.toContain("HKQuantityTypeIdentifierBasalEnergyBurned"); + // Resting energy (#1340) left the deferred set for a type of + // its own, so the client is told the server takes it. + expect(accepted).toContain("HKQuantityTypeIdentifierBasalEnergyBurned"); }); it("lists only identifiers the batch mapper turns into a row", () => { diff --git a/src/lib/measurements/__tests__/import-apple-health-export.test.ts b/src/lib/measurements/__tests__/import-apple-health-export.test.ts index 5c994f5f2..2ceb05fc1 100644 --- a/src/lib/measurements/__tests__/import-apple-health-export.test.ts +++ b/src/lib/measurements/__tests__/import-apple-health-export.test.ts @@ -687,8 +687,8 @@ async function importCumulativeFixture(xml: string) { } describe("streamParseExportXml — cumulative source-day estimates", () => { - it("covers exactly the ten cumulative HealthKit measurement types", () => { - expect(CUMULATIVE_HK_TYPES.size).toBe(10); + it("covers exactly the eleven cumulative HealthKit measurement types", () => { + expect(CUMULATIVE_HK_TYPES.size).toBe(11); expect( new Set(allCumulativeMappings.map((mapping) => mapping.measurementType)), ).toEqual(CUMULATIVE_HK_TYPES); diff --git a/src/lib/measurements/apple-health-mapping.ts b/src/lib/measurements/apple-health-mapping.ts index 6e8c9cf78..02a7931a0 100644 --- a/src/lib/measurements/apple-health-mapping.ts +++ b/src/lib/measurements/apple-health-mapping.ts @@ -380,6 +380,17 @@ export const APPLE_HEALTH_TYPE_MAP: Record = { convertToDbUnit: (v) => v, aggregation: "sum", }, + // Resting energy (#1340). A type of its own, never folded into + // ACTIVE_ENERGY_BURNED and never added to it: the two are different + // quantities, and no total is derived from them. + HKQuantityTypeIdentifierBasalEnergyBurned: { + hkIdentifier: "HKQuantityTypeIdentifierBasalEnergyBurned", + measurementType: "BASAL_ENERGY_BURNED", + hkUnit: "kcal", + dbUnit: "kcal", + convertToDbUnit: (v) => v, + aggregation: "sum", + }, HKQuantityTypeIdentifierFlightsClimbed: { hkIdentifier: "HKQuantityTypeIdentifierFlightsClimbed", measurementType: "FLIGHTS_CLIMBED", @@ -894,6 +905,8 @@ export const CUMULATIVE_HK_TYPES: ReadonlySet = new Set([ "ACTIVITY_STEPS", "ACTIVE_ENERGY_BURNED", + // Resting energy (#1340) accrues through the day the same way. + "BASAL_ENERGY_BURNED", "FLIGHTS_CLIMBED", "WALKING_RUNNING_DISTANCE", "TIME_IN_DAYLIGHT", @@ -1183,7 +1196,8 @@ export const HK_QUANTITY_TYPE_DEFERRED = new Set([ "HKQuantityTypeIdentifierHeight", // already on User.heightCm // v1.10.0 — `HeartRateRecoveryOneMinute` + `AppleSleepingWristTemperature` // moved into the mapping table (CARDIO_RECOVERY + WRIST_TEMPERATURE). - "HKQuantityTypeIdentifierBasalEnergyBurned", + // `BasalEnergyBurned` moved into the mapping table as BASAL_ENERGY_BURNED + // (#1340). // Running / walking form — v1.5+ // v1.5.5 — `WalkingStepLength` + `WalkingSpeed` moved into the // mapping table (raw SI on the wire — metres and m/s respectively). diff --git a/src/lib/measurements/categories.ts b/src/lib/measurements/categories.ts index 437442d26..1855bf5f3 100644 --- a/src/lib/measurements/categories.ts +++ b/src/lib/measurements/categories.ts @@ -89,6 +89,7 @@ export const MEASUREMENT_CATEGORIES: ReadonlyMap< // ── Activity (cumulative + fitness + mobility) ── ["ACTIVITY_STEPS", "activity"], ["ACTIVE_ENERGY_BURNED", "activity"], + ["BASAL_ENERGY_BURNED", "activity"], ["FLIGHTS_CLIMBED", "activity"], ["WALKING_RUNNING_DISTANCE", "activity"], ["VO2_MAX", "activity"], diff --git a/src/lib/measurements/display-transform.ts b/src/lib/measurements/display-transform.ts index c5066b275..891eeffaa 100644 --- a/src/lib/measurements/display-transform.ts +++ b/src/lib/measurements/display-transform.ts @@ -334,6 +334,7 @@ const INTEGER_DISPLAY_TYPES: ReadonlySet = new Set([ // kcal + daylight minutes — whole units are the meaningful display // grain for these day-cumulative counts. "ACTIVE_ENERGY_BURNED", + "BASAL_ENERGY_BURNED", "TIME_IN_DAYLIGHT", // v1.44 — the minute rings, mindful minutes and stood hours. "EXERCISE_MINUTES", diff --git a/src/lib/measurements/type-label-keys.ts b/src/lib/measurements/type-label-keys.ts index b18aa4084..b8d7315e8 100644 --- a/src/lib/measurements/type-label-keys.ts +++ b/src/lib/measurements/type-label-keys.ts @@ -29,6 +29,7 @@ export const MEASUREMENT_TYPE_LABEL_KEYS: Record = { HEART_RATE_VARIABILITY: "measurements.typeHeartRateVariability", RESTING_HEART_RATE: "measurements.typeRestingHeartRate", ACTIVE_ENERGY_BURNED: "measurements.typeActiveEnergyBurned", + BASAL_ENERGY_BURNED: "measurements.typeBasalEnergyBurned", FLIGHTS_CLIMBED: "measurements.typeFlightsClimbed", WALKING_RUNNING_DISTANCE: "measurements.typeWalkingRunningDistance", VO2_MAX: "measurements.typeVo2Max", diff --git a/src/lib/measurements/unit-map.ts b/src/lib/measurements/unit-map.ts index f1ecfa3f1..a9eef07f9 100644 --- a/src/lib/measurements/unit-map.ts +++ b/src/lib/measurements/unit-map.ts @@ -28,6 +28,7 @@ const unitMap: Record = { HEART_RATE_VARIABILITY: "ms", RESTING_HEART_RATE: "bpm", ACTIVE_ENERGY_BURNED: "kcal", + BASAL_ENERGY_BURNED: "kcal", FLIGHTS_CLIMBED: "flights", WALKING_RUNNING_DISTANCE: "m", VO2_MAX: "mL/(kg·min)", diff --git a/src/lib/measurements/value-ranges.ts b/src/lib/measurements/value-ranges.ts index 1bedd4b78..f03dd6b96 100644 --- a/src/lib/measurements/value-ranges.ts +++ b/src/lib/measurements/value-ranges.ts @@ -38,6 +38,9 @@ export const VALUE_RANGES: Record = { // Active energy kcal/sample — a daily-rollup sample tops out around // 6–8000 kcal even for ultra-endurance days. ACTIVE_ENERGY_BURNED: { min: 0, max: 10000 }, + // Resting energy kcal/sample — a whole day at rest sits around 1200–3000 + // kcal; the same ceiling as active energy leaves room for a large body. + BASAL_ENERGY_BURNED: { min: 0, max: 10000 }, // Flights — vertical-feet/3 ≈ flights; cap generous. FLIGHTS_CLIMBED: { min: 0, max: 1000 }, // Walking/running distance per sample (metres). 200 km covers an diff --git a/src/lib/modules/measurement-scope.ts b/src/lib/modules/measurement-scope.ts index eef50035c..689b00085 100644 --- a/src/lib/modules/measurement-scope.ts +++ b/src/lib/modules/measurement-scope.ts @@ -166,6 +166,7 @@ export const UNSCOPED_REVIEWED_TYPES: ReadonlySet = "WALKING_RUNNING_DISTANCE", "FLIGHTS_CLIMBED", "ACTIVE_ENERGY_BURNED", + "BASAL_ENERGY_BURNED", "ENERGY_EXPENDITURE_KJ", // v1.44 — the exercise and stand rings are ambient movement like steps, // not workout sessions. diff --git a/src/lib/personal-records/pr-direction.ts b/src/lib/personal-records/pr-direction.ts index c8bce2edf..0281e5f2c 100644 --- a/src/lib/personal-records/pr-direction.ts +++ b/src/lib/personal-records/pr-direction.ts @@ -185,10 +185,10 @@ export function getPRDirection( case "OXYGEN_SATURATION": case "SLEEP_DURATION": case "FAT_FREE_MASS": - // v1.4.30 — audio-exposure events fire on a threshold-cross, not - // on a goal axis. A "record" for "fewest loud-listening days in - // a row" is interesting but better surfaced as a streak than a - // PR direction. Defer. + // Resting energy follows body size, not effort. + case "BASAL_ENERGY_BURNED": + // v1.4.30 — audio-exposure events fire on a threshold-cross, not on a + // goal axis; a "fewest loud days" record reads better as a streak. case "AUDIO_EXPOSURE_EVENT": // v1.5.5 — homeostatic / display-only metrics. Respiratory rate // is goal-neutral (homeostatic) and walking HR average is fitness diff --git a/src/lib/report-selection/__tests__/catalogue-guard.test.ts b/src/lib/report-selection/__tests__/catalogue-guard.test.ts index b2e716604..b46c8b72b 100644 --- a/src/lib/report-selection/__tests__/catalogue-guard.test.ts +++ b/src/lib/report-selection/__tests__/catalogue-guard.test.ts @@ -63,9 +63,9 @@ function resolves(key: string): boolean { describe("report selection catalogue", () => { it("carries every measurement type and every structured leaf", () => { - expect(MEASUREMENT_LEAF_IDS).toHaveLength(90); + expect(MEASUREMENT_LEAF_IDS).toHaveLength(91); expect(STRUCTURED_LEAF_IDS).toHaveLength(20); - expect(ALL_LEAF_IDS).toHaveLength(110); + expect(ALL_LEAF_IDS).toHaveLength(111); }); it("resolves a label for every leaf", () => { diff --git a/src/lib/report-selection/catalogue.ts b/src/lib/report-selection/catalogue.ts index c7bc9bca9..69e8c001b 100644 --- a/src/lib/report-selection/catalogue.ts +++ b/src/lib/report-selection/catalogue.ts @@ -164,6 +164,7 @@ export const MEASUREMENT_LEAF_GROUP: Record = { WALKING_RUNNING_DISTANCE: "activity", FLIGHTS_CLIMBED: "activity", ACTIVE_ENERGY_BURNED: "activity", + BASAL_ENERGY_BURNED: "activity", ENERGY_EXPENDITURE_KJ: "activity", STRAIN_SCORE: "activity", DAY_STRAIN: "activity", @@ -286,6 +287,7 @@ export const LEAF_MODULE: Partial> = { // Activity / movement series and the training-load scores. ACTIVITY_STEPS: "workouts", ACTIVE_ENERGY_BURNED: "workouts", + BASAL_ENERGY_BURNED: "workouts", EXERCISE_MINUTES: "workouts", STAND_MINUTES: "workouts", STAND_HOURS: "workouts", diff --git a/src/lib/signals/__tests__/fixtures/expected-measurement-loinc.json b/src/lib/signals/__tests__/fixtures/expected-measurement-loinc.json index 1c1051344..6a09e748b 100644 --- a/src/lib/signals/__tests__/fixtures/expected-measurement-loinc.json +++ b/src/lib/signals/__tests__/fixtures/expected-measurement-loinc.json @@ -83,6 +83,12 @@ "unit": "kcal", "category": "activity" }, + "BASAL_ENERGY_BURNED": { + "loinc": "HKQuantityTypeIdentifierBasalEnergyBurned", + "display": "Resting energy", + "unit": "kcal", + "category": "activity" + }, "WALKING_SPEED": { "loinc": "HKQuantityTypeIdentifierWalkingSpeed", "display": "Walking speed", diff --git a/src/lib/signals/registry.ts b/src/lib/signals/registry.ts index 4725626a2..d807b5c25 100644 --- a/src/lib/signals/registry.ts +++ b/src/lib/signals/registry.ts @@ -520,6 +520,30 @@ export const SIGNALS: Record = { category: "activity", }, }, + // Resting energy (#1340). Stored, charted and exported; read + // through MCP like active energy, but kept off the Coach snapshot and the + // correlations, and no score or total is built from it. + BASAL_ENERGY: { + key: "BASAL_ENERGY", + kind: "measurement", + source: { measurementType: "BASAL_ENERGY_BURNED" }, + displayName: "Resting energy", + unit: "kcal/day", + direction: "target-band", + archetype: "activity-fitness", + surfaces: { + detailPage: false, + correlationEligible: false, + coachSnapshot: false, + mcp: true, + }, + fhir: { + loinc: "HKQuantityTypeIdentifierBasalEnergyBurned", + display: "Resting energy", + unit: "kcal", + category: "activity", + }, + }, FLIGHTS_CLIMBED: { key: "FLIGHTS_CLIMBED", kind: "measurement", diff --git a/src/lib/validations/measurement.ts b/src/lib/validations/measurement.ts index 704fe1a7b..df2b1b383 100644 --- a/src/lib/validations/measurement.ts +++ b/src/lib/validations/measurement.ts @@ -169,6 +169,8 @@ export const measurementTypeEnum = z.enum([ // them through the ingest token (`measurements:write`, source EXTERNAL). "TRAINING_READINESS", "ACUTE_TRAINING_LOAD", + // ── resting energy (#1340), HealthKit `basalEnergyBurned` ── + "BASAL_ENERGY_BURNED", ]); /** diff --git a/tests/integration/__golden__/backup-parity/refusals.json b/tests/integration/__golden__/backup-parity/refusals.json index 90dcf66df..356704fd6 100644 --- a/tests/integration/__golden__/backup-parity/refusals.json +++ b/tests/integration/__golden__/backup-parity/refusals.json @@ -68,7 +68,7 @@ "backupId": "", "ownerId": "round-trip-owner", "reason": "schema_invalid", - "message": "measurements.0.type: Invalid option: expected one of \"WEIGHT\"|\"BLOOD_PRESSURE_SYS\"|\"BLOOD_PRESSURE_DIA\"|\"PULSE\"|\"BODY_FAT\"|\"SLEEP_DURATION\"|\"ACTIVITY_STEPS\"|\"BLOOD_GLUCOSE\"|\"TOTAL_BODY_WATER\"|\"BONE_MASS\"|\"OXYGEN_SATURATION\"|\"HEART_RATE_VARIABILITY\"|\"RESTING_HEART_RATE\"|\"ACTIVE_ENERGY_BURNED\"|\"FLIGHTS_CLIMBED\"|\"WALKING_RUNNING_DISTANCE\"|\"VO2_MAX\"|\"BODY_TEMPERATURE\"|\"FAT_FREE_MASS\"|\"FAT_MASS\"|\"MUSCLE_MASS\"|\"SKIN_TEMPERATURE\"|\"PULSE_WAVE_VELOCITY\"|\"VASCULAR_AGE\"|\"VISCERAL_FAT\"|\"AUDIO_EXPOSURE_ENV\"|\"AUDIO_EXPOSURE_HEADPHONE\"|\"TIME_IN_DAYLIGHT\"|\"WALKING_STEADINESS\"|\"AUDIO_EXPOSURE_EVENT\"|\"RESPIRATORY_RATE\"|\"BODY_MASS_INDEX\"|\"LEAN_BODY_MASS\"|\"WALKING_HEART_RATE_AVERAGE\"|\"WALKING_ASYMMETRY\"|\"WALKING_DOUBLE_SUPPORT\"|\"WALKING_STEP_LENGTH\"|\"WALKING_SPEED\"|\"CARDIO_RECOVERY\"|\"WRIST_TEMPERATURE\"|\"FALL_COUNT\"|\"SIX_MINUTE_WALK_DISTANCE\"|\"STAIR_ASCENT_SPEED\"|\"STAIR_DESCENT_SPEED\"|\"BREATHING_DISTURBANCES\"|\"IRREGULAR_RHYTHM_NOTIFICATION\"|\"HIGH_HEART_RATE_EVENT\"|\"LOW_HEART_RATE_EVENT\"|\"WALKING_STEADINESS_EVENT\"|\"BREATHING_DISTURBANCE_EVENT\"|\"RECOVERY_SCORE\"|\"STRESS_SCORE\"|\"STRAIN_SCORE\"|\"HRV_RMSSD\"|\"DAY_STRAIN\"|\"WORKOUT_STRAIN\"|\"SLEEP_PERFORMANCE\"|\"SLEEP_EFFICIENCY\"|\"SLEEP_CONSISTENCY\"|\"SLEEP_NEED\"|\"ENERGY_EXPENDITURE_KJ\"|\"AVERAGE_HEART_RATE\"|\"MAX_HEART_RATE\"|\"SLEEP_DISTURBANCE_COUNT\"|\"ANS_CHARGE\"|\"CARDIO_LOAD\"|\"SLEEP_SCORE\"|\"BODY_TEMPERATURE_DEVIATION\"|\"RESILIENCE\"|\"PHQ9_SCORE\"|\"GAD7_SCORE\"|\"GRIP_STRENGTH\"|\"PAIN_NRS\"|\"WAIST_CIRCUMFERENCE\"|\"WAIST_TO_HEIGHT\"|\"WHO5_SCORE\"|\"SCI_SCORE\"|\"EXERCISE_MINUTES\"|\"STAND_MINUTES\"|\"STAND_HOURS\"|\"MINDFUL_MINUTES\"|\"STATE_OF_MIND_VALENCE\"|\"INDOOR_CO2\"|\"INDOOR_TEMPERATURE\"|\"INDOOR_HUMIDITY\"|\"AFIB_BURDEN\"|\"BODY_BATTERY\"|\"STRESS_LEVEL\"|\"TRAINING_READINESS\"|\"ACUTE_TRAINING_LOAD\"" + "message": "measurements.0.type: Invalid option: expected one of \"WEIGHT\"|\"BLOOD_PRESSURE_SYS\"|\"BLOOD_PRESSURE_DIA\"|\"PULSE\"|\"BODY_FAT\"|\"SLEEP_DURATION\"|\"ACTIVITY_STEPS\"|\"BLOOD_GLUCOSE\"|\"TOTAL_BODY_WATER\"|\"BONE_MASS\"|\"OXYGEN_SATURATION\"|\"HEART_RATE_VARIABILITY\"|\"RESTING_HEART_RATE\"|\"ACTIVE_ENERGY_BURNED\"|\"FLIGHTS_CLIMBED\"|\"WALKING_RUNNING_DISTANCE\"|\"VO2_MAX\"|\"BODY_TEMPERATURE\"|\"FAT_FREE_MASS\"|\"FAT_MASS\"|\"MUSCLE_MASS\"|\"SKIN_TEMPERATURE\"|\"PULSE_WAVE_VELOCITY\"|\"VASCULAR_AGE\"|\"VISCERAL_FAT\"|\"AUDIO_EXPOSURE_ENV\"|\"AUDIO_EXPOSURE_HEADPHONE\"|\"TIME_IN_DAYLIGHT\"|\"WALKING_STEADINESS\"|\"AUDIO_EXPOSURE_EVENT\"|\"RESPIRATORY_RATE\"|\"BODY_MASS_INDEX\"|\"LEAN_BODY_MASS\"|\"WALKING_HEART_RATE_AVERAGE\"|\"WALKING_ASYMMETRY\"|\"WALKING_DOUBLE_SUPPORT\"|\"WALKING_STEP_LENGTH\"|\"WALKING_SPEED\"|\"CARDIO_RECOVERY\"|\"WRIST_TEMPERATURE\"|\"FALL_COUNT\"|\"SIX_MINUTE_WALK_DISTANCE\"|\"STAIR_ASCENT_SPEED\"|\"STAIR_DESCENT_SPEED\"|\"BREATHING_DISTURBANCES\"|\"IRREGULAR_RHYTHM_NOTIFICATION\"|\"HIGH_HEART_RATE_EVENT\"|\"LOW_HEART_RATE_EVENT\"|\"WALKING_STEADINESS_EVENT\"|\"BREATHING_DISTURBANCE_EVENT\"|\"RECOVERY_SCORE\"|\"STRESS_SCORE\"|\"STRAIN_SCORE\"|\"HRV_RMSSD\"|\"DAY_STRAIN\"|\"WORKOUT_STRAIN\"|\"SLEEP_PERFORMANCE\"|\"SLEEP_EFFICIENCY\"|\"SLEEP_CONSISTENCY\"|\"SLEEP_NEED\"|\"ENERGY_EXPENDITURE_KJ\"|\"AVERAGE_HEART_RATE\"|\"MAX_HEART_RATE\"|\"SLEEP_DISTURBANCE_COUNT\"|\"ANS_CHARGE\"|\"CARDIO_LOAD\"|\"SLEEP_SCORE\"|\"BODY_TEMPERATURE_DEVIATION\"|\"RESILIENCE\"|\"PHQ9_SCORE\"|\"GAD7_SCORE\"|\"GRIP_STRENGTH\"|\"PAIN_NRS\"|\"WAIST_CIRCUMFERENCE\"|\"WAIST_TO_HEIGHT\"|\"WHO5_SCORE\"|\"SCI_SCORE\"|\"EXERCISE_MINUTES\"|\"STAND_MINUTES\"|\"STAND_HOURS\"|\"MINDFUL_MINUTES\"|\"STATE_OF_MIND_VALENCE\"|\"INDOOR_CO2\"|\"INDOOR_TEMPERATURE\"|\"INDOOR_HUMIDITY\"|\"AFIB_BURDEN\"|\"BODY_BATTERY\"|\"STRESS_LEVEL\"|\"TRAINING_READINESS\"|\"ACUTE_TRAINING_LOAD\"|\"BASAL_ENERGY_BURNED\"" } } ], diff --git a/tests/integration/basal-energy-ingest.test.ts b/tests/integration/basal-energy-ingest.test.ts new file mode 100644 index 000000000..ed1608faf --- /dev/null +++ b/tests/integration/basal-energy-ingest.test.ts @@ -0,0 +1,172 @@ +/** + * Resting energy from HealthKit (#1340), against real Postgres: + * `HKQuantityTypeIdentifierBasalEnergyBurned` used to sit in the deferred set + * and was refused per entry. It is a type of its own now. The batch route + * stores it as `BASAL_ENERGY_BURNED`, apart from active energy, the row comes + * back out of a full backup restore unchanged, and `GET + * /api/integrations/healthkit` tells the app the server takes it. + */ +import { NextRequest } from "next/server"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +process.env.ENCRYPTION_KEY = + "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +import { encrypt } from "@/lib/crypto"; +import { buildFullBackupPayload } from "@/lib/export/full-backup-payload"; +import { parseBackupPayload } from "@/lib/validations/backup"; +import { POST as RESTORE } from "./restore-job-driver"; + +import { cookieJar, headerJar } from "./mock-next-headers"; +import { getPrismaClient, truncateAllTables } from "./setup"; + +vi.mock("next/headers", async () => { + const { cookieJar, headerJar } = await import("./mock-next-headers"); + return { + headers: vi.fn(async () => ({ + get: (name: string) => headerJar.get(name.toLowerCase()) ?? null, + })), + cookies: vi.fn(async () => ({ + get: (name: string) => { + const value = cookieJar.get(name); + return value ? { name, value } : undefined; + }, + set: (name: string, value: string) => cookieJar.set(name, value), + delete: (name: string) => cookieJar.delete(name), + })), + }; +}); + +vi.mock("@/lib/cache/invalidate", async (importOriginal) => { + const actual = + await importOriginal(); + return { ...actual, invalidateUserData: vi.fn() }; +}); + +const BASAL = "HKQuantityTypeIdentifierBasalEnergyBurned"; + +beforeEach(async () => { + await truncateAllTables(getPrismaClient()); + cookieJar.clear(); + headerJar.clear(); +}); + +async function signIn(): Promise { + const prisma = getPrismaClient(); + const owner = await prisma.user.create({ + data: { + username: "basal-energy-owner", + email: "basal-energy-owner@example.test", + timezone: "UTC", + role: "ADMIN", + }, + }); + const session = await prisma.session.create({ + data: { userId: owner.id, expiresAt: new Date(Date.now() + 3_600_000) }, + }); + cookieJar.set("healthlog_session", session.id); + return owner.id; +} + +describe("resting energy ingest (#1340)", () => { + it("lists the identifier in acceptedTypes", async () => { + await signIn(); + const { GET } = await import("@/app/api/integrations/healthkit/route"); + const res = await GET(); + expect(res.status).toBe(200); + const body = (await res.json()) as { data: { acceptedTypes: string[] } }; + expect(body.data.acceptedTypes).toContain(BASAL); + }); + + it("stores a batch entry as BASAL_ENERGY_BURNED and restores it from a backup", async () => { + const userId = await signIn(); + const prisma = getPrismaClient(); + const { POST } = await import("@/app/api/measurements/batch/route"); + const at = "2026-10-01T10:00:00.000Z"; + const res = await POST( + new NextRequest("http://localhost/api/measurements/batch", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + entries: [ + { + hkIdentifier: BASAL, + value: 1650, + unit: "kcal", + startDate: at, + endDate: at, + externalId: `stats:${BASAL}:2026-10-01`, + }, + { + hkIdentifier: "HKQuantityTypeIdentifierActiveEnergyBurned", + value: 420, + unit: "kcal", + startDate: at, + endDate: at, + externalId: + "stats:HKQuantityTypeIdentifierActiveEnergyBurned:2026-10-01", + }, + ], + }), + }), + ); + expect(res.status).toBe(200); + const body = (await res.json()) as { + data: { entries: Array<{ status: string }> }; + }; + expect(body.data.entries.map((e) => e.status)).toEqual([ + "inserted", + "inserted", + ]); + + const select = { + type: true, + value: true, + unit: true, + source: true, + externalId: true, + measuredAt: true, + } as const; + const stored = await prisma.measurement.findMany({ + where: { userId, deletedAt: null }, + orderBy: { type: "asc" }, + select, + }); + expect( + stored.map(({ type, value, unit }) => ({ type, value, unit })), + ).toEqual([ + { type: "ACTIVE_ENERGY_BURNED", value: 420, unit: "kcal" }, + { type: "BASAL_ENERGY_BURNED", value: 1650, unit: "kcal" }, + ]); + + const { payload: built } = await buildFullBackupPayload(prisma, userId, { + purpose: "disaster-recovery", + }); + const parsed = parseBackupPayload(JSON.stringify(built)); + const backup = await prisma.dataBackup.create({ + data: { + userId, + type: "BASAL_ENERGY_ROUND_TRIP", + data: encrypt(JSON.stringify(parsed)), + }, + }); + await prisma.measurement.deleteMany({ where: { userId } }); + + const restore = await RESTORE( + new Request(`http://localhost/api/admin/backups/${backup.id}/restore`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ confirm: "RESTORE" }), + }) as unknown as Parameters[0], + { params: Promise.resolve({ id: backup.id }) }, + ); + expect(restore.status).toBe(200); + + const restored = await prisma.measurement.findMany({ + where: { userId, deletedAt: null }, + orderBy: { type: "asc" }, + select, + }); + expect(restored).toEqual(stored); + }); +}); diff --git a/tests/integration/routes/parity/__snapshots__/analytics.02-summaries-cold.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.02-summaries-cold.json.snap index 6024670bf..6f0a8bc35 100644 --- a/tests/integration/routes/parity/__snapshots__/analytics.02-summaries-cold.json.snap +++ b/tests/integration/routes/parity/__snapshots__/analytics.02-summaries-cold.json.snap @@ -1343,6 +1343,20 @@ "slope30": null, "avg30LastMonth": null, "avg30LastYear": null + }, + "BASAL_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null } }, "bmi": null, @@ -1469,7 +1483,8 @@ "BODY_BATTERY": null, "STRESS_LEVEL": null, "TRAINING_READINESS": null, - "ACUTE_TRAINING_LOAD": null + "ACUTE_TRAINING_LOAD": null, + "BASAL_ENERGY_BURNED": null }, "sleepSourceDiscrepancy": null, "recoveryEmpty": "general" diff --git a/tests/integration/routes/parity/__snapshots__/analytics.03-summaries-warm.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.03-summaries-warm.json.snap index 2b513bcba..fd8eae42c 100644 --- a/tests/integration/routes/parity/__snapshots__/analytics.03-summaries-warm.json.snap +++ b/tests/integration/routes/parity/__snapshots__/analytics.03-summaries-warm.json.snap @@ -1343,6 +1343,20 @@ "slope30": null, "avg30LastMonth": null, "avg30LastYear": null + }, + "BASAL_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null } }, "bmi": null, @@ -1469,7 +1483,8 @@ "BODY_BATTERY": null, "STRESS_LEVEL": null, "TRAINING_READINESS": null, - "ACUTE_TRAINING_LOAD": null + "ACUTE_TRAINING_LOAD": null, + "BASAL_ENERGY_BURNED": null }, "sleepSourceDiscrepancy": null, "recoveryEmpty": "general" diff --git a/tests/integration/routes/parity/__snapshots__/analytics.04-default-cold.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.04-default-cold.json.snap index 8cfb9de3d..20b1ec632 100644 --- a/tests/integration/routes/parity/__snapshots__/analytics.04-default-cold.json.snap +++ b/tests/integration/routes/parity/__snapshots__/analytics.04-default-cold.json.snap @@ -1343,6 +1343,20 @@ "slope30": null, "avg30LastMonth": null, "avg30LastYear": null + }, + "BASAL_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null } }, "bmi": 25.9, @@ -2485,7 +2499,8 @@ "BODY_BATTERY": null, "STRESS_LEVEL": null, "TRAINING_READINESS": null, - "ACUTE_TRAINING_LOAD": null + "ACUTE_TRAINING_LOAD": null, + "BASAL_ENERGY_BURNED": null } }, "error": null diff --git a/tests/integration/routes/parity/__snapshots__/analytics.05-default-warm.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.05-default-warm.json.snap index 0c6d65c8f..070bee47c 100644 --- a/tests/integration/routes/parity/__snapshots__/analytics.05-default-warm.json.snap +++ b/tests/integration/routes/parity/__snapshots__/analytics.05-default-warm.json.snap @@ -1343,6 +1343,20 @@ "slope30": null, "avg30LastMonth": null, "avg30LastYear": null + }, + "BASAL_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null } }, "bmi": 25.9, @@ -2485,7 +2499,8 @@ "BODY_BATTERY": null, "STRESS_LEVEL": null, "TRAINING_READINESS": null, - "ACUTE_TRAINING_LOAD": null + "ACUTE_TRAINING_LOAD": null, + "BASAL_ENERGY_BURNED": null } }, "error": null diff --git a/tests/integration/routes/parity/__snapshots__/analytics.06-summaries-modules-off.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.06-summaries-modules-off.json.snap index b613d8eec..0080cc059 100644 --- a/tests/integration/routes/parity/__snapshots__/analytics.06-summaries-modules-off.json.snap +++ b/tests/integration/routes/parity/__snapshots__/analytics.06-summaries-modules-off.json.snap @@ -1193,6 +1193,20 @@ "slope30": null, "avg30LastMonth": null, "avg30LastYear": null + }, + "BASAL_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null } }, "bmi": null, @@ -1301,7 +1315,8 @@ "BODY_BATTERY": null, "STRESS_LEVEL": null, "TRAINING_READINESS": null, - "ACUTE_TRAINING_LOAD": null + "ACUTE_TRAINING_LOAD": null, + "BASAL_ENERGY_BURNED": null }, "sleepSourceDiscrepancy": null, "recoveryEmpty": "general" diff --git a/tests/integration/routes/parity/__snapshots__/analytics.07-default-de-personal-target.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.07-default-de-personal-target.json.snap index fd7861c6c..929cc4bc8 100644 --- a/tests/integration/routes/parity/__snapshots__/analytics.07-default-de-personal-target.json.snap +++ b/tests/integration/routes/parity/__snapshots__/analytics.07-default-de-personal-target.json.snap @@ -1193,6 +1193,20 @@ "slope30": null, "avg30LastMonth": null, "avg30LastYear": null + }, + "BASAL_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null } }, "bmi": 25.9, @@ -2149,7 +2163,8 @@ "BODY_BATTERY": null, "STRESS_LEVEL": null, "TRAINING_READINESS": null, - "ACUTE_TRAINING_LOAD": null + "ACUTE_TRAINING_LOAD": null, + "BASAL_ENERGY_BURNED": null } }, "error": null diff --git a/tests/integration/routes/parity/__snapshots__/analytics.08-empty-account.json.snap b/tests/integration/routes/parity/__snapshots__/analytics.08-empty-account.json.snap index 81028211a..e05ff02a7 100644 --- a/tests/integration/routes/parity/__snapshots__/analytics.08-empty-account.json.snap +++ b/tests/integration/routes/parity/__snapshots__/analytics.08-empty-account.json.snap @@ -1263,6 +1263,20 @@ "slope30": null, "avg30LastMonth": null, "avg30LastYear": null + }, + "BASAL_ENERGY_BURNED": { + "count": 0, + "latest": null, + "min": null, + "max": null, + "mean": null, + "median": null, + "avg7": null, + "avg30": null, + "slope7": null, + "slope30": null, + "avg30LastMonth": null, + "avg30LastYear": null } }, "bmi": null, @@ -1632,7 +1646,8 @@ "BODY_BATTERY": null, "STRESS_LEVEL": null, "TRAINING_READINESS": null, - "ACUTE_TRAINING_LOAD": null + "ACUTE_TRAINING_LOAD": null, + "BASAL_ENERGY_BURNED": null } }, "error": null