fix(targets): show the weight target the user set, not the WHO band

The targets reference panel graded weight against the WHO BMI band even
for an account that had entered its own target, so the panel under the
weight chart contradicted the chart above it. An override now wins and
the card's source reads Custom, the same posture the glucose builder has
always taken with a user-pinned band. The BMI classification stays
WHO-derived: naming which category a reading falls in is a reference
fact, not a goal.
This commit is contained in:
Marc-André Bombeck committed 2026-07-27 23:27:14 +02:00
1 parent be348f8820
commit a7fcd6d249
3 files changed
+59 -3

No files matched your search

@@ -20,6 +20,7 @@ describe("target section builders", () => {
gender: null,
timezone: TZ,
now: NOW,
weightTargetOverride: null,
});
expect(result.targets.map((target) => target.type)).toEqual([
@@ -163,3 +164,39 @@ describe("target section builders", () => {
});
});
});
/**
* v1.34 — the weight card grades against the user's own target when one is set,
* so the `/targets` reference panel and the weight chart above it name the same
* band. The panel used to show the WHO BMI band unconditionally.
*/
describe("buildVitalTargets — user weight target", () => {
const base = {
recentMeasurements: [],
latestByType: {},
average30ByType: {},
heightCm: 178,
age: 40,
gender: null,
timezone: TZ,
now: NOW,
};
it("shows the WHO BMI band when no target is set", () => {
const weight = buildVitalTargets({
...base,
weightTargetOverride: null,
}).targets.find((t) => t.type === "WEIGHT");
expect(weight?.source).toBe("WHO BMI");
expect(weight?.range).not.toEqual({ min: 74, max: 78 });
});
it("shows the user's own band, labelled as theirs, when one is set", () => {
const weight = buildVitalTargets({
...base,
weightTargetOverride: { min: 74, max: 78 },
}).targets.find((t) => t.type === "WEIGHT");
expect(weight?.range).toEqual({ min: 74, max: 78 });
expect(weight?.source).toBe("Custom");
});
});
+8 -1
View File
@@ -16,7 +16,10 @@ import { SCHEDULE_COMPLIANCE_SELECT } from "@/lib/analytics/compliance";
import { DEFAULT_TIMEZONE } from "@/lib/tz/resolver";
import pLimit from "p-limit";
import type { MeasurementType } from "@/generated/prisma/client";
import type { ThresholdOverridesJson } from "@/lib/analytics/effective-range";
import {
resolveWeightTargetOverride,
type ThresholdOverridesJson,
} from "@/lib/analytics/effective-range";
import { resolveGlucoseUnit } from "@/lib/glucose";
import { toProfileSex } from "@/lib/profile/sex";
import {
@@ -264,6 +267,10 @@ export async function buildTargetsResponse(user: AuthedUser) {
gender,
timezone,
now,
// v1.34 — the weight card grades against the user's OWN target when they
// set one, so the reference panel and the weight chart above it name the
// same band.
weightTargetOverride: resolveWeightTargetOverride(dbUser?.thresholdsJson),
});
const sleepTarget = buildSleepTarget({
sleepStageRows,
+14 -2
View File
@@ -47,6 +47,13 @@ interface VitalTargetsInput {
gender: ProfileSex;
timezone: string;
now: Date;
/**
* v1.34 — the user's own weight band from `User.thresholdsJson`, in kg.
* When present it replaces the WHO BMI band on the weight card and the
* card's source reads "Custom", matching how the glucose builder has
* always treated a user override. `null` keeps the height-derived band.
*/
weightTargetOverride: { min: number; max: number } | null;
}
export interface VitalTargetsResult {
@@ -140,6 +147,7 @@ export function buildVitalTargets({
gender,
timezone,
now,
weightTargetOverride,
}: VitalTargetsInput): VitalTargetsResult {
const targets: TargetItem[] = [];
const consistencyClock = { timezone, now };
@@ -148,7 +156,11 @@ export function buildVitalTargets({
// answers a null with a neutral value, never with one of the two sides.
const referenceSex = binaryReferenceSex(gender);
const weightRange = heightCm ? getWeightRange(heightCm) : null;
// v1.34 — a user-set target outranks the WHO BMI band. The BMI
// classification below stays WHO-derived either way: it names which BMI
// category a reading falls in, which is a reference fact and not a goal.
const weightRange =
weightTargetOverride ?? (heightCm ? getWeightRange(heightCm) : null);
let weightClassification: TargetItem["classification"] = null;
if (latestByType.WEIGHT != null && heightCm) {
const heightM = heightCm / 100;
@@ -169,7 +181,7 @@ export function buildVitalTargets({
unit: "kg",
range: weightRange,
classification: weightClassification,
source: "WHO BMI",
source: weightTargetOverride ? "Custom" : "WHO BMI",
...rollupConsistency({
events: recentMeasurements.filter(
(measurement) => measurement.type === "WEIGHT",