refactor(insights): de-duplicate and reorder the overview to match the per-metric pages

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).
This commit is contained in:
Marc-André Bombeck committed 2026-06-04 23:22:37 +02:00
1 parent 81096b9553
commit 2bb810852e
10 files changed
+57 -297

No files matched your search

@@ -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 — `<CorrelationRow>` 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 = () => <div data-slot="scatter-stub">scatter</div>;
Stub.displayName = "ScatterStub";
return Stub;
},
}));
import { CorrelationRow } from "../correlation-row";
function render(node: React.ReactNode, locale: "en" | "de" = "en") {
return renderToStaticMarkup(
<I18nProvider initialLocale={locale}>{node}</I18nProvider>,
);
}
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("<CorrelationRow>", () => {
it("renders only the ok-status hypothesis cards (insufficient ones are dropped)", () => {
const html = render(
<CorrelationRow
results={{
bpCompliance: okBpCompliance,
moodPulse: insufficientMoodPulse,
weightWeekday: okWeightWeekday,
}}
/>,
);
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(
<CorrelationRow
results={{
bpCompliance: okBpCompliance,
moodPulse: insufficientMoodPulse,
weightWeekday: okWeightWeekday,
}}
/>,
);
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(
<CorrelationRow
results={{
bpCompliance: okBpCompliance,
moodPulse: insufficientMoodPulse,
weightWeekday: okWeightWeekday,
}}
/>,
);
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(
<CorrelationRow
results={{
bpCompliance: okBpCompliance,
moodPulse: insufficientMoodPulse,
weightWeekday: insufficientMoodPulse,
}}
/>,
);
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(
<CorrelationRow
results={{
bpCompliance: insufficientMoodPulse,
moodPulse: insufficientMoodPulse,
weightWeekday: insufficientMoodPulse,
}}
/>,
);
expect(html).toBe("");
});
it("renders German copy when locale=de", () => {
const html = render(
<CorrelationRow
results={{
bpCompliance: okBpCompliance,
moodPulse: insufficientMoodPulse,
weightWeekday: okWeightWeekday,
}}
/>,
"de",
);
expect(html).toContain("nicht kausal");
});
});
@@ -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 `<md`. A single correlation-disclaimer footer sits
* below the row — the disclaimer applies to every card so we don't
* repeat it per-card.
*
* v1.4.22 A4 — empty-state cards are dropped instead of rendered. Any
* hypothesis whose `status !== "ok"` is filtered out so the layout
* collapses cleanly: 2 ok cards → 50/50, 1 ok card → 100 %, 0 ok cards
* → the row hides itself entirely (no header, no disclaimer). Up to
* v1.4.21 the `<CorrelationCard>` 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 (
<section
data-slot="correlation-row"
aria-label={t("insights.correlationRow.title")}
className="space-y-3"
>
<div className="flex items-baseline justify-between">
<h2 className="text-lg font-semibold">
{t("insights.correlationRow.title")}
</h2>
<p className="text-muted-foreground text-xs">
{t("insights.correlationRow.subtitle")}
</p>
</div>
<div
className={cn(
"grid grid-cols-1 gap-4",
okResults.length > 1 && "md:grid-cols-2",
)}
>
{okResults.map((result) => (
<CorrelationCard key={result.kind} result={result} />
))}
</div>
<p
data-slot="correlation-row-disclaimer"
className="text-muted-foreground text-xs italic"
>
{t("insights.correlationRow.disclaimer")}
</p>
</section>
);
}