mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
fix(blood-pressure): unify in-target window on 90 days across headline, score, and tile
The "BD im Zielbereich" share was narrated on three different windows on the
same screen: the dashboard headline read the trailing 30 days, the dashboard
Health-Score BP pillar read all-time, and the analytics route's pillar read
all-time while its headline read 30 days. A user could see one in-target rate
on the tile and a pillar graded off a different window directly above it.
Standardise the headline, the Health-Score BP pillar input, and the
week-over-week delta on a single trailing-90-day window everywhere — a span
wide enough to stay stable yet recent enough to track current control.
`computeBpInTargetWindows` and the rollup fast-path now both surface a
`last90Days` window; the analytics route and the dashboard snapshot route the
headline and the pillar through it, and both feed the pillar from the identical
value so the dashboard ring and the insights card can never disagree.
Label the window in the tile ("BD-Ziel · 90 T") so the scope is visible; the
30-day figure stays as the short-horizon caption and the all-time aggregate
stays carried for the detail view. Visible-number change is expected: the
headline now reflects the quarter rather than the month.
This commit is contained in:
1 parent
e783d9a46a
commit
114db6bbca
13 files changed
+150
-16
No files matched your search
@@ -326,6 +326,7 @@
|
||||
"stepsShort": "Schritte",
|
||||
"vo2MaxShort": "VO₂ max",
|
||||
"bpInTargetShort": "BD-Ziel",
|
||||
"bpInTargetWindow90": "BD-Ziel · 90 T",
|
||||
"medications": "Medikamente",
|
||||
"quickAddMeasurement": "Messung erfassen",
|
||||
"quickAddMood": "Stimmung erfassen",
|
||||
|
||||
@@ -326,6 +326,7 @@
|
||||
"stepsShort": "Steps",
|
||||
"vo2MaxShort": "VO₂ max",
|
||||
"bpInTargetShort": "BP in Target",
|
||||
"bpInTargetWindow90": "BP in Target · 90d",
|
||||
"medications": "Medications",
|
||||
"quickAddMeasurement": "Log measurement",
|
||||
"quickAddMood": "Log mood",
|
||||
|
||||
@@ -326,6 +326,7 @@
|
||||
"stepsShort": "Pasos",
|
||||
"vo2MaxShort": "VO₂ máx",
|
||||
"bpInTargetShort": "PA-objetivo",
|
||||
"bpInTargetWindow90": "PA-objetivo · 90 d",
|
||||
"medications": "Medicamentos",
|
||||
"quickAddMeasurement": "Registrar medición",
|
||||
"quickAddMood": "Registrar estado de ánimo",
|
||||
|
||||
@@ -326,6 +326,7 @@
|
||||
"stepsShort": "Pas",
|
||||
"vo2MaxShort": "VO₂ max",
|
||||
"bpInTargetShort": "TA-cible",
|
||||
"bpInTargetWindow90": "TA-cible · 90 j",
|
||||
"medications": "Médicaments",
|
||||
"quickAddMeasurement": "Saisir une mesure",
|
||||
"quickAddMood": "Saisir une humeur",
|
||||
|
||||
@@ -326,6 +326,7 @@
|
||||
"stepsShort": "Passi",
|
||||
"vo2MaxShort": "VO₂ max",
|
||||
"bpInTargetShort": "PA-target",
|
||||
"bpInTargetWindow90": "PA-target · 90 gg",
|
||||
"medications": "Farmaci",
|
||||
"quickAddMeasurement": "Registra misurazione",
|
||||
"quickAddMood": "Registra umore",
|
||||
|
||||
@@ -326,6 +326,7 @@
|
||||
"stepsShort": "Kroki",
|
||||
"vo2MaxShort": "VO₂ max",
|
||||
"bpInTargetShort": "RR-cel",
|
||||
"bpInTargetWindow90": "RR-cel · 90 d",
|
||||
"medications": "Leki",
|
||||
"quickAddMeasurement": "Zarejestruj pomiar",
|
||||
"quickAddMood": "Zarejestruj nastrój",
|
||||
|
||||
@@ -242,6 +242,8 @@ async function buildAnalyticsResponse(user: AuthedUser) {
|
||||
let bpInTargetPct: number | null = null;
|
||||
let bpInTargetPct7d: number | null = null;
|
||||
let bpInTargetPct30d: number | null = null;
|
||||
// v1.17 W1d — the canonical 90-day window for the headline / score / coach.
|
||||
let bpInTargetPct90d: number | null = null;
|
||||
let bpInTargetPctAllTime: number | null = null;
|
||||
/**
|
||||
* v1.4.22 W5 reconcile (Code-H2) — period-aligned prior-window
|
||||
@@ -262,10 +264,11 @@ async function buildAnalyticsResponse(user: AuthedUser) {
|
||||
* the resulting `last30Days.pct`.
|
||||
*/
|
||||
let bpInTargetPctPriorWeek: number | null = null;
|
||||
// All-time prior-week BP pct — feeds the Health-Score delta so its BP
|
||||
// pillar compares the same window on both ends (the tile headline keeps
|
||||
// its 30-day scope; the score reads all-time per its documented contract).
|
||||
let windowsPriorWeekAllTime: number | null = null;
|
||||
// v1.17 W1d — prior-week 90-day BP pct. Feeds the Health-Score delta so
|
||||
// its BP pillar compares the same 90-day window on both ends, identical
|
||||
// to the headline scope. (Pre-v1.17 the score read all-time; W1d
|
||||
// unifies headline + score + coach on 90 days.)
|
||||
let windowsPriorWeekForScore: number | null = null;
|
||||
// v1.15.12 A1 — graded clinical-proximity BP score (recency-weighted
|
||||
// representative reading) for the Health-Score BP pillar value, plus
|
||||
// the prior-week run for the week-over-week delta.
|
||||
@@ -315,20 +318,24 @@ async function buildAnalyticsResponse(user: AuthedUser) {
|
||||
userTz,
|
||||
}),
|
||||
]);
|
||||
bpInTargetPct = windows.last30Days?.pct ?? null;
|
||||
// v1.17 W1d — the BD-Zielbereich headline standardises on the
|
||||
// trailing-90-day window (labelled "· 90 T" in the tile). All-time
|
||||
// remains carried below for the BP detail page's long view only.
|
||||
bpInTargetPct = windows.last90Days?.pct ?? null;
|
||||
bpInTargetPct7d = windows.last7Days?.pct ?? null;
|
||||
bpInTargetPct30d = windows.last30Days?.pct ?? null;
|
||||
bpInTargetPct90d = windows.last90Days?.pct ?? null;
|
||||
bpInTargetPctAllTime = windows.allTime?.pct ?? null;
|
||||
bpInTargetPctPriorMonth = windows.priorMonth?.pct ?? null;
|
||||
bpInTargetPctPriorYear = windows.priorYear?.pct ?? null;
|
||||
bpInTargetPctPriorWeek = windowsPriorWeek.last30Days?.pct ?? null;
|
||||
bpInTargetPctPriorWeek = windowsPriorWeek.last90Days?.pct ?? null;
|
||||
bpGradedScore = windows.gradedScore;
|
||||
bpGradedScorePriorWeek = windowsPriorWeek.gradedScore;
|
||||
// The Health-Score BP pillar reads the all-time window (see the
|
||||
// `bpInTargetPctForScore` derivation below). Capture the prior-week
|
||||
// run's all-time pct too so the week-over-week delta compares the
|
||||
// v1.17 W1d — the Health-Score BP pillar reads the same 90-day window
|
||||
// as the headline (see `bpInTargetPctForScore` below). Capture the
|
||||
// prior-week run's 90-day pct so the week-over-week delta compares the
|
||||
// same window on both ends.
|
||||
windowsPriorWeekAllTime = windowsPriorWeek.allTime?.pct ?? null;
|
||||
windowsPriorWeekForScore = windowsPriorWeek.last90Days?.pct ?? null;
|
||||
}
|
||||
|
||||
// Per-context glucose summaries (canonical mg/dL).
|
||||
@@ -396,9 +403,14 @@ async function buildAnalyticsResponse(user: AuthedUser) {
|
||||
// all-time rate, so route the all-time window through; fall back to the
|
||||
// 30-day window only when the all-time window is itself null (it never
|
||||
// is when 30-day has data), so a brand-new account is unaffected.
|
||||
const bpInTargetPctForScore = bpInTargetPctAllTime ?? bpInTargetPct;
|
||||
// v1.17 W1d — the Health-Score BP pillar reads the SAME 90-day window as
|
||||
// the BD-Zielbereich tile headline, so the dashboard ring and the
|
||||
// insights card grade the BP pillar off one number. Fall back to the
|
||||
// all-time rate only when the 90-day window is itself null so a
|
||||
// sparse-but-historical account still surfaces a BP pillar.
|
||||
const bpInTargetPctForScore = bpInTargetPct90d ?? bpInTargetPctAllTime;
|
||||
const bpInTargetPctPriorWeekForScore =
|
||||
windowsPriorWeekAllTime ?? bpInTargetPctPriorWeek;
|
||||
windowsPriorWeekForScore ?? bpInTargetPctPriorWeek;
|
||||
const healthScore = await computeUserHealthScoreFastPath({
|
||||
userId: user.id,
|
||||
bpInTargetPct: bpInTargetPctForScore,
|
||||
|
||||
+5
-1
@@ -1098,7 +1098,11 @@ export default function DashboardPage() {
|
||||
node: (
|
||||
<TrendCard
|
||||
key="bpInTarget"
|
||||
label={t("dashboard.bpInTargetShort")}
|
||||
// v1.17 W1d — the headline is the trailing-90-day in-target
|
||||
// rate; the label names the window so the tile never
|
||||
// narrates an unlabelled scope. The `30T:` caption below
|
||||
// still surfaces the 30-day figure for short-horizon context.
|
||||
label={t("dashboard.bpInTargetWindow90")}
|
||||
latest={data?.bpInTargetPct ?? null}
|
||||
unit="%"
|
||||
avg7={bp7}
|
||||
|
||||
@@ -301,6 +301,64 @@ describe("computeBpInTargetWindows — all-time headline (v1.4.19 A1)", () => {
|
||||
expect(result.allTime).toBeNull();
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.17 W1d — the BD-Zielbereich headline, the Health-Score BP pillar
|
||||
* and the coach grounding number standardise on a **90-day** window.
|
||||
* The helper must surface a `last90Days` window distinct from both the
|
||||
* 30-day and the all-time aggregate so the route can route the headline
|
||||
* through it while leaving all-time available for the detail page.
|
||||
*/
|
||||
it("returns a last90Days window distinct from 30-day and all-time", () => {
|
||||
// 10 pairs inside 30d (50 % in target), 10 pairs at 45-60 days old
|
||||
// (all OUT of target), 20 pairs older than 90 days (all OUT).
|
||||
const sys: Array<ReturnType<typeof reading>> = [];
|
||||
const dia: Array<ReturnType<typeof reading>> = [];
|
||||
// 0-30d: 5 IN, 5 OUT.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const at = new Date(
|
||||
NOW_19.getTime() - (i + 0.5) * 24 * 60 * 60 * 1000,
|
||||
).toISOString();
|
||||
const inTarget = i % 2 === 0;
|
||||
sys.push(reading(at, inTarget ? 122 : 145));
|
||||
dia.push(reading(at, inTarget ? 75 : 95));
|
||||
}
|
||||
// 45-60d (inside 90d, outside 30d): 10 OUT.
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const at = new Date(
|
||||
NOW_19.getTime() - (45 + i) * 24 * 60 * 60 * 1000,
|
||||
).toISOString();
|
||||
sys.push(reading(at, 160));
|
||||
dia.push(reading(at, 100));
|
||||
}
|
||||
// >90d: 20 OUT.
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const at = new Date(
|
||||
NOW_19.getTime() - (100 + i) * 24 * 60 * 60 * 1000,
|
||||
).toISOString();
|
||||
sys.push(reading(at, 160));
|
||||
dia.push(reading(at, 100));
|
||||
}
|
||||
|
||||
const result = computeBpInTargetWindows(sys, dia, TARGETS_UNDER_65, NOW_19);
|
||||
|
||||
// 30d = 5 / 10 = 50 %.
|
||||
expect(result.last30Days).toEqual({ pct: 50, pairs: 10 });
|
||||
// 90d = 5 / 20 = 25 % (10 in-target-eligible + 10 OUT from 45-60d).
|
||||
expect(result.last90Days).not.toBeNull();
|
||||
expect(result.last90Days!.pairs).toBe(20);
|
||||
expect(result.last90Days!.pct).toBe(Math.round((5 / 20) * 100));
|
||||
// all-time = 5 / 40 ≈ 13 %.
|
||||
expect(result.allTime!.pairs).toBe(40);
|
||||
// The three windows must be genuinely different numbers.
|
||||
expect(result.last90Days!.pct).not.toBe(result.last30Days!.pct);
|
||||
expect(result.last90Days!.pct).not.toBe(result.allTime!.pct);
|
||||
});
|
||||
|
||||
it("returns null last90Days when both series are empty", () => {
|
||||
const result = computeBpInTargetWindows([], [], TARGETS_UNDER_65, NOW_19);
|
||||
expect(result.last90Days).toBeNull();
|
||||
});
|
||||
|
||||
it("regression: the live tenant's 30-day fixture mirrors the live tenant's prod headline bug", () => {
|
||||
// Reuse the maintainer's production fixture (10 pairs, 5 in target = 50 %).
|
||||
// When the input is JUST those 10 pairs, allTime equals 30d == 50 %
|
||||
|
||||
@@ -76,6 +76,12 @@ import type { BpTargets } from "./bp-targets";
|
||||
export interface BpInTargetEnvelope {
|
||||
last7Days: { pct: number; pairs: number } | null;
|
||||
last30Days: { pct: number; pairs: number } | null;
|
||||
/**
|
||||
* v1.17 W1d — trailing-90-day window. Canonical window for the
|
||||
* BD-Zielbereich headline, the Health-Score BP pillar and the coach
|
||||
* grounding number. See `computeBpInTargetWindows` for the rationale.
|
||||
*/
|
||||
last90Days: { pct: number; pairs: number } | null;
|
||||
allTime: { pct: number; pairs: number } | null;
|
||||
priorMonth: { pct: number; pairs: number } | null;
|
||||
priorYear: { pct: number; pairs: number } | null;
|
||||
@@ -274,12 +280,15 @@ async function computeFromRollups(
|
||||
|
||||
const sevenDaysAgo = new Date(now.getTime() - 7 * DAY_MS);
|
||||
const thirtyDaysAgo = new Date(now.getTime() - 30 * DAY_MS);
|
||||
const ninetyDaysAgo = new Date(now.getTime() - 90 * DAY_MS);
|
||||
const sixtyDaysAgo = new Date(now.getTime() - 60 * DAY_MS);
|
||||
const threeSixtyFiveDaysAgo = new Date(now.getTime() - 365 * DAY_MS);
|
||||
const threeNinetyFiveDaysAgo = new Date(now.getTime() - 395 * DAY_MS);
|
||||
|
||||
const last7 = bucketWindow(pairsByDay, sevenDaysAgo, now, targets);
|
||||
const last30 = bucketWindow(pairsByDay, thirtyDaysAgo, now, targets);
|
||||
// v1.17 W1d — canonical headline / score / coach window.
|
||||
const last90 = bucketWindow(pairsByDay, ninetyDaysAgo, now, targets);
|
||||
// All-time on the rollup path is bounded to the 395-day read window.
|
||||
// Documented above — same trade-off the per-event path makes when the
|
||||
// user's history extends beyond 365 days.
|
||||
@@ -339,6 +348,7 @@ async function computeFromRollups(
|
||||
return {
|
||||
last7Days: last7,
|
||||
last30Days: last30,
|
||||
last90Days: last90,
|
||||
allTime,
|
||||
priorMonth,
|
||||
priorYear,
|
||||
@@ -453,6 +463,7 @@ async function computeFromLive(
|
||||
return {
|
||||
last7Days: windows.last7Days,
|
||||
last30Days: windows.last30Days,
|
||||
last90Days: windows.last90Days,
|
||||
allTime: windows.allTime,
|
||||
priorMonth: windows.priorMonth,
|
||||
priorYear: windows.priorYear,
|
||||
|
||||
@@ -237,6 +237,14 @@ export function computeBpInTargetWindows(
|
||||
): {
|
||||
last7Days: { pct: number; pairs: number } | null;
|
||||
last30Days: { pct: number; pairs: number } | null;
|
||||
/**
|
||||
* v1.17 W1d — the trailing-90-day window. This is the canonical window
|
||||
* the BD-Zielbereich headline, the Health-Score BP pillar and the coach
|
||||
* grounding number all standardise on (a 90-day arc balances recency
|
||||
* against enough pairs to be stable). The all-time aggregate stays
|
||||
* available below but is reserved for the BP detail page's long view.
|
||||
*/
|
||||
last90Days: { pct: number; pairs: number } | null;
|
||||
allTime: { pct: number; pairs: number } | null;
|
||||
/**
|
||||
* v1.4.22 W5 reconcile (Code-H2) — period-aligned 30-day window
|
||||
@@ -254,6 +262,7 @@ export function computeBpInTargetWindows(
|
||||
} {
|
||||
const sevenDaysAgoMs = now.getTime() - 7 * DAY_MS;
|
||||
const thirtyDaysAgoMs = now.getTime() - 30 * DAY_MS;
|
||||
const ninetyDaysAgoMs = now.getTime() - 90 * DAY_MS;
|
||||
const sixtyDaysAgoMs = now.getTime() - 60 * DAY_MS;
|
||||
const oneYearMinus30DaysAgoMs = now.getTime() - 365 * DAY_MS;
|
||||
const oneYearAgoMs = now.getTime() - 395 * DAY_MS;
|
||||
@@ -270,6 +279,12 @@ export function computeBpInTargetWindows(
|
||||
const diaLast30 = diaSeries.filter(
|
||||
(r) => r.measuredAt.getTime() >= thirtyDaysAgoMs,
|
||||
);
|
||||
const sysLast90 = sysSeries.filter(
|
||||
(r) => r.measuredAt.getTime() >= ninetyDaysAgoMs,
|
||||
);
|
||||
const diaLast90 = diaSeries.filter(
|
||||
(r) => r.measuredAt.getTime() >= ninetyDaysAgoMs,
|
||||
);
|
||||
|
||||
// Period-aligned prior windows. Same 30-day arc, shifted by 30 / 365
|
||||
// days respectively. Bounds are inclusive on the lower end and
|
||||
@@ -299,6 +314,8 @@ export function computeBpInTargetWindows(
|
||||
return {
|
||||
last7Days: computeBpInTargetPct(sysLast7, diaLast7, targets),
|
||||
last30Days: computeBpInTargetPct(sysLast30, diaLast30, targets),
|
||||
// v1.17 W1d — canonical headline / score / coach window.
|
||||
last90Days: computeBpInTargetPct(sysLast90, diaLast90, targets),
|
||||
// v1.4.19 A1 — independent aggregate over EVERY paired reading. The
|
||||
// analytics route now routes the dashboard tile's headline through
|
||||
// this so it stops mirroring `last30Days`.
|
||||
|
||||
@@ -142,6 +142,7 @@ describe("buildDashboardSnapshot — envelope shape", () => {
|
||||
computeBpInTargetFastPath.mockResolvedValue({
|
||||
last7Days: { pct: 70 },
|
||||
last30Days: { pct: 80 },
|
||||
last90Days: { pct: 80 },
|
||||
allTime: { pct: 75 },
|
||||
priorMonth: { pct: 60 },
|
||||
priorYear: { pct: 50 },
|
||||
@@ -182,6 +183,7 @@ describe("buildDashboardSnapshot — envelope shape", () => {
|
||||
computeBpInTargetFastPath.mockResolvedValue({
|
||||
last7Days: { pct: 70 },
|
||||
last30Days: { pct: 80 },
|
||||
last90Days: { pct: 80 },
|
||||
allTime: { pct: 75 },
|
||||
priorMonth: { pct: 60 },
|
||||
priorYear: { pct: 50 },
|
||||
@@ -238,6 +240,7 @@ describe("buildDashboardSnapshot — per-type thick-phase gate", () => {
|
||||
computeBpInTargetFastPath.mockResolvedValue({
|
||||
last7Days: { pct: 70 },
|
||||
last30Days: { pct: 80 },
|
||||
last90Days: { pct: 80 },
|
||||
allTime: { pct: 75 },
|
||||
priorMonth: { pct: 60 },
|
||||
priorYear: { pct: 50 },
|
||||
@@ -279,6 +282,7 @@ describe("buildDashboardSnapshot — per-type thick-phase gate", () => {
|
||||
computeBpInTargetFastPath.mockResolvedValue({
|
||||
last7Days: null,
|
||||
last30Days: null,
|
||||
last90Days: null,
|
||||
allTime: null,
|
||||
priorMonth: null,
|
||||
priorYear: null,
|
||||
@@ -364,6 +368,8 @@ describe("buildDashboardSnapshot — healthScore (warm phase only)", () => {
|
||||
computeBpInTargetFastPath.mockResolvedValue({
|
||||
last7Days: { pct: 70 },
|
||||
last30Days: { pct: 80 },
|
||||
// v1.17 W1d — the BP pillar reads this 90-day window, NOT last30Days.
|
||||
last90Days: { pct: 77 },
|
||||
allTime: { pct: 75 },
|
||||
priorMonth: { pct: 60 },
|
||||
priorYear: { pct: 50 },
|
||||
@@ -392,7 +398,10 @@ describe("buildDashboardSnapshot — healthScore (warm phase only)", () => {
|
||||
coverage: unknown;
|
||||
};
|
||||
expect(arg.userId).toBe("user-1");
|
||||
expect(arg.bpInTargetPct).toBe(80);
|
||||
// v1.17 W1d — the BP pillar reads the 90-day window, identical to the
|
||||
// analytics route, so dashboard ring and insights card score off one
|
||||
// number. Previously this was last30Days (80).
|
||||
expect(arg.bpInTargetPct).toBe(77);
|
||||
expect(arg.bpGradedScore).toBe(88);
|
||||
expect(arg.heightCm).toBe(180);
|
||||
expect(arg.coverage).toBe(coverageMap);
|
||||
@@ -404,6 +413,7 @@ describe("buildDashboardSnapshot — healthScore (warm phase only)", () => {
|
||||
computeBpInTargetFastPath.mockResolvedValue({
|
||||
last7Days: null,
|
||||
last30Days: null,
|
||||
last90Days: null,
|
||||
allTime: null,
|
||||
priorMonth: null,
|
||||
priorYear: null,
|
||||
@@ -705,6 +715,7 @@ describe("buildDashboardSnapshot — additive proof", () => {
|
||||
computeBpInTargetFastPath.mockResolvedValue({
|
||||
last7Days: { pct: 70 },
|
||||
last30Days: { pct: 80 },
|
||||
last90Days: { pct: 80 },
|
||||
allTime: { pct: 75 },
|
||||
priorMonth: { pct: 60 },
|
||||
priorYear: { pct: 50 },
|
||||
|
||||
@@ -440,6 +440,8 @@ async function buildExtras(
|
||||
let bpInTargetPct: number | null = null;
|
||||
let bpInTargetPct7d: number | null = null;
|
||||
let bpInTargetPct30d: number | null = null;
|
||||
// v1.17 W1d — canonical 90-day headline / score window.
|
||||
let bpInTargetPct90d: number | null = null;
|
||||
let bpInTargetPctAllTime: number | null = null;
|
||||
let bpInTargetPctPriorMonth: number | null = null;
|
||||
let bpInTargetPctPriorYear: number | null = null;
|
||||
@@ -454,9 +456,15 @@ async function buildExtras(
|
||||
coverage,
|
||||
userTz,
|
||||
});
|
||||
bpInTargetPct = windows.last30Days?.pct ?? null;
|
||||
// v1.17 W1d — the headline standardises on the trailing-90-day
|
||||
// window (labelled "· 90 T" in the tile), identical to the analytics
|
||||
// route, so the dashboard tile and the insights surface never narrate
|
||||
// two windows for the same metric. All-time stays carried for the
|
||||
// detail page's long view only.
|
||||
bpInTargetPct = windows.last90Days?.pct ?? null;
|
||||
bpInTargetPct7d = windows.last7Days?.pct ?? null;
|
||||
bpInTargetPct30d = windows.last30Days?.pct ?? null;
|
||||
bpInTargetPct90d = windows.last90Days?.pct ?? null;
|
||||
bpInTargetPctAllTime = windows.allTime?.pct ?? null;
|
||||
bpInTargetPctPriorMonth = windows.priorMonth?.pct ?? null;
|
||||
bpInTargetPctPriorYear = windows.priorYear?.pct ?? null;
|
||||
@@ -466,10 +474,17 @@ async function buildExtras(
|
||||
// Health score — reuses the BP windows captured above plus the
|
||||
// already-probed coverage map (no second probe). Score + band +
|
||||
// delta only; the component breakdown stays off this wire.
|
||||
//
|
||||
// v1.17 W1b/W1d — the BP pillar reads the SAME 90-day window as the
|
||||
// analytics route, so the dashboard ring and the insights card compute
|
||||
// the score from identical inputs (closing the dashboard-vs-insights
|
||||
// score divergence). Fall back to all-time only when the 90-day window
|
||||
// is null so a sparse-but-historical account keeps its BP pillar.
|
||||
const bpInTargetPctForScore = bpInTargetPct90d ?? bpInTargetPctAllTime;
|
||||
const scoreResult = await time("healthScore", () =>
|
||||
computeUserHealthScoreFastPath({
|
||||
userId: user.id,
|
||||
bpInTargetPct,
|
||||
bpInTargetPct: bpInTargetPctForScore,
|
||||
bpGradedScore,
|
||||
heightCm: user.heightCm,
|
||||
now,
|
||||
|
||||
Reference in new issue
Block a user