From 01c0d1745ff97f844882d4e291c0c1f76f3e09a0 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 22 Apr 2026 08:38:38 +0200 Subject: [PATCH] feat: implement merge_missing_catalog_widgets function to enhance dashboard layout - Added the `merge_missing_catalog_widgets` function to append missing widget IDs from the catalog to the dashboard layout while preserving the existing order. - Updated the admin and app dashboard routes to utilize the new function, ensuring that new catalog entries are visible without requiring users to reset their layouts. - Enhanced tests to validate the functionality of the new merging logic, ensuring proper integration with existing layouts. - Bumped application version to reflect these changes. --- backend/dashboard_layout_schema.py | 21 +++++++++++++++++++ backend/routers/admin.py | 11 +++++++--- backend/routers/app_dashboard.py | 5 ++++- backend/tests/test_dashboard_layout_schema.py | 17 +++++++++++++++ backend/version.py | 4 ++-- frontend/src/pages/DashboardConfigurePage.jsx | 1 + 6 files changed, 53 insertions(+), 6 deletions(-) diff --git a/backend/dashboard_layout_schema.py b/backend/dashboard_layout_schema.py index 8ed8713..017ab32 100644 --- a/backend/dashboard_layout_schema.py +++ b/backend/dashboard_layout_schema.py @@ -5,6 +5,7 @@ Erlaubte Widget-IDs und Reihenfolge: widget_catalog.WIDGET_CATALOG. """ from __future__ import annotations +import copy from typing import Any, Literal from pydantic import BaseModel, Field, field_validator, model_validator @@ -25,6 +26,7 @@ __all__ = [ "coalesce_effective_layout", "default_layout_dict", "lab_default_layout_dict", + "merge_missing_catalog_widgets", "product_default_layout_dict", ] @@ -52,6 +54,25 @@ def default_layout_dict() -> dict[str, Any]: return product_default_layout_dict() +def merge_missing_catalog_widgets(layout: dict[str, Any]) -> dict[str, Any]: + """ + Hängt fehlende Widget-IDs aus WIDGET_CATALOG an (enabled=False, leere config). + Bestehende Reihenfolge bleibt erhalten — nötig, damit neue Katalog-Einträge in + „Übersicht anpassen“ / Lab erscheinen, ohne dass Nutzer:innen das Layout resetten müssen. + """ + out = copy.deepcopy(layout) + widgets: list[dict[str, Any]] = list(out.get("widgets") or []) + seen: set[str] = {str(w["id"]) for w in widgets if w.get("id")} + for e in WIDGET_CATALOG: + wid = e["id"] + if wid not in seen: + widgets.append({"id": wid, "enabled": False, "config": {}}) + seen.add(wid) + out["version"] = out.get("version", 1) + out["widgets"] = widgets + return out + + class DashboardWidgetEntry(BaseModel): id: str = Field(min_length=1, max_length=64) enabled: bool = True diff --git a/backend/routers/admin.py b/backend/routers/admin.py index 940c5d4..a43dc0a 100644 --- a/backend/routers/admin.py +++ b/backend/routers/admin.py @@ -14,7 +14,12 @@ from fastapi import APIRouter, HTTPException, Depends from db import get_db, get_cursor, r2d from auth import require_admin, hash_pin from models import AdminProfileUpdate -from dashboard_layout_schema import ALLOWED_WIDGET_IDS, DashboardLayoutPayload, product_default_layout_dict +from dashboard_layout_schema import ( + ALLOWED_WIDGET_IDS, + DashboardLayoutPayload, + merge_missing_catalog_widgets, + product_default_layout_dict, +) from dashboard_widget_entitlements import widgets_catalog_admin_payload from widget_catalog import WIDGET_CATALOG from widget_feature_requirements_db import ( @@ -184,7 +189,7 @@ def admin_get_dashboard_product_default(session: dict = Depends(require_admin)): """Aktueller Produkt-Dashboard-Standard (DB oder Code).""" _ = session with get_db() as conn: - layout = get_product_default_base_dict(conn) + layout = merge_missing_catalog_widgets(get_product_default_base_dict(conn)) from_database = get_stored_product_default_validated(conn) is not None code_ref = product_default_layout_dict() return { @@ -217,7 +222,7 @@ def admin_delete_dashboard_product_default(session: dict = Depends(require_admin _ = session with get_db() as conn: delete_product_default_override(conn) - layout = get_product_default_base_dict(conn) + layout = merge_missing_catalog_widgets(get_product_default_base_dict(conn)) return {"ok": True, "layout": layout, "from_database": False} diff --git a/backend/routers/app_dashboard.py b/backend/routers/app_dashboard.py index 3f0aff6..fe8fdc5 100644 --- a/backend/routers/app_dashboard.py +++ b/backend/routers/app_dashboard.py @@ -13,6 +13,7 @@ from dashboard_layout_schema import ( DashboardLayoutPayload, coalesce_effective_layout, lab_default_layout_dict, + merge_missing_catalog_widgets, ) from dashboard_widget_entitlements import apply_entitlements_to_layout_dict, widgets_catalog_payload from db import get_cursor, get_db @@ -51,9 +52,11 @@ def get_dashboard_layout( raw = row["dashboard_layout"] if row else None custom, effective = coalesce_effective_layout(raw) with get_db() as conn: - base_product = get_product_default_base_dict(conn) + base_product = merge_missing_catalog_widgets(get_product_default_base_dict(conn)) if not custom: effective = base_product + else: + effective = merge_missing_catalog_widgets(effective) effective = apply_entitlements_to_layout_dict(effective, pid, conn) product_adj = apply_entitlements_to_layout_dict(base_product, pid, conn) lab_adj = apply_entitlements_to_layout_dict(lab_default_layout_dict(), pid, conn) diff --git a/backend/tests/test_dashboard_layout_schema.py b/backend/tests/test_dashboard_layout_schema.py index 60bc7ac..31112cc 100644 --- a/backend/tests/test_dashboard_layout_schema.py +++ b/backend/tests/test_dashboard_layout_schema.py @@ -5,6 +5,7 @@ from dashboard_layout_schema import ( DashboardLayoutPayload, coalesce_effective_layout, default_layout_dict, + merge_missing_catalog_widgets, ) from widget_catalog import DEFAULT_PRODUCT_DASHBOARD_WIDGET_IDS @@ -56,3 +57,19 @@ def test_coalesce_valid_raw(): custom, eff = coalesce_effective_layout(raw) assert custom is True assert eff == raw + + +def test_merge_missing_catalog_widgets_keeps_order_and_fills_ids(): + raw = { + "version": 1, + "widgets": [ + {"id": "kpi_board", "enabled": True, "config": {}}, + {"id": "welcome", "enabled": False, "config": {}}, + ], + } + merged = merge_missing_catalog_widgets(raw) + assert [w["id"] for w in merged["widgets"][:2]] == ["kpi_board", "welcome"] + assert {w["id"] for w in merged["widgets"]} == ALLOWED_WIDGET_IDS + extra = [w for w in merged["widgets"] if w["id"] not in ("kpi_board", "welcome")] + assert all(w["enabled"] is False for w in extra) + DashboardLayoutPayload.model_validate(merged) diff --git a/backend/version.py b/backend/version.py index 98586bd..c772c0d 100644 --- a/backend/version.py +++ b/backend/version.py @@ -24,13 +24,13 @@ MODULE_VERSIONS = { "photos": "1.0.0", "insights": "1.3.0", "prompts": "1.1.0", - "admin": "1.4.0", # Widget × Feature-Zuordnung (Migration 041) + "admin": "1.4.1", # Produkt-Dashboard-Standard GET/DELETE: merge_missing_catalog_widgets "stats": "1.0.1", "exportdata": "1.1.0", "importdata": "1.0.0", "membership": "2.1.0", "workflow": "0.7.0", # Part 3: Inline Prompts (reference + inline mode) - "app_dashboard": "1.12.0", # Widget body_history_viz (Verlauf body-history-viz Bundle) + "app_dashboard": "1.12.1", # GET layout: merge_missing_catalog_widgets (neue Katalog-IDs sichtbar) "csv_import": "0.3.2", # Import-Fehler: enrich_row_error / freundlichere 500-Hinweise "admin_csv_templates": "0.3.0", # POST /validate + Speichern nur bei valid (422 + warnings in Response) } diff --git a/frontend/src/pages/DashboardConfigurePage.jsx b/frontend/src/pages/DashboardConfigurePage.jsx index 17e5337..5271bbb 100644 --- a/frontend/src/pages/DashboardConfigurePage.jsx +++ b/frontend/src/pages/DashboardConfigurePage.jsx @@ -20,6 +20,7 @@ import { const CHART_DAYS_WIDGET_IDS = new Set([ 'body_overview', + 'body_history_viz', 'activity_overview', 'nutrition_detail_charts', 'recovery_charts_panel',