From 3b283507b5808505a8bb7951926772bf51a29a40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Mon, 5 Oct 2026 07:51:10 +0200 Subject: [PATCH] fix(dashboard): lead with the stored lines for a digest cached before the lead The hero reads only digest.lead. A digest the service worker cached before the update carries no such field, so the hero lost its lead line until the next fetch. Such a digest now falls back to the reaction line or the briefing lead it was built to show; a current digest whose lead is null stays silent. --- .../daily/__tests__/today-hero.test.tsx | 31 +++++++++++++++++++ src/components/daily/today-hero.tsx | 9 ++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/components/daily/__tests__/today-hero.test.tsx b/src/components/daily/__tests__/today-hero.test.tsx index 6285070da..648a838cd 100644 --- a/src/components/daily/__tests__/today-hero.test.tsx +++ b/src/components/daily/__tests__/today-hero.test.tsx @@ -185,6 +185,37 @@ describe("", () => { expect(html).toContain('data-slot="today-hero-lead"'); }); + it("falls back to the stored lines for a digest cached before the lead existed", () => { + // The service worker's offline cache can hand back a digest written by + // the previous version, which carries no `lead` field at all. + const cached = digest({ reactionLine: "The new reading fits your week." }); + delete (cached as Partial).lead; + const html = render(); + expect(visibleText(html)).toContain("The new reading fits your week."); + + const briefingOnly = digest(); + delete (briefingOnly as Partial).lead; + expect(visibleText(render())).toContain( + "Your week is trending steady.", + ); + }); + + it("does not resurrect a line the server left out of the lead", () => { + // A current digest with `lead: null` decided there is nothing to say; + // its reaction line (e.g. one that only repeated the score) stays out. + const html = render( + , + ); + expect(visibleText(html)).not.toContain("Score 82 today."); + }); + it("does not repeat the top signal under a lead made from it", () => { // A briefing whose only sentence repeated the score leads with the top // signal's headline (resolved on the server); the muted signal line under diff --git a/src/components/daily/today-hero.tsx b/src/components/daily/today-hero.tsx index a1544f88e..bdd649bb3 100644 --- a/src/components/daily/today-hero.tsx +++ b/src/components/daily/today-hero.tsx @@ -141,8 +141,13 @@ export function TodayHero({ // a briefing sentence with content while AI text is available, otherwise a // deterministic sentence about the day's strongest signal. Greetings and a // sentence that only repeats the ring's number never reach it, so the hero - // renders it verbatim and decides nothing about it. - const lead = digest.lead?.text ?? null; + // renders it verbatim and decides nothing about it. A digest the service + // worker cached before the field existed has no `lead` at all; it falls + // back to the lines that version led with. + const lead = + digest.lead === undefined + ? (digest.reactionLine ?? digest.briefingLead ?? null) + : (digest.lead?.text ?? null); const topSignal = digest.topSignal; // The briefing's top signal rides under an AI lead as its supporting line, // and only with what the lead does not already say: a headline the lead