diff --git a/messages/de.json b/messages/de.json index 1e7d0ea10..944bc9638 100644 --- a/messages/de.json +++ b/messages/de.json @@ -4389,6 +4389,10 @@ "consistency": "Konstanz", "timing": "Timing", "composition": "Zusammensetzung" + }, + "gap": { + "fewNights": "noch nicht genug Nächte ({count} von 3)", + "noMainSleep": "letzte Nacht ohne Hauptschlaf" } }, "READINESS": { @@ -4657,6 +4661,9 @@ "scoreCrossLink": "Den zusammengesetzten Erholungs-Score ansehen", "emptyTitle": "Noch keine Erholungsdaten", "empty": "Hier ist noch nichts zu sehen. Diese Seite zeigt die Werte zu Aufladung, Belastung und Trainingslast, die WHOOP und Polar aufzeichnen, und Ouras Resilienz-Stufe. Ouras Readiness erscheint als dein Erholungs-Score, oben verlinkt.", + "emptyOuraPermission": "Oura ist verbunden, aber die erteilte Berechtigung umfasst keine Herzgesundheitsdaten, und genau dahinter liegt Ouras Resilienz-Stufe. Verbinde Oura unter Einstellungen, Integrationen einmal neu und erlaube den Zugriff auf Herzgesundheit; die Stufe erscheint dann mit der nächsten Synchronisierung hier. Ouras Readiness ist bereits dein Erholungs-Score, oben verlinkt.", + "emptyOuraNoResilience": "Oura ist verbunden und die Berechtigung umfasst die Resilienz, aber Oura hat für dieses Konto noch keine Resilienz-Stufe geschickt. HealthLog fragt bei jeder Synchronisierung danach, sie erscheint hier, sobald Oura eine liefert. Ouras Readiness ist bereits dein Erholungs-Score, oben verlinkt.", + "emptyAction": "Integrationen öffnen", "block": { "ansChargeExplainer": "Wie aufgeladen dein autonomes Nervensystem in den Tag startet, ein Recharge-Wert aus deinen nächtlichen Herz- und Atemsignalen.", "dayStrainExplainer": "Die kardiovaskuläre Tagesbelastung auf der Belastungsskala deines Geräts. Lies sie im Verhältnis zu deiner Erholung, nicht als Ziel für sich.", diff --git a/messages/en.json b/messages/en.json index 5e59955ce..261eed209 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4389,6 +4389,10 @@ "consistency": "Consistency", "timing": "Timing", "composition": "Composition" + }, + "gap": { + "fewNights": "not enough nights yet ({count} of 3)", + "noMainSleep": "latest night had no main sleep" } }, "READINESS": { @@ -4657,6 +4661,9 @@ "scoreCrossLink": "See the composite recovery score", "emptyTitle": "No recovery data yet", "empty": "Nothing to chart here yet. This page shows the recharge, strain and load readings that WHOOP and Polar record, and Oura's resilience level. Oura's readiness appears as your recovery score, linked above.", + "emptyOuraPermission": "Oura is connected, but the permission it was given leaves out heart-health data, and Oura's resilience level sits behind it. Reconnect Oura once under Settings, Integrations and allow heart-health access; the level then appears here with the next sync. Oura's readiness is already your recovery score, linked above.", + "emptyOuraNoResilience": "Oura is connected and the permission covers resilience, but Oura has not sent a resilience level for this account yet. HealthLog asks for it on every sync, so it appears here as soon as Oura provides one. Oura's readiness is already your recovery score, linked above.", + "emptyAction": "Open integrations", "block": { "ansChargeExplainer": "How charged your autonomic nervous system is for the day ahead, a recharge reading drawn from your overnight heart and breathing signals.", "dayStrainExplainer": "The day's whole-body cardiovascular load on your device's strain scale. Read it against your recovery, not as a goal on its own.", diff --git a/messages/es.json b/messages/es.json index 1c799ce67..5b42d2099 100644 --- a/messages/es.json +++ b/messages/es.json @@ -4389,6 +4389,10 @@ "consistency": "Consistencia", "timing": "Horario", "composition": "Composición" + }, + "gap": { + "fewNights": "aún no hay suficientes noches ({count} de 3)", + "noMainSleep": "la última noche no tuvo sueño principal" } }, "READINESS": { @@ -4657,6 +4661,9 @@ "scoreCrossLink": "Ver la puntuación de recuperación combinada", "emptyTitle": "Aún no hay datos de recuperación", "empty": "Aún no hay nada que mostrar aquí. Esta página muestra las lecturas de recarga, esfuerzo y carga que registran WHOOP y Polar, y el nivel de resiliencia de Oura. La preparación (readiness) de Oura aparece como tu puntuación de recuperación, enlazada arriba.", + "emptyOuraPermission": "Oura está conectado, pero el permiso concedido no incluye los datos de salud cardíaca, y el nivel de resiliencia de Oura depende de ellos. Vuelve a conectar Oura una vez en Ajustes, Integraciones y permite el acceso a la salud cardíaca; el nivel aparecerá aquí con la próxima sincronización. La preparación (readiness) de Oura ya es tu puntuación de recuperación, enlazada arriba.", + "emptyOuraNoResilience": "Oura está conectado y el permiso incluye la resiliencia, pero Oura todavía no ha enviado un nivel de resiliencia para esta cuenta. HealthLog lo pide en cada sincronización, así que aparecerá aquí en cuanto Oura lo proporcione. La preparación (readiness) de Oura ya es tu puntuación de recuperación, enlazada arriba.", + "emptyAction": "Abrir integraciones", "block": { "ansChargeExplainer": "Cuán cargado está tu sistema nervioso autónomo para el día: una lectura de recarga obtenida de tus señales nocturnas de corazón y respiración.", "dayStrainExplainer": "La carga cardiovascular del día en la escala de esfuerzo de tu dispositivo. Léela en relación con tu recuperación, no como una meta en sí.", diff --git a/messages/fr.json b/messages/fr.json index 230f4a60a..46c71f58b 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -4389,6 +4389,10 @@ "consistency": "Régularité", "timing": "Timing", "composition": "Composition" + }, + "gap": { + "fewNights": "pas encore assez de nuits ({count} sur 3)", + "noMainSleep": "pas de sommeil principal la dernière nuit" } }, "READINESS": { @@ -4657,6 +4661,9 @@ "scoreCrossLink": "Voir le score de récupération composite", "emptyTitle": "Pas encore de données de récupération", "empty": "Rien à afficher ici pour l'instant. Cette page montre les mesures de recharge, d'effort et de charge qu'enregistrent WHOOP et Polar, ainsi que le niveau de résilience d'Oura. La disponibilité (readiness) d'Oura apparaît comme votre score de récupération, en lien ci-dessus.", + "emptyOuraPermission": "Oura est connecté, mais l'autorisation accordée n'inclut pas les données de santé cardiaque, dont dépend le niveau de résilience d'Oura. Reconnectez Oura une fois dans Réglages, Intégrations et autorisez l'accès à la santé cardiaque ; le niveau apparaîtra ici à la prochaine synchronisation. La disponibilité (readiness) d'Oura est déjà votre score de récupération, en lien ci-dessus.", + "emptyOuraNoResilience": "Oura est connecté et l'autorisation couvre la résilience, mais Oura n'a pas encore envoyé de niveau de résilience pour ce compte. HealthLog le demande à chaque synchronisation, il apparaîtra ici dès qu'Oura en fournira un. La disponibilité (readiness) d'Oura est déjà votre score de récupération, en lien ci-dessus.", + "emptyAction": "Ouvrir les intégrations", "block": { "ansChargeExplainer": "À quel point votre système nerveux autonome est rechargé pour la journée : une mesure de recharge issue de vos signaux nocturnes de cœur et de respiration.", "dayStrainExplainer": "La charge cardiovasculaire de la journée sur l'échelle d'effort de votre appareil. Lisez-la au regard de votre récupération, pas comme un objectif en soi.", diff --git a/messages/it.json b/messages/it.json index 5446a3205..dd8413a22 100644 --- a/messages/it.json +++ b/messages/it.json @@ -4389,6 +4389,10 @@ "consistency": "Costanza", "timing": "Orario", "composition": "Composizione" + }, + "gap": { + "fewNights": "notti ancora insufficienti ({count} su 3)", + "noMainSleep": "nessun sonno principale l'ultima notte" } }, "READINESS": { @@ -4657,6 +4661,9 @@ "scoreCrossLink": "Vedi il punteggio di recupero composito", "emptyTitle": "Ancora nessun dato di recupero", "empty": "Qui non c'è ancora nulla da mostrare. Questa pagina mostra i valori di ricarica, sforzo e carico che registrano WHOOP e Polar, e il livello di resilienza di Oura. La prontezza (readiness) di Oura compare come punteggio di recupero, collegato qui sopra.", + "emptyOuraPermission": "Oura è collegato, ma l'autorizzazione concessa non include i dati sulla salute cardiaca, da cui dipende il livello di resilienza di Oura. Ricollega Oura una volta in Impostazioni, Integrazioni e consenti l'accesso alla salute cardiaca; il livello comparirà qui con la prossima sincronizzazione. La prontezza (readiness) di Oura è già il tuo punteggio di recupero, collegato qui sopra.", + "emptyOuraNoResilience": "Oura è collegato e l'autorizzazione copre la resilienza, ma Oura non ha ancora inviato un livello di resilienza per questo account. HealthLog lo richiede a ogni sincronizzazione, quindi comparirà qui non appena Oura ne fornirà uno. La prontezza (readiness) di Oura è già il tuo punteggio di recupero, collegato qui sopra.", + "emptyAction": "Apri integrazioni", "block": { "ansChargeExplainer": "Quanto è carico il tuo sistema nervoso autonomo per la giornata: una lettura di ricarica ricavata dai segnali notturni di cuore e respiro.", "dayStrainExplainer": "Il carico cardiovascolare della giornata sulla scala di sforzo del tuo dispositivo. Leggilo in rapporto al tuo recupero, non come obiettivo a sé.", diff --git a/messages/ko.json b/messages/ko.json index 753810a79..e3f8d67a8 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -4389,6 +4389,10 @@ "consistency": "일관성", "timing": "타이밍", "composition": "구성" + }, + "gap": { + "fewNights": "아직 밤이 부족함 ({count}/3)", + "noMainSleep": "최근 밤에 주 수면 없음" } }, "READINESS": { @@ -4657,6 +4661,9 @@ "scoreCrossLink": "종합 회복 점수 보기", "emptyTitle": "아직 회복 데이터가 없어요", "empty": "아직 여기에 표시할 내용이 없어요. 이 페이지는 WHOOP과 Polar가 기록하는 충전, 부하, 운동량 값과 Oura의 회복 탄력성 단계를 보여줘요. Oura의 준비도(Readiness)는 위에 연결된 회복 점수에 표시돼요.", + "emptyOuraPermission": "Oura는 연결되어 있지만 허용된 권한에 심장 건강 데이터가 빠져 있어요. Oura의 회복 탄력성 단계는 바로 이 데이터에 속해요. 설정, 통합에서 Oura를 한 번 다시 연결하고 심장 건강 접근을 허용하면 다음 동기화 때 여기에 단계가 표시돼요. Oura의 준비도(Readiness)는 이미 위에 연결된 회복 점수에 표시돼요.", + "emptyOuraNoResilience": "Oura가 연결되어 있고 권한에도 회복 탄력성이 포함되어 있지만, Oura가 아직 이 계정의 회복 탄력성 단계를 보내지 않았어요. HealthLog는 동기화할 때마다 요청하므로 Oura가 보내는 즉시 여기에 표시돼요. Oura의 준비도(Readiness)는 이미 위에 연결된 회복 점수에 표시돼요.", + "emptyAction": "통합 열기", "block": { "ansChargeExplainer": "오늘 하루를 맞을 자율신경계의 충전 상태예요. 밤사이 심장·호흡 신호에서 읽어낸 재충전 지표예요.", "dayStrainExplainer": "기기의 부하 척도로 본 하루 전체의 심혈관 부하예요. 그 자체를 목표로 삼지 말고 회복과 견줘 보세요.", diff --git a/messages/pl.json b/messages/pl.json index 3046dc530..36cead91e 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -4389,6 +4389,10 @@ "consistency": "Regularność", "timing": "Pora", "composition": "Struktura" + }, + "gap": { + "fewNights": "za mało nocy ({count} z 3)", + "noMainSleep": "ostatniej nocy brak głównego snu" } }, "READINESS": { @@ -4657,6 +4661,9 @@ "scoreCrossLink": "Zobacz łączny wynik regeneracji", "emptyTitle": "Brak danych o regeneracji", "empty": "Na razie nie ma tu nic do pokazania. Ta strona pokazuje odczyty ładowania, wysiłku i obciążenia, które rejestrują WHOOP i Polar, oraz poziom odporności z Oura. Gotowość (readiness) z Oura pojawia się jako wynik regeneracji, link powyżej.", + "emptyOuraPermission": "Oura jest połączona, ale udzielone uprawnienie nie obejmuje danych o zdrowiu serca, od których zależy poziom odporności z Oura. Połącz Oura ponownie raz w Ustawieniach, Integracjach i zezwól na dostęp do zdrowia serca; poziom pojawi się tutaj przy następnej synchronizacji. Gotowość (readiness) z Oura jest już Twoim wynikiem regeneracji, link powyżej.", + "emptyOuraNoResilience": "Oura jest połączona, a uprawnienie obejmuje odporność, ale Oura nie przesłała jeszcze poziomu odporności dla tego konta. HealthLog pyta o niego przy każdej synchronizacji, więc pojawi się tutaj, gdy tylko Oura go dostarczy. Gotowość (readiness) z Oura jest już Twoim wynikiem regeneracji, link powyżej.", + "emptyAction": "Otwórz integracje", "block": { "ansChargeExplainer": "Jak naładowany jest Twój autonomiczny układ nerwowy na dany dzień: odczyt regeneracji oparty na nocnych sygnałach serca i oddechu.", "dayStrainExplainer": "Dzienne obciążenie sercowo-naczyniowe na skali wysiłku Twojego urządzenia. Odczytuj je w odniesieniu do regeneracji, nie jako cel sam w sobie.", diff --git a/src/components/insights/__tests__/recovery-empty-state.test.tsx b/src/components/insights/__tests__/recovery-empty-state.test.tsx new file mode 100644 index 000000000..2fb6d9358 --- /dev/null +++ b/src/components/insights/__tests__/recovery-empty-state.test.tsx @@ -0,0 +1,122 @@ +import { beforeEach, 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 { DataSummary } from "@/lib/analytics/trends"; + +/** + * The recovery page's empty note says what is missing for THIS account. An + * Oura ring fills the page through its resilience level alone, so a connected + * ring with an empty page is either a grant that leaves out the heart-health + * data the level sits behind, or Oura not having sent a level yet. The + * connection facts come from the server's integrations read; the page only + * picks the sentence. + */ + +vi.mock("next/dynamic", () => ({ + default: () => { + const Stub = () =>
; + Stub.displayName = "MiniChartStub"; + return Stub; + }, +})); + +vi.mock("@/hooks/use-auth", () => ({ + useAuth: () => ({ user: { timezone: "UTC" }, isAuthenticated: true }), +})); + +const analyticsMock = vi.fn(); +vi.mock("@/hooks/use-insights-analytics", () => ({ + useInsightsAnalytics: () => analyticsMock(), +})); + +import { RecoverySection } from "../recovery/recovery-section"; + +function summary(count: number): DataSummary { + return { + count, + latest: null, + min: null, + max: null, + mean: null, + median: null, + avg7: null, + avg30: null, + slope7: null, + slope30: null, + anomalyCount: 0, + }; +} + +/** An Oura-only account: readiness, HRV and sleep, no resilience level. */ +const OURA_SUMMARIES = { + SLEEP_DURATION: summary(30), + RECOVERY_SCORE: summary(30), + HRV_RMSSD: summary(30), +}; + +function render(oura: Record