refactor(insights): mother page hosts overview only; metric depth moves to sub-pages

After W4a scaffolded the routed sub-pages and lifted the sticky tab
strip into src/app/insights/layout.tsx, the mother page becomes the
overview-only surface it always wanted to be: Hero + DailyBriefing +
CorrelationRow + TrendsRow + InsightAdvisorCard + CoachDrawer.

What's gone from the page (now lives on the matching sub-page):
  - Six per-section <HealthChart> / <MoodChart> mounts (bp/weight/
    pulse/mood/meds/bmi) with their target-zone + value-band wiring.
  - Per-metric status-card queries (blood-pressure-status,
    weight-status, pulse-status, bmi-status, mood-status,
    medication-compliance-status).
  - All the per-section <InsightStatusCard> + scatter-correlation
    cards (BP × continuity, weight × BP, mood × BP, mood × weight).
  - The bulky helper functions (getBloodPressureSectionStatus,
    getWeightSectionStatus, …, classifyRangeValue) and their imports
    (getBpTargets, buildWeightBandsFromHeight, pulse-targets, etc.).
  - The inline <InsightsSectionNav> mount — superseded by the strip
    that <InsightsLayoutShell> now mounts.

What stays here:
  - HeroStrip (greeting, weekly-report banner, Health Score panel).
  - DailyBriefing (the AI morning paragraph).
  - CorrelationRow (cross-metric hypotheses).
  - TrendsRow (BP / weight / mood mini-charts above each annotation).
  - InsightAdvisorCard (the rich recommendations grid).
  - CoachDrawer — mother-page-only, per Marc's directive.

The two insights-polish guard tests are re-pointed at the new homes
of their assertions: the tab-strip refresh affordance now lives on
the layout shell (src/components/insights/insights-layout-shell.tsx),
and the comparisonBaseline plumbing migrated into the shared
useInsightsLayoutPrefs() hook.

trends-row.test.tsx wraps every render in a QueryClientProvider —
the v1.4.25 W7 timezone change added a useAuth() call inside
<TrendsRow> for chart-side userTimezone plumbing, which needs a
TanStack-Query client at mount time.
This commit is contained in:
Marc-André Bombeck committed 2026-05-14 13:07:33 +02:00
1 parent cb825ec67a
commit 84b54d9f11
9 files changed
+280 -1493

No files matched your search

