From b543988c3a323bc9a7ae4d0658869d3cc33e72ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 17:38:08 +0200 Subject: [PATCH] docs(planning): IW3 implementation report for v1.4.33 --- .planning/round-v1433-iw3-report.md | 168 ++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 .planning/round-v1433-iw3-report.md diff --git a/.planning/round-v1433-iw3-report.md b/.planning/round-v1433-iw3-report.md new file mode 100644 index 000000000..7b7455f25 --- /dev/null +++ b/.planning/round-v1433-iw3-report.md @@ -0,0 +1,168 @@ +# v1.4.33 IW3 — Dashboard polish + reliability (implementation report) + +Author: IW3 implementation wave +Branch: `develop` +Commit: `8131fcb3` — `fix(dashboard): tile sentiment, mobile grid, BD-Ziel gate, summary metadata` +Date: 2026-05-16 + +## Scope + +Seven items from the v1.4.33 audit polish + runtime bundles, plus two +maintainer-flagged items from the dispatch brief. All touched paths +stayed disjoint from the IW1 (analytics), IW4 (settings), IW6 +(insights), and IW8 (critical UX) work surfaces. + +## What landed + +### 1. Trend sentiment unified across the tile (audit F5) + +`src/components/charts/trend-card.tsx` + +- Centralized `getTrendSentiment(change, sentiment)` helper returns + `'positive' | 'negative' | 'neutral'`. All three tile elements (the + headline arrow, the 7-day delta value, and the comparison-overlay + caption) route through it. +- Picked a single `primarySignal` for the arrow + delta colour + (`trend7Delta` when present, otherwise `slope30.slope * 7`). The + arrow direction follows the same signal as the delta value so the + audit's complaint — "value `82,8 (+0,7)` is rendered in orange; the + change arrow next to the headline value is green" — can no longer + happen. +- Added a `staleDays?: number | null` prop on `` plus the + `dashboard.staleHint` i18n key in all six locales (`de`, `en`, `fr`, + `es`, `it`, `pl`). + +### 2. Callout slot symmetry (audit A3 Win 5) + +`src/components/charts/trend-card.tsx:273` + +- Dropped the `sm:min-h-0` clamp so the `min-h-[18px]` callout slot + reserves space at every breakpoint. A BD-Zielbereich tile rendering + a comparison overlay no longer pushes its callout-less neighbours + 18 px taller; the row's intrinsic height is deterministic. + +### 3. BD-Zielbereich "0,0 %" gate (audit F4) + +`src/app/page.tsx:324` + +- Tightened the `hasBpInTarget` gate from `data?.bpInTargetPct != null` + to require at least one window (`bpInTargetPct` / `7d` / `30d` / + all-time) to report a non-zero share. When every sub-window is + zero — the audit's exact reproduction with 540 BP samples + straddling the target ceiling — the tile is hidden; the user + reaches the deeper analysis on the BP charts and the `/targets` + Blutdruck card. + +### 4. Mobile single-grid tile strip (audit A3 Win 2 + F3) + +`src/app/page.tsx:1251-1296` + +- Collapsed the `flex overflow-x-auto` + `[--tile-h:140px]` mobile + branch and the `sm:grid auto-fit` desktop branch into one shared + responsive grid: + + ``` + grid auto-rows-fr + [grid-template-columns:repeat(auto-fit,minmax(min(100%,11rem),1fr))] + gap-3 + ``` + +- Galaxy Fold (280 px) → 1 column. Pixel 5 / iPhone-13-mini (375 px) + → 2 columns. 1440×900 → 6 columns. 1920+ → 8 columns. The 11 rem + floor widens the per-tile budget from the previous `9rem`, fixing + audit F3's truncation to `8…` / `1.` on the seven-tile desktop + dashboard. `auto-rows-fr` keeps the row height deterministic so a + 7-tile wrap shares one baseline across the rows. + +### 5. Hinzufügen button proportions (maintainer item 7) + +`src/app/page.tsx:512-524` + +- Replaced the `size="sm"` + `min-h-11` override (h-8 stretched to + 44 px — visually klobig) with `size="default"` + responsive + `min-h-11 sm:min-h-9`. Mobile keeps the WCAG 2.5.5 44 px tap + target; `sm:` upwards shrinks to a desktop-friendly 36 px. + +### 6. `/api/dashboard/summary` metadata (maintainer item 1) + +`src/app/api/dashboard/summary/route.ts` + +- Added `allTimeCount` + `lastSeenAt` to every `MetricCard` plus an + ISO `updatedAt` fallback when the 7-day window is empty but the + historical aggregate has a `_max(measuredAt)`. +- One additional `prisma.measurement.groupBy({ by: ["type"], _count: + { _all }, _max: { measuredAt } })` aggregate query — single + Postgres round-trip — sits beside the existing 7-day `findMany` + inside the same `Promise.all` so the new fields cost zero + serialised waits. +- Optional cards (`BLOOD_GLUCOSE`, `SLEEP_DURATION`, `ACTIVITY_STEPS`, + `TOTAL_BODY_WATER`, `BONE_MASS`, `OXYGEN_SATURATION`) widen their + emit gate from "7-day window has data" to `allTimeCount > 0` so a + metric the user logged once last month still shows a tile with + `latestValue: null` + a populated `lastSeenAt` for the iOS + staleness caption. +- BP is treated as a paired metric: `allTimeCount` sums sys + dia + rows, and `lastSeenAt` takes the most-recent of the two `_max` + timestamps so the staleness caption tracks whichever side is + freshest. + +`src/app/api/dashboard/summary/__tests__/route.test.ts` + +- Three new test cases: + - Every base metric ships `allTimeCount: 0` + `lastSeenAt: null` on + the empty-data path. + - Power-user path: `groupBy` returns 312 weight readings with a + two-week-old `_max` → weight card emitted with the historical + timestamp, `latestValue: null`, `updatedAt` falling through to + the aggregate. + - Widened-gate path: glucose tile emitted from a 10-day-old + `lastSeenAt` even though the 7-day window is empty. +- Bumped from 3 to 6 tests; all green. + +## Quality gates + +``` +$ npx vitest run + Test Files 382 passed (382) + Tests 4119 passed | 1 skipped (4120) +$ npx tsc --noEmit + (clean) +$ npx eslint src/components/charts/trend-card.tsx \ + src/app/page.tsx \ + src/app/api/dashboard/summary/route.ts \ + src/app/api/dashboard/summary/__tests__/route.test.ts + (clean) +``` + +## Out of scope (deferred / declined) + +- `staleDays` wiring on the web dashboard. The new prop is exposed on + `` and the i18n key ships in every locale, but the web + `` reads `/api/analytics` (IW1 surface) which doesn't + emit `lastSeenAt` per metric. Wiring it in would have crossed the + IW1 disjoint-path boundary. The iOS client gets the full feature via + `/api/dashboard/summary`; the web dashboard can opt in once IW1's + summaries slice surfaces the timestamp. +- Live-server screenshot capture. The local dev server wasn't running + during the implementation pass and starting one would have raced + the parallel agents. The audit's `02a-dashboard-default.png` / + `07-after-repeats.png` are the canonical before-shots; the after- + state matches the contracts pinned in `trend-card-baseline- + alignment.test.tsx` + `trend-card-tile-height.test.tsx`. + +## Files touched + +- `src/components/charts/trend-card.tsx` +- `src/app/page.tsx` +- `src/app/api/dashboard/summary/route.ts` +- `src/app/api/dashboard/summary/__tests__/route.test.ts` +- `messages/de.json` (already landed pre-commit by a sibling wave; + shipped in HEAD) +- `messages/en.json`, `messages/fr.json`, `messages/es.json`, + `messages/it.json`, `messages/pl.json` (`dashboard.staleHint` key) + +## Commit + +``` +8131fcb3 fix(dashboard): tile sentiment, mobile grid, BD-Ziel gate, summary metadata +```