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; }; }