diff --git a/.planning/STATE.md b/.planning/STATE.md index fae71a282..fa11cee58 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -18,13 +18,24 @@ Last update: 2026-05-10T12:48+02:00 ## Wave A — Bug fixes + polish (parallel buckets) -### A1 — BD-Zielbereich constant 50% (4th attempt) +### A1 — BD-Zielbereich constant 50% (4th attempt) ✅ -- [ ] Live-DB audit of Marc's BP measurements (he granted access) -- [ ] Identify why all three windows (7T, 30T, total) show exactly 50% - — likely calculation bug, NOT predicate -- [ ] Root-cause fix + integration test that reproduces with Marc's - exact data fixture +- [x] Live-DB audit of Marc's BP measurements (apps-01, + `db-pg8wggwogo8c4gc4ks0kk4ss-105148113251`): 572 paired + readings since 2022, last-30d = 50 %, last-7d = 50 %, all-time + ≈ 10.8 %. Confirmed the headline 50 % cannot be the all-time + value; it must be a copy of `30T`. +- [x] Root cause: `analytics/route.ts` set + `bpInTargetPct = windows.last30Days?.pct` — literal copy of the + `30T` sub-value. `computeBpInTargetWindows` returned only 7d+30d + so the headline could never legitimately differ. +- [x] Fix: helper returns a third `allTime` window; route fetches all + paired BP rows and routes the headline through `allTime`. +- [x] TDD: 3 failing unit cases + 1 integration case red→green. +- [x] `pnpm test`: 1640 / 1645 (5 pre-existing A3 failures unchanged). +- [x] `pnpm test:integration`: 67 / 67. +- [x] `pnpm typecheck`: clean. `pnpm lint`: 12 baseline 0 new. +- [x] Commit `a856272` pushed to origin/main first attempt. - Detailed report: `.planning/phase-A1-report.md` ### A2 — Charts mobile audit (axis-label overflow + X-axis density consistency) @@ -152,18 +163,18 @@ Last update: 2026-05-10T12:48+02:00 AI-prompt rework, A5 Settings/Integrations status-UI consolidation, A6 Settings mobile audit, A7 Admin polish, A8 quality-of-life write-only audit; Wave B applies A8 findings; Wave D multi-agent QA - + Product-Lead briefed for v1.4.20 Insights redesign + AI Coach; - Phase E release). Previous v1.4.18 / v1.4.17 / v1.4.16 entries - archived above. Tracked tree clean on entry; four untracked stale - dotted-segment route directories - (`src/app/api/export/{full-backup.json,measurements.csv, + - Product-Lead briefed for v1.4.20 Insights redesign + AI Coach; + Phase E release). Previous v1.4.18 / v1.4.17 / v1.4.16 entries + archived above. Tracked tree clean on entry; four untracked stale + dotted-segment route directories + (`src/app/api/export/{full-backup.json,measurements.csv, medications.csv,mood.csv}/`) left in place — same call as v1.4.16 / - v1.4.18 Phase 0, they belong to previous milestones. Phase 0 commit - contains only `.planning/STATE.md`, `.planning/ROADMAP.md`, - `.planning/phase-0-report.md`. v1.4.20 reserved for Insights - redesign with AI Coach (handoff at - `~/Downloads/design_handoff_insights_redesign`); v1.5 reserved for - iOS app + Apple Health. + v1.4.18 Phase 0, they belong to previous milestones. Phase 0 commit + contains only `.planning/STATE.md`, `.planning/ROADMAP.md`, + `.planning/phase-0-report.md`. v1.4.20 reserved for Insights + redesign with AI Coach (handoff at + `~/Downloads/design_handoff_insights_redesign`); v1.5 reserved for + iOS app + Apple Health. - 2026-05-10T12:59+02:00 — Wave A / A4 complete. Removed the default-positivity opener about data quality from the AI insights diff --git a/.planning/phase-A1-report.md b/.planning/phase-A1-report.md index 06da6b98a..64ce8ef21 100644 --- a/.planning/phase-A1-report.md +++ b/.planning/phase-A1-report.md @@ -1,76 +1,94 @@ -# Phase A1 — BD-Zielbereich tile 7T / 30T sub-values +# Phase A1 — BD-Zielbereich constant 50% (4th attempt) -Marathon: v1.4.18 Wave-A bucket A1 (parallel with A2 api-tokens, A3 -chart revert, B1 achievements) -Started: 2026-05-09T~10:08+02:00 -Finished: 2026-05-09T~10:15+02:00 +Marathon: v1.4.19 Wave-A bucket A1 +Started: 2026-05-10T12:55+02:00 +Finished: 2026-05-10T13:05+02:00 ## Symptom -Marc reported the dashboard "BD im Zielbereich" tile rendered the -30-day headline correctly (**50,0 %**, the v1.4.16 A2 ceiling-semantics -fix) but the row underneath the headline still showed **`7T: —`** and -**`30T: —`** even though Marc clearly has paired BP readings in both -windows. +Marc reported the dashboard BD-Zielbereich tile pinned at **EXACTLY 50 % +on 7T, 30T, AND the headline (total)** for his real production data — +the fourth attempt at this surface (v1.4.15 → v1.4.16 → v1.4.18 → +v1.4.19). The earlier passes legitimately landed: v1.4.15 fixed the +denominator (paired readings, not `sysData.length`), v1.4.16 fixed the +predicate (one-sided ceiling with hypotension floor instead of narrow +band), v1.4.18 wired the `avg7` / `avg30` sub-values from a windowed +helper. None of those addressed why the headline kept matching `30T`. + +## Live-DB audit (Marc's prod data on `apps-01`) + +Container `db-pg8wggwogo8c4gc4ks0kk4ss-105148113251`, user +`cmlupy4tn000001rpzx1pxvz7`, DOB 1985-07-09 → under-65 target band +(sysHigh 129 / diaHigh 79 + clinical floor 90/50). + +Closest-pair SQL aggregate over the canonical predicate: + +| Scope | Pairs | In target | % | +| -------- | ----- | --------- | ------ | +| 7d | 2 | 1 | 50.0 % | +| 30d | 10 | 5 | 50.0 % | +| All time | 572 | 62 | 10.8 % | + +The two recent 50 %s are a **real coincidence** (Marc's recent BP control +genuinely sits half above the diastolic ceiling). The headline cannot +legitimately be 50 % — it must be ~11 %. ## Root cause -`/api/analytics` (`src/app/api/analytics/route.ts`) only computed and -returned a single `bpInTargetPct` field — the share over the trailing -30 days. The dashboard tile (`src/app/page.tsx`) explicitly passed -`avg7={null}` and `avg30={null}` to the `TrendCard` component, which -correctly renders "—" as the friendly fallback when those fields are -null. So the sub-values were never wired to data, regardless of how -many readings the user had — pure unfinished feature, not a calculation -bug. +`src/app/api/analytics/route.ts` set +`bpInTargetPct = windows.last30Days?.pct`. The headline was a **literal +copy of the `30T` sub-value** — `bpInTargetPct === bpInTargetPct30d` by +construction. `computeBpInTargetWindows` only returned 7d + 30d windows, +so even with a different routing the headline could never differ from +one of the sub-values. -Confirmed against Marc's prod data on apps-01: user -`cmlupy4tn000001rpzx1pxvz7` has 2 paired sys+dia in last 7 days and -10 in last 30 days. Both windows have data; nothing should render "—". +The brief's hypothesis-1 was inverted in direction but otherwise +correct: not the 7T/30T branches reusing the all-time, but the +all-time headline reusing the 30-day. Hypothesis-2/3/4/5 (filter +no-op, rendering snap, boundary aliasing, cache hit) all ruled out +by the live-DB SQL aggregate. ## Fix -Added `computeBpInTargetWindows()` next to the existing -`computeBpInTargetPct()` helper in -`src/lib/analytics/bp-in-target.ts`. Same predicate + -`isBpReadingInTarget()` ceiling semantics from v1.4.16; the new -function filters the input series by `measuredAt >= now − Nd` and -delegates to `computeBpInTargetPct()` for each window. Returns -`{ last7Days, last30Days }`, both nullable so the tile renders "—" only -when a window genuinely has no paired readings. +1. `computeBpInTargetWindows` now returns a third `allTime` window + computed against the entire input series (no time filter). +2. Analytics route fetches all paired BP rows (drops the 30-day + `gte` filter) and routes the headline through `windows.allTime?.pct`. + Re-pairs once per request (`computeBpInTargetPct` runs three times + internally with progressively smaller filtered slices, but reads + the same input arrays). +3. 7T / 30T sub-values unchanged — same windowed slices as v1.4.18. -Wired into the analytics route — fetched 30-day data once, derived -both shares, and added `bpInTargetPct7d` / `bpInTargetPct30d` to the -response. Wired into the dashboard tile — -`avg7={data?.bpInTargetPct7d}`, `avg30={data?.bpInTargetPct30d}`. -Headline (`bpInTargetPct`) preserved for cached client bundles + -matches `last30Days` so a v1.4.17 PWA displays the same number. +## Tests (TDD red→green) -## Tests (TDD) +- **Unit** (`src/lib/analytics/__tests__/bp-in-target.test.ts`): 3 new + `computeBpInTargetWindows` cases for the all-time contract — diverging + recent vs older mix, empty-input null guard, Marc's 30-day fixture + re-asserted (10 pairs only, so allTime == 30d == 50 % is the legitimate + coincidence; the smoking-gun test layers older-history pairs to force + divergence). Failed first (`undefined` for `result.allTime`); turned + green after the helper landed. +- **Integration** (`tests/integration/bp-in-target.test.ts`): 1 new + testcontainer case seeds 40 paired readings (2 in last 7d, 10 in last + 30d, 30 older, all-out-of-target older) against real Postgres and + asserts the three numbers (50 % / 50 % / 13 %) are independent. +- Full suite: 1640 / 1645 unit (5 failing pre-existed in A3's + `insights-polish.test.ts` — verified by stash-and-rerun before any + edits), 67 / 67 integration, typecheck clean, lint 12 baseline 0 new. -1. **Unit** (`src/lib/analytics/__tests__/bp-in-target.test.ts`) — - 6 new cases for `computeBpInTargetWindows`: empty input, - mixed-window data, null-7d-with-real-30d, default-clock smoke, - Marc's production fixture. Failed first (`is not a function`), - went green after the helper landed. -2. **Integration** (`tests/integration/bp-in-target.test.ts`) — 2 new - testcontainer cases: seed 30 days of BP at fractional-day offsets - (avoids the boundary flakiness of integer-day seeds) and assert - both windows produce non-null hand-counted shares; second case - verifies a user with only 14-day-old data sees `7T = null, -30T = real`. +## Verification against Marc's expected values (post-deploy) -## Verification +Pre-fix tile: **50 % / 7T 50 / 30T 50** (headline=30d=50) +Post-fix tile: **11 % / 7T 50 / 30T 50** (headline=allTime ≈ 10.8 → 11) -- `pnpm test`: **1559 / 1559** green (was 1547; 8 new cases added) -- `pnpm test:integration`: **61 / 61** green (was 59; 2 new cases) -- `pnpm typecheck`: clean -- `pnpm lint`: 12 baseline warnings, **0 new** +Once deployed, the three numbers will diverge naturally for any user +whose older history shape differs from their recent 30-day window — +Marc included. ## Commit -`23363ca` — -`fix(dashboard): wire 7T and 30T sub-values on the BD-Zielbereich tile` +`a856272` — +`fix(dashboard): BD-Zielbereich headline shows all-time, not 30-day` -Pushed to origin/main on first attempt (no rebase race against the -other Wave-A agents). +Pushed to origin/main first attempt (no rebase race; A5's +`ba0d6b8` already on main, mine layered cleanly on top).