Merge time-of-day signals and dated latest readings into release/v1.41.0

This commit is contained in:
Marc-André Bombeck committed 2026-10-06 20:54:12 +02:00
commit c56a05925c
22 files changed
+660 -31

No files matched your search

@@ -0,0 +1,103 @@
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 type { Locale } from "@/lib/i18n/config";
import { queryKeys } from "@/lib/query-keys";
import type { CoachReadStripData } from "@/lib/insights/derived/coach-read-shape";
/**
* The strip used to call the newest reading "today's" on every metric page,
* even when it was days old. The server now says which day the reading is
* from and whether that is today; the sentence names the date otherwise.
*/
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 = "RESTING_HEART_RATE";
function render(data: CoachReadStripData, locale: Locale = "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="bpm" />
</I18nProvider>
</QueryClientProvider>,
);
}
function strip(
latestIsToday: boolean,
placement: "within" | "above" | "below" = "within",
band: { low: number; high: number } = { low: 55, high: 62 },
): CoachReadStripData {
return {
baseline: {
...band,
latest: 58,
latestDate: latestIsToday ? "2026-10-06" : "2026-10-01",
latestIsToday,
placement,
sampleDays: 20,
},
learning: false,
driver: null,
};
}
describe("CoachReadStrip — the latest reading's day", () => {
it("calls a reading from today today's", () => {
const markup = render(strip(true));
expect(markup).toContain("today&#x27;s 58 sits within it");
expect(markup).not.toContain("latest reading");
});
it("names the date of an older reading instead of calling it today's", () => {
const markup = render(strip(false));
expect(markup).not.toContain("today");
expect(markup).toContain(
"your latest reading, October 1, was 58, within it",
);
expect(render(strip(false, "above"))).toContain("was 58, above it");
expect(render(strip(false, "below"))).toContain("was 58, below it");
});
it("names the date on a steady band too", () => {
const markup = render(strip(false, "within", { low: 58, high: 58 }));
expect(markup).not.toContain("today");
expect(markup).toContain("your latest reading, October 1, was 58");
});
it("says it in every shipped language", () => {
const expected: Record<Locale, string> = {
de: "1. Oktober",
en: "October 1",
es: "1 de octubre",
fr: "1 octobre",
it: "1 ottobre",
ko: "10월 1일",
pl: "1 października",
};
for (const [locale, date] of Object.entries(expected) as Array<
[Locale, string]
>) {
const markup = render(strip(false), locale);
expect(markup, locale).toContain(date);
expect(markup, locale).toContain("58");
}
});
});
@@ -51,6 +51,8 @@ function sameHours(
latest,
placement,
sampleDays: 14,
latestDate: "2026-10-06",
latestIsToday: true,
basis: "sameHours",
},
learning: false,
@@ -85,6 +87,8 @@ describe("CoachReadStrip — a glucose day in progress", () => {
latest: 132,
placement: "within",
sampleDays: 30,
latestDate: "2026-10-06",
latestIsToday: true,
},
learning: false,
driver: null,
@@ -48,7 +48,15 @@ function band(
placement: "within" | "above" | "below",
): CoachReadStripData {
return {
baseline: { low, high, latest, placement, sampleDays: 30 },
baseline: {
low,
high,
latest,
placement,
sampleDays: 30,
latestDate: "2026-10-06",
latestIsToday: true,
},
learning: false,
driver: null,
};
@@ -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". For a glucose day still in progress the range is the
* above / below", or "your latest reading, <date>, was Z" when the
* newest reading is not from today in the reader's zone. 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
@@ -102,17 +103,34 @@ export function CoachReadStrip({
: value * valueScale,
);
// `YYYY-MM-DD` is already the reader's calendar day; formatting it at UTC
// midnight in UTC keeps a second zone conversion from shifting it.
const fmtDay = (day: string | undefined): string => {
const m = day ? /^(\d{4})-(\d{2})-(\d{2})$/.exec(day) : null;
if (!m) return "";
return new Intl.DateTimeFormat(locale, {
day: "numeric",
month: "long",
timeZone: "UTC",
}).format(new Date(Date.UTC(+m[1], +m[2] - 1, +m[3])));
};
const baselineLine = ((): string | null => {
if (data.learning || !data.baseline) {
return t("insights.coach.readStrip.insufficient");
}
const { low, high, latest, placement, basis } = data.baseline;
const { low, high, latest, placement, basis, latestIsToday } =
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);
// 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.
// A latest reading from an earlier day is not today's: the sentence names
// its date instead (`latestIsToday`, resolved server-side). The same-hours
// basis only exists for a day in progress, so it is always today.
const dated = basis !== "sameHours" && latestIsToday === false;
const key =
basis === "sameHours"
? placement === "above"
@@ -120,22 +138,35 @@ export function CoachReadStrip({
: 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";
: dated
? steady
? placement === "above"
? "insights.coach.readStrip.steadyLatestAbove"
: placement === "below"
? "insights.coach.readStrip.steadyLatestBelow"
: "insights.coach.readStrip.steadyLatestWithin"
: placement === "above"
? "insights.coach.readStrip.latestAbove"
: placement === "below"
? "insights.coach.readStrip.latestBelow"
: "insights.coach.readStrip.latestWithin"
: 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),
value: fmt(latest),
unit: resolvedUnit,
date: fmtDay(data.baseline.latestDate),
});
})();