fix(analytics): BP target headline reads last-30d, all-time becomes sub-row

Up to v1.4.19 the BD-Zielbereich tile pinned 50/50/50 because the
route routed the headline through `windows.last30Days?.pct` — making
the headline a literal copy of the `30T` sub-value. v1.4.19 A1
re-anchored the headline to all-time so the three windows could
diverge. That fixed the algorithmic pin but introduced an emotional
problem: the headline became the slowest-moving aggregate possible,
punishing users who had recently dragged their compliance up. A user
seeing 7d 50 % / 30d 50 % alongside a stubborn 11 % all-time read it
as "your work doesn't count yet."

Re-anchor the headline to `windows.last30Days?.pct` and surface
all-time as a third sub-value (`bpInTargetPctAllTime`) so power users
still see the long-arc number without it dominating. The integration
test pins the new contract: with three legitimately divergent windows
the route's `bpInTargetPct` equals `last30Days.pct`, and the new
all-time field carries the historical figure.

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:42:09 +02:00
1 parent e794f57bae
commit 8606767058
2 files changed
+226 -12

No files matched your search

+14 -11
View File
@@ -69,19 +69,20 @@ export const GET = apiHandler(async () => {
let bpInTargetPct: number | null = null;
let bpInTargetPct7d: number | null = null;
let bpInTargetPct30d: number | null = null;
let bpInTargetPctAllTime: number | null = null;
const bpTargets = getBpTargets(user.dateOfBirth);
if (bpTargets) {
const now = new Date();
// v1.4.19 A1 — fetch ALL paired BP rows, not just the trailing 30
// days. Up to v1.4.18 we filtered to the last 30 days at the DB
// level and the headline (`bpInTargetPct`) was routed through
// `windows.last30Days?.pct` — making the headline a literal copy
// of the `30T` sub-value. On a busy account (e.g. 572 paired
// readings with recent 30d = 50 %, all-time ≈ 11 %) the tile
// pinned 50/50/50 and looked algorithmically broken. The windowed
// helper now also returns an independent `allTime` aggregate,
// which we surface as the headline so the three numbers can
// diverge naturally.
// v1.4.22 A1 — re-anchor the BD-Zielbereich tile headline to the
// last-30-day window. Up to v1.4.19 the headline pinned to the
// 30-day average (so 7d / 30d / total all read 50 %). v1.4.19 A1
// flipped the headline to all-time, which made the tile correct
// but emotionally wrong: the headline was the slowest-moving
// aggregate possible, punishing recent improvement. v1.4.22 A1
// re-routes the headline to last-30-days and surfaces 7d / 30d /
// all-time as a 3-line sub-row so power users still see the
// long-arc number without it dominating. The helper still returns
// every window — only the headline pick changed.
const [sysData, diaData] = await Promise.all([
prisma.measurement.findMany({
where: { userId: user.id, type: "BLOOD_PRESSURE_SYS" },
@@ -94,9 +95,10 @@ export const GET = apiHandler(async () => {
]);
const windows = computeBpInTargetWindows(sysData, diaData, bpTargets, now);
bpInTargetPct = windows.allTime?.pct ?? null;
bpInTargetPct = windows.last30Days?.pct ?? null;
bpInTargetPct7d = windows.last7Days?.pct ?? null;
bpInTargetPct30d = windows.last30Days?.pct ?? null;
bpInTargetPctAllTime = windows.allTime?.pct ?? null;
}
// Per-context glucose summaries (canonical mg/dL).
@@ -150,6 +152,7 @@ export const GET = apiHandler(async () => {
bpInTargetPct,
bpInTargetPct7d,
bpInTargetPct30d,
bpInTargetPctAllTime,
glucoseByContext,
correlations,
healthScore,