Drop the just-in line from the Today hero

Under a number, 'Just in' did not say what had arrived - the score? a
reading? - and the clock face beside it changed nothing a person would
then do. The line is gone, and with it the only wall clock this surface
rendered.

What it was there for survives: the day still flips in place when last
night's sleep lands, and the pending-night note still says the number is
reading an incomplete day. An arrival on its own is no longer a reason
to paint a hero at all, so an account whose only news was the arrival
now gets nothing rather than a bordered empty card.

The digest still carries the arrival, which the morning line and the
wide event both read.
This commit is contained in:
Marc-André Bombeck committed 2026-08-01 11:09:37 +02:00
1 parent ee63b09e59
commit 4e934a191d
9 files changed
+91 -211

No files matched your search

+19 -18
View File
@@ -12,15 +12,21 @@ import { E2E_USER, STORAGE_STATE_PATH } from "./setup/global-setup";
*
* The spec drives that end-to-end against the real read path:
*
* 1. load the dashboard with nothing fresh — no chip, day is provisional;
* 2. land last night's sleep through the REAL batch endpoint, and the
* arrival marker the spine's worker would have written;
* 1. load the dashboard with last night's sleep still missing;
* 2. land it through the REAL batch endpoint, and the arrival marker the
* spine's worker would have written;
* 3. surface the tab's real visibility-change signal — the browser event
* TanStack Query's focus manager consumes for
* `refetchOnWindowFocus: "always"` (alongside the 120 s foreground poll);
* using that existing trigger keeps the test inside the real cadence
* while staying well under the poll interval;
* 4. assert the chip appeared and the phase flipped — in place, no reload.
* 4. assert the phase flipped and the pending note cleared — in place, no
* reload.
*
* The hero used to carry a "just in" chip here; it is gone, because under a
* number "just in" never said what had arrived. What the milestone was
* always about survives it: the open page reads differently on its own
* cadence.
*
* The marker is seeded directly rather than waited for from pg-boss: whether
* the queue drains inside the e2e web server is the SPINE's contract and has
@@ -43,9 +49,7 @@ function localDayKey(at: Date, timeZone: string): string {
}).format(at);
}
test("a fresh arrival surfaces the just-in chip and flips the day in place", async ({
page,
}) => {
test("a fresh arrival flips the day in place", async ({ page }) => {
const dbUrl = process.env.DATABASE_URL;
test.skip(!dbUrl, "DATABASE_URL is required to seed the arrival marker");
@@ -85,10 +89,10 @@ test("a fresh arrival surfaces the just-in chip and flips the day in place", asy
);
// ── BEFORE ────────────────────────────────────────────────────────────
// Wait until the digest settles, then prove there is no stale arrival
// reaction. Other browser specs deliberately reuse this account and may
// have recorded older health data already, so an ordinary briefing Hero
// is a valid baseline; the invariant here is that it has no just-in chip.
// Wait until the digest settles. Other browser specs deliberately reuse
// this account and may have recorded older health data already, so an
// ordinary briefing hero is a valid baseline. Nothing on it may name an
// arrival: that chip was removed.
await page.goto("/");
await expect(page.locator('[data-slot="today-hero-skeleton"]')).toHaveCount(
0,
@@ -149,9 +153,6 @@ test("a fresh arrival surfaces the just-in chip and flips the day in place", asy
window.dispatchEvent(new Event("visibilitychange")),
);
await expect(hero.locator('[data-slot="today-hero-just-in"]')).toBeVisible({
timeout: 20_000,
});
await expect(hero).toHaveAttribute("data-phase", "final", {
timeout: 20_000,
});
@@ -159,10 +160,10 @@ test("a fresh arrival surfaces the just-in chip and flips the day in place", asy
await expect(
hero.locator('[data-slot="today-hero-sleep-pending"]'),
).toHaveCount(0);
// The chip names the arrival kind — the stable attribute, not the copy.
await expect(
hero.locator('[data-slot="today-hero-just-in"]'),
).toHaveAttribute("data-just-in-kind", "sleep_night");
// And the arrival still raises no chip of its own, on either pass.
await expect(page.locator('[data-slot="today-hero-just-in"]')).toHaveCount(
0,
);
} finally {
client.release();
await pool.end();
-2
View File
@@ -9288,8 +9288,6 @@
"deltaVsBaseline": "{delta} ggü. deinem Ausgangswert",
"deltaFlat": "Im Bereich deines Ausgangswerts",
"sleepPending": "Der Schlaf von letzter Nacht fehlt noch – der heutige Wert ist vorläufig.",
"justIn": "Gerade eingetroffen",
"justInAt": "Gerade eingetroffen · {time}",
"worthALook": "Einen Blick wert",
"allClear": "Heute braucht nichts deine Aufmerksamkeit.",
"ringLink": "{metric}-Details öffnen"
-2
View File
@@ -9288,8 +9288,6 @@
"deltaVsBaseline": "{delta} vs your baseline",
"deltaFlat": "In line with your baseline",
"sleepPending": "Last night's sleep isn't in yet — today's read is provisional.",
"justIn": "Just in",
"justInAt": "Just in · {time}",
"worthALook": "Worth a look",
"allClear": "Nothing needs your attention today.",
"ringLink": "Open {metric} details"
-2
View File
@@ -9288,8 +9288,6 @@
"deltaVsBaseline": "{delta} frente a tu valor de referencia",
"deltaFlat": "En línea con tu valor de referencia",
"sleepPending": "El sueño de anoche aún no ha llegado; la lectura de hoy es provisional.",
"justIn": "Recién llegado",
"justInAt": "Recién llegado · {time}",
"worthALook": "Merece un vistazo",
"allClear": "Hoy nada necesita tu atención.",
"ringLink": "Abrir detalles de {metric}"
-2
View File
@@ -9288,8 +9288,6 @@
"deltaVsBaseline": "{delta} par rapport à ta référence",
"deltaFlat": "Conforme à ta référence",
"sleepPending": "Le sommeil de la nuit dernière n'est pas encore arrivé — la lecture du jour est provisoire.",
"justIn": "Vient d'arriver",
"justInAt": "Vient d'arriver · {time}",
"worthALook": "À regarder",
"allClear": "Rien ne requiert ton attention aujourd'hui.",
"ringLink": "Ouvrir les détails de {metric}"
-2
View File
@@ -9288,8 +9288,6 @@
"deltaVsBaseline": "{delta} rispetto al tuo valore di riferimento",
"deltaFlat": "In linea con il tuo valore di riferimento",
"sleepPending": "Il sonno di stanotte non è ancora arrivato: la lettura di oggi è provvisoria.",
"justIn": "Appena arrivato",
"justInAt": "Appena arrivato · {time}",
"worthALook": "Da tenere d'occhio",
"allClear": "Oggi nulla richiede la tua attenzione.",
"ringLink": "Apri i dettagli di {metric}"
-2
View File
@@ -9288,8 +9288,6 @@
"deltaVsBaseline": "{delta} wzgl. Twojej wartości bazowej",
"deltaFlat": "Zgodnie z Twoją wartością bazową",
"sleepPending": "Sen z ostatniej nocy jeszcze nie dotarł — dzisiejszy odczyt jest wstępny.",
"justIn": "Właśnie dotarło",
"justInAt": "Właśnie dotarło · {time}",
"worthALook": "Warto sprawdzić",
"allClear": "Dziś nic nie wymaga Twojej uwagi.",
"ringLink": "Otwórz szczegóły: {metric}"
@@ -1,22 +1,17 @@
/**
* The Today hero's "just in" chip and the reaction line's lead replacement.
* The Today hero's arrival handling and the reaction line's lead
* replacement.
*
* Two invariants, both of which have a specific failure this project has
* already paid for:
* The hero used to carry a "just in" chip under the score, with the
* arrival's local time on it. It is gone: under a number, "just in" did
* not say what had arrived — the score? a reading? — and the clock face
* changed nothing a person would then do. What is left is the invariant
* that an arrival alone is not a reason to paint anything, and the one
* the reaction line has always had.
*
* 1. HYDRATION. The hero is deliberately NOT mount-gated — it paints from the
* server-dehydrated digest on the SSR pass because it is the LCP element
* (v1.30.9). So anything inside it that differs between the server and the
* browser is a React #418 mismatch. A wall-clock time is exactly that: the
* server's locale and timezone are not the reader's. The chip therefore
* renders its SLOT on both passes and fills the time in only after mount.
* These tests render through `renderToStaticMarkup`, which takes the SERVER
* snapshot of `useSyncExternalStore` — i.e. precisely the pass that must
* carry no formatted time.
*
* 2. NO LAYOUT SHIFT. The reaction line REPLACES the lead; it is never a second
* paragraph. The hero stays one lead line tall, so nothing below it moves
* when the line arrives on a poll.
* NO LAYOUT SHIFT. The reaction line REPLACES the lead; it is never a
* second paragraph. The hero stays one lead line tall, so nothing below
* it moves when the line arrives on a poll.
*/
import { describe, it, expect } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
@@ -53,30 +48,56 @@ function digest(over: Partial<DailyDigest> = {}): DailyDigest {
};
}
/** Any hh:mm clock face — the thing the server must never emit. */
/** Any hh:mm clock face — the thing the hero must never print here. */
const CLOCK_FACE = /\d{1,2}:\d{2}/;
describe("TodayHero — the just-in chip", () => {
it("renders no chip when nothing landed", () => {
const html = render(<TodayHero digest={digest()} />);
expect(html).not.toContain('data-slot="today-hero-just-in"');
});
it("renders the chip slot on the server pass, carrying the kind", () => {
describe("TodayHero — an arrival is not a chip", () => {
it("paints no arrival marker when something has landed", () => {
const html = render(
<TodayHero
digest={digest({ justIn: { kind: "sleep_night", at: ARRIVED_AT } })}
/>,
);
// The slot is present on the SSR pass, so the hydration render — which
// takes the same server snapshot — produces identical HTML.
expect(html).toContain('data-slot="today-hero-just-in"');
expect(html).toContain('data-just-in-kind="sleep_night"');
expect(html).toContain("Just in");
expect(html).not.toContain('data-slot="today-hero-just-in"');
expect(html).not.toContain("Just in");
expect(html).not.toContain(ARRIVED_AT);
});
it("renders for a bare account when the arrival has no generated line", () => {
it("prints no clock face beside the score", () => {
// The removed chip was the only wall clock on this surface, and a
// wall clock rendered on the server pass is a hydration mismatch as
// well as noise. Neither is possible once nothing prints one.
const html = render(
<TodayHero
digest={digest({ justIn: { kind: "weight", at: ARRIVED_AT } })}
/>,
);
expect(html).not.toMatch(CLOCK_FACE);
});
it("still shows a pending night, which is a different sentence", () => {
// The freshness note says the day is not complete yet, which changes
// how the number should be read. That one stays.
const html = render(
<TodayHero
digest={digest({
phase: "provisional",
sleepPending: true,
justIn: { kind: "weight", at: ARRIVED_AT },
})}
/>,
);
expect(html).toContain('data-slot="today-hero-sleep-pending"');
expect(html).not.toContain('data-slot="today-hero-just-in"');
});
it("renders nothing at all for an account whose only news was the arrival", () => {
// With no chip there is nothing left for such a digest to say, and a
// bordered empty hero above the tile strip would be worse than
// absence.
const html = render(
<TodayHero
digest={digest({
@@ -89,68 +110,7 @@ describe("TodayHero — the just-in chip", () => {
/>,
);
expect(html).toContain('data-slot="today-hero"');
expect(html).toContain('data-slot="today-hero-just-in"');
});
it("emits NO formatted time and no raw ISO instant on the server pass", () => {
const html = render(
<TodayHero
digest={digest({ justIn: { kind: "weight", at: ARRIVED_AT } })}
/>,
);
const chip = html.slice(html.indexOf('data-slot="today-hero-just-in"'));
// The mismatch itself: a wall clock the browser would render differently.
expect(chip).not.toMatch(CLOCK_FACE);
// And the raw instant must not leak either — it is machine data, not copy.
expect(html).not.toContain(ARRIVED_AT);
});
it("keeps the chip muted meta — never an accent, never an opacity modifier", () => {
const html = render(
<TodayHero
digest={digest({ justIn: { kind: "workout", at: ARRIVED_AT } })}
/>,
);
const row = html.slice(
0,
html.indexOf('data-slot="today-hero-just-in"') + 400,
);
expect(row).toContain("text-muted-foreground");
expect(row).toContain("text-xs");
// UI-STANDARDS §text: alpha on muted text drops below AA contrast.
expect(html).not.toContain("text-muted-foreground/");
expect(html).not.toContain("text-primary");
});
it("shows the chip alongside a still-pending night", () => {
// Legitimately co-occurring: a weight landed this morning while last
// night's sleep has not arrived yet.
const html = render(
<TodayHero
digest={digest({
phase: "provisional",
sleepPending: true,
justIn: { kind: "weight", at: ARRIVED_AT },
})}
/>,
);
expect(html).toContain('data-slot="today-hero-sleep-pending"');
expect(html).toContain('data-slot="today-hero-just-in"');
});
it("localises the chip", () => {
const html = render(
<TodayHero
digest={digest({ justIn: { kind: "sleep_night", at: ARRIVED_AT } })}
/>,
"de",
);
expect(html).toContain("Gerade eingetroffen");
expect(html).not.toContain("Just in");
expect(html).not.toContain('data-slot="today-hero"');
});
});
+20 -89
View File
@@ -37,8 +37,6 @@
import Link from "next/link";
import { Moon } from "lucide-react";
import { useMounted } from "@/hooks/use-mounted";
import { formatTime } from "@/lib/format";
import { ScoreRing } from "@/components/insights/derived/score-ring";
import type { ScoreBand } from "@/components/insights/derived/band-tokens";
import { ProseBlocks } from "@/components/insights/prose-blocks";
@@ -86,32 +84,6 @@ function withoutRepeatedScore(
return result.length > 0 ? result : null;
}
/**
* The "just in" chip's clock face.
*
* Formatted CLIENT-side, and only after mount. `justIn.at` crosses the wire as
* an ISO instant precisely so this stays a client concern: the server has no
* business rendering a local wall-clock time it would format under its OWN
* locale and timezone, and any such string would differ from the browser's on
* the hydration render — React #418, a class of bug this project has already
* paid for once.
*
* Goes through the shared `formatTime`, which reads the profile's timezone and
* clock preference. The bare `toLocaleTimeString(undefined, …)` this replaces
* asked the BROWSER instead, which is a different question: someone whose
* profile says Europe/Berlin while their laptop sits in Manila saw a Manila
* time here and a Berlin time on every other surface, for the same instant.
*/
function formatJustInTime(iso: string): string | null {
const at = new Date(iso);
if (Number.isNaN(at.getTime())) return null;
try {
return formatTime(at);
} catch {
return null;
}
}
export function TodayHero({
digest,
renderFilteredAllClear = false,
@@ -125,7 +97,6 @@ export function TodayHero({
// paints from the server-dehydrated digest on both the SSR and the hydration
// render). So the slot renders on both passes and the formatted time fills in
// on the first client re-render, keeping those two passes byte-identical.
const mounted = useMounted();
const { keep, letGo } = useCoachCheckinAction();
const dismissItem = usePriorityItemDismiss();
@@ -166,7 +137,6 @@ export function TodayHero({
const signalFallbackLead =
hasScore && !lead ? topSignal?.headline?.trim() || null : null;
const displayLead = lead ?? signalFallbackLead;
const justInTime = digest.justIn ? formatJustInTime(digest.justIn.at) : null;
// A score-only all-clear digest has no narrative content for the leading
// column. Keeping the full md ring in that two-column shell left a blank
// column beside a 168 px dial and pushed the actual all-clear read below it.
@@ -191,8 +161,7 @@ export function TodayHero({
!hasScore &&
!hasItems &&
!digest.briefingLead &&
!digest.reactionLine &&
!digest.justIn
!digest.reactionLine
) {
return null;
}
@@ -267,28 +236,15 @@ export function TodayHero({
{/* In the compact fallback the quiet freshness tier belongs next
to the smaller ring. That uses the otherwise-empty leading
space and avoids adding a second row below the dial. */}
{compactAllClear && (digest.sleepPending || digest.justIn) ? (
{compactAllClear && digest.sleepPending ? (
<div className="text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
{digest.sleepPending ? (
<p
data-slot="today-hero-sleep-pending"
className="flex items-center gap-1.5"
>
<Moon className="size-3.5 shrink-0" aria-hidden="true" />
{t("daily.today.sleepPending")}
</p>
) : null}
{digest.justIn ? (
<p
data-slot="today-hero-just-in"
data-just-in-kind={digest.justIn.kind}
className="flex items-center gap-1.5"
>
{mounted && justInTime
? t("daily.today.justInAt", { time: justInTime })
: t("daily.today.justIn")}
</p>
) : null}
<p
data-slot="today-hero-sleep-pending"
className="flex items-center gap-1.5"
>
<Moon className="size-3.5 shrink-0" aria-hidden="true" />
{t("daily.today.sleepPending")}
</p>
</div>
) : null}
</div>
@@ -334,43 +290,18 @@ export function TodayHero({
{/* Meta row — the hero's quiet tier (UI-STANDARDS §text: `text-xs
text-muted-foreground` is the meta floor; never an accent, never an
opacity modifier). Holds the freshness note and the "just in" chip;
they can legitimately co-occur (a weight landed while last night's
sleep is still pending), so they share one wrapping row rather than
competing for the same slot. */}
{!compactAllClear && (digest.sleepPending || digest.justIn) ? (
opacity modifier). Freshness note (plan §2.4) — provisional day,
last night's sleep not yet folded in. Muted, non-blocking,
refreshes in place when the morning job lands. */}
{!compactAllClear && digest.sleepPending ? (
<div className="text-muted-foreground flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
{/* Freshness note (plan §2.4) — provisional day, last night's
sleep not yet folded in. Muted, non-blocking, refreshes in
place when the morning job lands. */}
{digest.sleepPending ? (
<p
data-slot="today-hero-sleep-pending"
className="flex items-center gap-1.5"
>
<Moon className="size-3.5 shrink-0" aria-hidden="true" />
{t("daily.today.sleepPending")}
</p>
) : null}
{/* The "just in" chip — a state change, not a notification. No
badge count, no accent, no animation: the record simply reads
differently on the next refresh, and this names why. It is
present for as long as the arrival is news (the server applies
the window, so the chip expires on a poll rather than on a
client timer) and carries the arrival's local time once the
client has mounted and can format it honestly. */}
{digest.justIn ? (
<p
data-slot="today-hero-just-in"
data-just-in-kind={digest.justIn.kind}
className="flex items-center gap-1.5"
>
{mounted && justInTime
? t("daily.today.justInAt", { time: justInTime })
: t("daily.today.justIn")}
</p>
) : null}
<p
data-slot="today-hero-sleep-pending"
className="flex items-center gap-1.5"
>
<Moon className="size-3.5 shrink-0" aria-hidden="true" />
{t("daily.today.sleepPending")}
</p>
</div>
) : null}