From 640793a3e88391aaac0b6cbed1463c962418920e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 15 Jun 2026 21:03:48 +0200 Subject: [PATCH] refactor(nav): surface Recovery as an Insights tab instead of a left-nav item Recovery is a composite wearable surface (WHOOP / Polar / Oura recovery, strain, ANS charge, cardio load), not a top-level tracking destination. It moves off the left nav and onto the Insights tab strip as a pill at /insights/recovery. The pill is always present (like Overview) since the page self-gates every block and falls back to a calm empty note rather than carrying its own availability signal. Adds the insights.navRecovery label across all six locales. --- messages/de.json | 1 + messages/en.json | 1 + messages/es.json | 1 + messages/fr.json | 1 + messages/it.json | 1 + messages/pl.json | 1 + .../__tests__/insights-tab-strip.test.tsx | 19 +++++++++++++++++++ .../insights/insights-tab-strip.tsx | 12 ++++++++++++ .../layout/__tests__/nav-model.test.ts | 9 ++++++++- src/components/layout/nav-model.ts | 15 +++------------ 10 files changed, 48 insertions(+), 13 deletions(-) diff --git a/messages/de.json b/messages/de.json index c25559632..0509f4a9d 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2784,6 +2784,7 @@ } }, "navWorkouts": "Workouts", + "navRecovery": "Erholung", "workouts": { "title": "Workouts", "description": "Letzte Läufe, Touren, Spaziergänge und Krafteinheiten — quellenübergreifend entdoppelt zwischen Apple Watch und Withings.", diff --git a/messages/en.json b/messages/en.json index 2d774166a..bbeb6b05a 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2784,6 +2784,7 @@ } }, "navWorkouts": "Workouts", + "navRecovery": "Recovery", "workouts": { "title": "Workouts", "description": "Recent runs, rides, walks and strength sessions, deduped across Apple Watch and Withings sources.", diff --git a/messages/es.json b/messages/es.json index 21d3c6f22..1eed54b4c 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2784,6 +2784,7 @@ } }, "navWorkouts": "Entrenamientos", + "navRecovery": "Recuperación", "workouts": { "title": "Entrenamientos", "description": "Carreras, salidas en bici, paseos y sesiones de fuerza recientes, deduplicados entre Apple Watch y Withings.", diff --git a/messages/fr.json b/messages/fr.json index e75419797..c3a2a9dcc 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -2784,6 +2784,7 @@ } }, "navWorkouts": "Entraînements", + "navRecovery": "Récupération", "workouts": { "title": "Entraînements", "description": "Courses, sorties à vélo, marches et séances de musculation récentes, dédupliquées entre Apple Watch et Withings.", diff --git a/messages/it.json b/messages/it.json index ca7636642..2afb522b8 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2784,6 +2784,7 @@ } }, "navWorkouts": "Allenamenti", + "navRecovery": "Recupero", "workouts": { "title": "Allenamenti", "description": "Corse, uscite in bici, camminate e sessioni di forza recenti, deduplicati tra Apple Watch e Withings.", diff --git a/messages/pl.json b/messages/pl.json index e9c9ca571..a14120194 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2784,6 +2784,7 @@ } }, "navWorkouts": "Treningi", + "navRecovery": "Regeneracja", "workouts": { "title": "Treningi", "description": "Ostatnie biegi, jazdy, marsze i treningi siłowe — zdeduplikowane między Apple Watch a Withings.", diff --git a/src/components/insights/__tests__/insights-tab-strip.test.tsx b/src/components/insights/__tests__/insights-tab-strip.test.tsx index 755b1567c..db8f61043 100644 --- a/src/components/insights/__tests__/insights-tab-strip.test.tsx +++ b/src/components/insights/__tests__/insights-tab-strip.test.tsx @@ -94,6 +94,25 @@ describe(" — availability gating (v1.4.27 F19)", () => { expect(html).toContain(">Overview<"); }); + it("always renders the Recovery pill (composite wearable surface, self-gated page)", () => { + // v1.18.0 — Recovery left the left-nav for an Insights pill. It is a + // composite WHOOP / Polar / Oura surface, not a single MeasurementType, + // so it has no `summaries[METRIC].count` to gate on; the page itself + // data-gates each block and falls back to a calm empty note, so the + // pill is always present (like Overview) regardless of availability. + const noAvailability = render(); + expect(noAvailability).toContain(">Recovery<"); + expect(noAvailability).toContain('href="/insights/recovery"'); + + const emptyAvailability: InsightInputs = { + summaries: {}, + hasMood: false, + hasMedication: false, + }; + const html = render(); + expect(html).toContain(">Recovery<"); + }); + it("lights up Mood when hasMood flips to true", () => { const availability: InsightInputs = { summaries: {}, diff --git a/src/components/insights/insights-tab-strip.tsx b/src/components/insights/insights-tab-strip.tsx index f54d00086..b61e02394 100644 --- a/src/components/insights/insights-tab-strip.tsx +++ b/src/components/insights/insights-tab-strip.tsx @@ -420,6 +420,18 @@ function buildTabs( href: INSIGHTS_OVERVIEW_PATH, labelKey: "insights.navOverview", }, + // v1.18.0 — Recovery is an Insights pill, no longer a left-nav home. + // It is a composite wearable surface (WHOOP / Polar / Oura recovery, + // strain, ANS charge, cardio load), not a single MeasurementType, so + // it has no `summaries[METRIC].count` to gate on. The page itself + // data-gates every block and falls back to a calm empty note, so the + // pill is always present like Overview rather than carrying its own + // availability signal. + { + kind: "link", + href: `${INSIGHTS_OVERVIEW_PATH}/recovery`, + labelKey: "insights.navRecovery", + }, ]; // v1.4.34 IW-D — track which groups have already been emitted so the diff --git a/src/components/layout/__tests__/nav-model.test.ts b/src/components/layout/__tests__/nav-model.test.ts index eb6684687..c7f64cc3f 100644 --- a/src/components/layout/__tests__/nav-model.test.ts +++ b/src/components/layout/__tests__/nav-model.test.ts @@ -53,11 +53,18 @@ describe("nav-model destination list", () => { expect(hrefs).toContain("/vorsorge"); const vorsorge = NAV_DESTINATIONS.find((d) => d.href === "/vorsorge"); expect(vorsorge?.tKey).toBe("nav.vorsorge"); - // Peer to Labs / Recovery: sits in the clinical spine, before Insights. + // Peer to Labs: sits in the clinical spine, before Insights. expect(hrefs.indexOf("/labs")).toBeLessThan(hrefs.indexOf("/vorsorge")); expect(hrefs.indexOf("/vorsorge")).toBeLessThan(hrefs.indexOf("/insights")); }); + it("does not list Recovery as a left-nav destination (it is an Insights pill)", () => { + // v1.18.0 — Recovery moved off the left nav and surfaces as an + // Insights tab-strip pill at `/insights/recovery` instead. + const hrefs = NAV_DESTINATIONS.map((d) => d.href); + expect(hrefs).not.toContain("/insights/recovery"); + }); + }); describe("nav-model utility tail (N-1 — one shared list)", () => { diff --git a/src/components/layout/nav-model.ts b/src/components/layout/nav-model.ts index 764aa5cac..5fce0b12b 100644 --- a/src/components/layout/nav-model.ts +++ b/src/components/layout/nav-model.ts @@ -5,7 +5,6 @@ import { Droplets, Dumbbell, FlaskConical, - HeartPulse, Home, Lightbulb, MessagesSquare, @@ -99,17 +98,9 @@ export const NAV_DESTINATIONS: ReadonlyArray = [ icon: Dumbbell, tourId: "nav-workouts", }, - // v1.17.1 — the Recovery surface gets a nav home alongside Workouts. It - // hosts the WHOOP / Polar device-native recovery + strain scores that were - // stored end-to-end but had no page. Like Workouts it is shown to every - // account (the page itself data-gates each block, so a non-wearable user - // lands on a calm empty note rather than a broken surface). - { - href: "/insights/recovery", - tKey: "nav.recovery", - icon: HeartPulse, - tourId: "nav-recovery", - }, + // v1.18.0 — Recovery left the left-nav: it surfaces as an Insights pill + // (`/insights/recovery`) rather than a top-level destination, so it no + // longer appears in `NAV_DESTINATIONS`. { href: "/insights", tKey: "nav.insights",