diff --git a/messages/de.json b/messages/de.json index dbbebfa63..d99c1821b 100644 --- a/messages/de.json +++ b/messages/de.json @@ -4662,7 +4662,7 @@ "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.", + "emptyOuraNoResilience": "Oura ist verbunden, aber für dieses Konto ist noch keine Resilienz-Stufe angekommen. HealthLog fragt bei jeder Synchronisierung danach, sie erscheint hier, sobald Oura eine liefert. Zeigt die Oura-App eine Stufe, nennt die Oura-Karte unter Einstellungen, Integrationen alles, was die Verbindung nicht liefern kann. 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.", diff --git a/messages/en.json b/messages/en.json index b2f276002..5eb6c6f94 100644 --- a/messages/en.json +++ b/messages/en.json @@ -4662,7 +4662,7 @@ "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.", + "emptyOuraNoResilience": "Oura is connected, but no resilience level has arrived for this account yet. HealthLog asks Oura for it on every sync, so it appears here as soon as Oura provides one. If the Oura app shows a level, the Oura card under Settings, Integrations names anything the connection cannot deliver. 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.", diff --git a/messages/es.json b/messages/es.json index 54a3ebd85..6427ccd2f 100644 --- a/messages/es.json +++ b/messages/es.json @@ -4662,7 +4662,7 @@ "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.", + "emptyOuraNoResilience": "Oura está conectado, pero todavía no ha llegado ningún nivel de resiliencia para esta cuenta. HealthLog lo pide en cada sincronización, así que aparecerá aquí en cuanto Oura lo proporcione. Si la app de Oura muestra un nivel, la tarjeta de Oura en Ajustes, Integraciones indica todo lo que la conexión no puede entregar. 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.", diff --git a/messages/fr.json b/messages/fr.json index 4bf94e642..50ee3f224 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -4662,7 +4662,7 @@ "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.", + "emptyOuraNoResilience": "Oura est connecté, mais aucun niveau de résilience n'est encore arrivé pour ce compte. HealthLog le demande à chaque synchronisation, il apparaîtra ici dès qu'Oura en fournira un. Si l'app Oura affiche un niveau, la carte Oura dans Réglages, Intégrations indique tout ce que la connexion ne peut pas fournir. 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.", diff --git a/messages/it.json b/messages/it.json index 5656b9413..009452201 100644 --- a/messages/it.json +++ b/messages/it.json @@ -4662,7 +4662,7 @@ "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.", + "emptyOuraNoResilience": "Oura è collegato, ma per questo account non è ancora arrivato alcun livello di resilienza. HealthLog lo richiede a ogni sincronizzazione, quindi comparirà qui non appena Oura ne fornirà uno. Se l'app Oura mostra un livello, la scheda Oura in Impostazioni, Integrazioni indica tutto ciò che la connessione non può fornire. 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.", diff --git a/messages/ko.json b/messages/ko.json index ae222e3f6..d900582ff 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -4662,7 +4662,7 @@ "emptyTitle": "아직 회복 데이터가 없어요", "empty": "아직 여기에 표시할 내용이 없어요. 이 페이지는 WHOOP과 Polar가 기록하는 충전, 부하, 운동량 값과 Oura의 회복 탄력성 단계를 보여줘요. Oura의 준비도(Readiness)는 위에 연결된 회복 점수에 표시돼요.", "emptyOuraPermission": "Oura는 연결되어 있지만 허용된 권한에 심장 건강 데이터가 빠져 있어요. Oura의 회복 탄력성 단계는 바로 이 데이터에 속해요. 설정, 통합에서 Oura를 한 번 다시 연결하고 심장 건강 접근을 허용하면 다음 동기화 때 여기에 단계가 표시돼요. Oura의 준비도(Readiness)는 이미 위에 연결된 회복 점수에 표시돼요.", - "emptyOuraNoResilience": "Oura가 연결되어 있고 권한에도 회복 탄력성이 포함되어 있지만, Oura가 아직 이 계정의 회복 탄력성 단계를 보내지 않았어요. HealthLog는 동기화할 때마다 요청하므로 Oura가 보내는 즉시 여기에 표시돼요. Oura의 준비도(Readiness)는 이미 위에 연결된 회복 점수에 표시돼요.", + "emptyOuraNoResilience": "Oura가 연결되어 있지만 이 계정의 회복 탄력성 단계가 아직 도착하지 않았어요. HealthLog는 동기화할 때마다 요청하므로 Oura가 보내는 즉시 여기에 표시돼요. Oura 앱에 단계가 보인다면 설정, 통합의 Oura 카드에서 이 연결이 가져올 수 없는 항목을 확인할 수 있어요. Oura의 준비도(Readiness)는 이미 위에 연결된 회복 점수에 표시돼요.", "emptyAction": "통합 열기", "block": { "ansChargeExplainer": "오늘 하루를 맞을 자율신경계의 충전 상태예요. 밤사이 심장·호흡 신호에서 읽어낸 재충전 지표예요.", diff --git a/messages/pl.json b/messages/pl.json index a8c699fd4..1c83b7121 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -4662,7 +4662,7 @@ "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.", + "emptyOuraNoResilience": "Oura jest połączona, ale dla tego konta nie dotarł jeszcze żaden poziom odporności. HealthLog pyta o niego przy każdej synchronizacji, więc pojawi się tutaj, gdy tylko Oura go dostarczy. Jeśli aplikacja Oura pokazuje poziom, karta Oura w Ustawieniach, Integracjach wymienia wszystko, czego połączenie nie może 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.", diff --git a/src/components/insights/__tests__/recovery-empty-state.test.tsx b/src/components/insights/__tests__/recovery-empty-state.test.tsx index 2fb6d9358..06f4f5240 100644 --- a/src/components/insights/__tests__/recovery-empty-state.test.tsx +++ b/src/components/insights/__tests__/recovery-empty-state.test.tsx @@ -9,10 +9,10 @@ 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. + * ring with an empty page asks for a reconnect only when the server recorded + * that the grant leaves resilience out, and otherwise says only that no + * level has arrived. The connection facts come from the server's + * integrations read; the page only picks the sentence, and waits for it. */ vi.mock("next/dynamic", () => ({ @@ -57,14 +57,19 @@ const OURA_SUMMARIES = { HRV_RMSSD: summary(30), }; -function render(oura: Record | null) { +function render( + oura: Record | null, + opts: { statusLoaded?: boolean } = {}, +) { const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } }, }); - queryClient.setQueryData(queryKeys.integrationsStatus(), { - threshold: 3, - integrations: oura ? [{ integration: "oura", ...oura }] : [], - }); + if (opts.statusLoaded !== false) { + queryClient.setQueryData(queryKeys.integrationsStatus(), { + threshold: 3, + integrations: oura ? [{ integration: "oura", ...oura }] : [], + }); + } return renderToStaticMarkup( @@ -104,16 +109,34 @@ describe(" empty note", () => { expect(html).toContain('href="/settings/integrations"'); }); - it("says Oura has sent no level yet when the grant covers it", () => { + it("says only that no level has arrived when no withheld permission is recorded", () => { const html = render({ connected: true, health: health([{ item: "spo2", cause: "permission" }]), }); expect(html).toContain('data-empty-case="oura-no-resilience"'); - expect(html).toContain("has not sent a resilience level"); + expect(html).toContain("no resilience level has arrived"); + // The server cannot prove the grant covers resilience, so the note + // never says it does. + expect(html).not.toContain("permission covers"); expect(html).not.toContain('href="/settings/integrations"'); }); + it("stays neutral when the connection carries no health block", () => { + const html = render({ connected: true }); + expect(html).toContain('data-empty-case="oura-no-resilience"'); + expect(html).not.toContain("permission covers"); + }); + + it("shows no note until the connection status has loaded", () => { + const html = render( + { connected: true, health: health([]) }, + { statusLoaded: false }, + ); + expect(html).toContain('data-slot="recovery-loading"'); + expect(html).not.toContain("data-empty-case"); + }); + it("keeps the general note for an account without Oura", () => { const html = render(null); expect(html).toContain('data-empty-case="general"'); diff --git a/src/components/insights/recovery/recovery-section.tsx b/src/components/insights/recovery/recovery-section.tsx index f916fd1df..4a2718ade 100644 --- a/src/components/insights/recovery/recovery-section.tsx +++ b/src/components/insights/recovery/recovery-section.tsx @@ -49,11 +49,12 @@ interface BlockGroup { /** * Why this account's recovery page is empty, from the server's view of its * Oura connection. An Oura ring feeds this page through its resilience level - * alone, so a connected ring with an empty page has exactly two causes: the - * grant does not cover the heart-health data the level sits behind (the - * server records that per collection and publishes it as an unavailable - * item), or Oura has not sent a level for this account. Everyone else gets - * the general note naming what fills the page. + * alone. When the server records that the grant leaves resilience out, the + * note asks for one reconnect. Otherwise it says only that no level has + * arrived: the server cannot tell "the grant covers it" from "not recorded + * yet" (the scope note is cleared on every reconnect and written only by a + * sync that reached Oura), so the note never claims coverage. Everyone else + * gets the general note naming what fills the page. */ type RecoveryEmptyCase = "general" | "oura-permission" | "oura-no-resilience"; @@ -222,7 +223,9 @@ export function RecoverySection() { ); const emptyCase = recoveryEmptyCase(pickStatus(statuses.data, "oura")); - if (isLoading || summaries == null) { + // The empty note waits for the connection status, so it never reads as + // the general note first and then changes under the reader. + if (isLoading || summaries == null || (!hasAny && statuses.isPending)) { return (