mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
docs(planning): A1 report + STATE update for BD-Zielbereich fix
Live-DB audit confirmed the headline was a literal copy of 30T (`bpInTargetPct = windows.last30Days?.pct`); fix added an `allTime` window so the three numbers compute independently. Marc's prod data verifies the asymmetry: 7d=50%, 30d=50%, all-time≈11%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a8562727a7
commit
dcef89c59e
2 files changed
+102
-73
No files matched your search
+28
-17
@@ -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
|
||||
|
||||
@@ -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).
|
||||
Reference in new issue
Block a user