mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
Merge time-of-day signals and dated latest readings into release/v1.41.0
This commit is contained in:
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'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),
|
||||
});
|
||||
})();
|
||||
|
||||
|
||||
Reference in new issue
Block a user