From b970ea746811efb441708972187d7a6dadbfe6ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 11 Nov 2026 22:02:50 +0100 Subject: [PATCH] perf(dashboard): seed the mood chart's series from the snapshot The dashboard snapshot carries the mood tile's series from the same engine `/api/mood/analytics` answers from, and the snapshot already seeds the widget layout, but nothing seeded the mood analytics cell the mood chart reads, so every dashboard open with the mood chart asked for the series again. Both seed points, the server render and the client snapshot read, now fill that cell too, under the same rule as the layout: only an empty cell, and only when the tile has readings, the one case where its summary is the route's. --- src/app/page.tsx | 13 ++++++ .../__tests__/use-dashboard-snapshot.test.tsx | 40 +++++++++++++++++++ src/lib/queries/use-dashboard-snapshot.ts | 15 +++++++ 3 files changed, 68 insertions(+) diff --git a/src/app/page.tsx b/src/app/page.tsx index 48af2ec52..bda704c49 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -102,6 +102,19 @@ export default async function DashboardPage() { const queryClient = new QueryClient(); queryClient.setQueryData(queryKeys.dashboardSnapshot(locale), wireBody); queryClient.setQueryData(queryKeys.dashboardWidgets(), wireBody.layout); + // The mood chart's series rides the mood tile (same engine as + // `/api/mood/analytics`); seeded with readings only, where the tile's + // summary is the route's. + const mood = ( + wireBody.tiles as + { mood?: { entries: unknown[]; summary: unknown } } | undefined + )?.mood; + if (mood?.summary && mood.entries.length > 0) { + queryClient.setQueryData(queryKeys.moodAnalytics(), { + entries: mood.entries, + summary: mood.summary, + }); + } // Above-the-fold prefetch runs ONLY in snapshot mode: legacy mode derives // its chart-type list from analytics queries the RSC did not run, so there diff --git a/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx b/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx index c19d91a69..892a178f3 100644 --- a/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx +++ b/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx @@ -143,6 +143,46 @@ describe("useDashboardSnapshot — auto-refresh on an open page", () => { await (opts.queryFn as () => Promise)(); expect(setQueryDataMock).not.toHaveBeenCalled(); }); + + it("seeds the mood chart's series from the mood tile, so the chart asks for nothing", async () => { + const entries = [{ date: "2026-11-10", score: 4, samples: 1 }]; + const summary = { count: 1, latest: 4 }; + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + envelopeResponse({ + layout: { widgets: [] }, + tiles: { mood: { entries, summary } }, + }), + ), + ); + getQueryDataMock.mockReturnValue(undefined); + useDashboardSnapshot(); + await (lastOpts().queryFn as () => Promise)(); + expect(setQueryDataMock).toHaveBeenCalledWith(queryKeys.moodAnalytics(), { + entries, + summary, + }); + }); + + it("leaves the mood chart to its own read when the tile has no readings", async () => { + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue( + envelopeResponse({ + layout: { widgets: [] }, + tiles: { mood: { entries: [], summary: null } }, + }), + ), + ); + getQueryDataMock.mockReturnValue(undefined); + useDashboardSnapshot(); + await (lastOpts().queryFn as () => Promise)(); + expect(setQueryDataMock).not.toHaveBeenCalledWith( + queryKeys.moodAnalytics(), + expect.anything(), + ); + }); }); describe("prefetchDashboardSnapshot — hydration-safe promise handoff", () => { diff --git a/src/lib/queries/use-dashboard-snapshot.ts b/src/lib/queries/use-dashboard-snapshot.ts index 280564b75..f5b85aff3 100644 --- a/src/lib/queries/use-dashboard-snapshot.ts +++ b/src/lib/queries/use-dashboard-snapshot.ts @@ -194,6 +194,21 @@ function makeSnapshotQueryFn(queryClient: QueryClient) { if (!queryClient.getQueryData(queryKeys.dashboardWidgets())) { queryClient.setQueryData(queryKeys.dashboardWidgets(), snap.layout); } + // The mood tile carries the series `/api/mood/analytics` answers (one + // engine, `buildMoodDailySeries`), which is what the mood chart reads. + // Same seed-when-empty rule, and only with readings, the one case where + // the tile's summary is the route's. + const mood = snap.tiles?.mood; + if ( + mood?.summary && + mood.entries.length > 0 && + !queryClient.getQueryData(queryKeys.moodAnalytics()) + ) { + queryClient.setQueryData(queryKeys.moodAnalytics(), { + entries: mood.entries, + summary: mood.summary, + }); + } return snap; }; }