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.
This commit is contained in:
Marc-André Bombeck committed 2026-11-11 22:05:48 +01:00
1 parent ea6e94adcf
commit b970ea7468
3 files changed
+68

No files matched your search

+13
View File
@@ -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
@@ -143,6 +143,46 @@ describe("useDashboardSnapshot — auto-refresh on an open page", () => {
await (opts.queryFn as () => Promise<unknown>)();
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<unknown>)();
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<unknown>)();
expect(setQueryDataMock).not.toHaveBeenCalledWith(
queryKeys.moodAnalytics(),
expect.anything(),
);
});
});
describe("prefetchDashboardSnapshot — hydration-safe promise handoff", () => {
+15
View File
@@ -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;
};
}