Files
HealthLog/e2e/setup/day-mock.ts
T
Marc-André Bombeck 6e2a8fcd9f feat(day): show the day's scores and the cycle day with its phase
The day view gains a scores section below the values: the health score as
recorded that day, the readiness the nightly job persisted for the night
that ended that morning, a device's own recovery, the sleep score of that
night and strain (computed, or the device's day strain on its 0 to 21
scale). Each tile carries the person's usual range over the 30 days before
and opens the score's own page; a score the day does not hold has no tile,
and a day without any has no section.

Every value comes from where its page reads its history: the stored health
score record, the persisted RECOVERY_SCORE and STRAIN_SCORE rows read on
the day they describe, the canonical recovery ladder, and the sleep score
computed over one bounded read of the 30 nights up to the day. The stored
score rows leave the values list of the day they are stamped on, so a
readiness no longer appears a day early among the readings.

Scores follow their own module (recovery, sleep; the health score is core)
and map to the whole record for sharing: a scoped grant sees them named as
not shared, a whole-record grant sees them. get_day sends them to the
Coach and the MCP endpoint by name and number, and the Coach exclusions
take out every score built on excluded data.

A running cycle now states its cycle day and phase ("Cycle day 12,
follicular phase"), computed the way the cycle calendar does. Only logged
cycles count, the next logged start closes a cycle, and an open cycle
stops counting past its typical length plus the grace window instead of
reading a day count nobody recorded.
2026-10-09 14:03:31 +02:00

126 lines
3.2 KiB
TypeScript

/**
* The day routes, answered in the contract's shape.
*
* The day itself (`GET /api/day/{date}`) and the index (`GET /api/day/index`)
* are route mocks, so a journey pins the client: which door opens which day,
* what the history does, where no door is. The server's own answers are
* pinned by the integration suite.
*/
import type { Page, Route } from "@playwright/test";
function dayFixture(date: string) {
return {
date,
tz: "Europe/Berlin",
counts: { values: 2, entries: 1 },
running: [
{
kind: "illness",
section: "illness",
id: "ep1",
title: "Common cold",
sub: null,
since: date,
until: null,
dayIndex: 1,
dayCount: null,
href: "/illness",
},
],
values: [
{
type: "BLOOD_PRESSURE_SYS",
value: 124,
unit: "mmHg",
at: `${date}T07:00:00.000Z`,
source: "MANUAL",
band: { lo: 118, hi: 128, n: 20 },
},
{
type: "BLOOD_PRESSURE_DIA",
value: 79,
unit: "mmHg",
at: `${date}T07:00:00.000Z`,
source: "MANUAL",
band: { lo: 76, hi: 82, n: 20 },
},
],
events: [
{
at: `${date}T07:00:00.000Z`,
kind: "mood",
section: "mood",
id: "m1",
title: "Mood: good",
meta: null,
note: null,
docs: [],
href: "/mood",
},
],
notable: [],
scores: [
{
key: "healthScore",
value: 74,
max: 100,
source: "COMPUTED",
band: { lo: 66, hi: 78, n: 30 },
},
{ key: "strain", value: 12.4, max: 21, source: "WHOOP", band: null },
],
sections: {},
};
}
/**
* Every day from `from` to `to`, each holding values: what the row of day
* dots under a chart reads when the whole window has entries.
*/
function everyDay(from: string, to: string): Record<string, string[]> {
const days: Record<string, string[]> = {};
if (!from || !to) return days;
const at = new Date(`${from}T12:00:00.000Z`);
const end = new Date(`${to}T12:00:00.000Z`);
while (at <= end) {
days[at.toISOString().slice(0, 10)] = ["values"];
at.setUTCDate(at.getUTCDate() + 1);
}
return days;
}
export async function mockDay(
page: Page,
options: { index?: "empty" | "full" } = {},
) {
await page.route("**/api/day/**", async (route: Route) => {
const url = new URL(route.request().url());
if (url.pathname === "/api/day/index") {
const from = url.searchParams.get("from") ?? "";
const to = url.searchParams.get("to") ?? "";
const days = options.index === "full" ? everyDay(from, to) : {};
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
data: {
from,
to,
days,
// The first day of the window stands out: the rug draws a ring.
notable: Object.keys(days).slice(0, 1),
},
error: null,
}),
});
return;
}
const date = url.pathname.split("/").pop() ?? "";
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ data: dayFixture(date), error: null }),
});
});
}