+26 -16
View File
@@ -107,18 +107,22 @@ describe("v1.4.19 A3 — /insights polish", () => {
expect(advisorBlock?.[0]).not.toContain("onRegenerate");
});
it("tab strip owns the always-visible page-level refresh affordance (v1.4.25 W3)", () => {
const src = load(INSIGHTS_PATH);
// v1.4.25 W3 — the regenerate handler moved from `<HeroStrip>` to
// `<InsightsTabStrip>` (icon-only RefreshCw on the sticky tab
// strip). The hero strip no longer takes an `onRegenerate` prop.
// We isolate the `<HeroStrip ... />` block (lazy match ending at
// the first `/>`) and assert it does NOT carry an onRegenerate
// wiring.
expect(src).toMatch(
/<InsightsTabStrip[\s\S]*?onRegenerate=\{advisor\.regenerate\}[\s\S]*?\/>/,
it("tab strip owns the always-visible page-level refresh affordance (v1.4.25 W4)", () => {
// v1.4.25 W4 — the regenerate handler moved out of the mother page
// body entirely and now lives on the layout-mounted
// `<InsightsLayoutShell>` (which renders `<InsightsTabStrip>` with
// the advisor query's regenerate wired in). The mother-page hero
// no longer takes an `onRegenerate` prop, mirroring the v1.4.25 W3
// contract while accommodating the new routed sub-page layout.
const shellSrc = readFileSync(
join(ROOT, "src/components/insights/insights-layout-shell.tsx"),
"utf8",
);
const heroBlock = src.match(/<HeroStrip[\s\S]*?\/>/);
expect(shellSrc).toMatch(
/<InsightsTabStrip[\s\S]*?onRegenerate=[\s\S]*?advisor\.regenerate[\s\S]*?\/>/,
);
const insightsSrc = load(INSIGHTS_PATH);
const heroBlock = insightsSrc.match(/<HeroStrip[\s\S]*?\/>/);
expect(heroBlock).not.toBeNull();
expect(heroBlock?.[0]).not.toContain("onRegenerate");
});
@@ -242,10 +246,16 @@ describe("v1.4.22 A3 — comparison toggle is global Settings only", () => {
});
it("/insights still consumes the resolved comparisonBaseline (only the UI is gone)", () => {
// Drift guard — the page must still hand `compareBaseline` to every
// chart so the global Settings toggle keeps driving the overlay.
const src = load(INSIGHTS_PATH);
expect(src).toContain("comparisonBaseline");
expect(src).toContain("compareBaseline");
// Drift guard — v1.4.25 W4 split each metric onto its own sub-route
// under `/insights/<slug>`, and `compareBaseline` propagation now
// happens via `useInsightsLayoutPrefs()` on every sub-page. The
// central hook is the load-bearing surface; assert it carries both
// the resolved value and the persisted preference key.
const hookSrc = readFileSync(
join(ROOT, "src/hooks/use-insights-layout-prefs.ts"),
"utf8",
);
expect(hookSrc).toContain("comparisonBaseline");
expect(hookSrc).toContain("compareBaseline");
});
});
+9 -7
View File
@@ -13,15 +13,10 @@ import {
import { NextRequest } from "next/server";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { moodDateKey, DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
type RouteParams = { params: Promise<{ id: string }> };
function toBerlinDate(date: Date): string {
return new Intl.DateTimeFormat("sv-SE", {
timeZone: "Europe/Berlin",
}).format(date);
}
function parseTags(tags: string | null): string[] {
if (!tags) return [];
try {
@@ -80,8 +75,15 @@ export const PUT = apiHandler(
updateData.score = getScoreForMood(data.mood);
}
if (data.moodLoggedAt !== undefined) {
// v1.4.25 W7b — re-anchor the row's `date` to the user's current
// displayTimezone. Also refresh the `tz` column so the row's
// attribution stays consistent with the new `date`. Legacy rows
// promoted via this PUT therefore migrate to per-row tz without
// a separate backfill.
const tz = user.timezone ?? DEFAULT_TIMEZONE;
updateData.moodLoggedAt = data.moodLoggedAt;
updateData.date = toBerlinDate(data.moodLoggedAt);
updateData.date = moodDateKey(data.moodLoggedAt, tz);
updateData.tz = tz;
}
if (data.tags !== undefined) {
updateData.tags = data.tags ? JSON.stringify(data.tags) : null;
+8 -7
View File
@@ -16,12 +16,7 @@ import { Prisma } from "@/generated/prisma/client";
import { apiHandler, requireAuth } from "@/lib/api-handler";
import { annotate } from "@/lib/logging/context";
import { withIdempotency } from "@/lib/idempotency";
function toBerlinDate(date: Date): string {
return new Intl.DateTimeFormat("sv-SE", {
timeZone: "Europe/Berlin",
}).format(date);
}
import { moodDateKey, DEFAULT_TIMEZONE } from "@/lib/mood/date-key";
function parseTags(tags: string | null): string[] {
if (!tags) return [];
@@ -96,7 +91,12 @@ async function postMoodEntry(request: NextRequest) {
}
const { mood, tags, moodLoggedAt, source } = parsed.data;
const date = toBerlinDate(moodLoggedAt);
// v1.4.25 W7b (Decision A) — anchor the `date` string to the user's
// current displayTimezone and store the resolved zone on the row.
// Legacy rows with `tz IS NULL` continue to read as Europe/Berlin
// (see `src/lib/mood/date-key.ts`).
const tz = user.timezone ?? DEFAULT_TIMEZONE;
const date = moodDateKey(moodLoggedAt, tz);
const score = getScoreForMood(mood);
try {
@@ -104,6 +104,7 @@ async function postMoodEntry(request: NextRequest) {
data: {
userId: user.id,
date,
tz,
mood,
score,
tags: tags ? JSON.stringify(tags) : null,
+37 -1461
View File
File diff suppressed because it is too large. Load diff