diff --git a/e2e/day-doors.spec.ts b/e2e/day-doors.spec.ts
index c83cfa352..357e2773c 100644
--- a/e2e/day-doors.spec.ts
+++ b/e2e/day-doors.spec.ts
@@ -1,5 +1,6 @@
import type { Locator, Page, TestInfo } from "@playwright/test";
+import { aiBlockAvailable, serveAiBlock } from "./setup/ai-capabilities";
import { DAY_DOORS_ANALYTE } from "./setup/day-doors-fixture";
import { mockDay } from "./setup/day-mock";
import {
@@ -203,6 +204,11 @@ test.describe("every chart drawn in days opens its days", () => {
.locator('[data-slot="chart-overlay-comparison-lastMonth"]')
.click();
await page.keyboard.press("Escape");
+ // The overlay menu is closed before the chart is touched, so the tap
+ // lands on the chart and not on the menu's outside-dismiss.
+ await expect(
+ page.locator('[data-slot="chart-overlay-controls-content"]'),
+ ).toHaveCount(0);
await expect(
page.locator('[data-slot="chart-compare-caption"]').first(),
).toBeAttached();
@@ -265,9 +271,13 @@ test.describe("the dashboard and the list of all values", () => {
}, testInfo) => {
test.skip(isPhone(testInfo), "docked from 1280 px");
await page.setViewportSize({ width: 1440, height: 900 });
+ // The launcher follows the `coach` capability, which needs a provider
+ // the suite cannot reach, so the capability reads available here; this
+ // journey is about where the button sits, not what it opens.
+ await serveAiBlock(page, aiBlockAvailable());
await page.goto("/insights/weight");
const fab = page.locator('[data-slot="coach-fab"]');
- test.skip((await fab.count()) === 0, "no Coach on this account");
+ await expect(fab).toBeVisible();
const plot = page
.locator('[data-slot="chart-plot"][data-day-links="true"]')
.first();
diff --git a/src/components/day/__tests__/use-day-value-format.test.tsx b/src/components/day/__tests__/use-day-value-format.test.tsx
new file mode 100644
index 000000000..7fbca9381
--- /dev/null
+++ b/src/components/day/__tests__/use-day-value-format.test.tsx
@@ -0,0 +1,77 @@
+import { describe, expect, it, vi } from "vitest";
+import { renderToStaticMarkup } from "react-dom/server";
+
+import deMessages from "../../../../messages/de.json";
+import enMessages from "../../../../messages/en.json";
+
+/**
+ * A day's values read as their charts read them. The day route sends a
+ * night's length in "min" and a device score in "score"; the first read as a
+ * bare 412 and the second as an untranslated "score" before both were
+ * spelled the way the sleep and recovery charts spell them.
+ */
+
+vi.mock("@/hooks/use-unit-display", async () => {
+ const { unitDisplayFor } =
+ await import("@/__tests__/helpers/unit-display-mock");
+ return { useUnitDisplay: () => unitDisplayFor("metric") };
+});
+
+import { I18nProvider } from "@/lib/i18n/context";
+
+import { useDayValueFormat } from "../use-day-value-format";
+
+function Probe({
+ type,
+ value,
+ unit,
+}: {
+ type: string;
+ value: number;
+ unit: string;
+}) {
+ const { formatTile } = useDayValueFormat();
+ const shown = formatTile({
+ key: type,
+ values: [{ type, value, unit, at: "", source: "", band: null }],
+ });
+ return (
+
+ {shown.value}|{shown.unit}
+
+ );
+}
+
+function read(
+ type: string,
+ value: number,
+ unit: string,
+ locale: "en" | "de" = "en",
+) {
+ return renderToStaticMarkup(
+
+
+ ,
+ )
+ .replace(/<[^>]+>/g, "")
+ .replace(//g, "");
+}
+
+describe("a day's values in their charts' spelling", () => {
+ it("reads a night sent in min as a duration", () => {
+ const [value, unit] = read("SLEEP_DURATION", 412, "min").split("|");
+ expect(value).toMatch(/6\s*h/);
+ expect(value).toMatch(/52/);
+ expect(unit).toBe("");
+ });
+
+ it("reads a device score in the reader's word for points", () => {
+ // German, where the wire's "score" and the chart's "Punkte" differ.
+ const [value, unit] = read("ANS_CHARGE", 8, "score", "de").split("|");
+ expect(value).toBe("8");
+ expect(unit).toBe("Punkte");
+ });
+});
diff --git a/src/components/day/day-view.tsx b/src/components/day/day-view.tsx
index 0b7ff57a1..55e317a23 100644
--- a/src/components/day/day-view.tsx
+++ b/src/components/day/day-view.tsx
@@ -54,14 +54,16 @@ import { useDayValueFormat } from "./use-day-value-format";
*/
/** "Saturday, 3 January 2026", for the header and the spoken name. */
-export function useLongDayLabel(): (date: DateKey) => string {
+export function useLongDayLabel(
+ length: "long" | "short" = "long",
+): (date: DateKey) => string {
const { locale } = useTranslations();
const intl = resolveIntlLocale(locale);
return (date: DateKey) =>
new Intl.DateTimeFormat(intl, {
- weekday: "long",
+ weekday: length,
day: "numeric",
- month: "long",
+ month: length,
year: "numeric",
timeZone: "UTC",
}).format(new Date(`${date}T12:00:00.000Z`));
@@ -118,6 +120,10 @@ export function DayView({
}: DayViewProps) {
const { t, tCount } = useTranslations();
const longLabel = useLongDayLabel();
+ // "Mo., 5. Okt. 2026" in the phone sheet's header: a September Thursday
+ // spelled out in full does not fit beside the arrows at 360 px, and a
+ // date cut off before its year reads as the wrong day.
+ const shortLabel = useLongDayLabel("short");
const pathname = usePathname();
const day = useDay(date);
const { labelFor, formatTile } = useDayValueFormat();
@@ -218,7 +224,14 @@ export function DayView({
tabIndex={-1}
className="min-w-0 flex-1 truncate px-1 text-base leading-snug font-semibold focus-visible:outline-none"
>
- {longLabel(date)}
+ {compact ? (
+ <>
+ {shortLabel(date)}
+ {longLabel(date)}
+ >
+ ) : (
+ longLabel(date)
+ )}