feat(glucose): resolve tighter ADA goal targets when diabetes is on

Add a diabetes-aware glucose target resolver: when the user has opted in, the
fasting/post-prandial targets switch to the tighter ADA glycemic GOAL bands
(fasting 80–130, post-meal under 180 mg/dL) instead of the general
non-diabetic normal bands. A user threshold override always wins over both, so
a clinician-pinned individualised goal is never replaced by the population
band. The targets surface now reads through the resolver and labels the source
accordingly. The general band display is untouched.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 20:18:37 +02:00
1 parent 2c87a93827
commit cf620de315
3 files changed
+275 -16

No files matched your search

@@ -0,0 +1,98 @@
/**
* v1.18.6 — diabetes-aware glucose target resolver.
*
* Pins: flag OFF → general non-diabetic band; flag ON → tighter ADA goal band;
* a user override always wins over BOTH; the flag is consumed verbatim (never
* derived from a value).
*/
import { describe, expect, it } from "vitest";
import {
resolveGlucoseTarget,
DIABETIC_GOAL_BANDS,
} from "../glucose-targets";
import type { UserProfileForRange } from "@/lib/analytics/effective-range";
const profile: UserProfileForRange = {
heightCm: 178,
dateOfBirth: "1985-01-01",
gender: "MALE",
};
describe("resolveGlucoseTarget", () => {
it("uses the general non-diabetic band when the flag is OFF", () => {
const r = resolveGlucoseTarget({
context: "FASTING",
hasDiabetes: false,
profile,
overrides: null,
});
expect(r.source).toBe("default");
expect(r.isOverride).toBe(false);
// General fasting normal band is 70–99 (non-diabetic).
expect(r.range?.greenMin).toBe(70);
expect(r.range?.greenMax).toBe(99);
});
it("uses the tighter ADA goal band when the flag is ON (fasting 80-130)", () => {
const r = resolveGlucoseTarget({
context: "FASTING",
hasDiabetes: true,
profile,
overrides: null,
});
expect(r.source).toBe("ADA goal (diabetes)");
expect(r.range?.greenMin).toBe(DIABETIC_GOAL_BANDS.FASTING.min);
expect(r.range?.greenMax).toBe(DIABETIC_GOAL_BANDS.FASTING.max);
expect(r.range?.greenMin).toBe(80);
expect(r.range?.greenMax).toBe(130);
});
it("uses the ADA postprandial goal band (< 180) when the flag is ON", () => {
const r = resolveGlucoseTarget({
context: "POSTPRANDIAL",
hasDiabetes: true,
profile,
overrides: null,
});
expect(r.range?.greenMin).toBe(80);
expect(r.range?.greenMax).toBe(180);
});
it("lets a user override win over the diabetic goal band", () => {
const r = resolveGlucoseTarget({
context: "FASTING",
hasDiabetes: true,
profile,
overrides: { BLOOD_GLUCOSE_FASTING: { min: 90, max: 120 } },
});
expect(r.source).toBe("custom");
expect(r.isOverride).toBe(true);
expect(r.range?.greenMin).toBe(90);
expect(r.range?.greenMax).toBe(120);
});
it("lets a user override win over the general band (flag OFF)", () => {
const r = resolveGlucoseTarget({
context: "FASTING",
hasDiabetes: false,
profile,
overrides: { BLOOD_GLUCOSE_FASTING: { min: 75, max: 105 } },
});
expect(r.source).toBe("custom");
expect(r.range?.greenMin).toBe(75);
expect(r.range?.greenMax).toBe(105);
});
it("resolves the diabetic goal band identically for every context", () => {
for (const ctx of ["FASTING", "POSTPRANDIAL", "RANDOM", "BEDTIME"] as const) {
const r = resolveGlucoseTarget({
context: ctx,
hasDiabetes: true,
profile,
overrides: null,
});
expect(r.range?.greenMin).toBe(DIABETIC_GOAL_BANDS[ctx].min);
expect(r.range?.greenMax).toBe(DIABETIC_GOAL_BANDS[ctx].max);
}
});
});
+30 -16
View File
@@ -46,12 +46,10 @@ import type {
MeasurementType,
GlucoseContext,
} from "@/generated/prisma/client";
import {
getEffectiveRange,
type ThresholdOverridesJson,
} from "@/lib/analytics/effective-range";
import type { ThresholdOverridesJson } from "@/lib/analytics/effective-range";
import { getBodyFatTargetRange } from "@/lib/analytics/value-bands";
import { thresholdMetricForContext, resolveGlucoseUnit } from "@/lib/glucose";
import { resolveGlucoseUnit } from "@/lib/glucose";
import { resolveGlucoseTarget } from "@/lib/targets/glucose-targets";
import {
ensureUserMoodRollupsFresh,
readMoodDayRollups,
@@ -219,6 +217,9 @@ export async function buildTargetsResponse(user: AuthedUser) {
dateOfBirth: true,
gender: true,
glucoseUnit: true,
// v1.18.6 — diabetes opt-in selects the tighter ADA glycemic goal
// band for glucose targets (resolved below via resolveGlucoseTarget).
hasDiabetes: true,
thresholdsJson: true,
// v1.11.5 — needed to collapse a dual-source sleep night to one
// canonical source before reconstructing per-night asleep totals.
@@ -1456,20 +1457,28 @@ export async function buildTargetsResponse(user: AuthedUser) {
) / 10
: null;
const metric = thresholdMetricForContext(ctx);
const eff = getEffectiveRange(metric, profileForRange, overrides);
const range = eff.range
? { min: eff.range.greenMin, max: eff.range.greenMax }
// v1.18.6 — diabetes-aware target band. When the user has declared
// diabetes (and has NOT pinned a custom override) the tighter ADA glycemic
// GOAL band wins; otherwise the general non-diabetic band (or the override)
// is used exactly as before. The flag is never inferred from a value.
const resolved = resolveGlucoseTarget({
context: ctx,
hasDiabetes: dbUser?.hasDiabetes ?? false,
profile: profileForRange,
overrides,
});
const effRange = resolved.range;
const range = effRange
? { min: effRange.greenMin, max: effRange.greenMax }
: null;
let classification: { category: string; color: string } | null = null;
if (range) {
if (range && effRange) {
if (latest >= range.min && latest <= range.max) {
classification = { category: "Optimal", color: "#50fa7b" };
} else if (
eff.range &&
latest >= eff.range.orangeMin &&
latest <= eff.range.orangeMax
latest >= effRange.orangeMin &&
latest <= effRange.orangeMax
) {
classification = { category: "Elevated", color: "#f1fa8c" };
} else {
@@ -1481,8 +1490,8 @@ export async function buildTargetsResponse(user: AuthedUser) {
recent.map((r) => ({ measuredAt: r.measuredAt, value: r.value })),
makeRangeClassifier(
range,
eff.range
? { orangeMin: eff.range.orangeMin, orangeMax: eff.range.orangeMax }
effRange
? { orangeMin: effRange.orangeMin, orangeMax: effRange.orangeMax }
: undefined,
),
);
@@ -1495,7 +1504,12 @@ export async function buildTargetsResponse(user: AuthedUser) {
unit: glucoseUnit,
range,
classification,
source: eff.isOverride ? "Custom" : "ADA 2024 / DDG",
source:
resolved.source === "custom"
? "Custom"
: resolved.source === "ADA goal (diabetes)"
? "ADA goal (diabetes)"
: "ADA 2024 / DDG",
...consistency,
});
}
+147
View File
@@ -0,0 +1,147 @@
/**
* Diabetes-aware blood-glucose TARGET resolver (v1.18.6).
*
* One question: "what glucose band should THIS user be judged against on this
* context, right now?" The answer depends on a single explicit, user-declared
* preference — `User.hasDiabetes` — and NEVER on any reading. The flag is set
* through Settings (`PATCH /api/auth/me/diabetes`); it is never inferred from
* a value and never asserts a diagnosis.
*
* - Flag OFF (default): the general non-diabetic normal bands — the existing
* behaviour, resolved by the shared reference-range resolver
* (`getEffectiveRange`, which owns the band display) so a user-override
* still wins and nothing about the calm-state UI changes.
* - Flag ON: the tighter ADA glycemic GOAL bands for people living with
* diabetes — fasting / pre-prandial 80–130 mg/dL, peak post-prandial
* < 180 mg/dL (ADA Standards of Care §6, stable 2024–2026). A user
* override still wins (a clinician may pin an individualised goal), but
* absent an override the diabetic goal band — not the normal band — is the
* target.
*
* The diabetic goal targets are GOALS, not screening thresholds: a fasting 80
* is "in goal" for someone managing diabetes, where the non-diabetic 70–99
* normal band would call the same reading low-normal. This is exactly why we
* keep the two resolvers distinct rather than widening the general band.
*
* Pure given its inputs. The route resolves the flag + overrides and passes
* them in. mg/dL throughout (HealthLog canonical store unit); display-time
* conversion to mmol/L is the caller's job (`src/lib/glucose.ts`).
*
* Citation: American Diabetes Association, Standards of Care in Diabetes
* §6 "Glycemic Goals and Hypoglycemia", Diabetes Care 2024 47(Suppl. 1):S111.
* Pre-prandial plasma glucose goal 80–130 mg/dL; peak post-prandial < 180
* mg/dL. https://diabetesjournals.org/care/article/47/Supplement_1/S111/153957
*/
import {
getEffectiveRange,
type ThresholdMetric,
type ThresholdOverridesJson,
type UserProfileForRange,
} from "@/lib/analytics/effective-range";
import type { TrafficRange } from "@/lib/analytics/value-bands";
import { thresholdMetricForContext } from "@/lib/glucose";
export type GlucoseContextKey =
| "FASTING"
| "POSTPRANDIAL"
| "RANDOM"
| "BEDTIME";
/**
* ADA glycemic GOAL bands for people living with diabetes (mg/dL).
*
* FASTING / pre-prandial: 80–130 (ADA pre-prandial plasma glucose goal).
* POSTPRANDIAL: floor 80 (avoid hypoglycemia), ceiling 180 (ADA peak
* post-prandial goal `< 180`; the inclusive 180 is the band edge).
* RANDOM: a random spot reading is not a defined ADA goal context; the
* post-prandial goal is the most clinically defensible comparator (a random
* reading is most often post-meal). Use the same 80–180 band.
* BEDTIME: ADA publishes no separate bedtime goal for adults; the ISPAD
* pediatric bedtime band (90–150) is the closest comparator and the slightly
* higher floor guards against nocturnal hypoglycemia. Kept identical to the
* non-diabetic bedtime band on purpose — the diabetic delta lives in the
* fasting/post-prandial goals, where ADA is explicit.
*/
const DIABETIC_GOAL_BANDS: Record<
GlucoseContextKey,
{ min: number; max: number }
> = {
FASTING: { min: 80, max: 130 },
POSTPRANDIAL: { min: 80, max: 180 },
RANDOM: { min: 80, max: 180 },
BEDTIME: { min: 90, max: 150 },
};
/** Source label surfaced on the resolved range for the targets surface. */
export type GlucoseTargetSource = "ADA goal (diabetes)" | "default" | "custom";
export interface ResolvedGlucoseTarget {
/** The green band the reading is judged against (mg/dL). */
range: TrafficRange | null;
/** True when a user threshold override is in play (override always wins). */
isOverride: boolean;
/**
* Which band family produced the range: the diabetic ADA goal, the general
* default, or a user custom override. Drives the targets-card source label.
*/
source: GlucoseTargetSource;
}
/**
* Build the diabetic-goal `TrafficRange`. The orange wings mirror the general
* resolver's glucose convention (floor − 10 below, a modest ceiling above) so
* a reading just outside goal reads "elevated", not "high".
*/
function diabeticGoalRange(ctx: GlucoseContextKey): TrafficRange {
const band = DIABETIC_GOAL_BANDS[ctx];
return {
greenMin: band.min,
greenMax: band.max,
orangeMin: band.min - 10,
// Above the goal ceiling stays "elevated" up to a modest margin before
// the red "high" band; matches the general resolver's wing width posture.
orangeMax: band.max + 30,
};
}
/**
* Resolve the glucose target band for one context.
*
* `hasDiabetes` is the user's explicit, declared preference. When `true` AND
* the user has NOT overridden the metric, the ADA diabetic goal band wins.
* Otherwise we defer entirely to the shared reference-range resolver so the
* general non-diabetic band (and any user override) behaves exactly as before.
*
* The flag is NEVER derived from a reading and asserts no diagnosis — it only
* selects the band the user is measured against.
*/
export function resolveGlucoseTarget(input: {
context: GlucoseContextKey;
hasDiabetes: boolean;
profile: UserProfileForRange;
overrides: ThresholdOverridesJson | null | undefined;
}): ResolvedGlucoseTarget {
const { context, hasDiabetes, profile, overrides } = input;
const metric = thresholdMetricForContext(context) as ThresholdMetric;
const eff = getEffectiveRange(metric, profile, overrides);
// A user override always wins — a clinician-pinned individualised goal must
// not be silently replaced by the population ADA goal band.
if (eff.isOverride) {
return { range: eff.range, isOverride: true, source: "custom" };
}
if (hasDiabetes) {
return {
range: diabeticGoalRange(context),
isOverride: false,
source: "ADA goal (diabetes)",
};
}
return { range: eff.range, isOverride: false, source: "default" };
}
/** The ADA diabetic goal bands, exported for tests + documentation. */
export { DIABETIC_GOAL_BANDS };