diff --git a/src/app/api/dashboard/widgets/route.ts b/src/app/api/dashboard/widgets/route.ts index cb56bb4bc..56f43351a 100644 --- a/src/app/api/dashboard/widgets/route.ts +++ b/src/app/api/dashboard/widgets/route.ts @@ -13,27 +13,24 @@ import { resolveDashboardLayout, serializeDashboardLayout, DEFAULT_DASHBOARD_LAYOUT, + DASHBOARD_WIDGET_IDS, type DashboardLayout, } from "@/lib/dashboard-layout"; import { Prisma } from "@/generated/prisma/client"; import { z } from "zod/v4"; import type { NextRequest } from "next/server"; -const widgetIdEnum = z.enum([ - "weight", - "bp", - "pulse", - "bodyFat", - "mood", - "medications", - "sleep", - "steps", - "glucose", - "totalBodyWater", - "boneMass", - "bpInTarget", - "oxygenSaturation", -]); +// Single source of truth — every widget id rendered by the Settings → +// Dashboard UI (`src/components/settings/dashboard-layout-section.tsx` +// iterates the full layout from `DEFAULT_DASHBOARD_LAYOUT`). Missing +// one here makes the PUT 422 silently — the toast surfaces "Layout +// konnte nicht gespeichert werden" — and Marc's tile-toggle looks +// like it does nothing because the save round-trip never completes. +// v1.4.16 A5 root-cause: `achievements` was absent from this enum so +// every save attempted with the achievements widget present (i.e. +// every save against the default layout) was rejected. We now derive +// the enum from `DASHBOARD_WIDGET_IDS` so the two lists cannot drift. +const widgetIdEnum = z.enum(DASHBOARD_WIDGET_IDS); const layoutSchema = z.object({ version: z.literal(1), diff --git a/src/app/page.tsx b/src/app/page.tsx index bed38dc08..b7d7e67c2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -995,28 +995,40 @@ export default function DashboardPage() { * makes the scroll feel deliberate rather than arbitrary on * touch. */} -
- {trendCards.map((entry) => ( -
- {entry.node} -
- ))} -
+ {/* v1.4.16 Fix A5: hide the strip entirely when the user + turned off every tile. Until v1.4.15 the wrapper rendered + an empty grid even with zero tiles — visually a thin gap + Marc described as "awkward". Charts below still render so + the page is not empty; the tile-strip just goes away. + The constraint Marc named — "immer die gesamte Spalte + breit und immer der gleichen Höhe" — is preserved by the + CSS-grid `auto-fit + minmax + auto-rows-fr` track that + continues to give every visible tile equal width / equal + height for any non-zero count. */} + {trendCards.length > 0 && ( +
+ {trendCards.map((entry) => ( +
+ {entry.node} +
+ ))} +
+ )} {charts.map((entry) => (
{entry.node} diff --git a/src/lib/__tests__/dashboard-layout.test.ts b/src/lib/__tests__/dashboard-layout.test.ts index ca37a22c5..1455d7e54 100644 --- a/src/lib/__tests__/dashboard-layout.test.ts +++ b/src/lib/__tests__/dashboard-layout.test.ts @@ -116,3 +116,41 @@ describe("serializeDashboardLayout() — tileVisible persistence", () => { expect(serialized.widgets.map((w) => w.order)).toEqual([0, 1]); }); }); + +/** + * v1.4.16 Fix A5 — top-tile selector real-fix. + * + * Marc reported the toggles in Settings → Dashboard didn't filter + * the dashboard tile-strip. Root cause: the `widgetIdEnum` Zod schema + * in `src/app/api/dashboard/widgets/route.ts` was missing + * `achievements` even though the default layout has it. Every PUT + * therefore 422'd silently against the achievements widget that the + * UI included unconditionally — the server never persisted the toggle + * change, so reload showed the old layout. + * + * The default layout itself is the contract — if a widget is in + * `DEFAULT_DASHBOARD_LAYOUT.widgets`, every consumer (settings UI, + * dashboard renderer, API schema) must support it. This test pins + * the contract by enumerating every widget the default layout + * advertises, so a future addition gets caught here before Marc + * finds it broken in production. + */ +describe("DEFAULT_DASHBOARD_LAYOUT contract", () => { + it("includes the achievements widget (v1.4.15 phase-B4 + v1.4.16 A5)", () => { + const ids = DEFAULT_DASHBOARD_LAYOUT.widgets.map((w) => w.id); + expect(ids).toContain("achievements"); + }); + + it("survives a round-trip through serialize → resolve unchanged", () => { + // The API persists `serializeDashboardLayout(parsed.data)` and + // reads back via `resolveDashboardLayout(row.dashboardWidgetsJson)`. + // A bug in either step (missing widget, wrong order normalization) + // would surface here. Marc's regression: the API schema rejected + // the layout *before* this round-trip even started. + const serialized = serializeDashboardLayout(DEFAULT_DASHBOARD_LAYOUT); + const resolved = resolveDashboardLayout(serialized); + expect(resolved.widgets.map((w) => w.id).sort()).toEqual( + DEFAULT_DASHBOARD_LAYOUT.widgets.map((w) => w.id).sort(), + ); + }); +}); diff --git a/src/lib/dashboard-layout.ts b/src/lib/dashboard-layout.ts index 0b18c2030..0bb7040b4 100644 --- a/src/lib/dashboard-layout.ts +++ b/src/lib/dashboard-layout.ts @@ -5,21 +5,33 @@ * what the default layout is for new users. Null / missing = default. */ -export type DashboardWidgetId = - | "weight" - | "bp" - | "pulse" - | "bodyFat" - | "mood" - | "medications" - | "sleep" - | "steps" - | "glucose" - | "totalBodyWater" - | "boneMass" - | "bpInTarget" - | "oxygenSaturation" - | "achievements"; +/** + * Every widget the dashboard layout knows about. The order of + * entries here is irrelevant — `DEFAULT_DASHBOARD_LAYOUT.widgets[].order` + * is the visual order. This array is exported so the API Zod + * `widgetIdEnum` (in `src/app/api/dashboard/widgets/route.ts`) can be + * built from a single source of truth — v1.4.16 Fix A5 root cause was + * the enum drifting away from the default-layout list, which silently + * 422'd every save. + */ +export const DASHBOARD_WIDGET_IDS = [ + "weight", + "bp", + "pulse", + "bodyFat", + "mood", + "medications", + "sleep", + "steps", + "glucose", + "totalBodyWater", + "boneMass", + "bpInTarget", + "oxygenSaturation", + "achievements", +] as const; + +export type DashboardWidgetId = (typeof DASHBOARD_WIDGET_IDS)[number]; export interface DashboardWidgetConfig { id: DashboardWidgetId;