From e9b2cb19684b68d5da5f2839df05b078f762faa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 08:54:30 +0200 Subject: [PATCH] fix(dashboard): drop retired widget ids from the saved layout on read Users who saved a dashboard layout before v1.4.28 still carry the retired `glp1` widget id in `dashboardWidgetsJson`. The PUT route's Zod enum rejects the entire blob on the next save, surfacing "Layout konnte nicht gespeichert werden" without recovery. The resolver now filters widget ids the current build does not know about, so the GET response is current-build-safe and the Settings UI can round-trip the save cleanly. Same migration shape the resolver already applies to comparison baselines and per-chart overlay prefs. --- src/lib/__tests__/dashboard-layout.test.ts | 22 ++++++++++++++++++++++ src/lib/dashboard-layout.ts | 15 ++++++++++++--- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/lib/__tests__/dashboard-layout.test.ts b/src/lib/__tests__/dashboard-layout.test.ts index 4a0ddc4a9..ccebc0c15 100644 --- a/src/lib/__tests__/dashboard-layout.test.ts +++ b/src/lib/__tests__/dashboard-layout.test.ts @@ -162,6 +162,28 @@ describe("DEFAULT_DASHBOARD_LAYOUT contract", () => { DEFAULT_DASHBOARD_LAYOUT.widgets.map((w) => w.id).sort(), ); }); + + it("drops widget ids the current build no longer knows about", () => { + // v1.4.28 retired the `glp1` tile. Users who saved a layout before + // then still have the orphan id in `dashboardWidgetsJson`; the PUT + // route's Zod enum would reject the entire blob on the next save + // round-trip, surfacing the "Speichern fehlgeschlagen" toast. + // The resolver filters unknown ids on read so the GET shape is + // current-build-safe and the next save round-trips cleanly. + const stored = { + version: 1, + widgets: [ + { id: "weight", visible: true, tileVisible: true, order: 0 }, + { id: "glp1", visible: true, tileVisible: true, order: 1 }, + { id: "bp", visible: true, tileVisible: true, order: 2 }, + ], + }; + const resolved = resolveDashboardLayout(stored); + const ids = resolved.widgets.map((w) => w.id); + expect(ids).not.toContain("glp1"); + expect(ids).toContain("weight"); + expect(ids).toContain("bp"); + }); }); /** diff --git a/src/lib/dashboard-layout.ts b/src/lib/dashboard-layout.ts index 7c9b184b4..0c6b542b0 100644 --- a/src/lib/dashboard-layout.ts +++ b/src/lib/dashboard-layout.ts @@ -229,13 +229,22 @@ export function resolveDashboardLayout(raw: unknown): DashboardLayout { return DEFAULT_DASHBOARD_LAYOUT; } + // Drop widget ids the current build does not know about. v1.4.28 + // retired the `glp1` tile; any user who saved a layout before then + // still has the orphan id in `dashboardWidgetsJson`, and the PUT + // route's Zod enum rejects the entire blob on the next save round- + // trip. Filtering on read keeps the GET shape current-build-safe + // and lets the Settings UI re-PUT a clean array. + const knownIds = new Set(DASHBOARD_WIDGET_IDS); + const filtered = candidate.widgets.filter((w) => knownIds.has(w.id)); + // Merge with defaults so new widgets introduced in later versions show up // automatically (invisible by default, users opt-in). - const savedIds = new Set(candidate.widgets.map((w) => w.id)); + const savedIds = new Set(filtered.map((w) => w.id)); const missing = DEFAULT_DASHBOARD_LAYOUT.widgets.filter( (w) => !savedIds.has(w.id), ); - const maxOrder = Math.max(0, ...candidate.widgets.map((w) => w.order)); + const maxOrder = Math.max(0, ...filtered.map((w) => w.order)); const appended = missing.map((w, i) => ({ ...w, visible: false, // default-invisible on auto-upgrade @@ -247,7 +256,7 @@ export function resolveDashboardLayout(raw: unknown): DashboardLayout { // never had the field (the v1.4.15 schema upgrade). Users who saved // a layout before this release see no behavioural change until they // explicitly toggle the new strip switch. - const normalized = candidate.widgets.map((w) => ({ + const normalized = filtered.map((w) => ({ ...w, tileVisible: typeof w.tileVisible === "boolean" ? w.tileVisible : w.visible, }));