mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
fix(insights): BD-Zielbereich computes correctly for real measurement data
The BD-Zielbereich tile rendered 0% for users whose readings sat below the ESH 2023 narrow goal band (Sys 120-129 / Dia 70-79 for under-65). Marc's actual production data has many readings like 117/79 — textbook normotensive, well-controlled — that v1.4.15 A4 still counted as OUT of target because sys < sysLow (120). 5 of 10 of his last-30-day pairs should be in-target by clinical interpretation; v1.4.15 reported 0. Root cause: "in target" was defined as the narrow goal band, which is the ESH treatment goal for hypertensive patients ON therapy — NOT the desirable range for a normotensive HealthLog user. Users (and the dashboard label) intuitively mean "BP is well-controlled = at or below the ceiling, not implausibly hypotensive". Fix: change semantics from `sysLow <= sys <= sysHigh && diaLow <= dia <= diaHigh` to `90 <= sys <= sysHigh && 50 <= dia <= diaHigh`. The 90/50 floors guard against counting symptomatic hypotension as "good control". Centralised the check in `isBpReadingInTarget()` so the 6 historical call sites (analytics route, blood-pressure-status, features, general-status, insights/cards, insights/comprehensive, insights/targets) share one definition — pre-fix they had 5 independent inline copies of the narrow-band formula, easy to drift on a future refactor. Tests: - 6 new unit tests pin the new ceiling+floor semantics including Marc's exact production fixture (10 paired readings -> 50% under v1.4.16, was 0% under v1.4.15). - New `tests/integration/bp-in-target.test.ts` seeds Postgres testcontainer with Marc's reading shape + null-tolerance + hypotension edge cases. Cache invalidation already correct: `measurementDependentKeys` includes `analytics()` so adding a BP measurement invalidates the tile's TanStack Query cache. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
e22d731f31
commit
577d8dd29f
9 files changed
+448
-68
No files matched your search
@@ -12,6 +12,7 @@ import { annotate } from "@/lib/logging/context";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { summarize, type DataPoint } from "@/lib/analytics/trends";
|
||||
import { getBpTargets } from "@/lib/analytics/bp-targets";
|
||||
import { isBpReadingInTarget } from "@/lib/analytics/bp-in-target";
|
||||
import {
|
||||
generateAlerts,
|
||||
type HealthAlert,
|
||||
@@ -112,12 +113,10 @@ export const GET = apiHandler(async () => {
|
||||
const diaData = byType("BLOOD_PRESSURE_DIA");
|
||||
const pairs = pairByTimestamp(sysData, diaData, 5 * 60_000);
|
||||
if (pairs.length > 0) {
|
||||
const inTarget = pairs.filter(
|
||||
(p) =>
|
||||
p.a >= bpTargets.sysLow &&
|
||||
p.a <= bpTargets.sysHigh &&
|
||||
p.b >= bpTargets.diaLow &&
|
||||
p.b <= bpTargets.diaHigh,
|
||||
// v1.4.16 A2 — one-sided ceiling semantics with hypotension
|
||||
// floor. See lib/analytics/bp-in-target.ts.
|
||||
const inTarget = pairs.filter((p) =>
|
||||
isBpReadingInTarget(p.a, p.b, bpTargets),
|
||||
).length;
|
||||
bpPctInTarget = Math.round((inTarget / pairs.length) * 100);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { resolveProvider } from "@/lib/ai/provider";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { summarize, type DataPoint } from "@/lib/analytics/trends";
|
||||
import { getBpTargets } from "@/lib/analytics/bp-targets";
|
||||
import { isBpReadingInTarget } from "@/lib/analytics/bp-in-target";
|
||||
import {
|
||||
classifyBMI,
|
||||
classifyBP,
|
||||
@@ -121,12 +122,10 @@ export const GET = apiHandler(async () => {
|
||||
const sysPairs = pairByTimestamp(sysData, diaData, 5 * 60 * 1000);
|
||||
|
||||
if (sysPairs.length > 0) {
|
||||
const inTarget = sysPairs.filter(
|
||||
(p) =>
|
||||
p.a >= bpTargets.sysLow &&
|
||||
p.a <= bpTargets.sysHigh &&
|
||||
p.b >= bpTargets.diaLow &&
|
||||
p.b <= bpTargets.diaHigh,
|
||||
// v1.4.16 A2 — one-sided ceiling semantics with hypotension
|
||||
// floor. See lib/analytics/bp-in-target.ts.
|
||||
const inTarget = sysPairs.filter((p) =>
|
||||
isBpReadingInTarget(p.a, p.b, bpTargets),
|
||||
).length;
|
||||
bpPctInTarget = Math.round((inTarget / sysPairs.length) * 100);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "@/lib/analytics/classifications";
|
||||
import { calculateCompliance } from "@/lib/analytics/compliance";
|
||||
import { pairByTimestamp } from "@/lib/analytics/correlations";
|
||||
import { isBpReadingInTarget } from "@/lib/analytics/bp-in-target";
|
||||
import {
|
||||
classifyPulseByTarget,
|
||||
getPersonalizedPulseTarget,
|
||||
@@ -228,12 +229,10 @@ export const GET = apiHandler(async () => {
|
||||
}));
|
||||
|
||||
const bpPairs = pairByTimestamp(sysPoints, diaPoints, 5 * 60 * 1000);
|
||||
const bpInTargetCount = bpPairs.filter(
|
||||
(pair) =>
|
||||
pair.a >= bpRange.sysLow &&
|
||||
pair.a <= bpRange.sysHigh &&
|
||||
pair.b >= bpRange.diaLow &&
|
||||
pair.b <= bpRange.diaHigh,
|
||||
// v1.4.16 A2 — one-sided ceiling semantics with hypotension
|
||||
// floor. See lib/analytics/bp-in-target.ts.
|
||||
const bpInTargetCount = bpPairs.filter((pair) =>
|
||||
isBpReadingInTarget(pair.a, pair.b, bpRange),
|
||||
).length;
|
||||
const bpInTargetRate =
|
||||
bpPairs.length > 0
|
||||
|
||||
Reference in new issue
Block a user