From 2bb810852e1965e0e4e36499d2020c559037f821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 4 Jun 2026 23:21:42 +0200 Subject: [PATCH] refactor(insights): de-duplicate and reorder the overview to match the per-metric pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The per-metric pages are the canonical home for each metric's primary tile, target band, and correlation card. The overview was still rendering its own correlation row (weight × weekday, mood × pulse, BP × compliance) — the exact cross-view duplication to kill now that the Weight and Pulse pages own those cards via MetricCorrelationCard. Remove the on-overview CorrelationRow (component + test) and reorder the overview toward the iOS Insights structure: hero (coach + briefing + prompt chips) → detailed daily briefing → dynamics/alerts (today's signal + rhythm events) → vitals dashboard → trends → period retrospective → warm-assessments footer control. The overview carried no target-tile grid, so nothing to remove there. Drop the now-dead insights.correlationRow.subtitle key across all six locales; title and disclaimer stay (the per-metric correlation card still uses them). --- messages/de.json | 1 - messages/en.json | 1 - messages/es.json | 1 - messages/fr.json | 1 - messages/it.json | 1 - messages/pl.json | 1 - src/app/__tests__/insights-b3-wiring.test.ts | 27 +-- src/app/insights/page.tsx | 80 ++++----- .../__tests__/correlation-row.test.tsx | 163 ------------------ src/components/insights/correlation-row.tsx | 78 --------- 10 files changed, 57 insertions(+), 297 deletions(-) delete mode 100644 src/components/insights/__tests__/correlation-row.test.tsx delete mode 100644 src/components/insights/correlation-row.tsx diff --git a/messages/de.json b/messages/de.json index a9eeb97ea..592c64139 100644 --- a/messages/de.json +++ b/messages/de.json @@ -1705,7 +1705,6 @@ }, "correlationRow": { "title": "Zusammenhänge", - "subtitle": "Vorab definierte Hypothesen, gegated auf n ≥ 14 und p < 0,05", "disclaimer": "Zusammenhänge sind beobachtend, nicht kausal — sprich mit deinem Arzt, wenn der Blutdruck hoch ist.", "experimentCta": "7-Tage-Experiment starten", "experimentTooltip": "Bald verfügbar", diff --git a/messages/en.json b/messages/en.json index a3bc9c9d4..80ce80aa3 100644 --- a/messages/en.json +++ b/messages/en.json @@ -1705,7 +1705,6 @@ }, "correlationRow": { "title": "Relationships", - "subtitle": "Pre-defined hypotheses, gated on n ≥ 14 and p < 0.05", "disclaimer": "Relationships are observational, not causal — talk to your doctor if BP runs high.", "experimentCta": "Try a 7-day experiment", "experimentTooltip": "Coming soon", diff --git a/messages/es.json b/messages/es.json index 2d7378e61..2d17d3ee3 100644 --- a/messages/es.json +++ b/messages/es.json @@ -1705,7 +1705,6 @@ }, "correlationRow": { "title": "Correlaciones", - "subtitle": "Hipótesis predefinidas, con umbral n ≥ 14 y p < 0,05", "disclaimer": "Las correlaciones son observacionales, no causales — habla con tu médico si tu tensión es alta.", "experimentCta": "Iniciar experimento de 7 días", "experimentTooltip": "Próximamente", diff --git a/messages/fr.json b/messages/fr.json index 8d2bf3284..1b3be05a7 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -1705,7 +1705,6 @@ }, "correlationRow": { "title": "Corrélations", - "subtitle": "Hypothèses prédéfinies, validées si n ≥ 14 et p < 0,05", "disclaimer": "Les corrélations sont observationnelles, pas causales — parle à ton médecin si ta tension est élevée.", "experimentCta": "Lancer une expérience de 7 jours", "experimentTooltip": "Bientôt disponible", diff --git a/messages/it.json b/messages/it.json index 51878b0d4..020d94744 100644 --- a/messages/it.json +++ b/messages/it.json @@ -1705,7 +1705,6 @@ }, "correlationRow": { "title": "Correlazioni", - "subtitle": "Ipotesi predefinite, vincolate a n ≥ 14 e p < 0,05", "disclaimer": "Le correlazioni sono osservazionali, non causali — parla con il medico se la pressione è alta.", "experimentCta": "Avvia esperimento di 7 giorni", "experimentTooltip": "In arrivo", diff --git a/messages/pl.json b/messages/pl.json index 77956ae38..0e026652e 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -1705,7 +1705,6 @@ }, "correlationRow": { "title": "Korelacje", - "subtitle": "Hipotezy zdefiniowane wcześniej, z progiem n ≥ 14 i p < 0,05", "disclaimer": "Korelacje są obserwacyjne, nie przyczynowe — porozmawiaj z lekarzem, jeśli ciśnienie jest wysokie.", "experimentCta": "Uruchom 7-dniowy eksperyment", "experimentTooltip": "Wkrótce", diff --git a/src/app/__tests__/insights-b3-wiring.test.ts b/src/app/__tests__/insights-b3-wiring.test.ts index 27a44a02a..1598c3e7c 100644 --- a/src/app/__tests__/insights-b3-wiring.test.ts +++ b/src/app/__tests__/insights-b3-wiring.test.ts @@ -3,13 +3,20 @@ import { readFileSync } from "node:fs"; import { join } from "node:path"; /** - * v1.4.20 phase B3 — guards for the `/insights` wiring of the new - * Trends row + Correlation row. + * v1.4.20 phase B3 — guards for the `/insights` wiring of the + * Trends row + the analytics correlations payload. * * The page-source scan pattern mirrors `insights-polish.test.ts` — * each test pins one load-bearing import / JSX mount so a future * refactor can't accidentally drop a wave of features without * breaking a CI lane. + * + * v1.12.0 — the on-overview correlation row was removed: the per-metric + * correlation cards moved onto the metric pages they belong to (Weight + * owns weight × weekday, Pulse owns mood × pulse), so the overview no + * longer duplicates them. The analytics route still surfaces the + * `correlations` block (the per-metric `MetricCorrelationCard` reads it), + * so those backend guards stay; the overview-mount guards were dropped. */ const ROOT = join(__dirname, "../../.."); @@ -24,25 +31,25 @@ function load(path: string): string { return readFileSync(path, "utf8"); } -describe("v1.4.20 B3 — /insights mounts CorrelationRow + TrendsRow", () => { - it("imports both new components", () => { +describe("v1.4.20 B3 — /insights mounts TrendsRow", () => { + it("imports the trends-row component", () => { // v1.4.33 IW2 deferred below-the-fold blocks behind `next/dynamic`, // so the static `from "..."` import-string was replaced with a // `dynamic(() => import("..."))` call. Either spelling counts as // a load-bearing reference to the module path. const src = load(INSIGHTS_PATH); - expect(src).toMatch( - /(?:from\s+"@\/components\/insights\/correlation-row"|import\("@\/components\/insights\/correlation-row"\))/, - ); expect(src).toMatch( /(?:from\s+"@\/components\/insights\/trends-row"|import\("@\/components\/insights\/trends-row"\))/, ); }); - it("mounts behind a non-null analytics.correlations guard", () => { + it("no longer mounts an on-overview correlation row (relocated to the metric pages)", () => { + // v1.12.0 — the correlation cards live on the per-metric pages now. + // The overview must not duplicate them: neither the module import + // nor the JSX mount may reappear here. const src = load(INSIGHTS_PATH); - expect(src).toMatch(/analytics\?\.correlations\s*&&\s*\(/); - expect(src).toMatch(/ with annotations from the advisor payload", () => { diff --git a/src/app/insights/page.tsx b/src/app/insights/page.tsx index bd35614f5..435c053a2 100644 --- a/src/app/insights/page.tsx +++ b/src/app/insights/page.tsx @@ -69,16 +69,6 @@ const DailyBriefing = dynamic( loading: () => , }, ); -const CorrelationRow = dynamic( - () => - import("@/components/insights/correlation-row").then((mod) => ({ - default: mod.CorrelationRow, - })), - { - ssr: false, - loading: () => , - }, -); const TrendsRow = dynamic( () => import("@/components/insights/trends-row").then((mod) => ({ @@ -160,8 +150,11 @@ const PeriodNarrativeCard = dynamic( * - The sticky tab strip lives in `src/app/insights/layout.tsx` * (the shared `` mounts it). The strip handles * navigation to every sub-page + the regenerate affordance. - * - Hero + DailyBriefing + Correlation row + Trends row + advisor - * card stay here — they're the cross-metric overview. + * - Hero + DailyBriefing + Trends row + advisor card stay here — + * they're the cross-metric overview. The per-metric correlation + * cards moved onto the metric pages they belong to (Weight owns + * weight × weekday, Pulse owns mood × pulse, …), so the overview + * no longer renders a duplicate correlation row. * - The CoachDrawer is mounted in the mother-page body only (Marc * directive). Navigating to a sub-page unmounts the drawer. * @@ -223,10 +216,12 @@ export default function InsightsPage() { // beyond the React-state subscription. const advisor = useInsightsAdvisorQuery(isAuthenticated); - // v1.4.33 IW2 — the mother page reads `correlations` + `healthScore` - // (thick-only fields) so it stays on the default thick slice. The - // shared hook still centralises the cache settings so the consumer - // dedups with the sub-page mounts that ride the slim slice instead. + // v1.4.33 IW2 — the mother page reads `healthScore` (a thick-only + // field) for the hero score card, so it stays on the default thick + // slice. The shared hook still centralises the cache settings so the + // consumer dedups with the sub-page mounts that ride the slim slice + // instead. Correlations now live on the per-metric pages, so the + // overview no longer reads `analytics.correlations`. const analyticsQuery = useAnalyticsQuery(); const analytics = analyticsQuery.data as AnalyticsData | undefined; @@ -278,6 +273,15 @@ export default function InsightsPage() { // server children would require a Server-Component refactor; that's // a v1.5.x track. + // v1.12.0 — section order mirrors the iOS Insights overview + // (`v0.14` app-structure handover §2c): Coach hero + briefing + chips + // (the HeroStrip bundles all three) → detailed "Heute auf einen Blick" + // briefing → dynamics / alerts zone (today's signal + rhythm events, + // out of the AI gate so a health alert is never hidden) → vitals + // dashboard → trends row → "Zeitraum im Rückblick" retrospective → + // footer (the warm-assessments utility control, no iOS equivalent — + // iOS auto-warms). The per-metric correlation cards moved onto the + // metric pages, so the overview renders no correlation row. return (
+ {flags.briefing && ( + + )} + + + + + + + {flags.briefing && } {/* v1.11.3 — anchor the warm-assessments control. It used to float @@ -320,30 +344,6 @@ export default function InsightsPage() { {t("insights.warmAssessments")}
- - - - - - {flags.briefing && ( - - )} - - {flags.correlations && analytics?.correlations && ( - - )} - - ); } diff --git a/src/components/insights/__tests__/correlation-row.test.tsx b/src/components/insights/__tests__/correlation-row.test.tsx deleted file mode 100644 index 9e5559c66..000000000 --- a/src/components/insights/__tests__/correlation-row.test.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { renderToStaticMarkup } from "react-dom/server"; -import { I18nProvider } from "@/lib/i18n/context"; -import type { CorrelationResult } from "@/lib/insights/correlations"; - -/** - * v1.4.20 phase B3 — `` layout + disclaimer. - * v1.4.22 A4 update — empty-state cards are dropped instead of rendered. - * - * Acceptance: - * 1. Renders only the hypothesis cards whose `status === "ok"`. - * 2. Mounts the correlation-disclaimer footer once at the row level - * when at least one card renders; the row hides itself entirely - * when zero cards render (no header, no disclaimer). - * 3. 2-up grid on >= md when 2+ cards, full-width single column when - * exactly 1 card renders. - * 4. Below-threshold cards are filtered at the row level so the - * layout collapses cleanly. - */ - -vi.mock("next/dynamic", () => ({ - default: () => { - const Stub = () =>
scatter
; - Stub.displayName = "ScatterStub"; - return Stub; - }, -})); - -import { CorrelationRow } from "../correlation-row"; - -function render(node: React.ReactNode, locale: "en" | "de" = "en") { - return renderToStaticMarkup( - {node}, - ); -} - -const okBpCompliance: CorrelationResult = { - kind: "bp-compliance", - status: "ok", - statistic: -0.62, - n: 28, - pValue: 0.001, - confidenceBand: { low: -0.85, high: -0.39, label: "high" }, - interpretation: - "Higher medication compliance lines up with lower systolic — a pattern worth watching.", - points: [{ x: 60, y: 140 }], - xLabel: "Compliance %", - yLabel: "Systolic (mmHg)", -}; - -const insufficientMoodPulse: CorrelationResult = { - kind: "mood-pulse", - status: "insufficient", - n: 8, - reason: "too_few_pairs", - points: [], -}; - -const okWeightWeekday: CorrelationResult = { - kind: "weight-weekday", - status: "ok", - statistic: 0.18, - n: 28, - pValue: 0.02, - confidenceBand: { low: 0.18, high: 0.18, label: "moderate" }, - interpretation: - "Monday weights run 0.6 kg above your other-day average — a pattern worth watching.", - points: [{ x: 0, y: 84 }], - xLabel: "Weekday", - yLabel: "Weight (kg)", -}; - -describe("", () => { - it("renders only the ok-status hypothesis cards (insufficient ones are dropped)", () => { - const html = render( - , - ); - const matches = html.match(/data-slot="correlation-card"/g) ?? []; - expect(matches.length).toBe(2); - expect(html).toMatch(/data-kind="bp-compliance"/); - expect(html).not.toMatch(/data-kind="mood-pulse"/); - expect(html).toMatch(/data-kind="weight-weekday"/); - }); - - it("renders the correlation-disclaimer once at the row level", () => { - const html = render( - , - ); - const matches = html.match(/data-slot="correlation-row-disclaimer"/g) ?? []; - expect(matches.length).toBe(1); - expect(html).toContain("Relationships are observational, not causal"); - }); - - it("applies the 2-up grid on >= md when 2+ ok cards render", () => { - const html = render( - , - ); - expect(html).toMatch(/grid-cols-1/); - expect(html).toMatch(/md:grid-cols-2/); - }); - - it("collapses to a full-width single column when exactly 1 ok card renders", () => { - const html = render( - , - ); - expect(html).toMatch(/grid-cols-1/); - // The 2-up rule must NOT activate when only one card has data — - // a single card should span 100 % width on its own row instead of - // half-width with empty space next to it. - expect(html).not.toMatch(/md:grid-cols-2/); - }); - - it("hides the row entirely (no header, no disclaimer) when zero cards have data", () => { - const html = render( - , - ); - expect(html).toBe(""); - }); - - it("renders German copy when locale=de", () => { - const html = render( - , - "de", - ); - expect(html).toContain("nicht kausal"); - }); -}); diff --git a/src/components/insights/correlation-row.tsx b/src/components/insights/correlation-row.tsx deleted file mode 100644 index 586a41723..000000000 --- a/src/components/insights/correlation-row.tsx +++ /dev/null @@ -1,78 +0,0 @@ -"use client"; - -import { cn } from "@/lib/utils"; -import { useTranslations } from "@/lib/i18n/context"; -import type { CorrelationResult } from "@/lib/insights/correlations"; -import { CorrelationCard } from "./correlation-card"; - -/** - * v1.4.20 phase B3 — Correlation row. - * - * Renders the pre-defined hypothesis cards in a 2-up grid on `>=md` and - * a single column on `` painted an EmptyState for below- - * threshold results, which left half-rows of greyed-out placeholders - * on a sparse account. - */ - -interface CorrelationRowProps { - results: { - bpCompliance: CorrelationResult; - moodPulse: CorrelationResult; - weightWeekday: CorrelationResult; - }; -} - -export function CorrelationRow({ results }: CorrelationRowProps) { - const { t } = useTranslations(); - - // Drop insufficient-data tiles up-front. The grid below picks 1-col vs - // 2-col automatically so 1 ok card spans 100 % width on its own row. - const okResults = [ - results.bpCompliance, - results.moodPulse, - results.weightWeekday, - ].filter((r) => r.status === "ok"); - - if (okResults.length === 0) return null; - - return ( -
-
-

- {t("insights.correlationRow.title")} -

-

- {t("insights.correlationRow.subtitle")} -

-
-
1 && "md:grid-cols-2", - )} - > - {okResults.map((result) => ( - - ))} -
-

- {t("insights.correlationRow.disclaimer")} -

-
- ); -}