feat(dashboard): the daily-overview band becomes opt-in

The hero renders only when the layout carries the literal
heroVisible: true. Legacy blobs without the field, malformed values,
and fresh accounts all keep it off; the Settings toggle persists an
explicit opt-in through the existing layout PUT.
This commit is contained in:
Marc-André Bombeck committed 2026-06-12 19:03:32 +02:00
1 parent 7eeab2ae98
commit 8a5a26b49a
8 files changed
+51 -50

No files matched your search

@@ -7,9 +7,9 @@ import { join } from "node:path";
*
* The hero band mounts between the page header and the tile strip:
*
* - it gates on the layout's `heroVisible` flag (anything but the
* literal `false` renders — the resolver's clamp) AND on snapshot
* mode (the verdict needs the snapshot payload);
* - it gates on the layout's `heroVisible` flag (only the literal
* `true` renders — the hero is opt-in) AND on snapshot mode (the
* verdict needs the snapshot payload);
* - while `primaryLoading` is true (same `!mounted ||` pin the tile
* silhouettes use) a footprint-identical skeleton holds the band,
* so the hero and the tiles swap to content in the SAME render
@@ -30,7 +30,7 @@ describe("dashboard hero — page wiring", () => {
it("gates the hero on snapshot mode + the layout's heroVisible flag", () => {
expect(src).toMatch(
/const\s+heroVisible\s*=\s*snapshotEnabled\s*&&\s*layout\.heroVisible\s*!==\s*false;/,
/const\s+heroVisible\s*=\s*snapshotEnabled\s*&&\s*layout\.heroVisible\s*===\s*true;/,
);
});
@@ -477,7 +477,7 @@ describe("PUT /api/dashboard/widgets — heroVisible thread-through", () => {
expect(updateArg.data.dashboardWidgetsJson.heroVisible).toBe(false);
});
it("defaults heroVisible to true for a first save with no stored layout", async () => {
it("defaults heroVisible to false for a first save with no stored layout", async () => {
vi.mocked(prisma.user.findUnique).mockResolvedValue({
dashboardWidgetsJson: null,
} as never);
@@ -495,7 +495,7 @@ describe("PUT /api/dashboard/widgets — heroVisible thread-through", () => {
.calls[0]?.[0] as unknown as {
data: { dashboardWidgetsJson: { heroVisible: boolean } };
};
expect(updateArg.data.dashboardWidgetsJson.heroVisible).toBe(true);
expect(updateArg.data.dashboardWidgetsJson.heroVisible).toBe(false);
});
it("422s on a non-boolean heroVisible", async () => {
+1 -1
View File
@@ -283,7 +283,7 @@ export const PUT = apiHandler(async (request: NextRequest) => {
mergedChartOverlayPrefs = existingLayout.chartOverlayPrefs ?? {};
}
if (mergedHeroVisible === undefined) {
mergedHeroVisible = existingLayout.heroVisible !== false;
mergedHeroVisible = existingLayout.heroVisible === true;
}
}
const normalized = serializeDashboardLayout({
+2 -2
View File
@@ -709,13 +709,13 @@ export default function DashboardPage() {
// hero band (it derives from the snapshot's server-computed
// `greetingHour`, so the old client-side Intl hour walk retired
// with it). The hero gates on the layout's `heroVisible` flag
// (default on; anything but the literal `false` renders) and holds
// (opt-in; only the literal `true` renders) and holds
// a footprint-identical skeleton while the snapshot is in flight,
// so the band swaps in place — no growth, no greeting flash. When
// the hero does NOT render (toggle off, or snapshot flag off) the
// header takes the greeting back — it must never disappear with the
// band.
const heroVisible = snapshotEnabled && layout.heroVisible !== false;
const heroVisible = snapshotEnabled && layout.heroVisible === true;
const heroSnapshot = snapshotQuery.data;
return (