feat: implement merge_missing_catalog_widgets function to enhance dashboard layout
All checks were successful
Deploy Development / deploy (push) Successful in 56s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 16s

- 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.
This commit is contained in:
Lars 2026-04-22 08:38:38 +02:00
parent 2453da0da1
commit 01c0d1745f
6 changed files with 53 additions and 6 deletions

View File

@ -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

View File

@ -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}

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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',