feat(glucose): read glucose in one unit across series, CSV, and detail page

Glucose is stored canonical mg/dL but the series DTO, CSV export, and detail
page emitted raw mg/dL while FHIR already converted to the user preference.

Convert at serialization through the single convertGlucose helper:
- series route sets unit from the user's glucoseUnit and converts each point
- CSV export converts BLOOD_GLUCOSE rows and emits the unit in the unit column
- the detail page + stat strip honour the preference via the chart value scale
- declare the trailing-90-day window in the glucose median label across locales

mmol/L-preference users now see the blood-glucose page, stat strip, iOS series,
and CSV in mmol/L; mg/dL users are unchanged.
This commit is contained in:
Marc-André Bombeck committed 2026-06-14 17:16:48 +02:00
1 parent a2260d5bb8
commit 25944ed57a
17 files changed
+398 -26

No files matched your search

@@ -70,6 +70,37 @@ describe("<MetricStatStrip>", () => {
expect(html).toBe("");
});
it("renders the default 'Median' label when no override is given", () => {
const html = render(<MetricStatStrip summary={populated} unit="bpm" />);
expect(html).toContain("Median");
});
// v1.16.16 — glucose declares the trailing-90-day p50 window in the label
// so the median is not read as an all-time central value.
it("renders an overridden median label when medianLabel is provided", () => {
const html = render(
<MetricStatStrip
summary={populated}
unit="mg/dL"
medianLabel="Median glucose (90 days)"
/>,
);
expect(html).toContain("Median glucose (90 days)");
});
it("honours fractionDigits for integer-unit metrics (0 decimals)", () => {
const html = render(
<MetricStatStrip
summary={{ ...populated, min: 100, max: 180, median: 126 }}
unit="mg/dL"
fractionDigits={0}
/>,
);
// No "100.0" — integer precision.
expect(html).toContain("100 mg/dL");
expect(html).not.toContain("100.0");
});
it("falls back to mean when median is absent (older payload)", () => {
const noMedian: DataSummary = { ...populated, median: null };
const html = render(<MetricStatStrip summary={noMedian} unit="bpm" />);
@@ -142,6 +142,18 @@ export interface HealthKitMetricPageProps {
* "no trend to project yet" state rather than a line.
*/
forecast?: boolean;
/**
* v1.16.16 — decimal precision for the stat-strip values. Defaults to the
* strip's own default (1). Blood glucose passes 0 for mg/dL (integer
* readings) and 1 for mmol/L so the numbers and the unit agree.
*/
statFractionDigits?: number;
/**
* v1.16.16 — optional override for the stat strip's "Median" label. Blood
* glucose passes a window- + context-declaring string so the trailing
* 90-day p50 is not read as an all-time central value.
*/
statMedianLabel?: string;
}
export function HealthKitMetricPage({
@@ -162,6 +174,8 @@ export function HealthKitMetricPage({
statusMetric,
statIcon,
forecast = false,
statFractionDigits,
statMedianLabel,
}: HealthKitMetricPageProps) {
const { user, isAuthenticated } = useAuth();
const { t } = useTranslations();
@@ -289,9 +303,11 @@ export function HealthKitMetricPage({
<MetricStatStrip
summary={summary}
unit={yAxisUnit ?? unit}
fractionDigits={statFractionDigits}
seriesLabel={title}
icon={statIcon}
windowStats={statsByType?.[measurementType] ?? null}
medianLabel={statMedianLabel}
/>
}
diversityNudge={
+13 -1
View File
@@ -92,6 +92,15 @@ export interface MetricStatSeries {
* single-series mode omits it.
*/
dataKey?: string;
/**
* v1.16.16 — optional override for the "Median" cell label. The strip's
* median is a trailing-90-day p50 (see `DataSummary.median` /
* `summaries-slice.ts`); the default generic "Median" label leaves that
* window undeclared. Blood glucose passes a window- + context-declaring
* string (e.g. "Median glucose (90 days)") so the number is not read as an
* all-time central value. Omit it to keep the generic label.
*/
medianLabel?: string;
}
type MetricStatStripProps = Partial<MetricStatSeries> & {
@@ -129,6 +138,7 @@ function SeriesBlock({
icon,
windowStats,
dataKey,
medianLabel,
}: MetricStatSeries) {
const { t } = useTranslations();
const fmt = useFormatters();
@@ -154,7 +164,7 @@ function SeriesBlock({
{ key: "max", label: t("insights.subPage.stats.max"), value: source.max },
{
key: "median",
label: t("insights.subPage.stats.median"),
label: medianLabel ?? t("insights.subPage.stats.median"),
value: source.median,
},
{
@@ -203,6 +213,7 @@ export function MetricStatStrip({
icon,
windowStats,
dataKey,
medianLabel,
series,
groupLabel,
}: MetricStatStripProps) {
@@ -264,6 +275,7 @@ export function MetricStatStrip({
icon={icon}
windowStats={windowStats}
dataKey={dataKey}
medianLabel={medianLabel}
/>
</CardContent>
</Card>