docs(planning): R1.1 dashboard UX audit and GLP-1 secondary tile spec

This commit is contained in:
Marc-André Bombeck committed 2026-05-15 16:07:42 +02:00
1 parent 43a91f2ffa
commit c29cd88b29
1 file changed
+203
+203
View File
@@ -0,0 +1,203 @@
# v1.4.27 R1.1 — Dashboard UX audit and GLP-1 secondary tile spec
## Headline
The dashboard's eight maintainer findings cluster into three coherent buckets: a GLP-1 tile rebuild (findings 1-3, plus an unused secondary inside the same card), a chart-row symmetry fix (finding 4), and four small leftover-removal jobs (findings 5-7) plus a hero-strip rebalance (finding 8). The biggest piece of work is finding 1: the existing `<Glp1Tile>` already pulls every byte we need from `/api/dashboard/glp1` and the standalone `<DrugLevelChart>` already exists in `src/components/medications/`, so we wire the chart in as a second tile-internal pane behind a small range selector and rip the green seam off in the same edit. Findings 4, 5, 6, 7 are short and surgical (one CSS class, one component delete, one prose strip, one tile click). Finding 8 needs the most thought because the Health Score card lives inside the `/insights` `<HeroStrip>`, not on the dashboard, and the maintainer wants width and height. Total estimated touched files: ~12; estimated commits if split per finding: 6-8. The whole bucket is touch-disjoint with R1.2/R1.3/R1.4 and can land as a single fix-surface in Round 3.
## Findings 1-8 — per-finding analysis
### Finding 1 — GLP-1 drug-level secondary tile
**Current state**: `src/components/dashboard/glp1-tile.tsx` renders one chart at lines 294-306, the mini weight chart with vertical injection markers. The drug-level chart already exists as a standalone surface in `src/components/medications/DrugLevelChart.tsx` (538 lines, full Research-Mode-gated AreaChart with one-compartment PK math). The standalone chart wraps its own `<section class="bg-card border-border rounded-xl border p-4 md:p-6">` so dropping it into `<Glp1Tile>` raw would paint a card inside a card. The chart accepts `medication: { id, name, dose }` as props (lines 99-107). The `/api/dashboard/glp1` route already returns `medicationId` (file lines 80-85 in `glp1-tile.tsx`) and the current dose value/unit, so we already have everything the standalone chart needs.
**Problem**: A weight chart is a downstream proxy — the maintainer cares about the actual estimated drug-level curve because it shows the dose-response across a 21-day window with three weekly sawtooth cycles. The drug-level surface is the explicit W19c outcome that today lives only inside `/medications/[id]/history`. Marc's directive on this is unambiguous: the chart belongs on the dashboard tile and `/insights/medikamente`.
**Proposed approach**: Add a small two-tab segmented control inside `<Glp1Tile>` between the dl with last/next injection (line 252-288) and the chart slot (line 294-306). Tabs: "Gewicht" (current) / "Wirkspiegel" (new). Default to "Wirkspiegel" because that's the more informative pane Marc asked for. Extract a `<Glp1ChartPane variant="weight" | "level">` carve-out that renders either the existing `<HealthChart mini ... verticalMarkers=… />` or a slimmed `<DrugLevelChart>`. Slim means: pass a `compact` prop into `DrugLevelChart` that drops its outer `section.bg-card` wrapper (the parent tile already owns the card surface) and shrinks the chart-area from `h-[240px]` to `h-[180px]` to match mini-mode density. Keep the Research-Mode gate intact — when the gate is closed the "Wirkspiegel" tab still appears but the body paints the existing `<GatedPlaceholder>` (with the Settings CTA). Estimated 80-120 LOC across the tile + 30-40 LOC of new `compact` plumbing inside `DrugLevelChart`. Low risk: the standalone surface and its tests stay intact; the new path is additive.
**Files touched (estimated)**:
- `src/components/dashboard/glp1-tile.tsx` (+~80 LOC for tab strip + pane carve-out)
- `src/components/medications/DrugLevelChart.tsx` (+~30 LOC for `compact` prop)
- `messages/de.json` + `messages/en.json` (2 new keys: `dashboard.glp1.tabWeight`, `dashboard.glp1.tabLevel`)
- `src/components/dashboard/__tests__/glp1-tile.tsx` (new + updated assertions for the tab strip + default-to-level behaviour)
### Finding 2 — green band optical clarity
**Current state**: `src/components/dashboard/glp1-tile.tsx` line 213-223. The tile carries `border-l-dracula-green/60 border-l-2` on the outer wrapper as a faint left seam. The `<dl>` block (lines 252-288) renders "Letzte Injektion" and "Nächste Injektion" mid-card. The two are visually disconnected: the seam reads as "active therapy" but the dl-block is regular muted-foreground prose with calendar icons and tabular-nums values.
**Problem**: The maintainer reads the green band as decoration with no semantic tie to the labels next to it. The two visual languages (seam vs. dl) don't reinforce each other, so the band feels arbitrary.
**Proposed approach**: Two clean options:
1. **Strip the seam, promote the injection dates to a header pill row.** Drop the `border-l-dracula-green/60 border-l-2` classes entirely (the Syringe-icon-in-green at line 226 already carries the "active therapy" signal). Restructure the `<dl>` into a single horizontal pill bar above the tab strip: a `Calendar`-iconed pill ("Letzte: Mo 12. Mai") and a `Syringe`-iconed pill ("Nächste: Mo 19. Mai · in 4 Tagen"). The pills carry a soft `bg-dracula-green/10 border-dracula-green/30` border-grouping so the user reads the two dates as one cohesive "therapy schedule" unit, not two separate dl entries.
2. **Keep the seam but anchor it to the schedule.** Move the seam from the outer wrapper to a thin background strip running only behind the `<dl>` rows so the green visually frames the schedule itself (the seam stops being a decoration on the whole card and starts being a "this is the schedule block" marker).
Maintainer's brief language ("Letzte Indikation / nächste Indikation mid-card") strongly suggests they want the dates more prominent and the seam less arbitrary, so option 1 is the recommendation. Estimated 30-40 LOC.
**Files touched (estimated)**:
- `src/components/dashboard/glp1-tile.tsx` (~40 LOC; new `<ScheduleRow>` carve-out, drop seam classes)
- `src/components/dashboard/__tests__/glp1-tile.tsx` (assert new `data-slot="glp1-tile-schedule"` pill, drop the `border-l-dracula-green/60` selector test if one exists)
### Finding 3 — point-count selector inconsistency
**Current state**: `src/components/dashboard/glp1-tile.tsx` line 297-304 mounts `<HealthChart mini types={["WEIGHT"]} ... />`. `mini={true}` short-circuits the header block in `health-chart.tsx` at line 1052 (`{!mini && (header)}`), so the range tabs (7d / 30d / 90d / All — see `TIME_RANGES_KEYS` line 46-67) never render in mini mode. The `<DrugLevelChart>` standalone surface also doesn't have a range picker — it hard-codes a 21-day window (`WINDOW_HOURS_BEFORE = 21 * 24` at line 71). Other dashboard charts (the bottom chart row) render range tabs at line 1118-1130.
**Problem**: Every other chart-tile on the dashboard lets the user pick 7d / 30d / 90d / All. The GLP-1 tile is the lone exception. Inconsistency reads as a bug.
**Proposed approach**: Mount a small four-button range strip inside `<Glp1Tile>` above the chart pane, owned by the tile itself (not by the underlying `<HealthChart>`/`<DrugLevelChart>`). Re-use the same `TIME_RANGES_KEYS` constant from `health-chart.tsx` (export it as `RANGE_PRESETS` or copy four lines if the export would couple too tightly). For the weight pane pass `rangePoints` through as a new prop into `<HealthChart>` so mini mode can honour it (one new `windowOverride`-like prop or a fork of the existing `windowOverride` flow). For the level pane pass `windowHoursBefore` into `<DrugLevelChart>` (new prop with current 21-day default). 7d/30d/90d/All maps cleanly: 7d ≈ 7·24h, 30d ≈ 30·24h, 90d ≈ 90·24h, All ≈ all dose events. Estimated 60-80 LOC. Risk: the level chart's sample step (`SAMPLE_STEP_HOURS = 6` at line 73) may need to scale with window so the AreaChart doesn't paint 1k+ points on the "All" path — pick `stepHours` proportional to window length.
**Files touched (estimated)**:
- `src/components/dashboard/glp1-tile.tsx` (~50 LOC; range strip + state)
- `src/components/charts/health-chart.tsx` (~10 LOC; thread `windowOverride` through mini path if not already wired)
- `src/components/medications/DrugLevelChart.tsx` (~20 LOC; accept `windowHoursBefore` prop, scale `stepHours`)
- `src/components/dashboard/__tests__/glp1-tile.tsx` (assert range buttons, clicking re-renders chart)
### Finding 4 — trend charts use different heights
**Current state**: Chart heights on the dashboard chart row:
- `src/components/charts/health-chart.tsx` line 1048: `chartHeightClass = mini ? "h-[140px]" : "h-[240px]"`.
- `src/components/charts/mood-chart.tsx` line 632: `<div className={\`${mini ? "h-[140px]" : "h-[280px]"} touch-pan-y\`}>`.
- `src/components/charts/medication-compliance-chart.tsx` line 386: `<div className="h-[240px] touch-pan-y">`.
So Weight / BP / Pulse / BodyFat / Sleep / Steps charts all render at 240 px. **MoodChart** renders at 280 px — the lone 40-px-taller outlier on the dashboard chart row.
**Problem**: A single 40-px-taller member of the chart strip breaks vertical rhythm. The user scrolling past five chart cards sees the row swell when it hits the mood card.
**Proposed approach**: Drop MoodChart's non-mini height to match the rest. Single-line CSS change: `"h-[240px]"` instead of `"h-[280px]"` at `mood-chart.tsx` line 632. The MoodChart's `<ComposedChart>` (line 634) is `ResponsiveContainer height="100%"` so the inner SVG re-flows automatically. Confirm by visual snapshot or a Playwright dashboard screenshot pre/post. Estimated 1-3 LOC plus a test update if the snapshot test pins the old 280 value. Zero risk.
Optionally lift `CHART_BODY_HEIGHT_PX = 240` into `src/lib/charts/constants.ts` so all three chart files share a single source of truth and the maintainer can re-tune from one place. Recommended.
**Files touched (estimated)**:
- `src/components/charts/mood-chart.tsx` (1-3 LOC)
- `src/lib/charts/constants.ts` (new file, ~10 LOC, optional but recommended)
- `src/components/charts/health-chart.tsx` + `medication-compliance-chart.tsx` (~2 LOC each to consume the constant)
### Finding 5 — "KI-Gesundheitsanalyse" dead leftover
**Current state**: Hunting the dashboard surface for "KI-Gesundheitsanalyse":
- `src/components/insights/insight-advisor-card.tsx` is the only consumer of the `insights.aiAnalysisTitle` ("KI-Gesundheitsanalyse") key (lines 348, 380, 472, 534).
- `<InsightAdvisorCard>` is mounted on `/insights` (`src/app/insights/page.tsx` line 213), **not** on `/`.
- On the dashboard the only insight-themed card is `<InsightsCardPreview>` (`src/app/page.tsx` line 1258-1262), which renders the `insights.aiInsights` ("KI-Insights") key (`src/components/insights/insights-card.tsx` line 72). It sits pinned above the chart row, not at the bottom.
The maintainer's "at bottom of dashboard" framing plus "KI-Gesundheitsanalyse" label is most likely loose wording for `<InsightsCardPreview>` — it's the only insight-shaped surface on the dashboard, and "bottom-ish" because the chart row scrolls past it. The card self-hides when no recommendations exist (line 55 of `insights-card.tsx`), so when the user has a fresh advisor payload it does render — Marc reads it as a duplicate of the much-richer `/insights` advisor surface and wants it gone.
**Problem**: The dashboard preview duplicates a surface that already lives a click away on `/insights` (which has the Daily Briefing, hero strip, correlation row, full advisor, weekly report banner). The preview is short and shallow; it competes visually with the Glp1Tile for the same vertical real estate above the charts.
**Proposed approach**: Remove the dashboard preview entirely.
1. Drop the import + render at `src/app/page.tsx` lines 49, 246, 348, 1258-1262 (the `useInsightsAdvisorQuery` hook + `showInsightsPreview` gate + JSX).
2. Drop the `insightsPreview` widget entry from `src/lib/dashboard-layout.ts` line 229 (or set it default-invisible AND remove the Settings → Dashboard row at `src/components/settings/dashboard-layout-section.tsx` line 50).
3. Delete `src/components/insights/insights-card.tsx` and its test file `src/components/insights/__tests__/insights-card.test.tsx` (the component has no other consumers per grep).
4. Drop the `dashboard.insightsPreview` i18n key (de.json + en.json).
5. Re-survey `useInsightsAdvisorQuery` consumers — if `<InsightsCardPreview>` is the only dashboard caller, the dashboard stops needing the advisor cache prefetch (it's still loaded by `/insights`).
If the maintainer actually means the `<InsightAdvisorCard>` on `/insights`, that's out of scope for the dashboard finding bucket and belongs to R1.3.
Estimated 40-60 LOC removed across 4-5 files. Zero risk — purely additive removal.
**Files touched (estimated)**:
- `src/app/page.tsx` (-12 LOC)
- `src/lib/dashboard-layout.ts` (-3 LOC, drop the widget entry)
- `src/components/settings/dashboard-layout-section.tsx` (-3 LOC)
- `src/components/insights/insights-card.tsx` (delete file, -119 LOC)
- `src/components/insights/__tests__/insights-card.test.tsx` (delete file)
- `messages/de.json` + `messages/en.json` (-2 keys: `dashboard.insightsPreview`, `insights.aiInsights` if no other consumers)
### Finding 6 — Daily Briefing duplicates the greeting
**Current state**:
- `/insights` hero greeting: `src/components/insights/hero-strip.tsx` line 144 — `${greetingBase}, ${userName}` ("Guten Tag, Marc"). The narrative subtitle directly underneath at line 145: `subtitle = briefing?.paragraph ?? t("insights.heroFallbackSubtitle")` — when the briefing payload exists, the hero's subtitle **is the briefing paragraph**.
- The page then mounts `<DailyBriefing briefing={briefingPayload} ... />` directly below the hero (`src/app/insights/page.tsx` line 199-205).
- `<DailyBriefing>` renders the same paragraph again at line 235-240 of `daily-briefing.tsx`: `<p data-slot="daily-briefing-paragraph">{stripChartTokens(briefing.paragraph)}</p>`.
So the same paragraph paints twice within 200 px of vertical space.
**Problem**: The hero already carries the paragraph as its subtitle. The Daily Briefing card mounts the same string a second time inside its CardContent before the key-findings list. The user reads the same text twice in a row.
**Proposed approach**: Strip the paragraph render from `<DailyBriefing>` and keep only the structured key-findings block plus the `updatedAt` meta line. Concrete edit:
- `src/components/insights/daily-briefing.tsx` lines 234-258: delete the `<p data-slot="daily-briefing-paragraph">` block (lines 235-240) so the card opens directly with the "WICHTIGSTE BEFUNDE" eyebrow + the key-findings list.
- The card header (Sparkles + "Daily Briefing" title) and the `updatedAt` footer stay.
- Empty-state branch (lines 270-296) stays — when there's no briefing payload the empty-state still drives the "Generate" CTA. The paragraph-strip applies only to the populated branch.
- Add a regression test asserting `<p data-slot="daily-briefing-paragraph">` no longer renders when a briefing is provided.
Alternative: keep the paragraph in the card and strip it from the hero subtitle (use `t("insights.heroFallbackSubtitle")` regardless). Less attractive because the maintainer's reading is "the briefing duplicates the hero greeting text", not "the hero duplicates the briefing".
Estimated 10-15 LOC removed plus one new test. Zero risk.
**Files touched (estimated)**:
- `src/components/insights/daily-briefing.tsx` (-10 LOC)
- `src/components/insights/__tests__/daily-briefing.test.tsx` (new assertion: paragraph-slot absent in populated render)
### Finding 7 — weekly report click is dead
**Current state**: The route DOES exist:
- `src/app/insights/report/[week]/page.tsx` mounts `<WeeklyReportView weekISO={...} autoPrint={...} />`.
- `src/components/insights/weekly-report-view.tsx` renders the full report surface (hero, summary, going-well, worth-watching, tips, data-quality, footer).
- The hero strip at `/insights` already wires two routes into the report:
- "Generate weekly report" action button at `src/components/insights/hero-strip.tsx` line 240-248 links to `weeklyReportHref` (the current ISO week).
- The `<WeeklyReportBanner>` (`hero-strip.tsx` line 332-425) renders only when a fresh `weeklyReport` payload arrived and links to `/insights/report/[week]`.
So the route is implemented. The dead-click is probably a different tile that visually offers "view weekly report" but isn't wired. Best guess: the `<DailyBriefing>` card's footer or the `<InsightsCardPreview>` on the dashboard — neither of which has a weekly-report click target today. A second possibility: a tile on the dashboard that the maintainer thinks is "weekly report" but is actually `<RecentAchievementsCard>` (which routes to `/achievements`, not weekly report). A third possibility: the maintainer means the dashboard side of the surface and wants a "View weekly report" entry point on `/` itself.
**Proposed approach**: Two options:
**Option A — add a dashboard surface that links to the weekly report.** Add a slim banner under the tile strip on `/` that mirrors the `<WeeklyReportBanner>` from the hero: "Dein Wochenreport für KW {N} ist bereit — Öffnen". Gated on the advisor payload carrying a fresh `weeklyReport`. Re-use the same banner component. Estimated 30 LOC plus one wire-up in `src/app/page.tsx`. Acceptable because the dashboard now grows a real entry point and the route already exists.
**Option B — find and remove the dead affordance.** Audit every "weekly report" / "Wochenreport" string in `src/` and confirm each affordance routes correctly. Take 30 minutes to scan. If something is genuinely dead, delete it.
Recommendation: do B first (cheap), then if nothing turns up, ask the maintainer to point to the dead element with a screenshot. If they confirm the dashboard side, do A.
Effort estimate: Option B = 30 minutes + 0-10 LOC. Option A = 1-2 hours + ~30 LOC plus tests.
**Files touched (estimated)**:
- TBD pending maintainer screenshot. Probably `src/app/page.tsx` (Option A) or one of the components surfacing a "report" string today.
### Finding 8 — Health Score card should fill more of the hero
**Current state**:
- `src/components/insights/health-score-card.tsx` line 241: outer wrapper is `"w-full lg:w-[260px] lg:shrink-0"`. On `lg+` the card pins to **260 px** wide. Vertically it carries: label row + score number (`text-4xl`) + progress bar + delta line + 4-row sub-bar list + provenance accordion + disclaimer + "Ask the Coach" button — roughly 320-380 px tall depending on the provenance state.
- `src/components/insights/hero-strip.tsx` lines 172-316: on `lg+` the hero splits into a title block (left, `flex-1`) and the Health Score (right, fixed 260 px). On smaller viewports the score stacks below the title (`flex-col` default, `lg:flex-row` switches at 1024 px).
- The hero band itself is `px-4 py-5 sm:px-6 sm:py-6` plus `rounded-xl` — roughly 24-px padding all around.
**Problem**: 260 px is roughly 20-25 % of the hero width on a 1280-px viewport. The maintainer reads the score as a small inset, not as a co-equal column of the hero band. They want it to occupy more of the hero, both width AND height.
**Proposed approach**: Three calibrated changes:
1. **Width**: bump `lg:w-[260px]` to `lg:w-[360px] xl:w-[400px]`. On 1280-px viewports the score now takes ~28-31 % of the hero; on 1536+ it grows to 36 % and reads as a true second column. Reflow the title block via `min-w-0` on the left flex child (already in place at line 178) so the narrative subtitle wraps gracefully.
2. **Height**: drop the `<Button>` "Ask the Coach" out of the score card and rely on the hero's existing "Ask the coach" action button (already at `hero-strip.tsx` line 269-281). That removes ~40 px of bottom padding. Then enlarge the score number from `text-4xl leading-none` to `text-5xl sm:text-6xl leading-none` and grow the progress bar from `h-1.5` to `h-2`. The visual centre of gravity shifts to the number, which is what the maintainer's "occupy a larger share" is really asking for.
3. **Stacking**: the existing component sub-bars + provenance accordion stay but get more breathing room: bump the `space-y-1.5` to `space-y-2` on the component list and add 4 px of padding between the disclaimer and the bottom.
The card already self-handles the lg-only split, so no media-query refactor is needed. Estimated 20-30 LOC of class changes. Risk: the test at `src/components/insights/__tests__/health-score-card.test.tsx` may pin the 260-px class; update if needed.
**Files touched (estimated)**:
- `src/components/insights/health-score-card.tsx` (~25 LOC of class/copy edits)
- `src/components/insights/hero-strip.tsx` (~5 LOC if the score's `onAskCoach` prop wiring stays — keep the prop but the card stops mounting a button)
- `src/components/insights/__tests__/health-score-card.test.tsx` + `health-score-card-provenance.test.tsx` (update pinned classes if asserted)
## Cross-finding patterns
- **Shared chart-height constant**: findings 1 and 4 both touch chart heights (240 vs 280, plus a new 180 for the level pane). Extracting `CHART_HEIGHT_PX = 240` and `CHART_MINI_HEIGHT_PX = 140` into `src/lib/charts/constants.ts` lets all three chart components (health, mood, compliance) and both GLP-1 panes read from one place.
- **Shared range-preset list**: findings 1 and 3 both want range tabs on the GLP-1 tile. The `TIME_RANGES_KEYS` constant at `src/components/charts/health-chart.tsx` line 46-67 is already module-private — exporting it (or copying to `src/lib/charts/range-presets.ts`) lets `<Glp1Tile>` reuse the exact same labels + step counts.
- **Empty/null pruning grammar**: findings 5 and 7 both involve removing dead surfaces and tightening the dashboard's hide-when-empty discipline. Worth a single integration test that asserts the dashboard renders zero insight-themed cards above the chart row when `useInsightsAdvisorQuery()` returns `null`.
## Recommended sequencing
Single fix-surface bucket for Round 3. Order inside the bucket:
1. **Finding 5 first** (delete `<InsightsCardPreview>`). Smallest, riskiest only in the sense that the deletion clears space for finding 1's tab strip. Touches `src/app/page.tsx`, two i18n keys, the dashboard layout module, plus a component delete.
2. **Finding 4 + chart-height constant**. Extracts the shared constant, drops mood chart from 280 to 240. Touches three chart files + a new constants module.
3. **Finding 1 (GLP-1 secondary tile) + Finding 3 (range picker)**. Done together because they share the tab strip + range strip surface area inside `<Glp1Tile>`. Touches `glp1-tile.tsx`, `DrugLevelChart.tsx`, `health-chart.tsx`, two i18n keys, tests.
4. **Finding 2 (green band)**. Touch-disjoint from 1+3 only by line range; safest to land after 1+3 so the schedule-pill row sits cleanly above the new tab strip.
5. **Finding 6 (Daily Briefing strip)**. Independent — touches `daily-briefing.tsx` + its test only.
6. **Finding 8 (Health Score size)**. Independent — touches `health-score-card.tsx` + `hero-strip.tsx`. Could land in parallel with 6.
7. **Finding 7 (weekly report click)**. Pending maintainer clarification on which element is dead. Defer to v1.4.27 R2 triage if the affordance can't be located in 30 minutes.
Findings 5, 4, 6, 8, 7 are all small and could ship as one PR; findings 1+2+3 are the meat and could ship as a second PR if the maintainer wants the GLP-1 changes isolated for review.
## Out-of-scope deferrals
- **DrugLevelChart Research-Mode gate redesign**: the current `<GatedPlaceholder>` UI in `DrugLevelChart.tsx` was designed for the standalone Medication-detail surface. Mounting it inside the tile may need a tighter visual treatment (a single-line link instead of a card). Defer to v1.4.28 if the gate behaviour reads acceptably in v1.4.27; revisit if Marc reports the placeholder feels heavy inside the tile.
- **Multi-concurrent GLP-1 prescription handling**: `<Glp1Tile>` renders `data.medications[0]` and ignores any second active GLP-1 (line 209 comment). Defer to `/insights/medikamente` per the existing comment — the dashboard tile stays single-medication.
- **Insights mother-page hero/briefing reflow**: finding 6 only strips the duplicate paragraph; the larger question "should the Daily Briefing card exist at all on `/insights` now that the hero owns the prose?" is a v1.4.28 / v1.5 product call.
- **Tile-strip layout overhaul**: finding 4 reads as "give every chart the same height". If the maintainer separately wants the tile strip itself rebalanced (column widths, gap rhythm), that's a v1.4.28 design-review item.
- **Server-side weekly report generation cadence**: finding 7's path A only adds a banner. The underlying advisor pipeline still owns when `weeklyReport` lands in the payload. If the banner feels stale, that's a server-side cadence question for v1.4.28.
- **Health Score card on the dashboard itself**: finding 8 keeps the score on `/insights`. If the maintainer separately wants the score promoted to the dashboard hero, that's a much larger scope (move the component, plumb the analytics-route healthScore payload through the dashboard's `useQuery`, build a dashboard-shaped hero) and belongs in v1.4.28 R1.