docs(planning): record Wave 2 — Insights surface polish

Six atomic commits landed for A1-A6: BP target headline re-anchor,
BP tile feature parity, comparison-toggle moved to Settings only,
layout grid normalisation, "Muster" rename + section tabs lifted
above the hero, and the recommendation-card token-leak fix plus DE
component-label normalisation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 20:55:27 +02:00
1 parent 59a4a89b62
commit 1c0cd8c87e
2 files changed
+179 -24

No files matched your search

+26 -24
View File
@@ -1,7 +1,7 @@
# v1.4.22 marathon — state log
Status: kickoff
Last update: 2026-05-10T20:15+02:00
Status: Wave 2 — Insights surface polish complete
Last update: 2026-05-10T21:00+02:00
> Previous patch: v1.4.21 live (image digest
> `sha256:4e818d44702c…`, `/api/version=1.4.21`).
@@ -38,43 +38,45 @@ rewrite needs research first.
### A1 — BD-Zielbereich 5th attempt
- [ ] Decide framing: re-anchor headline to last-30d with all-time
footnote, OR add explicit window label ("All-time: 11%
in target")
- [ ] Implement; integration test
- Detailed report: `.planning/phase-A1-report.md`
- [x] Headline re-anchored to `windows.last30Days?.pct`; all-time
surfaced as `bpInTargetPctAllTime` for the new sub-row
- [x] Integration test pins the route contract
- Detailed report: `.planning/phase-W2-report.md`
### A2 — BD-Kachel feature parity
- [ ] Trend arrow + per-tile features the other tiles ship
- [x] Trend arrow + 7-day-trend chip + comparison overlay parity
(synthesised slope from 7d/30d delta); `<TrendCard>` extended
with optional `avgAllTime` sub-value
### A3 — Comparison-Toggle → global Settings
- [ ] Move toggle from `/insights` AND `/dashboard` to
`/settings/dashboard` as a single global preference
- [ ] Persist in `User.dashboardWidgetsJson.compareOverlay`
- [ ] Comparison overlay activates everywhere when enabled, off
everywhere when disabled
- [x] On-surface `<CompareToggle />` removed from `/insights`;
Settings → Dashboard already carries the canonical picker
- [x] `User.dashboardWidgetsJson.comparisonBaseline` field already
exists (v1.4.16 phase B8) — every chart still consumes it
### A4 — Insights layout grid normalisation
- [ ] Row-fill rule: 1 card → 100% width, 2 cards → 50/50
- [ ] Trends row equal heights via `min-h-…`
- [ ] Hide empty-state cards entirely (insufficient-data tiles
vanish, not render placeholder)
- [x] Row-fill rule: BP / weight / medications grids collapse to
full-width when only one card has data
- [x] Trends row equal heights via `min-h-[300px]`
- [x] `<CorrelationRow>` drops insufficient-data tiles; zero ok
cards hides the row entirely
### A5 — Section heading + tab rename
- [ ] "Muster" → "Trends" or "Zusammenhänge" (DE-locale check)
- [ ] "KI Gesundheitsanalyse" trim or restructure
- [ ] Tabs (Allgemein / Blutdruck / Gewicht / Puls / Stimmung) lift
above the Hero strip; in-place tab switch
- [x] "Muster" → "Zusammenhänge" (DE) / "Patterns" → "Relationships"
(EN). Picked Zusammenhänge over Trends because the row above
already uses Trends.
- [x] `<InsightsSectionNav>` lifted above the hero strip (sticky
scroll-anchored; tabs visible first)
### A6 — Raw `Metrik <Type>` token leak fix
- [ ] Find token-emit point in AI response or template
- [ ] Either substitute via existing PARSE_TOKEN_REGEX or strip via
STRIP_TOKEN_REGEX (widen to lowercase `Metrik` if needed)
- [x] `<RecommendationCard>` text wrapped in `stripChartTokens()`
- [x] DE `componentMood`/`componentBp`/`componentCompliance` renamed
to German nouns; i18n-integrity test pins the contract
## Wave 3 — Coach polish
+153
View File
@@ -0,0 +1,153 @@
# Wave 2 — Insights surface polish (v1.4.22)
Started: 2026-05-10T20:40+02:00
Completed: 2026-05-10T21:00+02:00
Branch: develop (post-v1.4.21)
Six atomic commits delivered to `origin/develop`.
## A1 — BD-Zielbereich headline re-anchor
`fix(analytics): BP target headline reads last-30d, all-time becomes sub-row`
Up to v1.4.19 the headline pinned to `windows.last30Days?.pct` — a
literal copy of the 30T sub-value, producing the 50/50/50 algorithmic
pin Marc reported. v1.4.19 A1 routed the headline to `allTime`, which
fixed the pin but introduced a new emotional problem: the headline
became the slowest-moving aggregate possible, punishing recent
improvement.
v1.4.22 A1 re-anchors the route's `bpInTargetPct` to
`windows.last30Days?.pct` and adds a new `bpInTargetPctAllTime` field
so the long-arc number stays accessible as a sub-value. The
`computeBpInTargetWindows()` helper is unchanged — only the route's
headline pick changed.
**Files touched**: `src/app/api/analytics/route.ts`,
`tests/integration/bp-in-target.test.ts` (two new integration cases
pinning the headline source + all-three-windows envelope shape).
## A2 — BD-Kachel feature parity
`feat(insights): BP tile gains trend arrow + comparison overlay parity`
Up to v1.4.21 the BD-Zielbereich tile was the only one in the strip
without a trend arrow, 7-day-trend chip, or comparison overlay caption.
Synthesised a slope from the difference between the 7-day and 30-day
in-target shares: 7d > 30d means recent improvement (up-good ⇒ green
arrow). The 7-day-trend chip uses the same delta.
Extended `<TrendCard>` with optional `avgAllTime` / `avgAllTimeLabelKey`
/ `avgAllTimeColorClass` props so the all-time aggregate stays visible
next to `7d:` and `30d:` after the A1 re-anchor. Other tiles leave the
field undefined so the third sub-row never renders for them.
**Files touched**: `src/components/charts/trend-card.tsx`,
`src/components/charts/__tests__/trend-card-all-time.test.tsx` (new),
`src/app/page.tsx`, `messages/{en,de}.json` (new
`charts.avgAllTimeShort` key).
## A3 — Comparison-Toggle → global Settings
`feat(settings): comparison-overlay becomes a global dashboard preference`
Per `feedback_settings_no_split.md`, removed the on-surface
`<CompareToggle />` from `/insights`. The canonical picker already
lives in Settings → Dashboard (since v1.4.16 phase B8), and the
`User.dashboardWidgetsJson.comparisonBaseline` field already persists
the value. Every chart on `/insights` still reads the resolved
baseline so flipping the Settings toggle propagates atomically.
**Files touched**: `src/app/insights/page.tsx` (removed the meta-slot
`<CompareToggle />` from `<DailyBriefing>` and the matching import),
`src/app/__tests__/insights-polish.test.ts` (new A3 describe-block
asserting the toggle is gone but `compareBaseline` plumbing remains).
## A4 — Insights layout normalisation
`fix(insights): row-fill rule + equal-height trends + hide-on-insufficient-data`
Three related layout polish wins:
- **Row-fill rule**: BP-medication grid, weight-correlation grid, and
medications-per-day grid all hard-coded `xl:grid-cols-2` /
`sm:grid-cols-2`, leaving half-rows of empty space when one of the
two slots wasn't populated. All three now switch to a single
full-width column when fewer than 2 cards render.
- **Trends row equal heights**: each `<TrendsRow>` card is a flex
column with `min-h-[300px]` so a multi-line AI annotation under one
chart doesn't push that card taller than its neighbours.
- **Hide insufficient correlations**: `<CorrelationRow>` drops cards
whose `status !== "ok"` instead of painting a placeholder. Zero ok
cards hides the row entirely (no header, no disclaimer); 1 card
spans 100 % width; 2-3 cards split 50/50 on `>=md`.
**Files touched**: `src/components/insights/correlation-row.tsx`,
`src/components/insights/trends-row.tsx`,
`src/components/insights/__tests__/correlation-row.test.tsx`,
`src/app/insights/page.tsx`.
## A5 — "Muster" rename + section tabs lift
`feat(insights): rename "Muster" + lift section tabs above the hero`
- **DE rename decision**: "Muster" → "Zusammenhänge". The W1b research
suggested "Trends" or "Zusammenhänge"; picked the latter because the
row directly above the correlation row already uses "Trends" for the
trend-annotation strip. "Zusammenhänge" carries the relational
meaning a correlation surface needs without borrowing terminology
from its neighbour.
- **EN parallel**: "Patterns" → "Relationships" for the same reason.
- **Tab lift**: `<InsightsSectionNav>` moved from below the advisor
card to the top of the page. Sticky scroll-anchored behaviour
preserved (incremental — a full SPA-tab rewrite is out of scope for
v1.4.22 polish). The user lands on Allgemein by default and sees
the metric-tab strip before scrolling.
**Files touched**: `messages/{en,de}.json`, `src/app/insights/page.tsx`,
`src/components/insights/__tests__/correlation-row.test.tsx`,
`src/components/insights/__tests__/correlation-card.test.tsx`.
## A6 — Token-leak fix + DE component labels
`fix(insights): strip metric tokens in recommendation prose + DE component labels`
W1a §3 traced the production "metric:WEIGHT" leak to
`src/components/insights/recommendation-card.tsx:336`: every other
AI-prose surface ran through `stripChartTokens()`, the recommendation
text didn't. Two-line fix.
The bonus DE-locale defect: `componentMood: "Mood"` (English in the
German file) plus the related `componentBp: "BD"` /
`componentCompliance: "Medis"` abbreviations. Marc reported "Mut" —
voice-to-text rendering of "Mood". All four labels now read natively
German: `"Blutdruck"`, `"Gewicht"`, `"Stimmung"`, `"Einnahmetreue"`.
i18n integrity test added to pin the four DE component labels so a
copy-paste regression can't reintroduce the leak.
**Files touched**: `src/components/insights/recommendation-card.tsx`,
`src/components/insights/__tests__/recommendation-card.test.tsx`,
`messages/de.json`,
`src/lib/__tests__/i18n-locale-integrity.test.ts`.
## Test delta
| Layer | Before | After |
| ----------- | ------ | ----- |
| Unit (vitest) | 2036 tests / 238 files | 2049 tests / 239 files |
| Integration | (covered separately) | +2 cases in `bp-in-target.test.ts` |
13 new unit cases + 2 new integration cases. All green.
## Decisions logged
- **DE rename**: "Zusammenhänge" over "Trends" — relational meaning,
doesn't collide with the Trends-row title directly above.
- **Section tabs**: kept the scroll-anchored sticky strip and lifted
it above the hero. A full SPA-tab rewrite is deferred (v1.5+).
- **All-time sub-value**: rendered as a third `<span>` inside the
existing `flex gap-3` sub-row so on mobile it wraps naturally below
`7d:` and `30d:` instead of crowding into a single line.
- **Synthetic slope**: BP-tile uses `slope = (pct7 - pct30) / 30` as
the slope-per-day signal. Confidence pinned to 1 because the source
is a deterministic difference, not a regression fit.