diff --git a/messages/de.json b/messages/de.json index 13bcb27aa..7e1b58038 100644 --- a/messages/de.json +++ b/messages/de.json @@ -3554,7 +3554,8 @@ "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." - } + }, + "coachPrefill": "Ich habe noch keine HRV-Daten erfasst. Was sagt mir die Herzfrequenzvariabilität, und wie erfasse ich sie?" }, "restingHr": { "title": "Ruhepuls", @@ -3573,7 +3574,8 @@ "emptyState": { "title": "Noch keine SpO₂-Werte", "description": "SpO₂-Werte erscheinen, sobald ein kompatibles Wearable (Apple Watch, Withings ScanWatch) eine Messung erfasst." - } + }, + "coachPrefill": "Ich habe noch keine Sauerstoffsättigung erfasst. Was sagt mir die SpO₂, und welchen Bereich sollte ich anstreben?" }, "bodyTemperature": { "title": "Körpertemperatur", @@ -3583,7 +3585,8 @@ "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" - } + }, + "coachPrefill": "Ich habe noch keine Körpertemperatur erfasst. Was ist der gesunde Bereich, und was sollte ich tun, wenn sie abweicht?" }, "steps": { "title": "Schritte", @@ -3592,7 +3595,8 @@ "emptyState": { "title": "Noch keine Schrittdaten", "description": "Tägliche Schritt-Summen erscheinen hier, sobald Apple Health oder ein Aktivitätsgerät einen Tagesumfang synchronisiert." - } + }, + "coachPrefill": "Ich habe noch keine Schritte erfasst. Was ist ein sinnvolles tägliches Schrittziel, und wie hilft mehr Gehen meiner Gesundheit?" }, "activeEnergy": { "title": "Aktive Energie", @@ -3601,7 +3605,8 @@ "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." - } + }, + "coachPrefill": "Ich habe noch keine aktive Energie erfasst. Was ist ein sinnvolles Tagesziel, und wie verhält es sich zum gesamten Kalorienverbrauch?" }, "subPage": { "loadError": "Diese Messgröße konnte gerade nicht geladen werden.", @@ -3696,7 +3701,8 @@ "valuesBack": "Zurück zu Insights", "valuesBackToMetric": "Zurück zu {metric}", "scoresBack": "Zurück zu Insights", - "ecgDescription": "Einkanal-EKG-Aufzeichnungen deines Geräts, jeweils mit dem Ergebnis des Geräts." + "ecgDescription": "Einkanal-EKG-Aufzeichnungen deines Geräts, jeweils mit dem Ergebnis des Geräts.", + "emptyCoachPrefill": "{metric}: Ich habe hier noch nichts erfasst. Was sagt dieser Wert über meine Gesundheit aus, und wie verbessere ich ihn?" }, "units": { "steps": "Schritte", @@ -3796,27 +3802,32 @@ "bloodPressure": { "title": "Noch keine Blutdruckwerte", "description": "Sobald du Blutdruck loggst, erscheinen hier Trend, Zielbereichs-Anteil und WHO-Klassifizierung.", - "cta": "Blutdruck loggen" + "cta": "Blutdruck loggen", + "coachPrefill": "Ich habe noch keinen Blutdruck erfasst. Warum ist er wichtig, und was sollte ich wissen, bevor ich anfange?" }, "weight": { "title": "Noch keine Gewichtswerte", "description": "Logge ein Gewicht, und Verlauf, gleitender Durchschnitt und Zielbereich erscheinen automatisch.", - "cta": "Gewicht loggen" + "cta": "Gewicht loggen", + "coachPrefill": "Ich habe noch kein Gewicht erfasst. Warum ist es wichtig, und was sollte ich wissen, bevor ich mit dem Erfassen anfange?" }, "pulse": { "title": "Noch keine Pulswerte", "description": "Logge einen Puls, dann erscheinen Ruhepuls, HRV und VO₂ max in dieser Ansicht.", - "cta": "Puls loggen" + "cta": "Puls loggen", + "coachPrefill": "Ich habe noch keinen Ruhepuls erfasst. Warum ist er wichtig, und was sollte ich wissen, bevor ich anfange?" }, "bmi": { "title": "BMI noch nicht berechnet", "description": "Sobald Größe und mindestens ein Gewichtswert vorliegen, erscheint hier der BMI-Verlauf samt WHO-Zonen.", - "cta": "Gewicht loggen" + "cta": "Gewicht loggen", + "coachPrefill": "Ich habe noch kein Gewicht erfasst. Warum ist der BMI für mich wichtig, und was sollte ich wissen, bevor ich ihn verfolge?" }, "mood": { "title": "Noch keine Stimmungseinträge", "description": "Logge eine Stimmung, dann erscheinen Verlauf, Stabilitätsband und Korrelationen.", - "cta": "Stimmung loggen" + "cta": "Stimmung loggen", + "coachPrefill": "Ich habe noch keine Stimmung erfasst. Warum lohnt es sich, die Stimmung zu erfassen, und wie fange ich am besten an?" }, "medication": { "title": "Noch keine Medikamente angelegt", @@ -3826,7 +3837,8 @@ "sleep": { "title": "Noch keine Schlafwerte", "description": "Sobald Schlafdaten vorliegen (manuell oder via Withings / Apple Health), erscheint hier der nächtliche Stadien-Verlauf.", - "cta": "Quelle verbinden" + "cta": "Quelle verbinden", + "coachPrefill": "Ich habe noch keine Schlafdaten. Warum lohnt es sich, Schlaf zu erfassen, und was sollte ich wissen, bevor ich eine Quelle verbinde?" } }, "navWorkouts": "Workouts", @@ -3836,7 +3848,8 @@ "description": "Letzte Läufe, Touren, Spaziergänge und Krafteinheiten, quellenübergreifend entdoppelt zwischen Apple Watch und Withings.", "emptyState": { "title": "Noch keine Workouts", - "description": "Workouts kommen per Apple Health vom iPhone und von Withings. Du kannst eins auch von Hand erfassen." + "description": "Workouts kommen per Apple Health vom iPhone und von Withings. Du kannst eins auch von Hand erfassen.", + "coachPrefill": "Ich habe noch keine Workouts erfasst. Warum lohnt es sich, sie zu erfassen, und worauf sollte ich mich zuerst konzentrieren?" }, "detail": { "backToList": "Zurück zu Workouts", @@ -4716,7 +4729,8 @@ "emptyState": { "title": "Noch keine Sturzdaten", "description": "Erkannte Stürze erscheinen hier, sobald Apple Health synchronisiert. Keine Stürze ist das Ziel." - } + }, + "coachPrefill": "Ich habe noch keine Sturzdaten erfasst. Was sagt dieser Wert über meine Gesundheit aus, und wie senke ich mein Sturzrisiko?" }, "navFalls": "Stürze", "sixMinuteWalk": { diff --git a/messages/en.json b/messages/en.json index 6ba260ae3..e9686526d 100644 --- a/messages/en.json +++ b/messages/en.json @@ -3554,7 +3554,8 @@ "emptyState": { "title": "No HRV samples yet", "description": "HRV samples land here once Apple Watch (or a compatible Withings device) starts syncing nightly readings." - } + }, + "coachPrefill": "I haven't logged any HRV data yet. What does heart-rate variability tell me, and how do I capture it?" }, "restingHr": { "title": "Resting heart rate", @@ -3573,7 +3574,8 @@ "emptyState": { "title": "No SpO₂ samples yet", "description": "SpO₂ samples land here once a compatible wearable (Apple Watch, Withings ScanWatch) captures a reading." - } + }, + "coachPrefill": "I haven't logged any oxygen-saturation data yet. What does SpO₂ tell me, and what range should I aim for?" }, "bodyTemperature": { "title": "Body temperature", @@ -3583,7 +3585,8 @@ "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" - } + }, + "coachPrefill": "I haven't logged any body temperature yet. What's the healthy range, and what should I do if it drifts?" }, "steps": { "title": "Steps", @@ -3592,7 +3595,8 @@ "emptyState": { "title": "No step data yet", "description": "Daily step totals appear here once Apple Health or an activity device syncs a day's worth of movement." - } + }, + "coachPrefill": "I haven't logged any steps yet. What's a reasonable daily step target, and how does walking more help my health?" }, "activeEnergy": { "title": "Active energy", @@ -3601,7 +3605,8 @@ "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." - } + }, + "coachPrefill": "I haven't logged any active energy yet. What's a reasonable daily target, and how does it compare to total calories?" }, "subPage": { "loadError": "Could not load this metric right now.", @@ -3696,7 +3701,8 @@ "valuesBack": "Back to insights", "valuesBackToMetric": "Back to {metric}", "scoresBack": "Back to insights", - "ecgDescription": "Single-lead ECG recordings from your device, each with the device's own result." + "ecgDescription": "Single-lead ECG recordings from your device, each with the device's own result.", + "emptyCoachPrefill": "{metric}: I haven't logged anything here yet. What does this metric tell me about my health, and how do I improve it?" }, "units": { "steps": "steps", @@ -3796,27 +3802,32 @@ "bloodPressure": { "title": "No blood pressure entries yet", "description": "Log a blood pressure reading and the trend chart, in-range share, and WHO classification will appear here.", - "cta": "Log blood pressure" + "cta": "Log blood pressure", + "coachPrefill": "I haven't recorded any blood pressure yet. Why does it matter, and what should I know before I start?" }, "weight": { "title": "No weight entries yet", "description": "Log a weight and the trend, moving average, and target band will appear automatically.", - "cta": "Log weight" + "cta": "Log weight", + "coachPrefill": "I haven't recorded any weight yet. Why does it matter, and what should I know before I start tracking?" }, "pulse": { "title": "No pulse entries yet", "description": "Log a pulse reading and resting pulse, HRV and VO₂ max will appear in this view.", - "cta": "Log pulse" + "cta": "Log pulse", + "coachPrefill": "I haven't recorded any resting pulse yet. Why does it matter, and what should I know before I start?" }, "bmi": { "title": "BMI not yet computed", "description": "Once height and at least one weight entry are present, the BMI trend with WHO zones will appear here.", - "cta": "Log weight" + "cta": "Log weight", + "coachPrefill": "I haven't recorded any weight yet. Why does BMI matter for me, and what should I know before I start tracking it?" }, "mood": { "title": "No mood entries yet", "description": "Log a mood entry and the trend, stability band and correlations will appear.", - "cta": "Log mood" + "cta": "Log mood", + "coachPrefill": "I haven't logged any mood entries yet. Why does mood tracking matter, and how should I start?" }, "medication": { "title": "No medications set up yet", @@ -3826,7 +3837,8 @@ "sleep": { "title": "No sleep entries yet", "description": "Once sleep data lands (manual or via Withings / Apple Health), the per-night stage breakdown will appear here.", - "cta": "Connect a source" + "cta": "Connect a source", + "coachPrefill": "I don't have any sleep data yet. Why does sleep tracking matter, and what should I know before I connect a source?" } }, "navWorkouts": "Workouts", @@ -3836,7 +3848,8 @@ "description": "Recent runs, rides, walks and strength sessions, deduped across Apple Watch and Withings sources.", "emptyState": { "title": "No workouts yet", - "description": "Workouts sync from Apple Health on your iPhone and from Withings. You can also log one by hand." + "description": "Workouts sync from Apple Health on your iPhone and from Withings. You can also log one by hand.", + "coachPrefill": "I haven't logged any workouts yet. Why does tracking them matter, and what should I focus on first?" }, "detail": { "backToList": "Back to workouts", @@ -4716,7 +4729,8 @@ "emptyState": { "title": "No fall data yet", "description": "Detected falls appear here once Apple Health syncs. No falls is the goal." - } + }, + "coachPrefill": "I haven't logged any fall data yet. What does this metric tell me about my health, and how do I lower my fall risk?" }, "navFalls": "Falls", "sixMinuteWalk": { diff --git a/messages/es.json b/messages/es.json index 62128c707..877ccf275 100644 --- a/messages/es.json +++ b/messages/es.json @@ -3554,7 +3554,8 @@ "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." - } + }, + "coachPrefill": "Aún no he registrado datos de VFC: ¿qué me dice la variabilidad de la frecuencia cardíaca y cómo la registro?" }, "restingHr": { "title": "FC en reposo", @@ -3573,7 +3574,8 @@ "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." - } + }, + "coachPrefill": "Aún no he registrado la saturación de oxígeno: ¿qué me dice la SpO₂ y a qué rango debería aspirar?" }, "bodyTemperature": { "title": "Temperatura corporal", @@ -3583,7 +3585,8 @@ "title": "Aún no hay temperatura corporal", "description": "Registra una lectura o deja que tu wearable sincronice una, y la tendencia aparecerá aquí.", "cta": "Registrar temperatura corporal" - } + }, + "coachPrefill": "Aún no he registrado la temperatura corporal: ¿cuál es el rango saludable y qué debo hacer si se desvía?" }, "steps": { "title": "Pasos", @@ -3592,7 +3595,8 @@ "emptyState": { "title": "Aún no hay datos de pasos", "description": "Los totales diarios de pasos aparecerán aquí en cuanto Apple Salud o un dispositivo de actividad sincronice un día completo." - } + }, + "coachPrefill": "Aún no he registrado pasos: ¿cuál es un objetivo diario de pasos razonable y cómo ayuda a mi salud caminar más?" }, "activeEnergy": { "title": "Energía activa", @@ -3601,7 +3605,8 @@ "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." - } + }, + "coachPrefill": "Aún no he registrado energía activa: ¿cuál es un objetivo diario razonable y cómo se compara con las calorías totales?" }, "subPage": { "loadError": "No se pudo cargar esta métrica ahora mismo.", @@ -3696,7 +3701,8 @@ "valuesBack": "Volver a Insights", "valuesBackToMetric": "Volver a {metric}", "scoresBack": "Volver a Insights", - "ecgDescription": "Registros de ECG de una derivación de tu dispositivo, cada uno con el resultado del propio dispositivo." + "ecgDescription": "Registros de ECG de una derivación de tu dispositivo, cada uno con el resultado del propio dispositivo.", + "emptyCoachPrefill": "{metric}: aún no he registrado nada aquí. ¿Qué dice esta métrica sobre mi salud y cómo la mejoro?" }, "units": { "steps": "pasos", @@ -3796,27 +3802,32 @@ "bloodPressure": { "title": "Aún no hay registros de presión arterial", "description": "Registra una medición de presión y aquí aparecerán la tendencia, el porcentaje en rango y la clasificación de la OMS.", - "cta": "Registrar presión" + "cta": "Registrar presión", + "coachPrefill": "Aún no he registrado la presión arterial: ¿por qué importa y qué debería saber antes de empezar?" }, "weight": { "title": "Aún no hay registros de peso", "description": "Registra un peso y aparecerán automáticamente la tendencia, la media móvil y la zona objetivo.", - "cta": "Registrar peso" + "cta": "Registrar peso", + "coachPrefill": "Aún no he registrado mi peso: ¿por qué importa y qué debería saber antes de empezar a registrarlo?" }, "pulse": { "title": "Aún no hay registros de pulso", "description": "Registra un pulso y aquí aparecerán pulso en reposo, VFC y VO₂ máx.", - "cta": "Registrar pulso" + "cta": "Registrar pulso", + "coachPrefill": "Aún no he registrado el pulso en reposo: ¿por qué importa y qué debería saber antes de empezar?" }, "bmi": { "title": "IMC aún no calculado", "description": "Cuando haya altura y al menos un registro de peso, aquí aparecerá la tendencia del IMC con las zonas de la OMS.", - "cta": "Registrar peso" + "cta": "Registrar peso", + "coachPrefill": "Aún no he registrado mi peso: ¿por qué me importa el IMC y qué debería saber antes de empezar a seguirlo?" }, "mood": { "title": "Aún no hay entradas de estado de ánimo", "description": "Registra un estado de ánimo y aparecerán la tendencia, la banda de estabilidad y las correlaciones.", - "cta": "Registrar ánimo" + "cta": "Registrar ánimo", + "coachPrefill": "Aún no he registrado mi estado de ánimo: ¿por qué importa registrarlo y cómo debería empezar?" }, "medication": { "title": "Aún no hay medicamentos configurados", @@ -3826,7 +3837,8 @@ "sleep": { "title": "Aún no hay registros de sueño", "description": "Una vez disponibles los datos de sueño (manual o vía Withings / Apple Health), aquí aparecerá el desglose por fases.", - "cta": "Conectar una fuente" + "cta": "Conectar una fuente", + "coachPrefill": "Aún no tengo datos de sueño: ¿por qué importa registrar el sueño y qué debería saber antes de conectar una fuente?" } }, "navWorkouts": "Entrenamientos", @@ -3836,7 +3848,8 @@ "description": "Carreras, salidas en bici, paseos y sesiones de fuerza recientes, deduplicados entre Apple Watch y Withings.", "emptyState": { "title": "Aún no hay entrenamientos", - "description": "Los entrenamientos llegan desde Apple Salud en el iPhone y desde Withings. También puedes registrar uno a mano." + "description": "Los entrenamientos llegan desde Apple Salud en el iPhone y desde Withings. También puedes registrar uno a mano.", + "coachPrefill": "Aún no he registrado entrenamientos: ¿por qué importa registrarlos y en qué debería centrarme primero?" }, "detail": { "backToList": "Volver a entrenamientos", @@ -4716,7 +4729,8 @@ "emptyState": { "title": "Aún no hay datos de caídas", "description": "Las caídas detectadas aparecen aquí cuando Apple Health se sincroniza. Cero caídas es el objetivo." - } + }, + "coachPrefill": "Aún no he registrado datos de caídas: ¿qué dice esta métrica sobre mi salud y cómo reduzco mi riesgo de caídas?" }, "navFalls": "Caídas", "sixMinuteWalk": { diff --git a/messages/fr.json b/messages/fr.json index f9885e3c5..e0cec9458 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -3554,7 +3554,8 @@ "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." - } + }, + "coachPrefill": "Je n’ai pas encore enregistré de données de VFC. Que m’apprend la variabilité de la fréquence cardiaque, et comment la mesurer ?" }, "restingHr": { "title": "FC au repos", @@ -3573,7 +3574,8 @@ "emptyState": { "title": "Aucune mesure SpO₂", "description": "Les mesures SpO₂ apparaîtront dès qu'un wearable compatible (Apple Watch, Withings ScanWatch) en capturera une." - } + }, + "coachPrefill": "Je n’ai pas encore enregistré de saturation en oxygène. Que m’apprend la SpO₂, et quelle plage viser ?" }, "bodyTemperature": { "title": "Température corporelle", @@ -3583,7 +3585,8 @@ "title": "Aucune température corporelle", "description": "Enregistrez une mesure ou laissez votre wearable en synchroniser une, et la tendance apparaîtra ici.", "cta": "Enregistrer la température" - } + }, + "coachPrefill": "Je n’ai pas encore enregistré de température corporelle. Quelle est la plage normale, et que faire si elle s’en écarte ?" }, "steps": { "title": "Pas", @@ -3592,7 +3595,8 @@ "emptyState": { "title": "Aucune donnée de pas", "description": "Les totaux de pas quotidiens apparaîtront ici dès qu'Apple Santé ou un appareil d'activité synchronise une journée complète." - } + }, + "coachPrefill": "Je n’ai pas encore enregistré de pas. Quel objectif quotidien de pas est raisonnable, et en quoi marcher davantage aide-t-il ma santé ?" }, "activeEnergy": { "title": "Énergie active", @@ -3601,7 +3605,8 @@ "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." - } + }, + "coachPrefill": "Je n’ai pas encore enregistré d’énergie active. Quel objectif quotidien est raisonnable, et comment se compare-t-il aux calories totales ?" }, "subPage": { "loadError": "Impossible de charger cette mesure pour le moment.", @@ -3696,7 +3701,8 @@ "valuesBack": "Retour aux Insights", "valuesBackToMetric": "Retour à {metric}", "scoresBack": "Retour aux Insights", - "ecgDescription": "Enregistrements ECG à dérivation unique de votre appareil, chacun accompagné du résultat de l'appareil." + "ecgDescription": "Enregistrements ECG à dérivation unique de votre appareil, chacun accompagné du résultat de l'appareil.", + "emptyCoachPrefill": "{metric} : je n’ai encore rien enregistré ici. Que dit cette mesure sur ma santé, et comment l’améliorer ?" }, "units": { "steps": "pas", @@ -3796,27 +3802,32 @@ "bloodPressure": { "title": "Aucune mesure de tension artérielle", "description": "Enregistrez une mesure de tension : la tendance, la part dans la cible et la classification OMS s'afficheront ici.", - "cta": "Enregistrer une tension" + "cta": "Enregistrer une tension", + "coachPrefill": "Je n’ai pas encore enregistré de tension artérielle. Pourquoi est-ce important, et que dois-je savoir avant de commencer ?" }, "weight": { "title": "Aucune mesure de poids", "description": "Enregistrez un poids : la tendance, la moyenne glissante et la zone cible apparaîtront automatiquement.", - "cta": "Enregistrer un poids" + "cta": "Enregistrer un poids", + "coachPrefill": "Je n’ai pas encore enregistré de poids. Pourquoi est-ce important, et que dois-je savoir avant de commencer le suivi ?" }, "pulse": { "title": "Aucune mesure de pouls", "description": "Enregistrez un pouls : pouls au repos, VFC et VO₂ max apparaîtront dans cette vue.", - "cta": "Enregistrer un pouls" + "cta": "Enregistrer un pouls", + "coachPrefill": "Je n’ai pas encore enregistré de pouls au repos. Pourquoi est-ce important, et que dois-je savoir avant de commencer ?" }, "bmi": { "title": "IMC pas encore calculé", "description": "Dès qu'une taille et au moins un poids sont enregistrés, la tendance IMC avec les zones OMS s'affichera ici.", - "cta": "Enregistrer un poids" + "cta": "Enregistrer un poids", + "coachPrefill": "Je n’ai pas encore enregistré de poids. Pourquoi l’IMC compte-t-il pour moi, et que dois-je savoir avant de le suivre ?" }, "mood": { "title": "Aucune entrée d'humeur", "description": "Enregistrez une humeur : la tendance, la bande de stabilité et les corrélations s'afficheront.", - "cta": "Enregistrer une humeur" + "cta": "Enregistrer une humeur", + "coachPrefill": "Je n’ai pas encore enregistré d’humeur. Pourquoi suivre mon humeur, et comment commencer ?" }, "medication": { "title": "Aucun médicament configuré", @@ -3826,7 +3837,8 @@ "sleep": { "title": "Aucune donnée de sommeil", "description": "Dès que des données de sommeil sont disponibles (manuel ou via Withings / Apple Health), la répartition par stade apparaîtra ici.", - "cta": "Connecter une source" + "cta": "Connecter une source", + "coachPrefill": "Je n’ai pas encore de données de sommeil. Pourquoi suivre mon sommeil, et que dois-je savoir avant de connecter une source ?" } }, "navWorkouts": "Entraînements", @@ -3836,7 +3848,8 @@ "description": "Courses, sorties à vélo, marches et séances de musculation récentes, dédupliquées entre Apple Watch et Withings.", "emptyState": { "title": "Aucun entraînement", - "description": "Vos entraînements arrivent d'Apple Santé sur l'iPhone et de Withings. Vous pouvez aussi en saisir un à la main." + "description": "Vos entraînements arrivent d'Apple Santé sur l'iPhone et de Withings. Vous pouvez aussi en saisir un à la main.", + "coachPrefill": "Je n’ai pas encore enregistré d’entraînement. Pourquoi les suivre, et sur quoi me concentrer en premier ?" }, "detail": { "backToList": "Retour aux entraînements", @@ -4716,7 +4729,8 @@ "emptyState": { "title": "Pas encore de données de chutes", "description": "Les chutes détectées apparaissent ici dès qu'Apple Santé se synchronise. L'objectif est zéro chute." - } + }, + "coachPrefill": "Je n’ai pas encore enregistré de données de chute. Que dit cette mesure sur ma santé, et comment réduire mon risque de chute ?" }, "navFalls": "Chutes", "sixMinuteWalk": { diff --git a/messages/it.json b/messages/it.json index f78033c05..e4a3c7425 100644 --- a/messages/it.json +++ b/messages/it.json @@ -3554,7 +3554,8 @@ "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." - } + }, + "coachPrefill": "Non ho ancora registrato dati HRV. Cosa mi dice la variabilità della frequenza cardiaca e come la rilevo?" }, "restingHr": { "title": "FC a riposo", @@ -3573,7 +3574,8 @@ "emptyState": { "title": "Ancora nessun campione SpO₂", "description": "I campioni SpO₂ appariranno qui non appena un wearable compatibile (Apple Watch, Withings ScanWatch) cattura una lettura." - } + }, + "coachPrefill": "Non ho ancora registrato la saturazione di ossigeno. Cosa mi dice la SpO₂ e a quale intervallo dovrei puntare?" }, "bodyTemperature": { "title": "Temperatura corporea", @@ -3583,7 +3585,8 @@ "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" - } + }, + "coachPrefill": "Non ho ancora registrato la temperatura corporea. Qual è l’intervallo sano e cosa dovrei fare se si discosta?" }, "steps": { "title": "Passi", @@ -3592,7 +3595,8 @@ "emptyState": { "title": "Ancora nessun dato sui passi", "description": "I totali giornalieri dei passi appariranno qui non appena Apple Salute o un dispositivo di attività sincronizza un'intera giornata." - } + }, + "coachPrefill": "Non ho ancora registrato passi. Qual è un obiettivo giornaliero di passi ragionevole e in che modo camminare di più aiuta la mia salute?" }, "activeEnergy": { "title": "Energia attiva", @@ -3601,7 +3605,8 @@ "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." - } + }, + "coachPrefill": "Non ho ancora registrato energia attiva. Qual è un obiettivo giornaliero ragionevole e come si confronta con le calorie totali?" }, "subPage": { "loadError": "Impossibile caricare questa metrica al momento.", @@ -3696,7 +3701,8 @@ "valuesBack": "Torna a Insights", "valuesBackToMetric": "Torna a {metric}", "scoresBack": "Torna a Insights", - "ecgDescription": "Registrazioni ECG a singola derivazione dal tuo dispositivo, ciascuna con il risultato del dispositivo." + "ecgDescription": "Registrazioni ECG a singola derivazione dal tuo dispositivo, ciascuna con il risultato del dispositivo.", + "emptyCoachPrefill": "{metric}: non ho ancora registrato nulla qui. Cosa dice questo valore sulla mia salute e come lo miglioro?" }, "units": { "steps": "passi", @@ -3796,27 +3802,32 @@ "bloodPressure": { "title": "Nessuna misurazione della pressione", "description": "Registra una misurazione e qui appariranno l'andamento, la percentuale in target e la classificazione OMS.", - "cta": "Registra pressione" + "cta": "Registra pressione", + "coachPrefill": "Non ho ancora registrato la pressione arteriosa. Perché è importante e cosa dovrei sapere prima di iniziare?" }, "weight": { "title": "Nessuna misurazione del peso", "description": "Registra un peso: andamento, media mobile e fascia obiettivo appariranno automaticamente.", - "cta": "Registra peso" + "cta": "Registra peso", + "coachPrefill": "Non ho ancora registrato il peso. Perché è importante e cosa dovrei sapere prima di iniziare a monitorarlo?" }, "pulse": { "title": "Nessuna misurazione del polso", "description": "Registra una misurazione: polso a riposo, HRV e VO₂ max appariranno in questa vista.", - "cta": "Registra polso" + "cta": "Registra polso", + "coachPrefill": "Non ho ancora registrato il polso a riposo. Perché è importante e cosa dovrei sapere prima di iniziare?" }, "bmi": { "title": "BMI non ancora calcolato", "description": "Una volta inseriti altezza e almeno un peso, qui apparirà l'andamento del BMI con le zone OMS.", - "cta": "Registra peso" + "cta": "Registra peso", + "coachPrefill": "Non ho ancora registrato il peso. Perché il BMI conta per me e cosa dovrei sapere prima di iniziare a monitorarlo?" }, "mood": { "title": "Nessuna voce d'umore", "description": "Registra un umore: appariranno andamento, fascia di stabilità e correlazioni.", - "cta": "Registra umore" + "cta": "Registra umore", + "coachPrefill": "Non ho ancora registrato l’umore. Perché conta monitorarlo e come dovrei iniziare?" }, "medication": { "title": "Nessun farmaco configurato", @@ -3826,7 +3837,8 @@ "sleep": { "title": "Nessuna voce di sonno", "description": "Quando saranno disponibili dati sul sonno (manuale o tramite Withings / Apple Health), qui apparirà la suddivisione per fase.", - "cta": "Collega una fonte" + "cta": "Collega una fonte", + "coachPrefill": "Non ho ancora dati sul sonno. Perché conta monitorare il sonno e cosa dovrei sapere prima di collegare una fonte?" } }, "navWorkouts": "Allenamenti", @@ -3836,7 +3848,8 @@ "description": "Corse, uscite in bici, camminate e sessioni di forza recenti, deduplicati tra Apple Watch e Withings.", "emptyState": { "title": "Nessun allenamento", - "description": "Gli allenamenti arrivano da Salute di Apple sull'iPhone e da Withings. Puoi anche registrarne uno a mano." + "description": "Gli allenamenti arrivano da Salute di Apple sull'iPhone e da Withings. Puoi anche registrarne uno a mano.", + "coachPrefill": "Non ho ancora registrato allenamenti. Perché conta monitorarli e su cosa dovrei concentrarmi per primo?" }, "detail": { "backToList": "Torna agli allenamenti", @@ -4716,7 +4729,8 @@ "emptyState": { "title": "Nessun dato sulle cadute", "description": "Le cadute rilevate compaiono qui quando Apple Health si sincronizza. Nessuna caduta è l'obiettivo." - } + }, + "coachPrefill": "Non ho ancora registrato dati sulle cadute. Cosa dice questo valore sulla mia salute e come riduco il rischio di cadute?" }, "navFalls": "Cadute", "sixMinuteWalk": { diff --git a/messages/ko.json b/messages/ko.json index c62750df2..84a15bb40 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -3554,7 +3554,8 @@ "emptyState": { "title": "아직 심박변이도 기록이 없어요", "description": "Apple Watch(또는 호환 Withings 기기)가 밤마다 측정값을 동기화하기 시작하면 심박변이도 기록이 여기에 쌓여요." - } + }, + "coachPrefill": "아직 심박변이도 데이터를 기록한 적이 없어요. 심박변이도는 무엇을 알려주고, 어떻게 측정하나요?" }, "restingHr": { "title": "안정 시 심박수", @@ -3573,7 +3574,8 @@ "emptyState": { "title": "아직 SpO₂ 기록이 없어요", "description": "호환 웨어러블(Apple Watch, Withings ScanWatch)이 측정하면 SpO₂ 기록이 여기에 쌓여요." - } + }, + "coachPrefill": "아직 산소포화도를 기록한 적이 없어요. SpO₂는 무엇을 알려주고, 어느 범위를 목표로 해야 하나요?" }, "bodyTemperature": { "title": "체온", @@ -3583,7 +3585,8 @@ "title": "아직 체온 기록이 없어요", "description": "체온을 기록하거나 웨어러블이 동기화하면 여기에 추이가 나타나요.", "cta": "체온 기록" - } + }, + "coachPrefill": "아직 체온을 기록한 적이 없어요. 건강한 범위는 어느 정도이고, 벗어나면 어떻게 해야 하나요?" }, "steps": { "title": "걸음 수", @@ -3592,7 +3595,8 @@ "emptyState": { "title": "아직 걸음 수 데이터가 없어요", "description": "Apple Health나 활동 기기가 하루치 움직임을 동기화하면 여기에 하루 걸음 수가 나타나요." - } + }, + "coachPrefill": "아직 걸음 수를 기록한 적이 없어요. 적당한 하루 걸음 목표는 얼마이고, 더 많이 걸으면 건강에 어떻게 도움이 되나요?" }, "activeEnergy": { "title": "활동 에너지", @@ -3601,7 +3605,8 @@ "emptyState": { "title": "아직 활동 에너지 데이터가 없어요", "description": "Apple Health나 Withings 활동 기기가 하루치 움직임을 동기화하면 여기에 활동 에너지 합계가 나타나요." - } + }, + "coachPrefill": "아직 활동 에너지를 기록한 적이 없어요. 적당한 하루 목표는 얼마이고, 총 칼로리와는 어떻게 비교되나요?" }, "subPage": { "loadError": "지금은 이 지표를 불러오지 못했어요.", @@ -3696,7 +3701,8 @@ "valuesBack": "인사이트로 돌아가기", "valuesBackToMetric": "돌아가기: {metric}", "scoresBack": "인사이트로 돌아가기", - "ecgDescription": "기기에서 기록한 단일 유도 ECG로, 각각 기기가 낸 결과가 함께 있어요." + "ecgDescription": "기기에서 기록한 단일 유도 ECG로, 각각 기기가 낸 결과가 함께 있어요.", + "emptyCoachPrefill": "{metric}: 아직 여기에 기록한 게 없어요. 이 지표는 내 건강에 대해 무엇을 알려주고, 어떻게 개선하나요?" }, "units": { "steps": "걸음", @@ -3796,27 +3802,32 @@ "bloodPressure": { "title": "아직 혈압 기록이 없어요", "description": "혈압을 기록하면 추이 차트, 정상 범위 비율, WHO 분류가 여기에 나타나요.", - "cta": "혈압 기록" + "cta": "혈압 기록", + "coachPrefill": "아직 혈압을 기록한 적이 없어요. 혈압이 왜 중요하고, 시작하기 전에 무엇을 알아야 하나요?" }, "weight": { "title": "아직 체중 기록이 없어요", "description": "체중을 기록하면 추이, 이동 평균, 목표 구간이 자동으로 나타나요.", - "cta": "체중 기록" + "cta": "체중 기록", + "coachPrefill": "아직 체중을 기록한 적이 없어요. 체중이 왜 중요하고, 기록을 시작하기 전에 무엇을 알아야 하나요?" }, "pulse": { "title": "아직 맥박 기록이 없어요", "description": "맥박을 기록하면 안정 시 맥박, 심박변이도, VO₂ max가 이 화면에 나타나요.", - "cta": "맥박 기록" + "cta": "맥박 기록", + "coachPrefill": "아직 안정 시 맥박을 기록한 적이 없어요. 맥박이 왜 중요하고, 시작하기 전에 무엇을 알아야 하나요?" }, "bmi": { "title": "아직 BMI를 계산하지 못했어요", "description": "키와 체중 기록이 하나 이상 있으면 WHO 구간과 함께 BMI 추이가 여기에 나타나요.", - "cta": "체중 기록" + "cta": "체중 기록", + "coachPrefill": "아직 체중을 기록한 적이 없어요. BMI가 내게 왜 중요하고, 기록을 시작하기 전에 무엇을 알아야 하나요?" }, "mood": { "title": "아직 기분 기록이 없어요", "description": "기분을 기록하면 추이, 안정성 구간, 상관관계가 나타나요.", - "cta": "기분 기록" + "cta": "기분 기록", + "coachPrefill": "아직 기분을 기록한 적이 없어요. 기분 기록이 왜 중요하고, 어떻게 시작하면 좋을까요?" }, "medication": { "title": "아직 등록한 약이 없어요", @@ -3826,7 +3837,8 @@ "sleep": { "title": "아직 수면 기록이 없어요", "description": "수면 데이터가 들어오면(직접 기록하거나 Withings / Apple Health를 통해) 밤별 단계 구성이 여기에 나타나요.", - "cta": "소스 연결" + "cta": "소스 연결", + "coachPrefill": "아직 수면 데이터가 없어요. 수면 기록이 왜 중요하고, 데이터 소스를 연결하기 전에 무엇을 알아야 하나요?" } }, "navWorkouts": "운동", @@ -3836,7 +3848,8 @@ "description": "최근 달리기, 자전거, 걷기, 근력 운동 세션이에요. Apple Watch와 Withings 소스 사이 중복은 정리했어요.", "emptyState": { "title": "아직 운동 기록이 없어요", - "description": "운동은 iPhone의 Apple Health와 Withings에서 동기화돼요. 직접 기록할 수도 있어요." + "description": "운동은 iPhone의 Apple Health와 Withings에서 동기화돼요. 직접 기록할 수도 있어요.", + "coachPrefill": "아직 운동을 기록한 적이 없어요. 운동 기록이 왜 중요하고, 무엇부터 신경 써야 하나요?" }, "detail": { "backToList": "운동으로 돌아가기", @@ -4716,7 +4729,8 @@ "emptyState": { "title": "아직 낙상 데이터가 없어요", "description": "Apple Health가 동기화하면 감지된 낙상이 여기에 표시돼요. 낙상이 없는 게 가장 좋아요." - } + }, + "coachPrefill": "아직 낙상 데이터를 기록한 적이 없어요. 이 지표는 내 건강에 대해 무엇을 알려주고, 낙상 위험은 어떻게 줄이나요?" }, "navFalls": "낙상", "sixMinuteWalk": { diff --git a/messages/nl.json b/messages/nl.json index da1aa2e27..feadb4d7a 100644 --- a/messages/nl.json +++ b/messages/nl.json @@ -3554,7 +3554,8 @@ "emptyState": { "title": "Nog geen HRV-metingen", "description": "HRV-metingen verschijnen hier zodra Apple Watch (of een compatibel Withings-apparaat) nachtelijke metingen begint te synchroniseren." - } + }, + "coachPrefill": "Ik heb nog geen HRV-gegevens vastgelegd. Wat zegt hartslagvariabiliteit me, en hoe leg ik die vast?" }, "restingHr": { "title": "Rusthartslag", @@ -3573,7 +3574,8 @@ "emptyState": { "title": "Nog geen SpO₂-metingen", "description": "SpO₂-metingen verschijnen hier zodra een compatibele wearable (Apple Watch, Withings ScanWatch) een meting vastlegt." - } + }, + "coachPrefill": "Ik heb nog geen zuurstofverzadiging vastgelegd. Wat zegt SpO₂ me, en naar welk bereik moet ik streven?" }, "bodyTemperature": { "title": "Lichaamstemperatuur", @@ -3583,7 +3585,8 @@ "title": "Nog geen lichaamstemperatuur", "description": "Leg een temperatuurmeting vast of laat je wearable er een synchroniseren, dan verschijnt de trend hier.", "cta": "Lichaamstemperatuur vastleggen" - } + }, + "coachPrefill": "Ik heb nog geen lichaamstemperatuur vastgelegd. Wat is het gezonde bereik, en wat moet ik doen als die afwijkt?" }, "steps": { "title": "Stappen", @@ -3592,7 +3595,8 @@ "emptyState": { "title": "Nog geen stappengegevens", "description": "Dagtotalen van je stappen verschijnen hier zodra Apple Health of een activiteitsapparaat een volledige dag beweging synchroniseert." - } + }, + "coachPrefill": "Ik heb nog geen stappen vastgelegd. Wat is een redelijk dagelijks stappendoel, en hoe helpt meer lopen mijn gezondheid?" }, "activeEnergy": { "title": "Actieve energie", @@ -3601,7 +3605,8 @@ "emptyState": { "title": "Nog geen gegevens over actieve energie", "description": "Dagtotalen van actieve energie verschijnen hier zodra Apple Health of een Withings-activiteitsapparaat een volledige dag beweging synchroniseert." - } + }, + "coachPrefill": "Ik heb nog geen actieve energie vastgelegd. Wat is een redelijk dagdoel, en hoe verhoudt het zich tot het totale aantal calorieën?" }, "subPage": { "loadError": "Deze meetwaarde kan nu niet worden geladen.", @@ -3696,7 +3701,8 @@ "valuesBack": "Terug naar inzichten", "valuesBackToMetric": "Terug naar {metric}", "scoresBack": "Terug naar inzichten", - "ecgDescription": "Eenkanaals-ECG-opnamen van je apparaat, elk met de eigen uitslag van het apparaat." + "ecgDescription": "Eenkanaals-ECG-opnamen van je apparaat, elk met de eigen uitslag van het apparaat.", + "emptyCoachPrefill": "{metric}: ik heb hier nog niets vastgelegd. Wat zegt deze waarde over mijn gezondheid, en hoe verbeter ik die?" }, "units": { "steps": "stappen", @@ -3796,27 +3802,32 @@ "bloodPressure": { "title": "Nog geen bloeddrukmetingen", "description": "Leg een bloeddrukmeting vast, dan verschijnen hier de trendgrafiek, het aandeel binnen het bereik en de WHO-classificatie.", - "cta": "Bloeddruk vastleggen" + "cta": "Bloeddruk vastleggen", + "coachPrefill": "Ik heb nog geen bloeddruk vastgelegd. Waarom is die belangrijk, en wat moet ik weten voordat ik begin?" }, "weight": { "title": "Nog geen gewichtsmetingen", "description": "Leg je gewicht vast, dan verschijnen de trend, het voortschrijdend gemiddelde en de doelzone automatisch.", - "cta": "Gewicht vastleggen" + "cta": "Gewicht vastleggen", + "coachPrefill": "Ik heb nog geen gewicht vastgelegd. Waarom is dat belangrijk, en wat moet ik weten voordat ik het ga bijhouden?" }, "pulse": { "title": "Nog geen hartslagmetingen", "description": "Leg een hartslagmeting vast, dan verschijnen rusthartslag, HRV en VO₂max in deze weergave.", - "cta": "Hartslag vastleggen" + "cta": "Hartslag vastleggen", + "coachPrefill": "Ik heb nog geen rustpols vastgelegd. Waarom is die belangrijk, en wat moet ik weten voordat ik begin?" }, "bmi": { "title": "BMI nog niet berekend", "description": "Zodra je lengte en minstens één gewichtsmeting bekend zijn, verschijnt hier de BMI-trend met de WHO-zones.", - "cta": "Gewicht vastleggen" + "cta": "Gewicht vastleggen", + "coachPrefill": "Ik heb nog geen gewicht vastgelegd. Waarom is BMI belangrijk voor mij, en wat moet ik weten voordat ik het ga bijhouden?" }, "mood": { "title": "Nog geen stemmingsregistraties", "description": "Leg je stemming vast, dan verschijnen de trend, de stabiliteitszone en de correlaties.", - "cta": "Stemming vastleggen" + "cta": "Stemming vastleggen", + "coachPrefill": "Ik heb nog geen stemming vastgelegd. Waarom is stemming bijhouden belangrijk, en hoe begin ik?" }, "medication": { "title": "Nog geen medicatie ingesteld", @@ -3826,7 +3837,8 @@ "sleep": { "title": "Nog geen slaapregistraties", "description": "Zodra er slaapgegevens binnenkomen (handmatig of via Withings / Apple Health), verschijnt hier de verdeling over slaapfasen per nacht.", - "cta": "Bron koppelen" + "cta": "Bron koppelen", + "coachPrefill": "Ik heb nog geen slaapgegevens. Waarom is slaap bijhouden belangrijk, en wat moet ik weten voordat ik een bron koppel?" } }, "navWorkouts": "Workouts", @@ -3836,7 +3848,8 @@ "description": "Recente hardloop-, fiets-, wandel- en krachttrainingen, ontdubbeld over bronnen van Apple Watch en Withings.", "emptyState": { "title": "Nog geen workouts", - "description": "Workouts worden gesynchroniseerd vanuit Apple Health op je iPhone en vanuit Withings. Je kunt er ook zelf een vastleggen." + "description": "Workouts worden gesynchroniseerd vanuit Apple Health op je iPhone en vanuit Withings. Je kunt er ook zelf een vastleggen.", + "coachPrefill": "Ik heb nog geen workouts vastgelegd. Waarom is bijhouden belangrijk, en waar moet ik me eerst op richten?" }, "detail": { "backToList": "Terug naar workouts", @@ -4716,7 +4729,8 @@ "emptyState": { "title": "Nog geen valgegevens", "description": "Gedetecteerde valpartijen verschijnen hier zodra Apple Health synchroniseert. Geen valpartijen is het doel." - } + }, + "coachPrefill": "Ik heb nog geen valgegevens vastgelegd. Wat zegt deze waarde over mijn gezondheid, en hoe verklein ik mijn valrisico?" }, "navFalls": "Valpartijen", "sixMinuteWalk": { diff --git a/messages/pl.json b/messages/pl.json index 854b541fe..a2bb28063 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -3554,7 +3554,8 @@ "emptyState": { "title": "Brak wartości HRV", "description": "Wartości HRV pojawią się tutaj, gdy Apple Watch (lub zgodne urządzenie Withings) zsynchronizuje pomiary nocne." - } + }, + "coachPrefill": "Nie zapisałem jeszcze danych HRV. Co mówi mi zmienność rytmu serca i jak ją rejestrować?" }, "restingHr": { "title": "Tętno spoczynkowe", @@ -3573,7 +3574,8 @@ "emptyState": { "title": "Brak próbek SpO₂", "description": "Próbki SpO₂ trafią tutaj, gdy zgodne wearable (Apple Watch, Withings ScanWatch) wykona pomiar." - } + }, + "coachPrefill": "Nie zapisałem jeszcze saturacji tlenu. Co mówi mi SpO₂ i do jakiego zakresu powinienem dążyć?" }, "bodyTemperature": { "title": "Temperatura ciała", @@ -3583,7 +3585,8 @@ "title": "Brak temperatury ciała", "description": "Zarejestruj pomiar lub pozwól wearable zsynchronizować jeden, a trend pojawi się tutaj.", "cta": "Zarejestruj temperaturę ciała" - } + }, + "coachPrefill": "Nie zapisałem jeszcze temperatury ciała. Jaki zakres jest prawidłowy i co powinienem zrobić, gdy od niego odbiega?" }, "steps": { "title": "Kroki", @@ -3592,7 +3595,8 @@ "emptyState": { "title": "Brak danych o krokach", "description": "Dzienne sumy kroków pojawią się tutaj, gdy Apple Zdrowie lub urządzenie aktywności zsynchronizuje pełny dzień." - } + }, + "coachPrefill": "Nie zapisałem jeszcze kroków. Jaki dzienny cel kroków jest rozsądny i jak więcej chodzenia pomaga mojemu zdrowiu?" }, "activeEnergy": { "title": "Energia czynna", @@ -3601,7 +3605,8 @@ "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ń." - } + }, + "coachPrefill": "Nie zapisałem jeszcze energii czynnej. Jaki dzienny cel jest rozsądny i jak ma się on do łącznej liczby kalorii?" }, "subPage": { "loadError": "Nie udało się teraz wczytać tej metryki.", @@ -3696,7 +3701,8 @@ "valuesBack": "Powrót do Insights", "valuesBackToMetric": "Powrót do {metric}", "scoresBack": "Powrót do Insights", - "ecgDescription": "Zapisy EKG jednoodprowadzeniowe z Twojego urządzenia, każdy z wynikiem samego urządzenia." + "ecgDescription": "Zapisy EKG jednoodprowadzeniowe z Twojego urządzenia, każdy z wynikiem samego urządzenia.", + "emptyCoachPrefill": "{metric}: nie zapisałem tu jeszcze niczego. Co ten wskaźnik mówi o moim zdrowiu i jak mogę go poprawić?" }, "units": { "steps": "kroki", @@ -3796,27 +3802,32 @@ "bloodPressure": { "title": "Brak pomiarów ciśnienia krwi", "description": "Zarejestruj pomiar ciśnienia, a pojawi się tu trend, udział w zakresie docelowym i klasyfikacja WHO.", - "cta": "Zarejestruj ciśnienie" + "cta": "Zarejestruj ciśnienie", + "coachPrefill": "Nie zapisałem jeszcze ciśnienia krwi. Dlaczego ma ono znaczenie i co powinienem wiedzieć, zanim zacznę?" }, "weight": { "title": "Brak pomiarów wagi", "description": "Zarejestruj wagę, a trend, średnia krocząca i zakres docelowy pojawią się automatycznie.", - "cta": "Zarejestruj wagę" + "cta": "Zarejestruj wagę", + "coachPrefill": "Nie zapisałem jeszcze masy ciała. Dlaczego ma ona znaczenie i co powinienem wiedzieć, zanim zacznę ją śledzić?" }, "pulse": { "title": "Brak pomiarów tętna", "description": "Zarejestruj tętno, a w tym widoku pojawi się tętno spoczynkowe, HRV i VO₂ max.", - "cta": "Zarejestruj tętno" + "cta": "Zarejestruj tętno", + "coachPrefill": "Nie zapisałem jeszcze tętna spoczynkowego. Dlaczego ma ono znaczenie i co powinienem wiedzieć, zanim zacznę?" }, "bmi": { "title": "BMI jeszcze nie obliczone", "description": "Po wpisaniu wzrostu i co najmniej jednej wagi pojawi się tu trend BMI z zakresami WHO.", - "cta": "Zarejestruj wagę" + "cta": "Zarejestruj wagę", + "coachPrefill": "Nie zapisałem jeszcze masy ciała. Dlaczego BMI ma dla mnie znaczenie i co powinienem wiedzieć, zanim zacznę je śledzić?" }, "mood": { "title": "Brak wpisów nastroju", "description": "Zarejestruj nastrój, a pojawi się trend, pasmo stabilności i korelacje.", - "cta": "Zarejestruj nastrój" + "cta": "Zarejestruj nastrój", + "coachPrefill": "Nie zapisałem jeszcze nastroju. Dlaczego warto go śledzić i jak powinienem zacząć?" }, "medication": { "title": "Brak skonfigurowanych leków", @@ -3826,7 +3837,8 @@ "sleep": { "title": "Brak danych o śnie", "description": "Gdy pojawią się dane o śnie (ręcznie lub przez Withings / Apple Health), wyświetli się tu rozkład faz na noc.", - "cta": "Połącz źródło" + "cta": "Połącz źródło", + "coachPrefill": "Nie mam jeszcze danych o śnie. Dlaczego warto śledzić sen i co powinienem wiedzieć, zanim podłączę źródło?" } }, "navWorkouts": "Treningi", @@ -3836,7 +3848,8 @@ "description": "Ostatnie biegi, jazdy, marsze i treningi siłowe, zdeduplikowane między Apple Watch a Withings.", "emptyState": { "title": "Brak treningów", - "description": "Treningi trafiają tu z Apple Zdrowie na iPhonie i z Withings. Możesz też wpisać trening ręcznie." + "description": "Treningi trafiają tu z Apple Zdrowie na iPhonie i z Withings. Możesz też wpisać trening ręcznie.", + "coachPrefill": "Nie zapisałem jeszcze treningów. Dlaczego warto je śledzić i na czym powinienem się najpierw skupić?" }, "detail": { "backToList": "Powrót do treningów", @@ -4716,7 +4729,8 @@ "emptyState": { "title": "Brak danych o upadkach", "description": "Wykryte upadki pojawią się tutaj, gdy Apple Health się zsynchronizuje. Celem jest brak upadków." - } + }, + "coachPrefill": "Nie zapisałem jeszcze danych o upadkach. Co ten wskaźnik mówi o moim zdrowiu i jak mogę zmniejszyć ryzyko upadku?" }, "navFalls": "Upadki", "sixMinuteWalk": { diff --git a/messages/pt-BR.json b/messages/pt-BR.json index bf5265f6e..412d3efb4 100644 --- a/messages/pt-BR.json +++ b/messages/pt-BR.json @@ -3554,7 +3554,8 @@ "emptyState": { "title": "Ainda não há amostras de VFC", "description": "As amostras de VFC chegam aqui assim que o Apple Watch (ou um dispositivo Withings compatível) começa a sincronizar leituras noturnas." - } + }, + "coachPrefill": "Ainda não registrei dados de VFC. O que a variabilidade da frequência cardíaca me diz, e como eu a registro?" }, "restingHr": { "title": "Frequência cardíaca em repouso", @@ -3573,7 +3574,8 @@ "emptyState": { "title": "Ainda não há amostras de SpO₂", "description": "As amostras de SpO₂ chegam aqui assim que um wearable compatível (Apple Watch, Withings ScanWatch) captura uma leitura." - } + }, + "coachPrefill": "Ainda não registrei saturação de oxigênio. O que a SpO₂ me diz, e qual faixa devo buscar?" }, "bodyTemperature": { "title": "Temperatura corporal", @@ -3583,7 +3585,8 @@ "title": "Ainda não há temperatura corporal", "description": "Registre uma leitura de temperatura corporal ou deixe seu wearable sincronizar uma, e a tendência aparecerá aqui.", "cta": "Registrar temperatura corporal" - } + }, + "coachPrefill": "Ainda não registrei temperatura corporal. Qual é a faixa saudável, e o que devo fazer se ela se desviar?" }, "steps": { "title": "Passos", @@ -3592,7 +3595,8 @@ "emptyState": { "title": "Ainda não há dados de passos", "description": "Os totais diários de passos aparecem aqui assim que o Apple Health ou um dispositivo de atividade sincroniza um dia de movimento." - } + }, + "coachPrefill": "Ainda não registrei passos. Qual é uma meta diária de passos razoável, e como caminhar mais ajuda minha saúde?" }, "activeEnergy": { "title": "Energia ativa", @@ -3601,7 +3605,8 @@ "emptyState": { "title": "Ainda não há dados de energia ativa", "description": "Os totais de energia ativa chegam aqui assim que o Apple Health ou um dispositivo de atividade Withings sincroniza um dia de movimento." - } + }, + "coachPrefill": "Ainda não registrei energia ativa. Qual é uma meta diária razoável, e como ela se compara ao total de calorias?" }, "subPage": { "loadError": "Não foi possível carregar esta métrica agora.", @@ -3696,7 +3701,8 @@ "valuesBack": "Voltar aos insights", "valuesBackToMetric": "Voltar para {metric}", "scoresBack": "Voltar aos insights", - "ecgDescription": "Registros de ECG de uma derivação do seu dispositivo, cada um com o resultado do próprio dispositivo." + "ecgDescription": "Registros de ECG de uma derivação do seu dispositivo, cada um com o resultado do próprio dispositivo.", + "emptyCoachPrefill": "{metric}: ainda não registrei nada aqui. O que essa métrica me diz sobre minha saúde, e como eu a melhoro?" }, "units": { "steps": "passos", @@ -3796,27 +3802,32 @@ "bloodPressure": { "title": "Ainda não há registros de pressão arterial", "description": "Registre uma leitura de pressão arterial e o gráfico de tendência, a parcela dentro da faixa e a classificação da OMS aparecerão aqui.", - "cta": "Registrar pressão arterial" + "cta": "Registrar pressão arterial", + "coachPrefill": "Ainda não registrei a pressão arterial. Por que ela importa, e o que devo saber antes de começar?" }, "weight": { "title": "Ainda não há registros de peso", "description": "Registre um peso e a tendência, a média móvel e a faixa-alvo aparecerão automaticamente.", - "cta": "Registrar peso" + "cta": "Registrar peso", + "coachPrefill": "Ainda não registrei meu peso. Por que ele importa, e o que devo saber antes de começar a acompanhá-lo?" }, "pulse": { "title": "Ainda não há registros de pulso", "description": "Registre uma leitura de pulso e o pulso em repouso, a VFC e o VO₂ máx aparecerão nesta visão.", - "cta": "Registrar pulso" + "cta": "Registrar pulso", + "coachPrefill": "Ainda não registrei o pulso em repouso. Por que ele importa, e o que devo saber antes de começar?" }, "bmi": { "title": "IMC ainda não calculado", "description": "Assim que a altura e pelo menos um registro de peso estiverem presentes, a tendência do IMC com as zonas da OMS aparecerá aqui.", - "cta": "Registrar peso" + "cta": "Registrar peso", + "coachPrefill": "Ainda não registrei meu peso. Por que o IMC importa para mim, e o que devo saber antes de começar a acompanhá-lo?" }, "mood": { "title": "Ainda não há registros de humor", "description": "Registre uma entrada de humor e a tendência, a faixa de estabilidade e as correlações aparecerão.", - "cta": "Registrar humor" + "cta": "Registrar humor", + "coachPrefill": "Ainda não registrei meu humor. Por que acompanhar o humor importa, e como devo começar?" }, "medication": { "title": "Ainda não há medicamentos configurados", @@ -3826,7 +3837,8 @@ "sleep": { "title": "Ainda não há registros de sono", "description": "Assim que os dados de sono chegarem (manual ou via Withings / Apple Health), a divisão por estágio por noite aparecerá aqui.", - "cta": "Conectar uma fonte" + "cta": "Conectar uma fonte", + "coachPrefill": "Ainda não tenho dados de sono. Por que acompanhar o sono importa, e o que devo saber antes de conectar uma fonte?" } }, "navWorkouts": "Treinos", @@ -3836,7 +3848,8 @@ "description": "Corridas, pedaladas, caminhadas e sessões de força recentes, deduplicadas entre as fontes Apple Watch e Withings.", "emptyState": { "title": "Ainda não há treinos", - "description": "Os treinos sincronizam do Apple Health no seu iPhone e da Withings. Você também pode registrar um manualmente." + "description": "Os treinos sincronizam do Apple Health no seu iPhone e da Withings. Você também pode registrar um manualmente.", + "coachPrefill": "Ainda não registrei treinos. Por que acompanhá-los importa, e no que devo focar primeiro?" }, "detail": { "backToList": "Voltar aos treinos", @@ -4716,7 +4729,8 @@ "emptyState": { "title": "Ainda não há dados de quedas", "description": "As quedas detectadas aparecem aqui assim que o Apple Health sincroniza. Nenhuma queda é a meta." - } + }, + "coachPrefill": "Ainda não registrei dados de quedas. O que essa métrica me diz sobre minha saúde, e como reduzo meu risco de queda?" }, "navFalls": "Quedas", "sixMinuteWalk": { diff --git a/messages/ru.json b/messages/ru.json index d207f54f6..9db3347b8 100644 --- a/messages/ru.json +++ b/messages/ru.json @@ -3554,7 +3554,8 @@ "emptyState": { "title": "Пока нет данных HRV", "description": "Данные HRV появятся здесь, как только Apple Watch (или совместимое устройство Withings) начнёт синхронизировать ночные измерения." - } + }, + "coachPrefill": "Я ещё не записывал данные HRV. Что говорит вариабельность сердечного ритма и как её измерять?" }, "restingHr": { "title": "ЧСС в покое", @@ -3573,7 +3574,8 @@ "emptyState": { "title": "Пока нет данных SpO₂", "description": "Данные SpO₂ появятся здесь, как только совместимое носимое устройство (Apple Watch, Withings ScanWatch) сделает измерение." - } + }, + "coachPrefill": "Я ещё не записывал насыщение кислородом. Что говорит SpO₂ и к какому диапазону стоит стремиться?" }, "bodyTemperature": { "title": "Температура тела", @@ -3583,7 +3585,8 @@ "title": "Пока нет температуры тела", "description": "Запишите температуру тела или дайте носимо­му устройству синхронизировать её, и динамика появится здесь.", "cta": "Записать температуру тела" - } + }, + "coachPrefill": "Я ещё не записывал температуру тела. Какой диапазон считается нормальным и что делать, если она отклоняется?" }, "steps": { "title": "Шаги", @@ -3592,7 +3595,8 @@ "emptyState": { "title": "Пока нет данных о шагах", "description": "Ежедневные итоги шагов появятся здесь, как только Apple Health или устройство активности синхронизирует день движения." - } + }, + "coachPrefill": "Я ещё не записывал шаги. Какая дневная цель по шагам разумна и как больше ходьбы помогает здоровью?" }, "activeEnergy": { "title": "Активная энергия", @@ -3601,7 +3605,8 @@ "emptyState": { "title": "Пока нет данных об активной энергии", "description": "Итоги активной энергии появятся здесь, как только Apple Health или устройство активности Withings синхронизирует день движения." - } + }, + "coachPrefill": "Я ещё не записывал активную энергию. Какая дневная цель разумна и как она соотносится с общим расходом калорий?" }, "subPage": { "loadError": "Не удалось загрузить этот показатель прямо сейчас.", @@ -3696,7 +3701,8 @@ "valuesBack": "Назад к аналитике", "valuesBackToMetric": "Назад к {metric}", "scoresBack": "Назад к аналитике", - "ecgDescription": "Записи ЭКГ с одним отведением с вашего устройства, каждая с собственным результатом устройства." + "ecgDescription": "Записи ЭКГ с одним отведением с вашего устройства, каждая с собственным результатом устройства.", + "emptyCoachPrefill": "{metric}: я ещё ничего здесь не записывал. Что этот показатель говорит о моём здоровье и как его улучшить?" }, "units": { "steps": "шаги", @@ -3796,27 +3802,32 @@ "bloodPressure": { "title": "Пока нет записей АД", "description": "Запишите измерение АД, и здесь появятся график динамики, доля значений в диапазоне и классификация ВОЗ.", - "cta": "Записать АД" + "cta": "Записать АД", + "coachPrefill": "Я ещё не записывал давление. Почему оно важно и что нужно знать, прежде чем начать?" }, "weight": { "title": "Пока нет записей веса", "description": "Запишите вес, и динамика, скользящее среднее и целевой диапазон появятся автоматически.", - "cta": "Записать вес" + "cta": "Записать вес", + "coachPrefill": "Я ещё не записывал вес. Почему он важен и что нужно знать, прежде чем начать его отслеживать?" }, "pulse": { "title": "Пока нет записей пульса", "description": "Запишите измерение пульса, и в этом виде появятся пульс в покое, HRV и VO₂ max.", - "cta": "Записать пульс" + "cta": "Записать пульс", + "coachPrefill": "Я ещё не записывал пульс в покое. Почему он важен и что нужно знать, прежде чем начать?" }, "bmi": { "title": "ИМТ ещё не рассчитан", "description": "Как только появятся рост и хотя бы одна запись веса, здесь появится динамика ИМТ с зонами ВОЗ.", - "cta": "Записать вес" + "cta": "Записать вес", + "coachPrefill": "Я ещё не записывал вес. Почему ИМТ важен для меня и что нужно знать, прежде чем начать его отслеживать?" }, "mood": { "title": "Пока нет записей настроения", "description": "Запишите настроение, и появятся динамика, диапазон стабильности и корреляции.", - "cta": "Записать настроение" + "cta": "Записать настроение", + "coachPrefill": "Я ещё не записывал настроение. Почему важно его отслеживать и с чего начать?" }, "medication": { "title": "Пока не настроено ни одного лекарства", @@ -3826,7 +3837,8 @@ "sleep": { "title": "Пока нет записей сна", "description": "Как только появятся данные о сне (вручную или через Withings / Apple Health), здесь появится разбивка по фазам за каждую ночь.", - "cta": "Подключить источник" + "cta": "Подключить источник", + "coachPrefill": "У меня ещё нет данных о сне. Почему важно отслеживать сон и что нужно знать, прежде чем подключить источник?" } }, "navWorkouts": "Тренировки", @@ -3836,7 +3848,8 @@ "description": "Недавние пробежки, поездки на велосипеде, прогулки и силовые занятия, без дублей между источниками Apple Watch и Withings.", "emptyState": { "title": "Пока нет тренировок", - "description": "Тренировки синхронизируются из Apple Health на вашем iPhone и из Withings. Вы также можете записать одну вручную." + "description": "Тренировки синхронизируются из Apple Health на вашем iPhone и из Withings. Вы также можете записать одну вручную.", + "coachPrefill": "Я ещё не записывал тренировки. Почему важно их отслеживать и на чём сосредоточиться в первую очередь?" }, "detail": { "backToList": "Назад к тренировкам", @@ -4716,7 +4729,8 @@ "emptyState": { "title": "Пока нет данных о падениях", "description": "Обнаруженные падения появятся здесь, когда Apple Health синхронизирует их. Цель — ноль падений." - } + }, + "coachPrefill": "Я ещё не записывал данные о падениях. Что этот показатель говорит о моём здоровье и как снизить риск падений?" }, "navFalls": "Падения", "sixMinuteWalk": { diff --git a/src/__tests__/baselines/source-file-lines.json b/src/__tests__/baselines/source-file-lines.json index e49138989..73a08bc66 100644 --- a/src/__tests__/baselines/source-file-lines.json +++ b/src/__tests__/baselines/source-file-lines.json @@ -1,7 +1,7 @@ { "$comment": "Production files over 800 lines and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", "count": 84, - "total": 93833, + "total": 93830, "entries": { "app/medications/page-client.tsx": 839, "components/charts/mood-chart.tsx": 1166, @@ -11,7 +11,7 @@ "components/insights/coach-panel/use-coach.ts": 1486, "components/insights/derived/vitals-dashboard.tsx": 1033, "components/insights/health-score-card.tsx": 1023, - "components/insights/insights-tab-strip.tsx": 1087, + "components/insights/insights-tab-strip.tsx": 1084, "components/measurements/measurement-form.tsx": 815, "components/medications/detail/medication-detail-tabs.tsx": 1157, "components/medications/medication-table.tsx": 853, diff --git a/src/__tests__/baselines/source-function-lines.json b/src/__tests__/baselines/source-function-lines.json index 9b18f7b04..598e55bff 100644 --- a/src/__tests__/baselines/source-function-lines.json +++ b/src/__tests__/baselines/source-function-lines.json @@ -1,7 +1,7 @@ { "$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.", "count": 336, - "total": 109345, + "total": 109328, "entries": { "app/achievements/page.tsx :: AchievementsPage": 206, "app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 275, @@ -27,7 +27,7 @@ "app/api/mcp/oauth/token/route.ts :: POST": 250, "app/api/mcp/oauth/token/route.ts :: POST > withBackgroundEvent(arg)": 248, "app/api/measurement-reminders/[id]/route.ts :: apiHandler(arg) #2": 239, - "app/api/medications/[id]/dose-history/route.ts :: apiHandler(arg)": 256, + "app/api/medications/[id]/dose-history/route.ts :: apiHandler(arg)": 241, "app/api/medications/[id]/inventory/[itemId]/route.ts :: apiHandler(arg)": 204, "app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts :: apiHandler(arg)": 250, "app/api/mental-health/assessments/route.ts :: postAssessment": 249, @@ -80,7 +80,7 @@ "components/cycle/bbt-chart.tsx :: BbtChart": 244, "components/cycle/cycle-calendar.tsx :: CycleCalendar": 292, "components/cycle/cycle-settings.tsx :: CycleSettings": 228, - "components/cycle/cycle-view.tsx :: CycleView": 405, + "components/cycle/cycle-view.tsx :: CycleView": 403, "components/cycle/log-day-sheet.tsx :: LogDaySheet": 753, "components/daily/today-hero.tsx :: TodayHero": 488, "components/dashboard/dashboard-header.tsx :: DashboardHeader": 215, diff --git a/src/app/api/insights/health-status/__tests__/route.test.ts b/src/app/api/insights/health-status/__tests__/route.test.ts new file mode 100644 index 000000000..4f2b428e1 --- /dev/null +++ b/src/app/api/insights/health-status/__tests__/route.test.ts @@ -0,0 +1,113 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +/** + * The health status card reads two engines that walk every vital over up to + * 180 days. The answer is kept in the record's analytics cell: a second open + * of Insights serves it, and a write that sweeps the record's cells, or a + * module switch, computes it again. + */ + +const computeCoincidentDeviation = vi.fn(); +const buildExtendedChangepointSignals = vi.fn(); +const resolveModuleMap = vi.fn(); + +vi.mock("@/lib/insights/derived/coincident-deviation", () => ({ + computeCoincidentDeviation: (...args: unknown[]) => + computeCoincidentDeviation(...args), +})); +vi.mock("@/lib/insights/derived/changepoint", () => ({ + buildExtendedChangepointSignals: (...args: unknown[]) => + buildExtendedChangepointSignals(...args), +})); +vi.mock("@/lib/insights/derived", () => ({ + loadBaselineProfile: vi.fn().mockResolvedValue({ ageYears: 40 }), +})); +vi.mock("@/lib/db", () => ({ + prisma: { + auditLog: { create: vi.fn() }, + appSettings: { findUnique: vi.fn().mockResolvedValue(null) }, + }, +})); +vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn() })); +vi.mock("@/lib/auth/audit", () => ({ + auditLog: vi.fn().mockResolvedValue(undefined), +})); +vi.mock("@/lib/modules/gate", async (importOriginal) => ({ + ...(await importOriginal()), + resolveModuleMap: (userId: string) => resolveModuleMap(userId), +})); +vi.mock("@/lib/rate-limit", () => ({ + checkAnalyticsReadRateLimit: vi.fn(), +})); +vi.mock("@/lib/logging/transports", () => ({ emitIfSampled: vi.fn() })); +vi.mock("next/headers", () => ({ + headers: vi.fn(async () => ({ get: () => null })), + cookies: vi.fn(async () => ({ + get: () => undefined, + set: () => {}, + delete: () => {}, + })), +})); + +import { GET } from "../route"; +import { getSession } from "@/lib/auth/session"; +import { checkAnalyticsReadRateLimit } from "@/lib/rate-limit"; +import { __resetAllCachesForTests } from "@/lib/cache/server-cache"; +import { invalidateUserMeasurements } from "@/lib/cache/invalidate"; + +const call = () => GET(); + +beforeEach(() => { + __resetAllCachesForTests(); + vi.mocked(getSession).mockResolvedValue({ + session: { id: "sess-1", expiresAt: new Date(Date.now() + 3_600_000) }, + user: { + id: "user-1", + username: "testuser", + role: "USER", + timezone: "Europe/Berlin", + }, + } as unknown as Awaited>); + vi.mocked(checkAnalyticsReadRateLimit).mockResolvedValue({ + allowed: true, + } as unknown as Awaited>); + computeCoincidentDeviation + .mockReset() + .mockResolvedValue({ status: "insufficient" }); + buildExtendedChangepointSignals.mockReset().mockResolvedValue([]); + resolveModuleMap.mockReset().mockResolvedValue({}); +}); + +describe("GET /api/insights/health-status — the cached answer", () => { + it("computes once for two opens", async () => { + const first = await call(); + const second = await call(); + + expect(first.status).toBe(200); + expect(second.status).toBe(200); + expect(computeCoincidentDeviation).toHaveBeenCalledTimes(1); + expect(buildExtendedChangepointSignals).toHaveBeenCalledTimes(1); + expect((await second.json()).data).toEqual((await first.json()).data); + }); + + it("computes again after a measurement write", async () => { + await call(); + invalidateUserMeasurements("user-1", { evict: true }); + await call(); + + expect(computeCoincidentDeviation).toHaveBeenCalledTimes(2); + }); + + it("computes again when a module is switched off", async () => { + await call(); + resolveModuleMap.mockResolvedValue({ sleep: false }); + await call(); + + expect(buildExtendedChangepointSignals).toHaveBeenCalledTimes(2); + expect(buildExtendedChangepointSignals).toHaveBeenLastCalledWith( + "user-1", + expect.any(Date), + { sleep: false }, + ); + }); +}); diff --git a/src/app/api/insights/health-status/route.ts b/src/app/api/insights/health-status/route.ts index 601c472ee..9c96fb33c 100644 --- a/src/app/api/insights/health-status/route.ts +++ b/src/app/api/insights/health-status/route.ts @@ -24,8 +24,10 @@ import { prisma } from "@/lib/db"; import { loadBaselineProfile } from "@/lib/insights/derived"; import { computeCoincidentDeviation } from "@/lib/insights/derived/coincident-deviation"; import { buildExtendedChangepointSignals } from "@/lib/insights/derived/changepoint"; -import { resolveModuleMap } from "@/lib/modules/gate"; +import { MODULE_KEYS, resolveModuleMap } from "@/lib/modules/gate"; import { summariseHealthStatus } from "@/lib/insights/health-status"; +import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache"; +import { DEFAULT_TIMEZONE, userDayKey } from "@/lib/tz/format"; export const dynamic = "force-dynamic"; @@ -39,36 +41,56 @@ export const GET = apiHandler(async () => { return apiError("Too many analytics requests. Please retry later.", 429); } + // Both engines walk every vital over up to 180 days, so the answer goes + // through the analytics cell its sibling reads use. Measurement and mood + // writes sweep the record's cells; the reader's day and the switched-off + // modules are part of the key, so a new day or a module switch never serves + // the other answer. + const modules = await resolveModuleMap(user.id); + const modulesOff = MODULE_KEYS.filter((key) => modules[key] === false); const now = new Date(); - const profile = await loadBaselineProfile(prisma, user.id); - - const coincident = await computeCoincidentDeviation(user.id, profile, { - tz: user.timezone, - now, - }); - // v1.44 — the wider scan: every vital with a stable daily level over 180 - // days, a metric whose module is off left unread. - const shifts = await buildExtendedChangepointSignals( - user.id, - now, - await resolveModuleMap(user.id), + const day = userDayKey(now, user.timezone ?? DEFAULT_TIMEZONE); + const body = await cachedSwr( + caches.analytics as ServerCache, + `${user.id}|health-status|${day}|off:${modulesOff.join(",")}`, + () => buildHealthStatus(user.id, user.timezone, now, modules), + annotate, ); - const vitals = coincident.status === "ok" ? coincident.value.vitals : []; - const summary = summariseHealthStatus(vitals, shifts); - annotate({ action: { name: "insights.health-status.read" }, meta: { - deviations: summary.deviations.length, - shifts: summary.shifts.length, + deviations: body.deviations.length, + shifts: body.shifts.length, }, }); - return apiSuccess({ + return apiSuccess(body); +}); + +type HealthStatusBody = Awaited>; + +async function buildHealthStatus( + userId: string, + timezone: string | undefined, + now: Date, + modules: Awaited>, +) { + const profile = await loadBaselineProfile(prisma, userId); + const coincident = await computeCoincidentDeviation(userId, profile, { + tz: timezone, + now, + }); + // v1.44 — the wider scan: every vital with a stable daily level over 180 + // days, a metric whose module is off left unread. + const shifts = await buildExtendedChangepointSignals(userId, now, modules); + + const vitals = coincident.status === "ok" ? coincident.value.vitals : []; + const summary = summariseHealthStatus(vitals, shifts); + return { present: summary.present, deviations: summary.deviations, shifts: summary.shifts, generatedAt: now.toISOString(), - }); -}); + }; +} diff --git a/src/app/api/medications/[id]/dose-history/__tests__/route.test.ts b/src/app/api/medications/[id]/dose-history/__tests__/route.test.ts index 5c55e140a..8edbfb47d 100644 --- a/src/app/api/medications/[id]/dose-history/__tests__/route.test.ts +++ b/src/app/api/medications/[id]/dose-history/__tests__/route.test.ts @@ -19,7 +19,7 @@ import { NextRequest } from "next/server"; vi.mock("@/lib/db", () => ({ prisma: { medication: { findUnique: vi.fn() }, - medicationIntakeEvent: { findMany: vi.fn() }, + medicationIntakeEvent: { findMany: vi.fn(), findFirst: vi.fn() }, }, })); @@ -567,3 +567,74 @@ describe("GET /api/medications/[id]/dose-history — slots before creation (#102 }); } }); + +/** + * The history reads the window's rows, not every intake the medication ever + * had: for a medication taken for years each open of the tab read thousands + * of rows to show at most a year. Of the rows before the window only the + * latest take is read, because a rolling grid re-starts from it. + */ +describe("GET /api/medications/[id]/dose-history — the read is bounded to the window", () => { + const FROM = new Date("2026-06-01T00:00:00Z"); + const TO = new Date("2026-06-15T00:00:00Z"); + const windowQuery = `?from=${encodeURIComponent(FROM.toISOString())}&to=${encodeURIComponent(TO.toISOString())}`; + + it("asks only for rows anchored or taken in the window", async () => { + await GET(getReq(windowQuery), ROUTE_PARAMS); + + const where = vi.mocked(prisma.medicationIntakeEvent.findMany).mock + .calls[0][0]?.where; + expect(where).toMatchObject({ + OR: [{ scheduledFor: { gte: FROM } }, { takenAt: { gte: FROM } }], + }); + const before = vi.mocked(prisma.medicationIntakeEvent.findFirst).mock + .calls[0][0]; + expect(before?.where).toMatchObject({ + skipped: false, + scheduledFor: { lt: FROM }, + takenAt: { lt: FROM }, + }); + }); + + it("re-starts a rolling grid from the latest take before the window", async () => { + vi.mocked(prisma.medication.findUnique).mockResolvedValue({ + id: "med-1", + startsOn: null, + endsOn: null, + oneShot: false, + createdAt: new Date("2026-01-01T00:00:00Z"), + schedules: [ + { + id: "sched-1", + windowStart: "08:00", + windowEnd: "08:00", + daysOfWeek: null, + timesOfDay: ["08:00"], + reminderGraceMinutes: null, + rrule: null, + rollingIntervalDays: 7, + scheduleType: "SCHEDULED", + cyclicOnWeeks: null, + cyclicOffWeeks: null, + }, + ], + } as never); + // Taken four days before the window opens, nothing since: the next dose + // was due inside the window and reads as missed there. + vi.mocked(prisma.medicationIntakeEvent.findFirst).mockResolvedValue({ + takenAt: new Date("2026-05-28T06:00:00Z"), + } as never); + + const res = await GET(getReq(windowQuery), ROUTE_PARAMS); + expect(res.status).toBe(200); + const rows = (await res.json()).data.rows as Array<{ + at: string; + kind: string; + status: string; + }>; + + expect(rows.map((r) => [r.at.slice(0, 10), r.kind, r.status])).toEqual([ + ["2026-06-04", "slot", "missed"], + ]); + }); +}); diff --git a/src/app/api/medications/[id]/dose-history/route.ts b/src/app/api/medications/[id]/dose-history/route.ts index 7d9f7cf5b..652ae6d06 100644 --- a/src/app/api/medications/[id]/dose-history/route.ts +++ b/src/app/api/medications/[id]/dose-history/route.ts @@ -101,6 +101,61 @@ interface SerializedDoseHistoryRow { } | null; } +/** + * The intakes a window needs: the rows anchored in it or taken in it (a late + * take on an older slot is an intake instant of the window), and of all the + * rows before it only the latest take, the anchor a rolling grid re-starts + * from and the last intake when none falls in the window. Earlier takes place + * no slot in the window. The whole history used to be read on every open, so + * a medication taken for years read thousands of rows to show at most a year. + */ +async function readWindowIntakes( + medicationId: string, + userId: string, + from: Date, +) { + const [events, takenBefore] = await Promise.all([ + prisma.medicationIntakeEvent.findMany({ + where: { + medicationId, + userId, + deletedAt: null, + OR: [{ scheduledFor: { gte: from } }, { takenAt: { gte: from } }], + }, + orderBy: [{ scheduledFor: "desc" }, { id: "desc" }], + select: { + id: true, + scheduledFor: true, + takenAt: true, + skipped: true, + autoMissed: true, + // v1.15.20 — USER_PIN rows bind by anchor in the read ledger. + attributionSource: true, + // v1.16.4 — per-intake dose override for the ledger's deviation hint. + doseTaken: true, + // v1.32.8 (iOS #64) — write provenance so the client can label how + // each dose was recorded (Web / API / Reminder / Import / AppleHealth). + source: true, + // v1.42.2 — the history labels an automatically recorded dose. + autoRecorded: true, + }, + }), + prisma.medicationIntakeEvent.findFirst({ + where: { + medicationId, + userId, + deletedAt: null, + skipped: false, + scheduledFor: { lt: from }, + takenAt: { lt: from }, + }, + orderBy: [{ takenAt: "desc" }, { id: "desc" }], + select: { takenAt: true }, + }), + ]); + return { events, anchorBefore: takenBefore?.takenAt ?? null }; +} + export const GET = apiHandler( async (request: NextRequest, { params }: RouteParams) => { const { user } = await requireAuth(); @@ -165,26 +220,7 @@ export const GET = apiHandler( Math.max(requestedFrom.getTime(), spanFloor.getTime()), ); - const events = await prisma.medicationIntakeEvent.findMany({ - where: { medicationId: id, userId: user.id, deletedAt: null }, - orderBy: [{ scheduledFor: "desc" }, { id: "desc" }], - select: { - id: true, - scheduledFor: true, - takenAt: true, - skipped: true, - autoMissed: true, - // v1.15.20 — USER_PIN rows bind by anchor in the read ledger. - attributionSource: true, - // v1.16.4 — per-intake dose override for the ledger's deviation hint. - doseTaken: true, - // v1.32.8 (iOS #64) — write provenance so the client can label how - // each dose was recorded (Web / API / Reminder / Import / AppleHealth). - source: true, - // v1.42.2 — the history labels an automatically recorded dose. - autoRecorded: true, - }, - }); + const { events, anchorBefore } = await readWindowIntakes(id, user.id, from); const mapped = events.map((e) => ({ id: e.id, @@ -198,7 +234,10 @@ export const GET = apiHandler( autoRecorded: e.autoRecorded, })); - const lastIntakeAt = lastNonSkippedTakenAt(mapped); + const lastIntakeAt = lastNonSkippedTakenAt([ + ...mapped, + ...(anchorBefore ? [{ takenAt: anchorBefore, skipped: false }] : []), + ]); // A dose may be recorded before its slot's own time: the dose window opens // up to two hours early, and a skip can be logged ahead. Such a row is @@ -263,6 +302,7 @@ export const GET = apiHandler( const intakeInstants = mapped .filter((e) => !e.skipped && e.takenAt !== null && e.takenAt <= to) .map((e) => e.takenAt as Date) + .concat(anchorBefore ? [anchorBefore] : []) .sort((a, b) => a.getTime() - b.getTime()); const groups = buildBandsForSchedulesWithEras({ diff --git a/src/app/insights/active-energy/page.tsx b/src/app/insights/active-energy/page.tsx index 2e0768bb4..c37b5c1c6 100644 --- a/src/app/insights/active-energy/page.tsx +++ b/src/app/insights/active-energy/page.tsx @@ -3,6 +3,7 @@ import { Flame } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; +import { useTranslations } from "@/lib/i18n/context"; /** * v1.4.32 — `/insights/active-energy`. @@ -14,6 +15,7 @@ import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page * imports collapse cleanly via the v1.4.30 drain script. */ export default function InsightsActiveEnergyPage() { + const { t } = useTranslations(); 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?" + coachPrefill={t("insights.activeEnergy.coachPrefill")} /> ); } diff --git a/src/app/insights/audio-events/page.tsx b/src/app/insights/audio-events/page.tsx index c77ca3c68..feb1415bf 100644 --- a/src/app/insights/audio-events/page.tsx +++ b/src/app/insights/audio-events/page.tsx @@ -26,7 +26,6 @@ export default function InsightsLaermereignissePage() { yAxisUnit="" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any loud-environment events yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/blood-glucose/page.tsx b/src/app/insights/blood-glucose/page.tsx index 60508761c..6e3255739 100644 --- a/src/app/insights/blood-glucose/page.tsx +++ b/src/app/insights/blood-glucose/page.tsx @@ -60,7 +60,6 @@ export default function InsightsBlutzuckerPage() { captureType="BLOOD_GLUCOSE" targetSummarySlug="blood-glucose" afterChart={} - coachPrefill="I haven't logged any blood glucose yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/blood-pressure/page.tsx b/src/app/insights/blood-pressure/page.tsx index 6cfcd1a45..782110ac7 100644 --- a/src/app/insights/blood-pressure/page.tsx +++ b/src/app/insights/blood-pressure/page.tsx @@ -87,7 +87,7 @@ export default function InsightsBlutdruckPage() { } - coachPrefill="I haven't recorded any blood pressure yet. Why does it matter, and what should I know before I start?" + coachPrefill={t("insights.emptyState.bloodPressure.coachPrefill")} /> ); diff --git a/src/app/insights/bmi/page.tsx b/src/app/insights/bmi/page.tsx index cc6db7390..30d5b1ce1 100644 --- a/src/app/insights/bmi/page.tsx +++ b/src/app/insights/bmi/page.tsx @@ -90,7 +90,7 @@ export default function InsightsBmiPage() { } - coachPrefill="I haven't recorded any weight yet. Why does BMI matter for me, and what should I know before I start tracking it?" + coachPrefill={t("insights.emptyState.bmi.coachPrefill")} /> ); diff --git a/src/app/insights/body-temperature/page.tsx b/src/app/insights/body-temperature/page.tsx index 9c2ed241d..8ce4c7c02 100644 --- a/src/app/insights/body-temperature/page.tsx +++ b/src/app/insights/body-temperature/page.tsx @@ -3,6 +3,7 @@ import { Thermometer } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; +import { useTranslations } from "@/lib/i18n/context"; /** * v1.4.32 — `/insights/body-temperature`. @@ -13,6 +14,7 @@ import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page * them on separate enum values per the v1.4.30 categorisation overlay. */ export default function InsightsBodyTemperaturePage() { + const { t } = useTranslations(); 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?" + coachPrefill={t("insights.bodyTemperature.coachPrefill")} /> ); } diff --git a/src/app/insights/body-water/page.tsx b/src/app/insights/body-water/page.tsx index e5483e26c..e0954cc50 100644 --- a/src/app/insights/body-water/page.tsx +++ b/src/app/insights/body-water/page.tsx @@ -25,7 +25,6 @@ export default function InsightsKoerperwasserPage() { emptyStateIcon={} emptyStateCtaType={null} captureType="TOTAL_BODY_WATER" - coachPrefill="I haven't logged any total body water yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/bone-mass/page.tsx b/src/app/insights/bone-mass/page.tsx index acecafe5e..daf73c6fd 100644 --- a/src/app/insights/bone-mass/page.tsx +++ b/src/app/insights/bone-mass/page.tsx @@ -25,7 +25,6 @@ export default function InsightsKnochenmassePage() { emptyStateIcon={} emptyStateCtaType={null} captureType="BONE_MASS" - coachPrefill="I haven't logged any bone mass yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/breathing-disturbances/page.tsx b/src/app/insights/breathing-disturbances/page.tsx index 52d092204..76490022f 100644 --- a/src/app/insights/breathing-disturbances/page.tsx +++ b/src/app/insights/breathing-disturbances/page.tsx @@ -24,7 +24,6 @@ export default function InsightsBreathingDisturbancesPage() { unitKey="insights.units.breathingEvents" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any sleep-breathing-disturbance data yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/cardio-fitness/page.tsx b/src/app/insights/cardio-fitness/page.tsx index 8ea2756fc..582dd00d0 100644 --- a/src/app/insights/cardio-fitness/page.tsx +++ b/src/app/insights/cardio-fitness/page.tsx @@ -27,7 +27,6 @@ export default function InsightsCardioFitnessPage() { statIcon={Gauge} emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any cardio-fitness (VO₂ max) data yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/cardio-recovery/page.tsx b/src/app/insights/cardio-recovery/page.tsx index f54fdf5e1..347be3257 100644 --- a/src/app/insights/cardio-recovery/page.tsx +++ b/src/app/insights/cardio-recovery/page.tsx @@ -26,7 +26,6 @@ export default function InsightsCardioRecoveryPage() { yAxisUnit="bpm" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any cardio-recovery data yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/daylight/page.tsx b/src/app/insights/daylight/page.tsx index f6b4d023f..3760a54c1 100644 --- a/src/app/insights/daylight/page.tsx +++ b/src/app/insights/daylight/page.tsx @@ -26,7 +26,6 @@ export default function InsightsTageslichtPage() { yAxisUnit="min" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any time spent in daylight yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/double-support-time/page.tsx b/src/app/insights/double-support-time/page.tsx index 021ccb94a..5f767ac5d 100644 --- a/src/app/insights/double-support-time/page.tsx +++ b/src/app/insights/double-support-time/page.tsx @@ -26,7 +26,6 @@ export default function InsightsDoppelstandphasePage() { yAxisUnit="%" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any double support time yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/environmental-audio/page.tsx b/src/app/insights/environmental-audio/page.tsx index 42e9e41d9..0f82ac5f0 100644 --- a/src/app/insights/environmental-audio/page.tsx +++ b/src/app/insights/environmental-audio/page.tsx @@ -26,7 +26,6 @@ export default function InsightsLaermbelastungPage() { yAxisUnit="dBA" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any environmental sound exposure yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/falls/page.tsx b/src/app/insights/falls/page.tsx index 6144b740a..093c55178 100644 --- a/src/app/insights/falls/page.tsx +++ b/src/app/insights/falls/page.tsx @@ -3,6 +3,7 @@ import { PersonStanding } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; +import { useTranslations } from "@/lib/i18n/context"; /** * v1.10.0 — `/insights/falls`. @@ -11,6 +12,7 @@ import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page * better. The series arrives from Apple Health sync — no manual-entry CTA. */ export default function InsightsFallsPage() { + const { t } = useTranslations(); return ( } emptyStateCtaType={null} - coachPrefill="I haven't logged any fall data yet. What does this metric tell me about my health, and how do I lower my fall risk?" + coachPrefill={t("insights.falls.coachPrefill")} /> ); } diff --git a/src/app/insights/fat-free-mass/page.tsx b/src/app/insights/fat-free-mass/page.tsx index c51c7f995..d41e85dc6 100644 --- a/src/app/insights/fat-free-mass/page.tsx +++ b/src/app/insights/fat-free-mass/page.tsx @@ -24,7 +24,6 @@ export default function InsightsFettfreieMassePage() { color="var(--chart-1)" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any fat-free mass yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/fat-mass/page.tsx b/src/app/insights/fat-mass/page.tsx index 835faa73d..832463239 100644 --- a/src/app/insights/fat-mass/page.tsx +++ b/src/app/insights/fat-mass/page.tsx @@ -24,7 +24,6 @@ export default function InsightsFettmassePage() { color="var(--chart-5)" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any fat mass yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/flights-climbed/page.tsx b/src/app/insights/flights-climbed/page.tsx index 9aa247148..5aaa8399f 100644 --- a/src/app/insights/flights-climbed/page.tsx +++ b/src/app/insights/flights-climbed/page.tsx @@ -25,7 +25,6 @@ export default function InsightsStockwerkePage() { unitKey="insights.units.flights" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any flights climbed yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/headphone-audio/page.tsx b/src/app/insights/headphone-audio/page.tsx index fafcaabbb..169d5b965 100644 --- a/src/app/insights/headphone-audio/page.tsx +++ b/src/app/insights/headphone-audio/page.tsx @@ -26,7 +26,6 @@ export default function InsightsKopfhoererpegelPage() { yAxisUnit="dBA" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any headphone audio exposure yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/hrv/page.tsx b/src/app/insights/hrv/page.tsx index 06f3462a4..fb4ff3cca 100644 --- a/src/app/insights/hrv/page.tsx +++ b/src/app/insights/hrv/page.tsx @@ -3,6 +3,7 @@ import { Activity } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; +import { useTranslations } from "@/lib/i18n/context"; import { EcgCrossLink } from "@/components/insights/ecg-cross-link"; /** @@ -20,6 +21,7 @@ import { EcgCrossLink } from "@/components/insights/ecg-cross-link"; * own, labelled, because the two are not one measure. */ export default function InsightsHrvPage() { + const { t } = useTranslations(); 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?" + coachPrefill={t("insights.hrv.coachPrefill")} // S10 / H1 — device-attributed pointer into the ECG viewer, in the // HRV / heart context. Self-gates to nothing without recordings. afterAssessment={} diff --git a/src/app/insights/lean-body-mass/page.tsx b/src/app/insights/lean-body-mass/page.tsx index ac41b155f..4d9453b83 100644 --- a/src/app/insights/lean-body-mass/page.tsx +++ b/src/app/insights/lean-body-mass/page.tsx @@ -24,7 +24,6 @@ export default function InsightsMagermassePage() { color="var(--chart-1)" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any lean body mass yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/medications/__tests__/page.test.tsx b/src/app/insights/medications/__tests__/page.test.tsx new file mode 100644 index 000000000..7503f44fd --- /dev/null +++ b/src/app/insights/medications/__tests__/page.test.tsx @@ -0,0 +1,81 @@ +import { describe, expect, it, vi } from "vitest"; +import { renderToStaticMarkup } from "react-dom/server"; +import type { ReactNode } from "react"; + +/** + * Insights > Medications: the efficacy summary sits below every medication + * card and asks one efficacy read per medication. On entry it holds its slot + * with a sentinel and mounts only when scrolled near, so opening the page + * sends the cards' reads and not the summary's. + */ + +vi.mock("@tanstack/react-query", () => ({ + useQuery: ({ queryKey }: { queryKey: readonly unknown[] }) => + queryKey[1] === "comprehensive" + ? { + data: { + medications: [ + { + id: "m1", + name: "Ramipril", + dose: "5 mg", + category: "BLOOD_PRESSURE", + compliance7: 100, + compliance30: 95, + streak: 7, + taken7: 7, + skipped7: 0, + missed7: 0, + }, + ], + }, + isLoading: false, + isError: false, + } + : { data: undefined, isLoading: false, isError: false }, +})); +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ isAuthenticated: true }), +})); +vi.mock("@/hooks/use-ai-capability", () => ({ + useAiCapability: () => ({ available: false }), + useAiProviderState: () => ({ responseTimeoutMs: null }), +})); +vi.mock("@/components/insights/medication-efficacy-summary", () => ({ + MedicationEfficacySummary: () =>
, +})); +vi.mock("@/components/insights/sub-page-shell", () => ({ + SubPageShell: ({ children }: { children: ReactNode }) => <>{children}, +})); +vi.mock("@/components/insights/glp1-substance-curve-summary", () => ({ + Glp1SubstanceCurveSummary: () => null, +})); +vi.mock("@/components/insights/metric-target-summary", () => ({ + MetricTargetSummary: () => null, +})); +vi.mock("@/components/insights/insight-status-card", () => ({ + InsightStatusCard: () => null, +})); +vi.mock("@/components/insights/coach-launch-button", () => ({ + CoachLaunchButton: () => null, +})); +vi.mock("@/components/charts/compliance-heatmap", () => ({ + ComplianceHeatmap: () => null, +})); + +import { I18nProvider } from "@/lib/i18n/context"; +import InsightsMedikamentePage from "../page"; + +describe("/insights/medications on entry", () => { + it("renders the cards and holds the efficacy summary until scrolled near", () => { + const html = renderToStaticMarkup( + + + , + ); + + expect(html).toContain("Ramipril"); + expect(html).toContain('data-deferred-section="medication-efficacy"'); + expect(html).not.toContain("efficacy-summary-mounted"); + }); +}); diff --git a/src/app/insights/medications/page.tsx b/src/app/insights/medications/page.tsx index bd17dc53f..efe9abb03 100644 --- a/src/app/insights/medications/page.tsx +++ b/src/app/insights/medications/page.tsx @@ -24,6 +24,7 @@ import { CoachLaunchButton } from "@/components/insights/coach-launch-button"; import { InsightStatusCard } from "@/components/insights/insight-status-card"; import { MetricTargetSummary } from "@/components/insights/metric-target-summary"; import { MedicationEfficacySummary } from "@/components/insights/medication-efficacy-summary"; +import { DeferUntilNear } from "@/components/insights/defer-until-near"; import { Glp1SubstanceCurveSummary } from "@/components/insights/glp1-substance-curve-summary"; import { SubPageShell } from "@/components/insights/sub-page-shell"; import { TileHeader } from "@/components/insights/tile-header"; @@ -321,12 +322,12 @@ export default function InsightsMedikamentePage() { })}
- {/* v1.28 — compact efficacy summary: reuses the same server-computed - efficacy DTO the detail-page "Wirkung" tab reads (never a second - computation), association-only, linking through to the full view. */} - ({ id: m.id, name: m.name }))} - /> + {/* v1.28 — one efficacy read per medication, below every card: mounted when near. */} + + ({ id: m.id, name: m.name }))} + /> + {/* v1.28.20 — estimated active-substance curve for GLP-1 medications (additive; renders nothing when the user has no GLP-1 medication). */} diff --git a/src/app/insights/mood/page-client.tsx b/src/app/insights/mood/page-client.tsx index b543a4c08..00c75e12d 100644 --- a/src/app/insights/mood/page-client.tsx +++ b/src/app/insights/mood/page-client.tsx @@ -132,7 +132,7 @@ export default function InsightsMoodPageClient() { {t("insights.emptyState.mood.cta")} } - coachPrefill="I haven't logged any mood entries yet. Why does mood tracking matter, and how should I start?" + coachPrefill={t("insights.emptyState.mood.coachPrefill")} /> ); diff --git a/src/app/insights/muscle-mass/page.tsx b/src/app/insights/muscle-mass/page.tsx index 42ce5fa8f..4538d0bb9 100644 --- a/src/app/insights/muscle-mass/page.tsx +++ b/src/app/insights/muscle-mass/page.tsx @@ -24,7 +24,6 @@ export default function InsightsMuskelmassePage() { color="var(--chart-1)" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any muscle mass yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/oxygen/__tests__/page.test.tsx b/src/app/insights/oxygen/__tests__/page.test.tsx index ca7a6b1aa..9580f2f86 100644 --- a/src/app/insights/oxygen/__tests__/page.test.tsx +++ b/src/app/insights/oxygen/__tests__/page.test.tsx @@ -13,6 +13,7 @@ import { describe, expect, it, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { I18nProvider } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import type { ValueBand } from "@/lib/analytics/value-bands"; @@ -37,7 +38,9 @@ function bandsRendered(thresholds: unknown): ValueBand[] { } const html = renderToStaticMarkup( - + + + , ); const match = /]*>([^<]*)<\/pre>/.exec(html); diff --git a/src/app/insights/oxygen/page.tsx b/src/app/insights/oxygen/page.tsx index f45c47a81..9281fadec 100644 --- a/src/app/insights/oxygen/page.tsx +++ b/src/app/insights/oxygen/page.tsx @@ -3,6 +3,7 @@ import { Wind } from "lucide-react"; import { HealthKitMetricPage } from "@/components/insights/healthkit-metric-page"; +import { useTranslations } from "@/lib/i18n/context"; import { buildOxygenSaturationBands } from "@/lib/analytics/value-bands"; import { useOwnTargetRange } from "@/lib/queries/use-own-target-range"; @@ -20,6 +21,7 @@ import { useOwnTargetRange } from "@/lib/queries/use-own-target-range"; * `/targets`, and the clinical bands otherwise. */ export default function InsightsOxygenSaturationPage() { + const { t } = useTranslations(); const ownRange = useOwnTargetRange("OXYGEN_SATURATION"); return ( } emptyStateCtaType={null} captureType="OXYGEN_SATURATION" - coachPrefill="I haven't logged any oxygen-saturation data yet. What does SpO₂ tell me, and what range should I aim for?" + coachPrefill={t("insights.oxygenSaturation.coachPrefill")} /> ); } diff --git a/src/app/insights/pulse-wave-velocity/page.tsx b/src/app/insights/pulse-wave-velocity/page.tsx index ae16b14b4..069723f38 100644 --- a/src/app/insights/pulse-wave-velocity/page.tsx +++ b/src/app/insights/pulse-wave-velocity/page.tsx @@ -26,7 +26,6 @@ export default function InsightsPulswellengeschwindigkeitPage() { yAxisUnit="m/s" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any pulse wave velocity yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/pulse/page.tsx b/src/app/insights/pulse/page.tsx index 9f41dbe02..49c1d2357 100644 --- a/src/app/insights/pulse/page.tsx +++ b/src/app/insights/pulse/page.tsx @@ -115,7 +115,7 @@ export default function InsightsPulsPage() { } - coachPrefill="I haven't recorded any resting pulse yet. Why does it matter, and what should I know before I start?" + coachPrefill={t("insights.emptyState.pulse.coachPrefill")} /> ); diff --git a/src/app/insights/respiratory-rate/page.tsx b/src/app/insights/respiratory-rate/page.tsx index ac58a3568..6b4dfd20f 100644 --- a/src/app/insights/respiratory-rate/page.tsx +++ b/src/app/insights/respiratory-rate/page.tsx @@ -25,7 +25,6 @@ export default function InsightsAtemfrequenzPage() { unitKey="insights.units.respiratoryRate" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any respiratory rate yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/six-minute-walk/page.tsx b/src/app/insights/six-minute-walk/page.tsx index 3bc1905f6..e75ea0013 100644 --- a/src/app/insights/six-minute-walk/page.tsx +++ b/src/app/insights/six-minute-walk/page.tsx @@ -25,7 +25,6 @@ export default function InsightsSixMinuteWalkPage() { yAxisUnit="m" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any six-minute-walk data yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/skin-temperature/page.tsx b/src/app/insights/skin-temperature/page.tsx index 1fff4f129..ca810282e 100644 --- a/src/app/insights/skin-temperature/page.tsx +++ b/src/app/insights/skin-temperature/page.tsx @@ -32,7 +32,6 @@ export default function InsightsHauttemperaturPage() { color="var(--chart-5)" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any skin temperature yet. What does this metric tell me about my health, and how do I improve it?" afterChart={ } - coachPrefill="I don't have any sleep data yet. Why does sleep tracking matter, and what should I know before I connect a source?" + coachPrefill={t("insights.emptyState.sleep.coachPrefill")} /> ); diff --git a/src/app/insights/stair-ascent-speed/page.tsx b/src/app/insights/stair-ascent-speed/page.tsx index 0d4e85b3b..c712ce997 100644 --- a/src/app/insights/stair-ascent-speed/page.tsx +++ b/src/app/insights/stair-ascent-speed/page.tsx @@ -25,7 +25,6 @@ export default function InsightsStairAscentSpeedPage() { yAxisUnit="m/s" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any stair-ascent-speed data yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/stair-descent-speed/page.tsx b/src/app/insights/stair-descent-speed/page.tsx index 948a67404..d4bbe2ed4 100644 --- a/src/app/insights/stair-descent-speed/page.tsx +++ b/src/app/insights/stair-descent-speed/page.tsx @@ -25,7 +25,6 @@ export default function InsightsStairDescentSpeedPage() { yAxisUnit="m/s" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any stair-descent-speed data yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/step-length/page.tsx b/src/app/insights/step-length/page.tsx index 2eed765ca..474b932de 100644 --- a/src/app/insights/step-length/page.tsx +++ b/src/app/insights/step-length/page.tsx @@ -26,7 +26,6 @@ export default function InsightsSchrittlaengePage() { yAxisUnit="m" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any walking step length yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/steps/page.tsx b/src/app/insights/steps/page.tsx index a562c4baf..ef07e757d 100644 --- a/src/app/insights/steps/page.tsx +++ b/src/app/insights/steps/page.tsx @@ -40,7 +40,7 @@ export default function InsightsStepsPage() { emptyStateIcon={} emptyStateCtaType={null} captureType="ACTIVITY_STEPS" - coachPrefill="I haven't logged any steps yet. What's a reasonable daily step target, and how does walking more help my health?" + coachPrefill={t("insights.steps.coachPrefill")} afterChart={ calendarVisible ? ( } emptyStateCtaType={null} - coachPrefill="I haven't logged any vascular age yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/visceral-fat/page.tsx b/src/app/insights/visceral-fat/page.tsx index ae22f62b8..15358bb02 100644 --- a/src/app/insights/visceral-fat/page.tsx +++ b/src/app/insights/visceral-fat/page.tsx @@ -26,7 +26,6 @@ export default function InsightsViszeralfettPage() { yAxisUnit="" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any visceral fat yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/walking-asymmetry/page.tsx b/src/app/insights/walking-asymmetry/page.tsx index 640f05f06..26f04a998 100644 --- a/src/app/insights/walking-asymmetry/page.tsx +++ b/src/app/insights/walking-asymmetry/page.tsx @@ -26,7 +26,6 @@ export default function InsightsGangasymmetriePage() { yAxisUnit="%" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any walking asymmetry yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/walking-distance/page.tsx b/src/app/insights/walking-distance/page.tsx index 5d4c4cf4b..bc8c1a334 100644 --- a/src/app/insights/walking-distance/page.tsx +++ b/src/app/insights/walking-distance/page.tsx @@ -24,7 +24,6 @@ export default function InsightsGehstreckePage() { color="var(--chart-2)" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any walking and running distance yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/walking-heart-rate/page.tsx b/src/app/insights/walking-heart-rate/page.tsx index ae4ba79d6..5d1b7aba1 100644 --- a/src/app/insights/walking-heart-rate/page.tsx +++ b/src/app/insights/walking-heart-rate/page.tsx @@ -26,7 +26,6 @@ export default function InsightsGehpulsPage() { yAxisUnit="bpm" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any walking heart rate average yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/walking-speed/page.tsx b/src/app/insights/walking-speed/page.tsx index fc4b5d809..39128371e 100644 --- a/src/app/insights/walking-speed/page.tsx +++ b/src/app/insights/walking-speed/page.tsx @@ -24,7 +24,6 @@ export default function InsightsGehgeschwindigkeitPage() { color="var(--chart-2)" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any walking speed yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/walking-steadiness/page.tsx b/src/app/insights/walking-steadiness/page.tsx index 0fa566184..33649e17a 100644 --- a/src/app/insights/walking-steadiness/page.tsx +++ b/src/app/insights/walking-steadiness/page.tsx @@ -26,7 +26,6 @@ export default function InsightsGangstabilitaetPage() { yAxisUnit="%" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any walking steadiness yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/insights/weight/page.tsx b/src/app/insights/weight/page.tsx index 75101098f..2023ee9ed 100644 --- a/src/app/insights/weight/page.tsx +++ b/src/app/insights/weight/page.tsx @@ -122,7 +122,7 @@ export default function InsightsGewichtPage() { } - coachPrefill="I haven't recorded any weight yet. Why does it matter, and what should I know before I start tracking?" + coachPrefill={t("insights.emptyState.weight.coachPrefill")} /> ); diff --git a/src/app/insights/workouts/page-client.tsx b/src/app/insights/workouts/page-client.tsx index e9ea0f979..f92a8ae9b 100644 --- a/src/app/insights/workouts/page-client.tsx +++ b/src/app/insights/workouts/page-client.tsx @@ -142,7 +142,7 @@ export default function InsightsWorkoutsPageClient() { ) : null } - coachPrefill="I haven't logged any workouts yet. Why does tracking them matter, and what should I focus on first?" + coachPrefill={t("insights.workouts.emptyState.coachPrefill")} /> ) : ( <> diff --git a/src/app/insights/wrist-temperature/page.tsx b/src/app/insights/wrist-temperature/page.tsx index 44a005ff2..2f1a5f23a 100644 --- a/src/app/insights/wrist-temperature/page.tsx +++ b/src/app/insights/wrist-temperature/page.tsx @@ -24,7 +24,6 @@ export default function InsightsWristTemperaturePage() { color="var(--chart-5)" emptyStateIcon={} emptyStateCtaType={null} - coachPrefill="I haven't logged any wrist-temperature data yet. What does this metric tell me about my health, and how do I improve it?" /> ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2e41e3b20..ad5c734a4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -8,6 +8,7 @@ import "./globals.css"; import { Providers } from "@/components/providers"; import { AuthShell } from "@/components/layout/auth-shell"; import { MonitoringBootstrap } from "@/components/monitoring/bootstrap"; +import { getPublicMonitoringSettings } from "@/lib/monitoring-settings"; import { WebVitalsReporter } from "@/components/monitoring/web-vitals-reporter"; import { resolveInitialLocale } from "@/lib/i18n/resolve-initial-locale"; import { isKeyMismatch } from "@/lib/boot/key-mismatch-state"; @@ -86,7 +87,12 @@ export default async function RootLayout({ ? ((await headers()).get("x-nonce") ?? undefined) : undefined; - const initialLocale = await resolveInitialLocale(); + // The monitoring switches ride the document render instead of a request + // from every page load; the key-mismatch page reads no data. + const [initialLocale, monitoring] = await Promise.all([ + resolveInitialLocale(), + isKeyMismatch() ? null : getPublicMonitoringSettings(), + ]); return ( @@ -139,7 +145,15 @@ export default async function RootLayout({ ) : ( - + {/* `DEMO_MODE` is a server-only env var; the proxy uses it to block mutations. Resolve it here (server component) and diff --git a/src/app/page.tsx b/src/app/page.tsx index 48af2ec52..bda704c49 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -102,6 +102,19 @@ export default async function DashboardPage() { const queryClient = new QueryClient(); queryClient.setQueryData(queryKeys.dashboardSnapshot(locale), wireBody); queryClient.setQueryData(queryKeys.dashboardWidgets(), wireBody.layout); + // The mood chart's series rides the mood tile (same engine as + // `/api/mood/analytics`); seeded with readings only, where the tile's + // summary is the route's. + const mood = ( + wireBody.tiles as + { mood?: { entries: unknown[]; summary: unknown } } | undefined + )?.mood; + if (mood?.summary && mood.entries.length > 0) { + queryClient.setQueryData(queryKeys.moodAnalytics(), { + entries: mood.entries, + summary: mood.summary, + }); + } // Above-the-fold prefetch runs ONLY in snapshot mode: legacy mode derives // its chart-type list from analytics queries the RSC did not run, so there diff --git a/src/components/__tests__/version-poller.test.ts b/src/components/__tests__/version-poller.test.ts index 14b0a24da..c9a25defa 100644 --- a/src/components/__tests__/version-poller.test.ts +++ b/src/components/__tests__/version-poller.test.ts @@ -15,7 +15,11 @@ import { describe, expect, it } from "vitest"; -import { cachesToEvict, resolveVersionPollDecision } from "../version-poller"; +import { + cachesToEvict, + resolveVersionPollDecision, + versionCheckDueOnReturn, +} from "../version-poller"; describe("resolveVersionPollDecision", () => { it("is up-to-date when live matches the shell", () => { @@ -172,3 +176,20 @@ describe("cachesToEvict", () => { expect(cachesToEvict(keys, "1.43.0")).not.toContain("another-app-cache"); }); }); + +describe("versionCheckDueOnReturn", () => { + const NOW = 1_000_000_000; + + it("asks at once when the tab comes back after a full interval away", () => { + expect(versionCheckDueOnReturn("visible", NOW - 60_000, NOW)).toBe(true); + expect(versionCheckDueOnReturn("visible", 0, NOW)).toBe(true); + }); + + it("does not ask again on a quick switch back", () => { + expect(versionCheckDueOnReturn("visible", NOW - 5_000, NOW)).toBe(false); + }); + + it("never asks while the tab is hidden", () => { + expect(versionCheckDueOnReturn("hidden", 0, NOW)).toBe(false); + }); +}); diff --git a/src/components/admin/__tests__/coach-feedback-section.test.tsx b/src/components/admin/__tests__/coach-feedback-section.test.tsx index 8886fb700..a68e821d3 100644 --- a/src/components/admin/__tests__/coach-feedback-section.test.tsx +++ b/src/components/admin/__tests__/coach-feedback-section.test.tsx @@ -19,6 +19,7 @@ vi.mock("@tanstack/react-query", () => ({ })); import { I18nProvider } from "@/lib/i18n/context"; +import { AiQualitySection } from "../ai-quality-section"; import { CoachFeedbackSection } from "../coach-feedback-section"; function render() { @@ -138,3 +139,25 @@ describe("CoachFeedbackSection", () => { } }); }); + +describe("the Coach admin page's two quality cards", () => { + it("read one cache cell, so the page asks for the summary once", () => { + useQueryMock.mockReset().mockReturnValue({ + isLoading: true, + isError: false, + data: null, + }); + renderToStaticMarkup( + + + + , + ); + + const keys = useQueryMock.mock.calls.map( + ([options]) => (options as { queryKey: unknown }).queryKey, + ); + expect(keys).toHaveLength(2); + expect(keys[1]).toEqual(keys[0]); + }); +}); diff --git a/src/components/admin/__tests__/recovery-readiness-card.test.tsx b/src/components/admin/__tests__/recovery-readiness-card.test.tsx index 3d130ed3b..ac09f58fa 100644 --- a/src/components/admin/__tests__/recovery-readiness-card.test.tsx +++ b/src/components/admin/__tests__/recovery-readiness-card.test.tsx @@ -24,7 +24,10 @@ import type { ReadinessSignal, RecoveryReadiness, } from "@/components/admin/use-recovery-readiness"; -import { READINESS_FIX_PATHS } from "@/lib/recovery/readiness-model"; +import { + READINESS_FIX_PATHS, + READINESS_SIGNALS, +} from "@/lib/recovery/readiness-model"; import { primeLocales } from "@/__tests__/helpers/prime-locales"; primeLocales("de"); @@ -97,6 +100,23 @@ function row(html: string, id: string): string { } describe("RecoveryReadinessCard", () => { + it("holds one row per signal while it loads, so the cards below stay put", () => { + // A read that has not answered: no data, no error. + const client = new QueryClient(); + const html = renderToStaticMarkup( + + + + + , + ); + const start = html.indexOf('data-testid="recovery-readiness-loading"'); + expect(start).toBeGreaterThan(-1); + expect(html.slice(start).match(/
  • { pathname.current = "/admin"; const html = render(READINESS); diff --git a/src/components/admin/coach-feedback-section.tsx b/src/components/admin/coach-feedback-section.tsx index 3eb6659bb..1fcdab3bc 100644 --- a/src/components/admin/coach-feedback-section.tsx +++ b/src/components/admin/coach-feedback-section.tsx @@ -51,7 +51,9 @@ export function CoachFeedbackSection() { const fmt = useFormatters(); const query = useQuery({ - queryKey: queryKeys.adminCoachFeedback(), + // The same read as the AI quality section beside it, so one request + // serves both cards. + queryKey: queryKeys.adminAiQuality(), queryFn: async () => { const data = await apiGet<{ summary: FeedbackSummary | null } | null>( "/api/admin/ai-quality", diff --git a/src/components/admin/recovery-readiness-card.tsx b/src/components/admin/recovery-readiness-card.tsx index d77ae64d1..34189912b 100644 --- a/src/components/admin/recovery-readiness-card.tsx +++ b/src/components/admin/recovery-readiness-card.tsx @@ -24,7 +24,6 @@ import { CheckCircle2, Clock, LifeBuoy, - Loader2, MinusCircle, XCircle, } from "lucide-react"; @@ -34,6 +33,7 @@ import { SettingsCard } from "@/components/settings/settings-card"; import { Badge } from "@/components/ui/badge"; import { InlineAction } from "@/components/ui/inline-action"; import { QueryErrorRow } from "@/components/ui/query-error-row"; +import { Skeleton } from "@/components/ui/skeleton"; import { useFormatters, useTranslations } from "@/lib/i18n/context"; import { cn } from "@/lib/utils"; @@ -44,6 +44,17 @@ import { type RecoveryReadiness, } from "./use-recovery-readiness"; +/** One placeholder row per readiness signal the card always lists. */ +const READINESS_SIGNAL_SLOTS = [ + "database_dump", + "weekly_copy", + "offhost_copies", + "restore_drill", + "offhost_setup", + "dump_retention", + "recovery_kit", +] as const satisfies readonly ReadinessSignal["id"][]; + const STATE_ICON: Record< ReadinessState, React.ComponentType<{ className?: string; "aria-hidden"?: boolean }> @@ -194,14 +205,27 @@ export function RecoveryReadinessCard() { onRetry={() => void refetch()} /> ) : ( -
    -