fix(insights): align BD-Zielbereich delta math with comparison window

The tile computed `bp30 - bpAll` regardless of which baseline the
user picked under Settings → Dashboard, while rendering a caption
keyed on `comparisonBaseline` ("vs. last month" / "vs. last year").
The math therefore lied to the user. Every other trend tile uses
`tileCompareDelta()` against period-aligned `avg30LastMonth` /
`avg30LastYear` baselines.

Add `priorMonth` (now-60d…now-30d) and `priorYear` (now-395d…
now-365d) windows to `computeBpInTargetWindows()`. Ship the pcts on
the analytics envelope as `bpInTargetPctPriorMonth` /
`bpInTargetPctPriorYear`. Route the dashboard's `bpCompareDelta` to
subtract the prior window matching the user's baseline; fall back
to null for `none`.

Three new bp-in-target unit tests (priorMonth, priorYear, null
windows) and two insights-polish guards (envelope contains the
fields, page no longer subtracts `bpAll`).

Closes Code-H2.

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 22:08:03 +02:00
1 parent 75ccc01984
commit ef597bfbbd
5 files changed
+180 -2

No files matched your search

+34
View File
@@ -181,6 +181,40 @@ describe("v1.4.19 A3 — chart-token leak hardening", () => {
});
});
describe("v1.4.22 W5 reconcile (Code-H2) — BD-Zielbereich tile compareDelta uses period-aligned baseline", () => {
/**
* Up to v1.4.22 W4 the BD tile computed `bp30 - bpAll`
* (last-30-days minus all-time) regardless of which window the user
* picked under Settings → Dashboard. The caption still rendered "vs.
* last month" / "vs. last year" via `comparison.captionLastMonth` /
* `comparison.captionLastYear`, so the user read a sentence whose
* numerator was actually a 30d-vs-all-time delta. Every other tile
* routes through `tileCompareDelta()` with `summary.avg30LastMonth`
* / `summary.avg30LastYear` for honest period-aligned math.
*
* This test pins that the dashboard now subtracts the prior-period
* window (priorMonth / priorYear) instead of all-time.
*/
it("subtracts bpInTargetPctPriorMonth when comparisonBaseline=lastMonth", () => {
const src = load(DASHBOARD_PATH);
// The fix introduces `bpComparePrior` keyed by `compareBaseline`.
expect(src).toContain("bpInTargetPctPriorMonth");
expect(src).toContain("bpInTargetPctPriorYear");
// The all-time shortcut from v1.4.22 A2 must not subtract `bpAll`
// for the comparison delta any more.
expect(src).not.toMatch(/bp30\s*-\s*bpAll/);
});
it("ships priorMonth + priorYear pcts in the analytics envelope", () => {
const ROUTE_SRC = readFileSync(
join(ROOT, "src/app/api/analytics/route.ts"),
"utf8",
);
expect(ROUTE_SRC).toContain("bpInTargetPctPriorMonth");
expect(ROUTE_SRC).toContain("bpInTargetPctPriorYear");
});
});
describe("v1.4.22 A3 — comparison toggle is global Settings only", () => {
/**
* The comparison-overlay toggle is a global preference. Up to v1.4.21
+14
View File
@@ -70,6 +70,16 @@ export const GET = apiHandler(async () => {
let bpInTargetPct7d: number | null = null;
let bpInTargetPct30d: number | null = null;
let bpInTargetPctAllTime: number | null = null;
/**
* v1.4.22 W5 reconcile (Code-H2) — period-aligned prior-window
* pcts so the BD-Zielbereich tile's comparison-overlay caption
* stops mismatching its math with its label. The tile's
* `compareDelta` is `last30Days - priorMonth` (or `… - priorYear`)
* matching the user's `comparisonBaseline` selection, never
* `last30Days - allTime` (the v1.4.22 A2 shortcut).
*/
let bpInTargetPctPriorMonth: number | null = null;
let bpInTargetPctPriorYear: number | null = null;
const bpTargets = getBpTargets(user.dateOfBirth);
if (bpTargets) {
const now = new Date();
@@ -99,6 +109,8 @@ export const GET = apiHandler(async () => {
bpInTargetPct7d = windows.last7Days?.pct ?? null;
bpInTargetPct30d = windows.last30Days?.pct ?? null;
bpInTargetPctAllTime = windows.allTime?.pct ?? null;
bpInTargetPctPriorMonth = windows.priorMonth?.pct ?? null;
bpInTargetPctPriorYear = windows.priorYear?.pct ?? null;
}
// Per-context glucose summaries (canonical mg/dL).
@@ -153,6 +165,8 @@ export const GET = apiHandler(async () => {
bpInTargetPct7d,
bpInTargetPct30d,
bpInTargetPctAllTime,
bpInTargetPctPriorMonth,
bpInTargetPctPriorYear,
glucoseByContext,
correlations,
healthScore,
+29 -2
View File
@@ -103,6 +103,15 @@ interface AnalyticsData {
* on the BD-Zielbereich tile (alongside `7d` and `30d`).
*/
bpInTargetPctAllTime?: number | null;
/**
* v1.4.22 W5 reconcile (Code-H2) — period-aligned prior-window
* pcts. The BD-Zielbereich tile's comparison-overlay caption picks
* `priorMonth` for `comparisonBaseline === "lastMonth"` and
* `priorYear` for `lastYear` so the rendered "Δ X% vs. last month"
* stays honest. Null when the prior window has no paired readings.
*/
bpInTargetPctPriorMonth?: number | null;
bpInTargetPctPriorYear?: number | null;
glucoseByContext?: Record<string, DataSummaryType>;
}
@@ -817,6 +826,8 @@ export default function DashboardPage() {
const bp7 = data?.bpInTargetPct7d ?? null;
const bp30 = data?.bpInTargetPct30d ?? null;
const bpAll = data?.bpInTargetPctAllTime ?? null;
const bpPriorMonth = data?.bpInTargetPctPriorMonth ?? null;
const bpPriorYear = data?.bpInTargetPctPriorYear ?? null;
const bpTrendDelta =
bp7 !== null && bp30 !== null ? bp7 - bp30 : null;
const bpSlope30: import("@/lib/analytics/trends").TrendSlope | null =
@@ -835,10 +846,26 @@ export default function DashboardPage() {
// mis-interpret a `0` as "very low confidence".
confidence: 1,
};
// v1.4.22 W5 reconcile (Code-H2) — match the comparison
// window the user picked. `lastMonth` baseline ⇒ compare
// last30 against the now-60d…now-30d window (priorMonth);
// `lastYear` baseline ⇒ compare against the matching window
// shifted back 365 days. The previous shortcut subtracted
// `bpAll` regardless of baseline, which produced a number
// whose magnitude was honest but whose label ("vs. last
// month") lied to the user.
const bpComparePrior =
compareBaseline === "lastMonth"
? bpPriorMonth
: compareBaseline === "lastYear"
? bpPriorYear
: null;
const bpCompareDelta =
compareBaseline === "none" || bp30 === null || bpAll === null
compareBaseline === "none" ||
bp30 === null ||
bpComparePrior === null
? null
: Math.round((bp30 - bpAll) * 10) / 10;
: Math.round((bp30 - bpComparePrior) * 10) / 10;
trendCards.push({
id: "bpInTarget",
order: widgetOrder("bpInTarget"),