diff --git a/messages/de.json b/messages/de.json
index 02eaf4e21..7d94e9cab 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -1118,6 +1118,56 @@
"navBodyTemperature": "Temperatur",
"navActiveEnergy": "Aktive Energie",
"navAriaLabel": "Zu Abschnitt springen",
+ "hrv": {
+ "title": "HRV",
+ "description": "Herzratenvariabilität im Verlauf — dein Erholungssignal des autonomen Nervensystems, nächtlich von Apple Watch und kompatiblen Wearables erfasst.",
+ "chartTitle": "Herzratenvariabilität",
+ "emptyState": {
+ "title": "Noch keine HRV-Werte",
+ "description": "HRV-Werte erscheinen hier, sobald die Apple Watch (oder ein kompatibles Withings-Gerät) nächtliche Messungen synchronisiert.",
+ "cta": ""
+ }
+ },
+ "restingHr": {
+ "title": "Ruhepuls",
+ "description": "Die tägliche Grundlinie deines Herzschlags — unterschieden vom Spot-Puls. Apple Health liefert einen Wert pro Tag.",
+ "chartTitle": "Ruhepuls",
+ "emptyState": {
+ "title": "Noch kein Ruhepuls",
+ "description": "Der Ruhepuls erscheint, sobald die Apple Watch oder ein kompatibles Withings-Gerät einen ruhigen Tag durchgetragen hat.",
+ "cta": ""
+ }
+ },
+ "oxygenSaturation": {
+ "title": "Sauerstoffsättigung",
+ "description": "Blutsauerstoff (SpO₂) — klinischer Normalbereich 95–100 %; Zielzonen sind am Chart hinterlegt.",
+ "chartTitle": "Sauerstoffsättigung",
+ "emptyState": {
+ "title": "Noch keine SpO₂-Werte",
+ "description": "SpO₂-Werte erscheinen, sobald ein kompatibles Wearable (Apple Watch, Withings ScanWatch) eine Messung erfasst.",
+ "cta": ""
+ }
+ },
+ "bodyTemperature": {
+ "title": "Körpertemperatur",
+ "description": "Verlauf der Körperkerntemperatur in Celsius. Hauttemperaturwerte von Withings laufen über eine separate Metrik.",
+ "chartTitle": "Körpertemperatur",
+ "emptyState": {
+ "title": "Noch keine Körpertemperatur",
+ "description": "Erfasse eine Körpertemperatur oder lass dein Wearable einen Wert synchronisieren — der Verlauf erscheint dann hier.",
+ "cta": "Körpertemperatur erfassen"
+ }
+ },
+ "activeEnergy": {
+ "title": "Aktive Energie",
+ "description": "Tägliche aktive-Energie-Summe in Kilokalorien — eine Zeile pro Tag, aus Apple Health oder Withings-Aktivität.",
+ "chartTitle": "Aktive Energie",
+ "emptyState": {
+ "title": "Noch keine Aktive-Energie-Werte",
+ "description": "Aktive-Energie-Summen erscheinen hier, sobald Apple Health oder ein Withings-Aktivitätsgerät einen Tagesumfang synchronisiert.",
+ "cta": ""
+ }
+ },
"subPage": {
"blutdruckDescription": "Dein systolisch-/diastolisch-Verlauf mit altersangepassten Zielbereichen und einer textbasierten KI-Einschätzung.",
"gewichtDescription": "Gewichtsverlauf gegen das aus deiner Größe abgeleitete gesunde Band, mit KI-Einschätzung.",
diff --git a/messages/en.json b/messages/en.json
index 930a8d047..4087254fc 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -1118,6 +1118,56 @@
"navBodyTemperature": "Temperature",
"navActiveEnergy": "Active energy",
"navAriaLabel": "Skip to section",
+ "hrv": {
+ "title": "HRV",
+ "description": "Heart-rate variability over time — your nervous-system recovery signal, captured during sleep by Apple Watch and other compatible wearables.",
+ "chartTitle": "Heart-rate variability",
+ "emptyState": {
+ "title": "No HRV samples yet",
+ "description": "HRV samples land here once Apple Watch (or a compatible Withings device) starts syncing nightly readings.",
+ "cta": ""
+ }
+ },
+ "restingHr": {
+ "title": "Resting heart rate",
+ "description": "The daily floor your heart settles at — distinct from the spot pulse samples on the pulse page. Apple Health surfaces one value per day.",
+ "chartTitle": "Resting heart rate",
+ "emptyState": {
+ "title": "No resting heart rate yet",
+ "description": "Resting heart rate arrives once Apple Watch or a compatible Withings device has worn through one quiet day.",
+ "cta": ""
+ }
+ },
+ "oxygenSaturation": {
+ "title": "Oxygen saturation",
+ "description": "Blood oxygen (SpO₂) — clinical normal is 95–100 %, with target zones shaded on the chart.",
+ "chartTitle": "Oxygen saturation",
+ "emptyState": {
+ "title": "No SpO₂ samples yet",
+ "description": "SpO₂ samples land here once a compatible wearable (Apple Watch, Withings ScanWatch) captures a reading.",
+ "cta": ""
+ }
+ },
+ "bodyTemperature": {
+ "title": "Body temperature",
+ "description": "Core body temperature trend — Celsius. Skin-temperature readings from Withings live on a separate metric.",
+ "chartTitle": "Body temperature",
+ "emptyState": {
+ "title": "No body temperature yet",
+ "description": "Log a body-temperature reading or let your wearable sync one, and the trend will appear here.",
+ "cta": "Log body temperature"
+ }
+ },
+ "activeEnergy": {
+ "title": "Active energy",
+ "description": "Daily active-energy total in kilocalories — one row per day, sourced from Apple Health or Withings activity.",
+ "chartTitle": "Active energy",
+ "emptyState": {
+ "title": "No active-energy data yet",
+ "description": "Active-energy totals land here once Apple Health or a Withings activity device syncs a day's worth of movement.",
+ "cta": ""
+ }
+ },
"subPage": {
"blutdruckDescription": "Your systolic / diastolic trend with age-personalised target zones and a written AI assessment.",
"gewichtDescription": "Weight trend mapped against your height-derived healthy band, plus an AI assessment.",
diff --git a/messages/es.json b/messages/es.json
index 29809030d..a7013c92d 100644
--- a/messages/es.json
+++ b/messages/es.json
@@ -1118,6 +1118,56 @@
"navBodyTemperature": "Temperatura",
"navActiveEnergy": "Energía activa",
"navAriaLabel": "Skip to section",
+ "hrv": {
+ "title": "VFC",
+ "description": "Variabilidad de la frecuencia cardíaca a lo largo del tiempo — señal de recuperación del sistema nervioso, captada por la noche por Apple Watch y otros wearables compatibles.",
+ "chartTitle": "Variabilidad de la frecuencia cardíaca",
+ "emptyState": {
+ "title": "Aún no hay valores de VFC",
+ "description": "Los valores de VFC aparecerán aquí en cuanto Apple Watch (o un dispositivo Withings compatible) sincronice las mediciones nocturnas.",
+ "cta": ""
+ }
+ },
+ "restingHr": {
+ "title": "FC en reposo",
+ "description": "El nivel basal diario del corazón — distinto del pulso puntual de la página de pulso. Apple Salud aporta un valor al día.",
+ "chartTitle": "Frecuencia cardíaca en reposo",
+ "emptyState": {
+ "title": "Aún no hay FC en reposo",
+ "description": "La FC en reposo aparece cuando Apple Watch o un Withings compatible ha llevado puesto un día tranquilo.",
+ "cta": ""
+ }
+ },
+ "oxygenSaturation": {
+ "title": "Saturación de oxígeno",
+ "description": "Oxígeno en sangre (SpO₂) — rango clínico normal 95–100 %, con zonas objetivo sombreadas en el gráfico.",
+ "chartTitle": "Saturación de oxígeno",
+ "emptyState": {
+ "title": "Aún no hay muestras de SpO₂",
+ "description": "Las muestras de SpO₂ llegan aquí en cuanto un wearable compatible (Apple Watch, Withings ScanWatch) capture una lectura.",
+ "cta": ""
+ }
+ },
+ "bodyTemperature": {
+ "title": "Temperatura corporal",
+ "description": "Tendencia de temperatura corporal central en grados Celsius. La temperatura cutánea de Withings vive en una métrica distinta.",
+ "chartTitle": "Temperatura corporal",
+ "emptyState": {
+ "title": "Aún no hay temperatura corporal",
+ "description": "Registra una lectura o deja que tu wearable sincronice una — la tendencia aparecerá aquí.",
+ "cta": "Registrar temperatura corporal"
+ }
+ },
+ "activeEnergy": {
+ "title": "Energía activa",
+ "description": "Total diario de energía activa en kilocalorías — una fila por día, desde Apple Salud o la actividad de Withings.",
+ "chartTitle": "Energía activa",
+ "emptyState": {
+ "title": "Aún no hay datos de energía activa",
+ "description": "Los totales de energía activa aparecerán aquí en cuanto Apple Salud o un dispositivo de actividad Withings sincronice un día completo.",
+ "cta": ""
+ }
+ },
"subPage": {
"blutdruckDescription": "Your systolic / diastolic trend with age-personalised target zones and a written AI assessment.",
"gewichtDescription": "Weight trend mapped against your height-derived healthy band, plus an AI assessment.",
diff --git a/messages/fr.json b/messages/fr.json
index ad78d7b30..b4d229b1f 100644
--- a/messages/fr.json
+++ b/messages/fr.json
@@ -1118,6 +1118,56 @@
"navBodyTemperature": "Température",
"navActiveEnergy": "Énergie active",
"navAriaLabel": "Skip to section",
+ "hrv": {
+ "title": "VFC",
+ "description": "Variabilité de la fréquence cardiaque dans le temps — signal de récupération du système nerveux, capté la nuit par l'Apple Watch et d'autres wearables compatibles.",
+ "chartTitle": "Variabilité de la fréquence cardiaque",
+ "emptyState": {
+ "title": "Aucune mesure de VFC",
+ "description": "Les mesures de VFC apparaîtront ici dès que l'Apple Watch (ou un appareil Withings compatible) synchronisera des relevés nocturnes.",
+ "cta": ""
+ }
+ },
+ "restingHr": {
+ "title": "FC au repos",
+ "description": "La fréquence cardiaque de base sur la journée — distincte des pointages ponctuels de la page Pulse. Apple Santé fournit une valeur par jour.",
+ "chartTitle": "Fréquence cardiaque au repos",
+ "emptyState": {
+ "title": "Aucune FC au repos",
+ "description": "La FC au repos arrive dès que l'Apple Watch ou un appareil Withings compatible a porté une journée calme.",
+ "cta": ""
+ }
+ },
+ "oxygenSaturation": {
+ "title": "Saturation en oxygène",
+ "description": "Oxygène sanguin (SpO₂) — plage clinique normale 95–100 %, avec zones cibles ombrées sur le graphique.",
+ "chartTitle": "Saturation en oxygène",
+ "emptyState": {
+ "title": "Aucune mesure SpO₂",
+ "description": "Les mesures SpO₂ apparaîtront dès qu'un wearable compatible (Apple Watch, Withings ScanWatch) en capturera une.",
+ "cta": ""
+ }
+ },
+ "bodyTemperature": {
+ "title": "Température corporelle",
+ "description": "Tendance de température corporelle centrale en Celsius. La température cutanée Withings vit sur une métrique distincte.",
+ "chartTitle": "Température corporelle",
+ "emptyState": {
+ "title": "Aucune température corporelle",
+ "description": "Enregistre une mesure ou laisse ton wearable en synchroniser une — la tendance apparaîtra ici.",
+ "cta": "Enregistrer la température"
+ }
+ },
+ "activeEnergy": {
+ "title": "Énergie active",
+ "description": "Total quotidien d'énergie active en kilocalories — une ligne par jour, depuis Apple Santé ou Withings activité.",
+ "chartTitle": "Énergie active",
+ "emptyState": {
+ "title": "Aucune donnée d'énergie active",
+ "description": "Les totaux d'énergie active apparaîtront dès qu'Apple Santé ou un appareil Withings activité synchronise une journée complète.",
+ "cta": ""
+ }
+ },
"subPage": {
"blutdruckDescription": "Your systolic / diastolic trend with age-personalised target zones and a written AI assessment.",
"gewichtDescription": "Weight trend mapped against your height-derived healthy band, plus an AI assessment.",
diff --git a/messages/it.json b/messages/it.json
index e4065b941..f40fedee0 100644
--- a/messages/it.json
+++ b/messages/it.json
@@ -1118,6 +1118,56 @@
"navBodyTemperature": "Temperatura",
"navActiveEnergy": "Energia attiva",
"navAriaLabel": "Skip to section",
+ "hrv": {
+ "title": "HRV",
+ "description": "Variabilità della frequenza cardiaca nel tempo — segnale di recupero del sistema nervoso, captato di notte da Apple Watch e altri wearable compatibili.",
+ "chartTitle": "Variabilità della frequenza cardiaca",
+ "emptyState": {
+ "title": "Ancora nessun valore HRV",
+ "description": "I valori HRV appariranno qui non appena Apple Watch (o un dispositivo Withings compatibile) sincronizza le misurazioni notturne.",
+ "cta": ""
+ }
+ },
+ "restingHr": {
+ "title": "FC a riposo",
+ "description": "Il livello cardiaco di base sulla giornata — distinto dalle frequenze puntuali della pagina Polso. Apple Salute fornisce un valore al giorno.",
+ "chartTitle": "Frequenza cardiaca a riposo",
+ "emptyState": {
+ "title": "Ancora nessuna FC a riposo",
+ "description": "La FC a riposo arriva quando Apple Watch o un Withings compatibile ha indossato una giornata tranquilla.",
+ "cta": ""
+ }
+ },
+ "oxygenSaturation": {
+ "title": "Saturazione di ossigeno",
+ "description": "Ossigeno nel sangue (SpO₂) — intervallo clinico normale 95–100 %, con zone obiettivo ombreggiate sul grafico.",
+ "chartTitle": "Saturazione di ossigeno",
+ "emptyState": {
+ "title": "Ancora nessun campione SpO₂",
+ "description": "I campioni SpO₂ appariranno qui non appena un wearable compatibile (Apple Watch, Withings ScanWatch) cattura una lettura.",
+ "cta": ""
+ }
+ },
+ "bodyTemperature": {
+ "title": "Temperatura corporea",
+ "description": "Andamento della temperatura corporea centrale in Celsius. La temperatura cutanea di Withings vive su una metrica separata.",
+ "chartTitle": "Temperatura corporea",
+ "emptyState": {
+ "title": "Ancora nessuna temperatura corporea",
+ "description": "Registra una misura o lascia che il tuo wearable ne sincronizzi una — l'andamento apparirà qui.",
+ "cta": "Registra temperatura corporea"
+ }
+ },
+ "activeEnergy": {
+ "title": "Energia attiva",
+ "description": "Totale giornaliero di energia attiva in kilocalorie — una riga al giorno, da Apple Salute o dall'attività Withings.",
+ "chartTitle": "Energia attiva",
+ "emptyState": {
+ "title": "Ancora nessun dato di energia attiva",
+ "description": "I totali di energia attiva appariranno qui non appena Apple Salute o un dispositivo di attività Withings sincronizza un'intera giornata.",
+ "cta": ""
+ }
+ },
"subPage": {
"blutdruckDescription": "Your systolic / diastolic trend with age-personalised target zones and a written AI assessment.",
"gewichtDescription": "Weight trend mapped against your height-derived healthy band, plus an AI assessment.",
diff --git a/messages/pl.json b/messages/pl.json
index db1c5a183..722774831 100644
--- a/messages/pl.json
+++ b/messages/pl.json
@@ -1118,6 +1118,56 @@
"navBodyTemperature": "Temperatura",
"navActiveEnergy": "Energia czynna",
"navAriaLabel": "Skip to section",
+ "hrv": {
+ "title": "HRV",
+ "description": "Zmienność rytmu serca w czasie — sygnał regeneracji układu nerwowego, rejestrowany nocą przez Apple Watch i inne zgodne wearable.",
+ "chartTitle": "Zmienność rytmu serca",
+ "emptyState": {
+ "title": "Brak wartości HRV",
+ "description": "Wartości HRV pojawią się tutaj, gdy Apple Watch (lub zgodne urządzenie Withings) zsynchronizuje pomiary nocne.",
+ "cta": ""
+ }
+ },
+ "restingHr": {
+ "title": "Tętno spoczynkowe",
+ "description": "Dzienna linia bazowa rytmu serca — różna od tętna punktowego na stronie Pulse. Apple Zdrowie podaje jedną wartość na dobę.",
+ "chartTitle": "Tętno spoczynkowe",
+ "emptyState": {
+ "title": "Brak tętna spoczynkowego",
+ "description": "Tętno spoczynkowe pojawi się, gdy Apple Watch lub zgodne urządzenie Withings spędzi spokojny dzień na nadgarstku.",
+ "cta": ""
+ }
+ },
+ "oxygenSaturation": {
+ "title": "Saturacja tlenu",
+ "description": "Tlen we krwi (SpO₂) — kliniczny zakres normy 95–100 %, ze strefami docelowymi zacienionymi na wykresie.",
+ "chartTitle": "Saturacja tlenu",
+ "emptyState": {
+ "title": "Brak próbek SpO₂",
+ "description": "Próbki SpO₂ trafią tutaj, gdy zgodne wearable (Apple Watch, Withings ScanWatch) wykona pomiar.",
+ "cta": ""
+ }
+ },
+ "bodyTemperature": {
+ "title": "Temperatura ciała",
+ "description": "Trend temperatury rdzenia ciała w stopniach Celsjusza. Temperatura skóry z Withings działa na osobnej metryce.",
+ "chartTitle": "Temperatura ciała",
+ "emptyState": {
+ "title": "Brak temperatury ciała",
+ "description": "Zarejestruj pomiar lub pozwól wearable zsynchronizować jeden — trend pojawi się tutaj.",
+ "cta": "Zarejestruj temperaturę ciała"
+ }
+ },
+ "activeEnergy": {
+ "title": "Energia czynna",
+ "description": "Dzienna suma energii czynnej w kilokaloriach — jeden wiersz na dobę, z Apple Zdrowie lub aktywności Withings.",
+ "chartTitle": "Energia czynna",
+ "emptyState": {
+ "title": "Brak danych o energii czynnej",
+ "description": "Sumy energii czynnej pojawią się tutaj, gdy Apple Zdrowie lub urządzenie Withings zsynchronizuje pełny dzień.",
+ "cta": ""
+ }
+ },
"subPage": {
"blutdruckDescription": "Your systolic / diastolic trend with age-personalised target zones and a written AI assessment.",
"gewichtDescription": "Weight trend mapped against your height-derived healthy band, plus an AI assessment.",
diff --git a/src/app/insights/aktive-energie/page.tsx b/src/app/insights/aktive-energie/page.tsx
new file mode 100644
index 000000000..146d81cfd
--- /dev/null
+++ b/src/app/insights/aktive-energie/page.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { Flame } from "lucide-react";
+
+import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
+
+/**
+ * v1.4.32 — `/insights/aktive-energie`.
+ *
+ * Active-energy-burned sub-page. The MeasurementType is cumulative
+ * (kcal per day, per R-A Option A); the v1.4.30 server-side daily-
+ * stats helper collapses per-sample iOS posts to a single daily row.
+ * The chart shows that one-row-per-day shape — long-tail back-fill
+ * imports collapse cleanly via the v1.4.30 drain script.
+ */
+export default function InsightsActiveEnergyPage() {
+ return (
+ }
+ emptyStateCtaType={null}
+ coachPrefill="I haven't logged any active energy yet — what's a reasonable daily target, and how does it compare to total calories?"
+ />
+ );
+}
diff --git a/src/app/insights/hrv/page.tsx b/src/app/insights/hrv/page.tsx
new file mode 100644
index 000000000..5076fbaee
--- /dev/null
+++ b/src/app/insights/hrv/page.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { Activity } from "lucide-react";
+
+import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
+
+/**
+ * v1.4.32 — `/insights/hrv`.
+ *
+ * Heart-rate variability sub-page. Pairs with the iOS HKQuantityType
+ * `heartRateVariabilitySDNN` ingest path; the user typically logs one
+ * sample per night during sleep, so the chart is sparse but the
+ * weekly trend is the load-bearing signal. Stored as millisecond
+ * `HEART_RATE_VARIABILITY` rows by every existing ingest path.
+ */
+export default function InsightsHrvPage() {
+ return (
+ }
+ emptyStateCtaType={null}
+ coachPrefill="I haven't logged any HRV data yet — what does heart-rate variability tell me, and how do I capture it?"
+ />
+ );
+}
diff --git a/src/app/insights/koerpertemperatur/page.tsx b/src/app/insights/koerpertemperatur/page.tsx
new file mode 100644
index 000000000..0d7657ce1
--- /dev/null
+++ b/src/app/insights/koerpertemperatur/page.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { Thermometer } from "lucide-react";
+
+import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
+
+/**
+ * v1.4.32 — `/insights/koerpertemperatur`.
+ *
+ * Body-temperature sub-page. Canonical storage is Celsius. Distinct
+ * from skin temperature (Withings ScanWatch dermal reading) — surface
+ * temps run ~32 °C, core ~37 °C, so the schema deliberately keeps
+ * them on separate enum values per the v1.4.30 categorisation overlay.
+ */
+export default function InsightsBodyTemperaturePage() {
+ return (
+ }
+ emptyStateCtaType="BODY_TEMPERATURE"
+ coachPrefill="I haven't logged any body temperature yet — what's the healthy range, and what should I do if it drifts?"
+ />
+ );
+}
diff --git a/src/app/insights/ruhepuls/page.tsx b/src/app/insights/ruhepuls/page.tsx
new file mode 100644
index 000000000..dff1f1e79
--- /dev/null
+++ b/src/app/insights/ruhepuls/page.tsx
@@ -0,0 +1,31 @@
+"use client";
+
+import { Heart } from "lucide-react";
+
+import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
+
+/**
+ * v1.4.32 — `/insights/ruhepuls`.
+ *
+ * Resting-heart-rate sub-page. Distinct from `/insights/puls` — Apple
+ * Health splits the steady-state daily `restingHeartRate` value from
+ * the spot `PULSE` samples logged manually or by Withings. The split
+ * here mirrors that distinction so the user has a "daily floor"
+ * trend that's not muddied by mid-walk readings.
+ */
+export default function InsightsRestingHrPage() {
+ return (
+ }
+ emptyStateCtaType={null}
+ coachPrefill="I haven't logged any resting heart rate yet — why is the daily resting value different from a spot pulse, and how do I improve it?"
+ />
+ );
+}
diff --git a/src/app/insights/sauerstoff/page.tsx b/src/app/insights/sauerstoff/page.tsx
new file mode 100644
index 000000000..4309e9abd
--- /dev/null
+++ b/src/app/insights/sauerstoff/page.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { Wind } from "lucide-react";
+
+import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page";
+
+/**
+ * v1.4.32 — `/insights/sauerstoff`.
+ *
+ * Oxygen-saturation (SpO₂) sub-page. Canonical storage is percent
+ * (0..100); the chart axis runs 85..100 because clinical normal is
+ * 95..100 and the 85 floor leaves room for nightly dips without
+ * compressing the trend. Withings ScanWatch + Apple Watch both
+ * report SpO₂; the v1.4.25 source-priority picker keeps the surface
+ * deduped.
+ */
+export default function InsightsOxygenSaturationPage() {
+ return (
+ }
+ emptyStateCtaType={null}
+ coachPrefill="I haven't logged any oxygen-saturation data yet — what does SpO₂ tell me, and what range should I aim for?"
+ />
+ );
+}
diff --git a/src/components/insights/healthkit-metric-page.tsx b/src/components/insights/healthkit-metric-page.tsx
new file mode 100644
index 000000000..16e323c2a
--- /dev/null
+++ b/src/components/insights/healthkit-metric-page.tsx
@@ -0,0 +1,139 @@
+"use client";
+
+import Link from "next/link";
+import type { ComponentProps, ReactNode } from "react";
+
+import { useAuth } from "@/hooks/use-auth";
+import { useInsightsAnalytics } from "@/hooks/use-insights-analytics";
+import { useInsightsLayoutPrefs } from "@/hooks/use-insights-layout-prefs";
+import { useTranslations } from "@/lib/i18n/context";
+import type { InsightMetric } from "@/lib/insights/metric-availability";
+import type { ChartOverlayKey } from "@/lib/dashboard-layout";
+import { Button } from "@/components/ui/button";
+import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
+import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
+import { MetricEmptyState } from "@/components/insights/metric-empty-state";
+import { SubPageShell } from "@/components/insights/sub-page-shell";
+
+/**
+ * v1.4.32 — shared scaffold for the wave-A HealthKit metric pages.
+ *
+ * Each of the five new metric sub-pages (HRV, resting HR, oxygen
+ * saturation, body temperature, active energy) shares the same
+ * envelope:
+ *
+ * 1. `` with a localised title + description.
+ * 2. Empty-state CTA when the user has no observations for the
+ * metric — points at `/measurements?add=` so the existing
+ * quick-entry dialog can light up. The CTA target is optional;
+ * Apple-Health-only metrics (active energy, body temperature)
+ * pass `null` to render an onboarding hint without a dead link.
+ * 3. `` mounted on the canonical
+ * `ChartOverlayKey` so the chart-cog popover persists per metric.
+ * 4. `` at the foot — feature-flag gating runs
+ * inside the button so this scaffold stays agnostic.
+ *
+ * Adding a new metric is a four-line page module that hands the right
+ * Zod / MeasurementType + ChartOverlayKey + i18n-key prefix to this
+ * component; the template stays single-source-of-truth.
+ */
+
+export interface HealthKitMetricPageProps {
+ /** The MeasurementType that backs the chart. */
+ measurementType: string;
+ /** The InsightMetric key used by `useInsightsAnalytics()`. */
+ insightMetric: InsightMetric;
+ /** The chart-overlay slot id. */
+ chartKey: ChartOverlayKey;
+ /**
+ * i18n key prefix that drives `title`, `description`, `chartTitle`,
+ * `emptyState.title`, `emptyState.description`, `emptyState.cta`.
+ * Pages pass e.g. `"insights.hrv"` and the scaffold resolves each
+ * sub-key off the prefix.
+ */
+ i18nPrefix: string;
+ /** Single canonical colour for the chart line. */
+ color: string;
+ /** Unit suffix the chart renders next to the value. */
+ unit: string;
+ /** Optional y-axis label shown above the unit. */
+ yAxisUnit?: string;
+ /** Optional value bands (Apple-Health-style target zone shading). */
+ valueBands?: ComponentProps["valueBands"];
+ /**
+ * Optional empty-state CTA target. `null` renders the empty state
+ * without a primary action (Apple-Health-only metrics that have no
+ * manual entry form). String values land in `/measurements?add=`.
+ */
+ emptyStateCtaType?: string | null;
+ /** Icon node mounted in the empty-state card. */
+ emptyStateIcon: ReactNode;
+ /**
+ * Optional Coach prefill for the empty-state launch. Falls back to a
+ * generic onboarding prompt threaded through ``'s
+ * default.
+ */
+ coachPrefill?: string;
+}
+
+export function HealthKitMetricPage({
+ measurementType,
+ insightMetric,
+ chartKey,
+ i18nPrefix,
+ color,
+ unit,
+ yAxisUnit,
+ valueBands,
+ emptyStateCtaType,
+ emptyStateIcon,
+ coachPrefill,
+}: HealthKitMetricPageProps) {
+ const { user, isAuthenticated } = useAuth();
+ const { t } = useTranslations();
+ const { compareBaseline } = useInsightsLayoutPrefs(isAuthenticated);
+
+ const { isEmpty } = useInsightsAnalytics(insightMetric);
+
+ const title = t(`${i18nPrefix}.title`);
+ const description = t(`${i18nPrefix}.description`);
+
+ if (isEmpty) {
+ const ctaNode =
+ emptyStateCtaType != null ? (
+
+ ) : null;
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/lib/dashboard-layout.ts b/src/lib/dashboard-layout.ts
index fb6e73331..efa564769 100644
--- a/src/lib/dashboard-layout.ts
+++ b/src/lib/dashboard-layout.ts
@@ -131,6 +131,14 @@ export const CHART_OVERLAY_KEYS = [
// overlay-prefs slot so the cardio sub-page's VO2 chart-cog state
// doesn't bleed into the pulse chart sitting directly above it.
"vo2Max",
+ // v1.4.32 — wave-A HealthKit chart cards. Each one owns its own
+ // overlay-prefs slot so the chart-cog popover (7-day trend / Trend
+ // arrow / Target range / Comparison baseline) persists per metric.
+ "hrv",
+ "restingHr",
+ "oxygenSaturation",
+ "bodyTemperature",
+ "activeEnergy",
] as const;
export type ChartOverlayKey = (typeof CHART_OVERLAY_KEYS)[number];
diff --git a/src/lib/measurements/categories.ts b/src/lib/measurements/categories.ts
index a522aafae..fc1876242 100644
--- a/src/lib/measurements/categories.ts
+++ b/src/lib/measurements/categories.ts
@@ -62,8 +62,6 @@ export const MEASUREMENT_CATEGORIES: ReadonlyMap<
["BLOOD_PRESSURE_SYS", "vitals"],
["BLOOD_PRESSURE_DIA", "vitals"],
["PULSE", "vitals"],
- ["RESTING_HEART_RATE", "vitals"],
- ["HEART_RATE_VARIABILITY", "vitals"],
["OXYGEN_SATURATION", "vitals"],
["BODY_TEMPERATURE", "vitals"],
@@ -96,7 +94,15 @@ export const MEASUREMENT_CATEGORIES: ReadonlyMap<
// ── Environment ──
["TIME_IN_DAYLIGHT", "environment"],
- // ── Cardiovascular (derived risk markers) ──
+ // ── Cardiovascular (derived risk markers + Apple-Health-grouped
+ // heart-rate variants). v1.4.32 — RESTING_HEART_RATE +
+ // HEART_RATE_VARIABILITY moved here from `vitals` to align with the
+ // iOS handoff brief's category table (RESTING_HR, HRV →
+ // cardiovascular). PULSE stays in `vitals` because spot-pulse
+ // samples are point-in-time signals; the resting + variability
+ // metrics are the derived cardiac-risk surface. ──
+ ["RESTING_HEART_RATE", "cardiovascular"],
+ ["HEART_RATE_VARIABILITY", "cardiovascular"],
["PULSE_WAVE_VELOCITY", "cardiovascular"],
["VASCULAR_AGE", "cardiovascular"],