From dc23d4044661be2005255b780b170f3e43c15f34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 15 May 2026 20:42:46 +0200 Subject: [PATCH] docs(planning): R4 simplifier review report for v1.4.27 --- .planning/research/v1427-r4-simplifier.md | 418 ++++++++++++++++++++++ 1 file changed, 418 insertions(+) create mode 100644 .planning/research/v1427-r4-simplifier.md diff --git a/.planning/research/v1427-r4-simplifier.md b/.planning/research/v1427-r4-simplifier.md new file mode 100644 index 000000000..467574502 --- /dev/null +++ b/.planning/research/v1427-r4-simplifier.md @@ -0,0 +1,418 @@ +--- +file: .planning/research/v1427-r4-simplifier.md +purpose: R4 code-simplifier review for v1.4.27 (read-only) +contributor: R4 +baseline: develop @ 617d4518 +scope: 422 commits / 268 files / +19,103 / -3,142 between main..develop +--- + +# v1.4.27 R4 — code-simplifier review + +Read-only sweep of the v1.4.27 diff for duplication, dead code, inline +constants, premature abstraction, convoluted control flow, stale +comments, component bloat, and test-fixture duplication. Most findings +are Medium-Low because R3b B7 already retired the v1.4.25 W4 dead-code +candidates and B7-C8 already centralised the i18n resolver and source- +priority schema derivation. + +Severity legend: **High** (ship-blocking-on-merit), **Medium** (worth a +v1.4.27.1 or early v1.4.28 follow-up), **Low** (background hygiene the +next sweep can fold in). + +## Summary + +| Tier | Count | +|---|---| +| High | 1 | +| Medium | 7 | +| Low | 9 | +| Total | 17 | + +| Theme | Findings | +|---|---| +| Insights sub-page duplication | F-H1, F-M1, F-M2 | +| Premature abstraction / unused props | F-M3, F-M4, F-L1 | +| Test-fixture duplication | F-M5 | +| Component bloat | F-M6, F-L2 | +| Stale comments / version markers | F-M7, F-L3 | +| Inline constants / convoluted control flow | F-L4, F-L5, F-L6, F-L7 | +| Minor cosmetic | F-L8, F-L9 | + +--- + +## High + +### F-H1 — Seven insights sub-pages duplicate the same data-fetch + empty-state scaffold + +`src/app/insights/{blutdruck,bmi,gewicht,puls,schlaf,stimmung,medikamente}/page.tsx` each carry an identical block: + +```ts +const { data: analytics } = useQuery({ + queryKey: ["analytics"], + queryFn: async () => { + const res = await fetch("/api/analytics"); + if (!res.ok) throw new Error("Failed"); + const json = await res.json(); + return json.data as AnalyticsData; + }, + enabled: isAuthenticated, + staleTime: 60 * 1000, +}); + +if ( + isAuthenticated && + analytics && + !hasMetricData("WEIGHT", { + summaries: analytics.summaries, + hasMood: false, + hasMedication: false, + }) +) { + return ( + + } title={…} description={…} + action={} + /> + + ); +} +``` + +The same six-line `useQuery({ queryKey: ["analytics"] … })` repeats in +**eight** locations (verified): + +- `src/app/insights/page.tsx:122` +- `src/app/insights/blutdruck/page.tsx:57` +- `src/app/insights/bmi/page.tsx:57` +- `src/app/insights/gewicht/page.tsx:51` +- `src/app/insights/puls/page.tsx:60` +- `src/app/insights/schlaf/page.tsx:39` +- `src/app/insights/stimmung/page.tsx:50` (via `["insights","comprehensive"]` variant) +- `src/components/insights/sleep-overview.tsx:71` +- `src/components/insights/insights-layout-shell.tsx:47` + +`InsightsLayoutShell` already runs the same fetch and threads the +result into `availability` for the tab strip; the sub-page replicas +hit the same React-Query cache key so they're "free" beyond +duplicated typings (`AnalyticsData` redeclared seven times) and the +identical 14-line `EmptyState` block. + +**Fix**: Extract `useInsightsAnalytics()` + a `` +helper that takes `(metric, icon, addType)` and renders the +`SubPageShell` + `EmptyState` + CTA in one place. Each sub-page +shrinks ~30-40 LOC and the next metric (`steps`, `active-energy`, +`vo2-max`) added in v1.4.28 is a one-file change. Same goes for the +five-line `` block repeated identically across +six pages — fold into `` and unwrap inside the card. + +Estimated impact: ~250 LOC across seven page files collapses to +~70-90 LOC plus one shared hook + one shared component. + +--- + +## Medium + +### F-M1 — `AnalyticsData` interface declared seven times + +Same shape repeats verbatim across every insights sub-page plus +`InsightsLayoutShell`: + +```ts +interface AnalyticsData { + summaries: Record; +} +``` + +(blutdruck:37, bmi:20, gewicht:21, puls:43, schlaf:30, stimmung — uses +a slimmer `ComprehensiveMoodData`, sleep-overview — uses its own, +insights-layout-shell uses `AnalyticsPayload`). Move to +`src/lib/analytics/types.ts` (already houses `DataSummary`) and import. + +### F-M2 — `dynamic(() => import("@/components/charts/health-chart"))` repeats six times + +The same `next/dynamic` block plus the `{ default: mod.HealthChart }` +unwrap appears unchanged in: + +- `blutdruck/page.tsx:40-46` +- `bmi/page.tsx:33-39` +- `gewicht/page.tsx:34-40` +- `puls/page.tsx:35-41` +- `dashboard/glp1-tile.tsx:38-44` (plus the same shape for `DrugLevelChart`) +- `insights/page.tsx` (similar) + +Each consumer pays the same boilerplate to keep SSR off the chart's +Recharts bundle. Promote a single `src/components/charts/health-chart.dynamic.ts` +re-export (`export const HealthChartDynamic = dynamic(...)`) so the +six consumer files lose four lines apiece. + +### F-M3 — `` prop landed with zero consumers + +`src/components/ui/empty-state.tsx:48` introduced a `ctaSize` prop in +this release for the eight insights sub-page CTAs (CF-36 in the +mobile-fix-plan). The prop, the `[&>a]:min-h-11 …` selector logic, +and the doc-comment all landed — but no consumer passes `ctaSize="lg"`: + +``` +grep -rn "ctaSize" src/ → 4 hits, all inside empty-state.tsx itself +``` + +Either the eight CTAs (puls/blutdruck/bmi/gewicht/schlaf/stimmung/ +medikamente/main insights page) should be lifted to `ctaSize="lg"`, +or the prop should be revisited in v1.4.28 once consumers actually +opt in. The plan-fix expected eight migrations; the diff has zero. + +### F-M4 — `useCoachLaunch().setOpen` exported but only one consumer + +`src/lib/insights/coach-launch-context.tsx:50` exposes `setOpen` on +the context value, but the only consumer is +`src/components/insights/layout-coach-mount.tsx` which forwards it to +``. Every other consumer +(`coach-launch-button.tsx`, the hero strip's suggested-prompt chips, +and the main `/insights` page) reads only `open` + `askCoach`. +Tighten the public surface to `{ open, prefill, askCoach }` and have +the drawer mount read a sibling helper (`closeCoach()`). + +Less importantly: `CoachLaunchScope.metric` is doc-stamped "Reserved +for v1.4.28" and is currently `void`-discarded in `askCoach`. Per the +project's "no premature abstraction" principle, drop the parameter +until v1.4.28 actually wires it; today every call site passes one +argument, never two. + +### F-M5 — Seven insight-status test files duplicate the same mock prelude + +`src/lib/insights/__tests__/{blood-pressure,bmi,general,medication-compliance,mood,pulse,weight}-status.test.ts` +each lead with the same 15-25 line `vi.mock()` block for `@/lib/db`, +`@/lib/ai/provider`, `@/lib/insights/memory`, and (where applicable) +`@/lib/medication-category`: + +```ts +vi.mock("@/lib/db", () => ({ prisma: { user: { findUnique: vi.fn() }, … } })); +vi.mock("@/lib/ai/provider", () => ({ resolveProvider: vi.fn() })); +vi.mock("@/lib/insights/memory", () => ({ + getPreviousInsightContext: vi.fn().mockResolvedValue(null), + formatPreviousContextForPrompt: vi.fn().mockReturnValue(""), +})); +``` + +Extract `src/lib/insights/__tests__/_mocks.ts` with `setupInsightStatusMocks()` +(or rely on Vitest's `setupFiles`) so adding the next status surface in +v1.4.28 doesn't duplicate the prelude again. + +### F-M6 — `CoachDrawer` weighs in at ~560 LOC + +`src/components/insights/coach-panel/coach-drawer.tsx` carries the +single bottom-sheet/right-sheet switcher, the prefill-reset hook, +the window-pill `