feat(insights): mood calendar and chart arrive together; add via the header plus

On /insights/mood the line chart used to paint first and the calendar,
whose aggregate is the slower read, pushed it down when it landed
(CLS 0.16 at 390 px on a cold load). The page now holds both on
final-height skeletons until the chart's read has settled and the
calendar's has landed, then shows them in the same frame. The calendar's
height is written in container-query units, so its box is the right size
from the first paint, in the skeleton and the painted grid alike. A
server wrapper prefetches both reads in parallel (time-boxed, skipped
under a record switch, off when SSR prefetch is off), so on a warm cache
they are in the first HTML. The route and the prefetch share
`moodInsightsWire`, so the seeded cell is the route's shape.

The "Log a mood entry" and "Take a mental-wellbeing check-in" text links
under the heading are gone. Logging is the header plus, which opens the
mood form in place; the check-in keeps its own place in the navigation.
The plus is one shared component, `SubPageAddButton`, now also behind
the measurement pages' capture link and the workouts page, which drew
its own. An audit of every Insights page found no other text add-button
of this kind; the remaining buttons are empty-state calls to action.
A sweep keeps it that way.

`e2e/v142-qol.spec.ts` covers all three surfaces of this change set.
This commit is contained in:
Marc-André Bombeck committed 2026-10-08 16:45:49 +02:00
1 parent 1a5eab9f7c
commit 41ed8eb6a3
21 files changed
+1223 -339

No files matched your search

+2 -13
View File
@@ -7,6 +7,7 @@ import { cachedSwr, caches, type ServerCache } from "@/lib/cache/server-cache";
import {
fetchMoodAggregates,
MOOD_STABILITY_WINDOWS,
moodInsightsWire,
type MoodAggregates,
type MoodStabilityWindow,
} from "@/lib/insights/mood-aggregates";
@@ -74,17 +75,5 @@ export const GET = apiHandler(async (request: Request) => {
},
});
// The per-window map is the cache's, not the wire's: the chosen window is
// served as `stability`. An aggregate cached before the map existed has
// only the year, so a shorter window reads as not yet computed (`null`)
// until the next rebuild rather than as the year's score under a new name.
const { stabilityByWindow, ...wire } = result;
return apiSuccess({
...wire,
stability:
stabilityWindowDays === 365
? result.stability
: (stabilityByWindow?.[stabilityWindowDays] ?? null),
stabilityWindowDays,
});
return apiSuccess(moodInsightsWire(result, stabilityWindowDays));
});