fix(insights): place a glucose day in progress against the same hours

The usual-range strip on a metric page placed the latest single reading
against a band built from whole-day means. For glucose the day mean climbs
with every meal, so at nine in the morning a fasting reading sat under a
band of whole days and the strip said "below your range" on an ordinary
morning; the MCP baseline read reported the same.

Glucose is now placed by its day mean. A day still in progress, in the
reader's own zone, is held against the earlier days cut at the local time
of today's latest reading, the comparison the health status already makes;
with fewer than seven earlier days that have readings in those hours there
is no verdict. The baseline carries `basis: "sameHours"` on the coach-read
route and the MCP read, and the strip names the range as the one for this
time of day, in all seven locales. Other metrics keep their placement.
This commit is contained in:
Marc-André Bombeck committed 2026-10-06 20:29:57 +02:00
1 parent 362818418e
commit 3e968f03a9
21 files changed
+518 -40

No files matched your search

@@ -0,0 +1,94 @@
import { describe, it, expect, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { I18nProvider } from "@/lib/i18n/context";
import { queryKeys } from "@/lib/query-keys";
import type { CoachReadStripData } from "@/lib/insights/derived/coach-read-shape";
/**
* A glucose day still in progress is placed against the same hours of the
* earlier days. The range is then the usual one for this time of day and the
* figure is today's mean so far, and the sentence has to say so: printed as
* "your usual range", a morning range would read as the whole-day one.
*/
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({
user: { timezone: "UTC", dateOfBirth: null, gender: null },
isAuthenticated: true,
}),
}));
vi.mock("@/hooks/use-mounted", () => ({ useMounted: () => true }));
import { CoachReadStrip } from "../coach-read-strip";
const METRIC = "BLOOD_GLUCOSE";
function render(data: CoachReadStripData, locale: "en" | "de" = "en"): string {
const client = new QueryClient({
defaultOptions: { queries: { retry: false, staleTime: Infinity } },
});
client.setQueryData(queryKeys.insightsCoachRead(METRIC, locale), data);
return renderToStaticMarkup(
<QueryClientProvider client={client}>
<I18nProvider initialLocale={locale}>
<CoachReadStrip metricType={METRIC} unit="mg/dL" />
</I18nProvider>
</QueryClientProvider>,
);
}
function sameHours(
latest: number,
placement: "within" | "above" | "below",
): CoachReadStripData {
return {
baseline: {
low: 80,
high: 90,
latest,
placement,
sampleDays: 14,
basis: "sameHours",
},
learning: false,
driver: null,
};
}
describe("CoachReadStrip — a glucose day in progress", () => {
it("names the range as the one for this time of day", () => {
const markup = render(sameHours(86, "within"));
expect(markup).toContain("By this time of day");
expect(markup).toContain("today so far, 86 is within that");
expect(markup).not.toContain("Your usual range");
});
it("keeps the placement", () => {
expect(render(sameHours(95, "above"))).toContain("is above that");
expect(render(sameHours(70, "below"))).toContain("is below that");
});
it("says it in the reader's language", () => {
expect(render(sameHours(86, "within"), "de")).toContain(
"Bis zu dieser Tageszeit",
);
});
it("keeps the whole-day sentence without the basis", () => {
const markup = render({
baseline: {
low: 110,
high: 150,
latest: 132,
placement: "within",
sampleDays: 30,
},
learning: false,
driver: null,
});
expect(markup).toContain("Your usual range is 110–150");
});
});
@@ -20,7 +20,8 @@ import type { CoachReadStripData } from "@/lib/insights/derived/coach-read-shape
* A compact two-line read rendered ABOVE the chart on each metric sub-page:
*
* 1. own-baseline — "Your usual range is X–Y; today's Z sits within /
* above / below". Below the engine's 7-day history floor it reads
* above / below". For a glucose day still in progress the range is the
* one for this time of day and Z today's mean so far. Below the engine's 7-day history floor it reads
* "still learning your range" — never a fabricated band.
* 2. one lagged association — the single strongest discovered driver whose
* outcome is this metric, stated in the engine's own never-causal voice.
@@ -105,21 +106,31 @@ export function CoachReadStrip({
if (data.learning || !data.baseline) {
return t("insights.coach.readStrip.insufficient");
}
const { low, high, latest, placement } = data.baseline;
const { low, high, latest, placement, basis } = data.baseline;
// A band whose ends format to the same figure ("61–61 bpm") is a steady
// value, not a range; say it as one.
const steady = fmt(low) === fmt(high);
const key = steady
? placement === "above"
? "insights.coach.readStrip.steadyAbove"
: placement === "below"
? "insights.coach.readStrip.steadyBelow"
: "insights.coach.readStrip.steadyWithin"
: placement === "above"
? "insights.coach.readStrip.baselineAbove"
: placement === "below"
? "insights.coach.readStrip.baselineBelow"
: "insights.coach.readStrip.baselineWithin";
// A glucose day still in progress is held against the same hours of the
// earlier days, so the range is the one for this time of day and today's
// figure is its mean so far; the sentence says both.
const key =
basis === "sameHours"
? placement === "above"
? "insights.coach.readStrip.sameHoursAbove"
: placement === "below"
? "insights.coach.readStrip.sameHoursBelow"
: "insights.coach.readStrip.sameHoursWithin"
: steady
? placement === "above"
? "insights.coach.readStrip.steadyAbove"
: placement === "below"
? "insights.coach.readStrip.steadyBelow"
: "insights.coach.readStrip.steadyWithin"
: placement === "above"
? "insights.coach.readStrip.baselineAbove"
: placement === "below"
? "insights.coach.readStrip.baselineBelow"
: "insights.coach.readStrip.baselineWithin";
return t(key, {
low: fmt(low),
high: fmt(high),