diff --git a/e2e/v1427-coach-launch.spec.ts b/e2e/v1427-coach-launch.spec.ts index 7ad75be77..cabecdadb 100644 --- a/e2e/v1427-coach-launch.spec.ts +++ b/e2e/v1427-coach-launch.spec.ts @@ -5,20 +5,21 @@ import { STORAGE_STATE_PATH } from "./setup/global-setup"; /** * Coach launch surfaces on `/insights/{slug}` sub-pages. * - * Contracts under test (v1.16.1): + * Contracts under test (v1.16.8): * - * 1. The floating bubble (`data-slot="coach-nudge-bubble"`) renders - * ONLY while an unread Coach-initiated nudge exists - * (`GET /api/insights/coach/nudge-status`). By default no bubble - * mounts on any viewport; the everyday entry point is the inline - * launch the sub-page shell mounts in the header - * (`data-slot="coach-launch-icon"`, visible across breakpoints). + * 1. The floating Coach FAB (`data-slot="coach-fab"`) is a permanent + * launcher on every authenticated page. An unread Coach-initiated + * nudge (`GET /api/insights/coach/nudge-status`) paints an unread + * dot (`data-slot="coach-fab-unread"`) on its corner; with nothing + * unread the FAB renders without the dot. The inline launch the + * sub-page shell mounts in the header + * (`data-slot="coach-launch-icon"`) stays the contextual entry. * * 2. Clicking the inline launch opens the Coach drawer — as a * bottom-sheet (`data-variant="bottom-sheet"`) on `=sm`. * - * 3. With an unread nudge the bubble mounts and clicking it + * 3. With an unread nudge the FAB carries the dot and clicking it * navigates to the full-page chat at `/insights/coach`. * * 4. The drawer's "Conversations" button no longer opens an in-panel @@ -124,7 +125,7 @@ test.describe("Coach launch surfaces on insights sub-pages", () => { }); for (const slug of ["blutdruck", "gewicht", "schlaf"] as const) { - test(`/insights/${slug} mounts the inline Coach launch and no bubble by default on Pixel 5`, async ({ + test(`/insights/${slug} mounts the inline Coach launch and the dot-free FAB on Pixel 5`, async ({ page, }, testInfo) => { test.skip( @@ -134,18 +135,20 @@ test.describe("Coach launch surfaces on insights sub-pages", () => { await page.goto(`/insights/${slug}`, { waitUntil: "domcontentloaded" }); - // The header launch icon is the everyday entry point — visible - // on mobile too since the layout bubble is nudge-gated. + // The header launch icon stays the contextual entry point. const icon = page.locator('[data-slot="coach-launch-icon"]').first(); await expect(icon).toBeVisible({ timeout: 10_000 }); - // No unread nudge → no floating bubble in the DOM. + // The FAB is a permanent launcher; with no unread nudge it + // renders without the unread dot. + const fab = page.locator('[data-slot="coach-fab"]'); + await expect(fab).toBeVisible({ timeout: 10_000 }); await expect( - page.locator('[data-slot="coach-nudge-bubble"]'), + page.locator('[data-slot="coach-fab-unread"]'), ).toHaveCount(0); }); - test(`/insights/${slug} mounts the inline Coach launch and no bubble by default on Desktop Chrome`, async ({ + test(`/insights/${slug} mounts the inline Coach launch and the dot-free FAB on Desktop Chrome`, async ({ page, }, testInfo) => { test.skip( @@ -158,8 +161,10 @@ test.describe("Coach launch surfaces on insights sub-pages", () => { const icon = page.locator('[data-slot="coach-launch-icon"]').first(); await expect(icon).toBeVisible({ timeout: 10_000 }); + const fab = page.locator('[data-slot="coach-fab"]'); + await expect(fab).toBeVisible({ timeout: 10_000 }); await expect( - page.locator('[data-slot="coach-nudge-bubble"]'), + page.locator('[data-slot="coach-fab-unread"]'), ).toHaveCount(0); }); } @@ -202,10 +207,10 @@ test.describe("Coach launch surfaces on insights sub-pages", () => { await expect(drawer).toHaveAttribute("data-variant", "side-sheet"); }); - test("an unread Coach nudge mounts the floating bubble and clicking it opens the full-page chat", async ({ + test("an unread Coach nudge paints the FAB dot and clicking the FAB opens the full-page chat", async ({ page, }) => { - // The bubble contract is viewport-independent — the test runs on + // The FAB contract is viewport-independent — the test runs on // both projects. Override the default nudge mock with an unread // one (later registration wins). await page.route("**/api/insights/coach/nudge-status*", (route) => @@ -221,9 +226,15 @@ test.describe("Coach launch surfaces on insights sub-pages", () => { await page.goto("/insights/blutdruck", { waitUntil: "domcontentloaded" }); - const bubble = page.locator('[data-slot="coach-nudge-bubble"]'); - await expect(bubble).toBeVisible({ timeout: 10_000 }); - await bubble.click(); + const fab = page.locator('[data-slot="coach-fab"]'); + await expect(fab).toBeVisible({ timeout: 10_000 }); + await expect(fab).toHaveAttribute("data-unread", "true", { + timeout: 10_000, + }); + await expect( + page.locator('[data-slot="coach-fab-unread"]'), + ).toBeVisible(); + await fab.click(); await page.waitForURL("**/insights/coach", { timeout: 10_000 }); await expect( diff --git a/messages/de.json b/messages/de.json index b35371b88..9850095b8 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1667,7 +1667,6 @@ "charts": { "loadingLabel": "Diagramm wird geladen…", "loadingSlowHint": "Auswertungen werden berechnet — das kann einen Moment dauern.", - "adjustTargets": "Zielwerte anpassen", "weight": "Gewicht", "bloodPressure": "Blutdruck", "pulse": "Puls", @@ -2036,6 +2035,7 @@ "thinking": "Denke nach…", "composerPlaceholder": "Frag mich etwas zu deinen Daten…", "nudgeBubbleLabel": "Neue Nachricht vom Coach — Chat öffnen", + "fabLabel": "Coach öffnen", "selfContextAdopt": { "offer": "Antwort in deine Selbstauskunft übernehmen?", "confirm": "Übernehmen", diff --git a/messages/en.json b/messages/en.json index af05ebee5..27f63a816 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1667,7 +1667,6 @@ "charts": { "loadingLabel": "Loading chart…", "loadingSlowHint": "Computing analytics — this can take a moment.", - "adjustTargets": "Adjust targets", "weight": "Weight", "bloodPressure": "Blood Pressure", "pulse": "Pulse", @@ -2036,6 +2035,7 @@ "thinking": "Thinking…", "composerPlaceholder": "Ask anything about your data…", "nudgeBubbleLabel": "New message from the Coach — open chat", + "fabLabel": "Open the Coach", "selfContextAdopt": { "offer": "Add this answer to your self-context?", "confirm": "Add", diff --git a/messages/es.json b/messages/es.json index 869f354f4..f34d8725c 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1667,7 +1667,6 @@ "charts": { "loadingLabel": "Cargando la gráfica…", "loadingSlowHint": "Calculando los análisis — puede tardar un momento.", - "adjustTargets": "Ajustar objetivos", "weight": "Peso", "bloodPressure": "Tensión arterial", "pulse": "Pulso", @@ -2036,6 +2035,7 @@ "thinking": "Pensando…", "composerPlaceholder": "Pregúntame sobre tus datos…", "nudgeBubbleLabel": "Nuevo mensaje del coach — abrir el chat", + "fabLabel": "Abrir el coach", "selfContextAdopt": { "offer": "¿Añadir esta respuesta a tu información personal?", "confirm": "Añadir", diff --git a/messages/fr.json b/messages/fr.json index 8d6eda75c..2bd929dc7 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1667,7 +1667,6 @@ "charts": { "loadingLabel": "Chargement du graphique…", "loadingSlowHint": "Calcul des analyses — cela peut prendre un instant.", - "adjustTargets": "Ajuster les valeurs cibles", "weight": "Poids", "bloodPressure": "Tension artérielle", "pulse": "Pouls", @@ -2036,6 +2035,7 @@ "thinking": "Réflexion en cours…", "composerPlaceholder": "Pose-moi une question sur tes données…", "nudgeBubbleLabel": "Nouveau message du coach — ouvrir le chat", + "fabLabel": "Ouvrir le coach", "selfContextAdopt": { "offer": "Ajouter cette réponse à vos informations personnelles ?", "confirm": "Ajouter", diff --git a/messages/it.json b/messages/it.json index 9162e2ef0..0425b7449 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1667,7 +1667,6 @@ "charts": { "loadingLabel": "Caricamento del grafico…", "loadingSlowHint": "Calcolo delle analisi — può richiedere un momento.", - "adjustTargets": "Regola i valori target", "weight": "Peso", "bloodPressure": "Pressione arteriosa", "pulse": "Pulsazioni", @@ -2036,6 +2035,7 @@ "thinking": "Sto pensando…", "composerPlaceholder": "Chiedimi qualcosa sui tuoi dati…", "nudgeBubbleLabel": "Nuovo messaggio dal coach — apri la chat", + "fabLabel": "Apri il coach", "selfContextAdopt": { "offer": "Aggiungere questa risposta alle tue informazioni personali?", "confirm": "Aggiungi", diff --git a/messages/pl.json b/messages/pl.json index f1bdaca93..60672612c 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1667,7 +1667,6 @@ "charts": { "loadingLabel": "Ładowanie wykresu…", "loadingSlowHint": "Obliczanie analiz — to może chwilę potrwać.", - "adjustTargets": "Dostosuj wartości docelowe", "weight": "Waga", "bloodPressure": "Ciśnienie tętnicze", "pulse": "Tętno", @@ -2036,6 +2035,7 @@ "thinking": "Myślę…", "composerPlaceholder": "Zapytaj o swoje dane…", "nudgeBubbleLabel": "Nowa wiadomość od trenera — otwórz czat", + "fabLabel": "Otwórz trenera", "selfContextAdopt": { "offer": "Dodać tę odpowiedź do Twoich informacji o sobie?", "confirm": "Dodaj", diff --git a/src/app/insights/blood-pressure/page.tsx b/src/app/insights/blood-pressure/page.tsx index f5f6d3b9a..7cb330f84 100644 --- a/src/app/insights/blood-pressure/page.tsx +++ b/src/app/insights/blood-pressure/page.tsx @@ -160,7 +160,6 @@ export default function InsightsBlutdruckPage() { unit="mmHg" yAxisUnit="mmHg" targetZones={bpTargetZones} - targetSettingsHref="/settings/thresholds" compareBaseline={compareBaseline} userTimezone={user?.timezone} onVisibleStats={onVisibleStats} diff --git a/src/app/insights/layout.tsx b/src/app/insights/layout.tsx index 5d7a83631..e1150ee07 100644 --- a/src/app/insights/layout.tsx +++ b/src/app/insights/layout.tsx @@ -1,7 +1,6 @@ import type { ReactNode } from "react"; import { InsightsLayoutShell } from "@/components/insights/insights-layout-shell"; -import { LayoutCoachFab } from "@/components/insights/layout-coach-fab"; /** * v1.4.25 W4 — shared layout for the routed `/insights` sub-pages. @@ -18,25 +17,16 @@ import { LayoutCoachFab } from "@/components/insights/layout-coach-fab"; * the mother `/insights/page.tsx` consumes the same hook for its hero * strip + suggested-prompt chips. * - * v1.4.28 R3c — the mobile FAB also lives here via `` - * (carved out of `` so each sub-page mount no - * longer duplicates the FAB into the a11y tree). The inline - * `` pill stays on the per-page action rows where - * copy + position matter. - * * v1.4.34 IW-B — `` + `` now * live on the global `` (`src/components/layout/auth-shell.tsx`) * so every authenticated route (dashboard included) can open the drawer - * from the same context. The FAB stays scoped to `/insights/**` only — - * the floating action would distract from the dashboard hero on - * surfaces where a contextual inline button already covers the same - * affordance. + * from the same context. + * + * v1.16.8 — the floating Coach launcher (`LayoutCoachFab`) moved to + * `` as well: it renders once for every authenticated route + * and hides itself on the Coach page, so this layout no longer mounts + * any Coach surface of its own. */ export default function InsightsLayout({ children }: { children: ReactNode }) { - return ( - <> - {children} - - - ); + return {children}; } diff --git a/src/app/insights/weight/page.tsx b/src/app/insights/weight/page.tsx index c1b946e72..b31c56ede 100644 --- a/src/app/insights/weight/page.tsx +++ b/src/app/insights/weight/page.tsx @@ -106,7 +106,6 @@ export default function InsightsGewichtPage() { colors={["#bd93f9"]} unit="kg" valueBands={weightBands} - targetSettingsHref="/settings/thresholds" compareBaseline={compareBaseline} userTimezone={user?.timezone} onVisibleStats={onVisibleStats} diff --git a/src/components/charts/__tests__/health-chart-target-link-removed.test.ts b/src/components/charts/__tests__/health-chart-target-link-removed.test.ts new file mode 100644 index 000000000..69b884356 --- /dev/null +++ b/src/components/charts/__tests__/health-chart-target-link-removed.test.ts @@ -0,0 +1,46 @@ +/** + * v1.16.8 — the in-header "adjust targets" link is gone from the chart + * card. The affordance duplicated the page-level target-adjust control + * in the insights sub-page header (``); the chart + * stays a read surface. Pinned at the source level (same convention as + * the auth-shell hoist contract) so the link doesn't quietly return on + * a future header rework. + */ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +const HEALTH_CHART_PATH = resolve(__dirname, "..", "health-chart.tsx"); +const WEIGHT_PAGE_PATH = resolve( + __dirname, + "../../../app/insights/weight/page.tsx", +); +const BP_PAGE_PATH = resolve( + __dirname, + "../../../app/insights/blood-pressure/page.tsx", +); +const EN_BUNDLE_PATH = resolve(__dirname, "../../../../messages/en.json"); + +describe("v1.16.8 — chart-card target-adjust link removed", () => { + it("ships no targetSettingsHref prop or header link in the chart", () => { + const source = readFileSync(HEALTH_CHART_PATH, "utf8"); + expect(source).not.toContain("targetSettingsHref"); + expect(source).not.toContain("chart-target-settings-link"); + expect(source).not.toContain("charts.adjustTargets"); + }); + + it("weight and blood-pressure pages no longer pass the link href", () => { + for (const path of [WEIGHT_PAGE_PATH, BP_PAGE_PATH]) { + const source = readFileSync(path, "utf8"); + expect(source).not.toContain("targetSettingsHref"); + } + }); + + it("the orphaned locale key is gone", () => { + const bundle = JSON.parse(readFileSync(EN_BUNDLE_PATH, "utf8")) as { + charts: Record; + }; + expect(bundle.charts.adjustTargets).toBeUndefined(); + }); +}); diff --git a/src/components/charts/health-chart.tsx b/src/components/charts/health-chart.tsx index c8c6be1d7..8814bb555 100644 --- a/src/components/charts/health-chart.tsx +++ b/src/components/charts/health-chart.tsx @@ -18,8 +18,6 @@ import { ReferenceArea, ReferenceDot, } from "recharts"; -import { SlidersHorizontal } from "lucide-react"; -import Link from "next/link"; import { useState, useMemo, @@ -111,15 +109,6 @@ interface HealthChartProps { textColor?: string; lineOpacity?: number; }>; - /** - * Optional href for a small "adjust targets" link in the chart - * header. Callers whose charts paint a target band / zones (weight, - * blood pressure) pass `/settings/thresholds` so the band's origin — - * and the page that edits personal target ranges — is one click - * away. Omitted (every other mount) renders no link. Ignored in - * `mini` mode, which has no header. - */ - targetSettingsHref?: string; /** * v1.4.16 phase B5c — compact chart mode used by the Oura-style * rationale card. Drops the range tabs, the moving-average / trend @@ -537,7 +526,6 @@ export function HealthChart({ showYAxisUnit = true, valueBands, targetZones, - targetSettingsHref, mini = false, windowOverride, compareBaseline = "none", @@ -1471,19 +1459,10 @@ export function HealthChart({ )} )} - {/* Discoverability link from the painted target band to the - page that edits personal target ranges. Only mounts when - a caller passes `targetSettingsHref` (weight + BP). */} - {targetSettingsHref ? ( - -