diff --git a/src/components/insights/metric-target-summary.tsx b/src/components/insights/metric-target-summary.tsx
index cfdaa0be4..845b1b8d6 100644
--- a/src/components/insights/metric-target-summary.tsx
+++ b/src/components/insights/metric-target-summary.tsx
@@ -8,7 +8,6 @@ import { useAuth } from "@/hooks/use-auth";
import { useUnitDisplay } from "@/hooks/use-unit-display";
import { queryKeys } from "@/lib/query-keys";
import { useTranslations } from "@/lib/i18n/context";
-import { convertGlucose, resolveGlucoseUnit } from "@/lib/glucose";
import { resolveTargetUnitAdapter } from "@/lib/targets/target-unit-display";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { RangeBar } from "@/components/targets/range-bar";
@@ -18,6 +17,7 @@ import { TileHeader } from "@/components/insights/tile-header";
import { useTargetAdjust } from "@/lib/insights/target-adjust-context";
import { getTargetSourceLink } from "@/lib/targets/source-link";
import { apiGet } from "@/lib/api/api-fetch";
+import { resolveGlucoseUnit, type GlucoseUnit } from "@/lib/glucose";
/**
* v1.8.0 → v1.8.5 — surface the per-metric target reference panel on each
@@ -152,28 +152,17 @@ export function MetricTargetSummary({ slug }: MetricTargetSummaryProps) {
// mg/dL values to the user's display unit. The route's per-context
// labels are i18n keys, so resolve them.
if (isGlucose) {
- const displayUnit = resolveGlucoseUnit(data.profile?.glucoseUnit ?? null);
- const convert = (v: number | null) =>
- v == null ? null : convertGlucose(v, displayUnit);
+ // The payload stays canonical mg/dL here; each panel converts through
+ // the shared target adapter, which owns the glucose unit as it owns
+ // pounds, so the panel and its edit sheet cannot convert twice.
+ const glucoseUnit = resolveGlucoseUnit(data.profile?.glucoseUnit ?? null);
const panels = GLUCOSE_TARGET_TYPES.map((type) =>
data.targets.find((entry) => entry.type === type),
)
.filter(
(entry): entry is TargetItem => entry != null && entry.range != null,
)
- .map((entry) => ({
- ...entry,
- label: t(entry.label),
- unit: displayUnit,
- current: convert(entry.current),
- average30: convert(entry.average30),
- range: entry.range
- ? {
- min: convertGlucose(entry.range.min, displayUnit),
- max: convertGlucose(entry.range.max, displayUnit),
- }
- : null,
- }));
+ .map((entry) => ({ ...entry, label: t(entry.label) }));
if (panels.length === 0) return null;
@@ -188,6 +177,7 @@ export function MetricTargetSummary({ slug }: MetricTargetSummaryProps) {
key={panel.type}
target={panel}
heading={panel.label}
+ glucoseUnit={glucoseUnit}
/>
))}
@@ -212,6 +202,12 @@ interface TargetReferencePanelProps {
bpDiastolic?: TargetsResponse["bpDiastolic"];
/** Optional sub-heading shown above the panel (used for glucose contexts). */
heading?: string;
+ /**
+ * The record's glucose unit as the targets payload resolved it. The
+ * glucose panels pass it so the band reads in the unit the server
+ * resolved for this record; every other panel leaves it to the hook.
+ */
+ glucoseUnit?: GlucoseUnit;
}
/**
@@ -232,21 +228,26 @@ function TargetReferencePanel({
target,
bpDiastolic,
heading,
+ glucoseUnit: payloadGlucoseUnit,
}: TargetReferencePanelProps) {
const { t } = useTranslations();
const adjust = useTargetAdjust();
- const { preference } = useUnitDisplay();
+ const { preference, glucoseUnit: accountGlucoseUnit } = useUnitDisplay();
+ const glucoseUnit = payloadGlucoseUnit ?? accountGlucoseUnit;
// v1.32.27 — the panel renders in the user's preferred unit. The
// `/api/insights/targets` payload is canonical (kg for weight); the
// adapter converts it once, here, and everything downstream — the
// range bar, the status pill, the 30-day average, and the edit
- // sheet's seed — consumes the converted pair. Glucose contexts are
- // pre-converted by the caller above and carry no display transform,
- // so they take the adapter's identity path and are never scaled
- // twice. For a metric account the adapter is the exact identity, so
- // this panel is byte-identical to the previous release.
- const units = resolveTargetUnitAdapter(target.type, target.unit, preference);
+ // sheet's seed — consumes the converted pair. The glucose contexts
+ // arrive canonical too and convert here to the reader's glucose unit.
+ // On the default preferences the adapter is the exact identity.
+ const units = resolveTargetUnitAdapter(
+ target.type,
+ target.unit,
+ preference,
+ glucoseUnit,
+ );
const unit = units.unit;
const range = target.range
? {
diff --git a/src/components/settings/__tests__/thresholds-editor-glucose-unit.test.tsx b/src/components/settings/__tests__/thresholds-editor-glucose-unit.test.tsx
new file mode 100644
index 000000000..362d029b6
--- /dev/null
+++ b/src/components/settings/__tests__/thresholds-editor-glucose-unit.test.tsx
@@ -0,0 +1,76 @@
+/**
+ * The settings threshold editor reads glucose in the reader's unit.
+ *
+ * It used to pass every glucose threshold through the identity path, so an
+ * mmol/L account was shown "Default: 70–99 mg/dL" and mg/dL input fields
+ * bounded at 40–400, while the insights sheet for the same threshold already
+ * spoke mmol/L. The stored value stays canonical mg/dL either way.
+ */
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+const authUser = vi.hoisted(() => ({
+ current: { glucoseUnit: "mmol/L", unitPreference: "metric" } as Record<
+ string,
+ unknown
+ >,
+}));
+
+vi.mock("@/hooks/use-auth", () => ({
+ useAuth: () => ({ user: authUser.current, isAuthenticated: true }),
+ useAccountOnceMounted: () => authUser.current,
+}));
+
+vi.mock("@tanstack/react-query", () => ({
+ useQuery: () => ({
+ isLoading: false,
+ data: {
+ effective: {
+ BLOOD_GLUCOSE_FASTING: {
+ range: { greenMin: 72, greenMax: 108 },
+ isOverride: true,
+ default: { greenMin: 70, greenMax: 99 },
+ bounds: { min: 40, max: 400, unit: "mg/dL" },
+ },
+ },
+ overrides: { BLOOD_GLUCOSE_FASTING: { min: 72, max: 108 } },
+ },
+ }),
+ useMutation: () => ({ mutate: vi.fn(), isPending: false }),
+ useQueryClient: () => ({ invalidateQueries: vi.fn() }),
+}));
+
+import { I18nProvider } from "@/lib/i18n/context";
+import { ThresholdsEditorSection } from "../thresholds-editor-section";
+
+function render(): string {
+ return renderToStaticMarkup(
+
+
+ ,
+ );
+}
+
+describe(" glucose unit", () => {
+ it("states the default, the fields and the bounds in mmol/L", () => {
+ authUser.current = { glucoseUnit: "mmol/L", unitPreference: "metric" };
+ const html = render();
+ // 70–99 mg/dL is 3.9–5.5 mmol/L.
+ expect(html).toMatch(/3\.9–5\.5 mmol\/L/);
+ expect(html).not.toMatch(/70(\.0)?–99(\.0)? mg\/dL/);
+ // The stored 72–108 mg/dL override seeds 4–6 mmol/L.
+ expect(html).toContain('value="4"');
+ expect(html).toContain('value="6"');
+ // Inward-rounded guardrail of 40–400 mg/dL.
+ expect(html).toContain('min="2.3"');
+ expect(html).toContain('max="22.1"');
+ });
+
+ it("keeps mg/dL for an mg/dL reader", () => {
+ authUser.current = { glucoseUnit: "mg/dL", unitPreference: "metric" };
+ const html = render();
+ expect(html).toMatch(/70(\.0)?–99(\.0)? mg\/dL/);
+ expect(html).toContain('value="72"');
+ expect(html).toContain('min="40"');
+ });
+});
diff --git a/src/components/settings/thresholds-editor-section.tsx b/src/components/settings/thresholds-editor-section.tsx
index 0d638024b..8a50c0fb8 100644
--- a/src/components/settings/thresholds-editor-section.tsx
+++ b/src/components/settings/thresholds-editor-section.tsx
@@ -237,7 +237,7 @@ function MetricRow({
}: MetricRowProps) {
const { t } = useTranslations();
const fmt = useFormatters();
- const { preference } = useUnitDisplay();
+ const { preference, glucoseUnit } = useUnitDisplay();
const canonicalBounds = METRIC_BOUNDS[metric];
// v1.32.27 — the row reads and writes in the user's preferred unit
@@ -246,12 +246,14 @@ function MetricRow({
// canonical, so it converts on the way into the fields and inverts on
// the way back out in `onSave`. Guardrails round inward so a value
// typed at the displayed limit still passes the server's canonical
- // check. Metrics without a transform — and every metric for a metric
- // user — take the adapter's identity path untouched.
+ // check. The glucose thresholds read in the reader's glucose unit the
+ // same way. Metrics without a transform — and every metric on the default
+ // preferences — take the adapter's identity path untouched.
const units = resolveTargetUnitAdapter(
metric,
canonicalBounds.unit,
preference,
+ glucoseUnit,
);
const bounds = { ...units.bounds(canonicalBounds), unit: units.unit };
diff --git a/src/components/targets/target-edit-sheet.tsx b/src/components/targets/target-edit-sheet.tsx
index d3a788d42..bddd162e0 100644
--- a/src/components/targets/target-edit-sheet.tsx
+++ b/src/components/targets/target-edit-sheet.tsx
@@ -17,8 +17,11 @@ import {
type ThresholdMetric,
type EffectiveRange,
} from "@/lib/analytics/effective-range";
-import { convertGlucose, toCanonicalMgdl } from "@/lib/glucose";
-import { resolveTargetUnitAdapter } from "@/lib/targets/target-unit-display";
+import {
+ isGlucoseTargetMetric,
+ resolveTargetUnitAdapter,
+} from "@/lib/targets/target-unit-display";
+import { resolveGlucoseUnit } from "@/lib/glucose";
import { useUnitDisplay } from "@/hooks/use-unit-display";
import { apiDelete, apiGet, apiPut } from "@/lib/api/api-fetch";
@@ -129,49 +132,33 @@ function TargetEditSheetBody({
const metric = TARGET_TYPE_TO_METRIC[targetType] ?? null;
const isDerivedMetric = !isBp && metric == null;
- // Glucose is the one metric whose display unit can differ from the
- // canonical storage unit: HealthLog stores mg/dL, but a user on the
- // mmol/L preference sees — and types — mmol/L. The parent already
- // hands us `unit="mmol/L"` and a `initialRange` pre-converted to that
- // unit. We must therefore (a) seed the mg/dL persisted override into
- // the display unit, (b) validate the typed value against bounds
- // expressed in the display unit, and (c) convert the typed value back
- // to canonical mg/dL before the PUT — otherwise a `5.5 mmol/L` target
- // is rejected by the 40–400 mg/dL bounds or stored verbatim as 5.5.
- const isGlucoseMmol =
- metric != null && metric.startsWith("BLOOD_GLUCOSE") && unit === "mmol/L";
-
- // v1.32.27 — the same coupled treatment now covers the metric/imperial
- // preference. Weight-class thresholds are stored in kg; an imperial
- // user seeds, validates, and types in lb, and the adapter inverts the
- // typed number back to kg before the PUT. Glucose keeps its own
- // mg/dL ↔ mmol/L dialect (it carries no display transform, so the two
- // never compose); every other metric takes the adapter's identity
- // path and is numerically untouched.
- const { preference } = useUnitDisplay();
+ // One adapter owns every direction of the unit conversion: an imperial
+ // user seeds, validates and types weight-class thresholds in pounds, an
+ // mmol/L user types glucose thresholds in mmol/L, and the typed number is
+ // inverted back to the canonical kg / mg/dL before the PUT. The parent
+ // hands `unit` and `initialRange` already in the display unit; the
+ // persisted override and `METRIC_BOUNDS` are canonical and go through the
+ // adapter. Every other metric takes the identity path.
+ // A glucose sheet speaks the unit its panel registered, so the sheet and
+ // the band it edits can never disagree.
+ const { preference, glucoseUnit: accountGlucoseUnit } = useUnitDisplay();
+ const adapterKey = isBp ? "BLOOD_PRESSURE_SYS" : (metric ?? "");
const units = resolveTargetUnitAdapter(
- isBp ? "BLOOD_PRESSURE_SYS" : (metric ?? ""),
+ adapterKey,
unit,
preference,
+ isGlucoseTargetMetric(adapterKey)
+ ? resolveGlucoseUnit(unit)
+ : accountGlucoseUnit,
);
- const toDisplay = (canonical: number) =>
- isGlucoseMmol
- ? convertGlucose(canonical, "mmol/L")
- : units.toDisplay(canonical);
- const toCanonical = (displayValue: number) =>
- isGlucoseMmol
- ? toCanonicalMgdl(displayValue, "mmol/L")
- : units.toCanonical(displayValue);
- // Guardrails the typed value is checked against, expressed in the
- // display unit. Glucose projects its mg/dL window through the same
- // conversion the fields use; every other metric rounds the window
- // INWARD so a value typed at the display bound never inverts to a
- // canonical value the server rejects.
+ const toDisplay = (canonical: number) => units.toDisplay(canonical);
+ const toCanonical = (displayValue: number) => units.toCanonical(displayValue);
+ // Guardrails the typed value is checked against, expressed in the display
+ // unit and rounded INWARD so a value typed at the display bound never
+ // inverts to a canonical value the server rejects.
const toDisplayBounds = (canonical: { min: number; max: number }) =>
- isGlucoseMmol
- ? { min: toDisplay(canonical.min), max: toDisplay(canonical.max) }
- : units.bounds(canonical);
+ units.bounds(canonical);
// Lazy-load the thresholds payload so the dialog also catches any
// already-persisted override even when the seeded `initialRange`
diff --git a/src/lib/targets/__tests__/target-unit-display.test.ts b/src/lib/targets/__tests__/target-unit-display.test.ts
index da8702179..3d2fa1522 100644
--- a/src/lib/targets/__tests__/target-unit-display.test.ts
+++ b/src/lib/targets/__tests__/target-unit-display.test.ts
@@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import { METRIC_BOUNDS } from "@/lib/analytics/effective-range";
import { resolveTargetUnitAdapter } from "../target-unit-display";
+import { toCanonicalMgdl } from "@/lib/glucose";
/**
* v1.32.27 — the target/threshold unit adapter.
@@ -18,7 +19,7 @@ const KG_PER_LB = 0.45359237;
describe("resolveTargetUnitAdapter — metric account (identity)", () => {
it("leaves a weight threshold bit-for-bit untouched", () => {
- const units = resolveTargetUnitAdapter("WEIGHT", "kg", "metric");
+ const units = resolveTargetUnitAdapter("WEIGHT", "kg", "metric", "mg/dL");
expect(units.rescales).toBe(false);
expect(units.unit).toBe("kg");
// Not "close to" — exactly the same double, in both directions.
@@ -37,6 +38,7 @@ describe("resolveTargetUnitAdapter — metric account (identity)", () => {
"BLOOD_PRESSURE_SYS",
"mmHg",
preference,
+ "mg/dL",
);
expect(units.rescales).toBe(false);
expect(units.unit).toBe("mmHg");
@@ -51,11 +53,14 @@ describe("resolveTargetUnitAdapter — metric account (identity)", () => {
it("is inert for the keys the edit sheet passes on its non-metric paths", () => {
// A derived card (BMI, mood, medication compliance) has no editable
- // threshold and the sheet resolves the adapter with an empty key;
- // glucose keeps its own mg/dL ↔ mmol/L dialect and must never pick
- // up a second conversion on top.
- for (const key of ["", "BMI", "BLOOD_GLUCOSE_FASTING"]) {
- const units = resolveTargetUnitAdapter(key, "mmol/L", "imperial");
+ // threshold and the sheet resolves the adapter with an empty key.
+ for (const key of ["", "BMI"]) {
+ const units = resolveTargetUnitAdapter(
+ key,
+ "mmol/L",
+ "imperial",
+ "mg/dL",
+ );
expect(units.rescales).toBe(false);
expect(units.unit).toBe("mmol/L");
expect(units.toDisplay(5.5)).toBe(5.5);
@@ -66,14 +71,15 @@ describe("resolveTargetUnitAdapter — metric account (identity)", () => {
it("keeps the whole-hundred step for a step-count target", () => {
for (const preference of ["metric", "imperial"] as const) {
expect(
- resolveTargetUnitAdapter("ACTIVITY_STEPS", "steps", preference).step,
+ resolveTargetUnitAdapter("ACTIVITY_STEPS", "steps", preference, "mg/dL")
+ .step,
).toBe(100);
}
});
});
describe("resolveTargetUnitAdapter — imperial weight class", () => {
- const units = resolveTargetUnitAdapter("WEIGHT", "kg", "imperial");
+ const units = resolveTargetUnitAdapter("WEIGHT", "kg", "imperial", "mg/dL");
it("announces pounds and a one-decimal step", () => {
expect(units.rescales).toBe(true);
@@ -145,7 +151,12 @@ describe("resolveTargetUnitAdapter — imperial weight class", () => {
it("carries the same treatment across the whole weight class", () => {
for (const metric of ["TOTAL_BODY_WATER", "BONE_MASS"] as const) {
- const adapter = resolveTargetUnitAdapter(metric, "kg", "imperial");
+ const adapter = resolveTargetUnitAdapter(
+ metric,
+ "kg",
+ "imperial",
+ "mg/dL",
+ );
expect(adapter.unit).toBe("lb");
const bounds = adapter.bounds(METRIC_BOUNDS[metric]);
expect(adapter.toCanonical(bounds.min)).toBeGreaterThanOrEqual(
@@ -169,6 +180,7 @@ describe("resolveTargetUnitAdapter — temperature (the affine one)", () => {
"BODY_TEMPERATURE",
"celsius",
"imperial",
+ "mg/dL",
);
it("shifts absolute readings but never a difference", () => {
@@ -208,6 +220,7 @@ describe("resolveTargetUnitAdapter — temperature (the affine one)", () => {
"BODY_TEMPERATURE",
"celsius",
"metric",
+ "mg/dL",
);
expect(metricUnits.rescales).toBe(false);
expect(metricUnits.toDisplay(37)).toBe(37);
@@ -224,3 +237,73 @@ describe("resolveTargetUnitAdapter — temperature (the affine one)", () => {
expect(units.toCanonical(bounds.max)).toBeLessThanOrEqual(45);
});
});
+
+describe("resolveTargetUnitAdapter — glucose thresholds follow the glucose unit", () => {
+ const contexts = [
+ "BLOOD_GLUCOSE_FASTING",
+ "BLOOD_GLUCOSE_POSTPRANDIAL",
+ "BLOOD_GLUCOSE_RANDOM",
+ "BLOOD_GLUCOSE_BEDTIME",
+ ] as const;
+
+ it("reads mmol/L for an mmol/L reader, whatever the metric/imperial branch", () => {
+ for (const metric of contexts) {
+ for (const preference of ["metric", "imperial"] as const) {
+ const units = resolveTargetUnitAdapter(
+ metric,
+ "mg/dL",
+ preference,
+ "mmol/L",
+ );
+ expect(units.unit).toBe("mmol/L");
+ expect(units.rescales).toBe(true);
+ expect(units.step).toBe(0.1);
+ // The 70–99 mg/dL fasting default reads 3.9–5.5.
+ expect(units.toDisplay(70)).toBe(3.9);
+ expect(units.toDisplay(99)).toBe(5.5);
+ }
+ }
+ });
+
+ it("round-trips a typed mmol/L value to whole mg/dL and back", () => {
+ const units = resolveTargetUnitAdapter(
+ "BLOOD_GLUCOSE_FASTING",
+ "mg/dL",
+ "metric",
+ "mmol/L",
+ );
+ expect(units.toCanonical(5.5)).toBe(99);
+ expect(units.toDisplay(units.toCanonical(5.5))).toBe(5.5);
+ expect(units.toCanonical(3.9)).toBe(toCanonicalMgdl(3.9, "mmol/L"));
+ });
+
+ it("rounds the canonical 40–400 mg/dL guardrail inward", () => {
+ const units = resolveTargetUnitAdapter(
+ "BLOOD_GLUCOSE_FASTING",
+ "mg/dL",
+ "metric",
+ "mmol/L",
+ );
+ const bounds = units.bounds(METRIC_BOUNDS.BLOOD_GLUCOSE_FASTING);
+ expect(bounds).toEqual({ min: 2.3, max: 22.1 });
+ expect(units.toCanonical(bounds.min)).toBeGreaterThanOrEqual(
+ METRIC_BOUNDS.BLOOD_GLUCOSE_FASTING.min,
+ );
+ expect(units.toCanonical(bounds.max)).toBeLessThanOrEqual(
+ METRIC_BOUNDS.BLOOD_GLUCOSE_FASTING.max,
+ );
+ });
+
+ it("stays the exact identity for an mg/dL reader", () => {
+ const units = resolveTargetUnitAdapter(
+ "BLOOD_GLUCOSE_FASTING",
+ "mg/dL",
+ "imperial",
+ "mg/dL",
+ );
+ expect(units.unit).toBe("mg/dL");
+ expect(units.rescales).toBe(false);
+ expect(units.toDisplay(99)).toBe(99);
+ expect(units.toCanonical(99)).toBe(99);
+ });
+});
diff --git a/src/lib/targets/target-unit-display.ts b/src/lib/targets/target-unit-display.ts
index a92218e4e..0afb78a7d 100644
--- a/src/lib/targets/target-unit-display.ts
+++ b/src/lib/targets/target-unit-display.ts
@@ -48,6 +48,12 @@ import {
transformRescales,
type UnitPreference,
} from "@/lib/measurements/display-transform";
+import {
+ MGDL_PER_MMOL,
+ convertGlucose,
+ toCanonicalMgdl,
+ type GlucoseUnit,
+} from "@/lib/glucose";
/** Canonical decimals a converted threshold value persists with. */
const CANONICAL_DECIMALS = 2;
@@ -119,6 +125,41 @@ function identityAdapter(metric: string, unit: string): TargetUnitAdapter {
};
}
+/**
+ * The glucose thresholds (`BLOOD_GLUCOSE_FASTING` and its siblings) are
+ * stored in mg/dL and read in the reader's glucose unit. The settings editor
+ * used to take the identity path for them, so an mmol/L account was asked
+ * for mg/dL figures under a label that said nothing, against 40–400 bounds
+ * and a "default: 70–99" hint — while the insights sheet for the same
+ * threshold already spoke mmol/L.
+ *
+ * mmol/L reads at one decimal; the canonical write rounds to whole mg/dL
+ * (`toCanonicalMgdl`), the precision every glucose path stores. Guardrails
+ * round inward exactly like the imperial branch: 40 mg/dL is 2.22 mmol/L,
+ * shown as 2.3 so that the typed bound inverts to 41 mg/dL and passes.
+ */
+function glucoseAdapter(glucoseUnit: GlucoseUnit): TargetUnitAdapter {
+ if (glucoseUnit === "mg/dL") return identityAdapter("BLOOD_GLUCOSE", "mg/dL");
+ const toMmol = (mgdl: number) => mgdl / MGDL_PER_MMOL;
+ return {
+ unit: "mmol/L",
+ step: 0.1,
+ rescales: true,
+ toDisplay: (canonical) => convertGlucose(canonical, "mmol/L"),
+ toCanonical: (display) => toCanonicalMgdl(display, "mmol/L"),
+ toDisplayDelta: (canonicalDelta) => roundTo(toMmol(canonicalDelta), 1),
+ bounds: (canonical) => ({
+ min: ceilTo(toMmol(canonical.min), 1),
+ max: floorTo(toMmol(canonical.max), 1),
+ }),
+ };
+}
+
+/** True for a glucose threshold metric or target-card type. */
+export function isGlucoseTargetMetric(metric: string): boolean {
+ return metric === "BLOOD_GLUCOSE" || metric.startsWith("BLOOD_GLUCOSE_");
+}
+
/**
* Resolve the unit adapter for one threshold metric / target-card type.
*
@@ -128,13 +169,17 @@ function identityAdapter(metric: string, unit: string): TargetUnitAdapter {
* on, so a metric participates exactly when it has a registered
* transform. `canonicalUnit` is the unit the surface announced before
* this release (`METRIC_BOUNDS[metric].unit` or the targets payload's
- * `unit`) and is what an untransformed metric keeps.
+ * `unit`) and is what an untransformed metric keeps. `glucoseUnit` is
+ * the reader's glucose preference; it decides the glucose thresholds and
+ * nothing else.
*/
export function resolveTargetUnitAdapter(
metric: string,
canonicalUnit: string,
preference: UnitPreference,
+ glucoseUnit: GlucoseUnit,
): TargetUnitAdapter {
+ if (isGlucoseTargetMetric(metric)) return glucoseAdapter(glucoseUnit);
if (!hasDisplayTransform(metric))
return identityAdapter(metric, canonicalUnit);