diff --git a/backend/dashboard_layout_schema.py b/backend/dashboard_layout_schema.py
index 63372a4..d0b30c4 100644
--- a/backend/dashboard_layout_schema.py
+++ b/backend/dashboard_layout_schema.py
@@ -1,7 +1,7 @@
"""
Dashboard-Layout v1 (Nutzer-Lab): Validierung und Standard-Layout.
-Single Source für erlaubte Widget-IDs (sync mit Frontend widgetRegistry).
+Erlaubte Widget-IDs und Standard-Reihenfolge: widget_catalog.WIDGET_CATALOG
"""
from __future__ import annotations
@@ -9,27 +9,22 @@ from typing import Any, Literal
from pydantic import BaseModel, Field, model_validator
-ALLOWED_WIDGET_IDS: frozenset[str] = frozenset(
- {
- "welcome",
- "quick_capture",
- "kpi_board",
- "body_overview",
- "activity_overview",
- }
-)
+from widget_catalog import ALLOWED_WIDGET_IDS, WIDGET_CATALOG
+
+# Abwärtskompatibel (Tests importieren weiterhin aus diesem Modul)
+__all__ = [
+ "ALLOWED_WIDGET_IDS",
+ "DashboardLayoutPayload",
+ "DashboardWidgetEntry",
+ "coalesce_effective_layout",
+ "default_layout_dict",
+]
def default_layout_dict() -> dict[str, Any]:
return {
"version": 1,
- "widgets": [
- {"id": "welcome", "enabled": True},
- {"id": "quick_capture", "enabled": True},
- {"id": "kpi_board", "enabled": True},
- {"id": "body_overview", "enabled": True},
- {"id": "activity_overview", "enabled": True},
- ],
+ "widgets": [{"id": e["id"], "enabled": True} for e in WIDGET_CATALOG],
}
diff --git a/backend/routers/app_dashboard.py b/backend/routers/app_dashboard.py
index 7124ed4..3598267 100644
--- a/backend/routers/app_dashboard.py
+++ b/backend/routers/app_dashboard.py
@@ -12,10 +12,18 @@ from auth import require_auth
from dashboard_layout_schema import DashboardLayoutPayload, coalesce_effective_layout, default_layout_dict
from db import get_cursor, get_db
from routers.profiles import get_pid
+from widget_catalog import catalog_response
router = APIRouter(prefix="/api/app", tags=["app-dashboard-lab"])
+@router.get("/widgets/catalog")
+def get_widgets_catalog(session: dict = Depends(require_auth)) -> dict[str, Any]:
+ """Metadaten aller registrierbaren Dashboard-Widgets (IDs, Titel)."""
+ _ = session
+ return catalog_response()
+
+
@router.get("/dashboard-layout")
def get_dashboard_layout(
x_profile_id: Optional[str] = Header(default=None),
diff --git a/backend/tests/test_widget_catalog.py b/backend/tests/test_widget_catalog.py
new file mode 100644
index 0000000..25e186d
--- /dev/null
+++ b/backend/tests/test_widget_catalog.py
@@ -0,0 +1,25 @@
+"""Widget-Katalog: Konsistenz (IDs, Default-Layout, Katalog-Response)."""
+
+from dashboard_layout_schema import default_layout_dict
+from widget_catalog import ALLOWED_WIDGET_IDS, WIDGET_CATALOG, catalog_response
+
+
+def test_catalog_ids_unique_and_match_allowed():
+ ids = [e["id"] for e in WIDGET_CATALOG]
+ assert len(ids) == len(set(ids))
+ assert frozenset(ids) == ALLOWED_WIDGET_IDS
+
+
+def test_default_layout_follows_catalog_order():
+ d = default_layout_dict()
+ assert d["version"] == 1
+ got = [w["id"] for w in d["widgets"]]
+ assert got == [e["id"] for e in WIDGET_CATALOG]
+ assert all(w["enabled"] is True for w in d["widgets"])
+
+
+def test_catalog_response_shape():
+ r = catalog_response()
+ assert r["catalog_version"] == 1
+ assert len(r["widgets"]) == len(WIDGET_CATALOG)
+ assert {w["id"] for w in r["widgets"]} == ALLOWED_WIDGET_IDS
diff --git a/backend/version.py b/backend/version.py
index a54f0a8..20a4d2c 100644
--- a/backend/version.py
+++ b/backend/version.py
@@ -30,7 +30,7 @@ MODULE_VERSIONS = {
"importdata": "1.0.0",
"membership": "2.1.0",
"workflow": "0.6.0", # Phase 4: End Node Template Engine
- "app_dashboard": "1.0.0", # Dashboard-Lab Layout API (/api/app)
+ "app_dashboard": "1.1.0", # Dashboard-Lab + GET /widgets/catalog (Widget-System Iteration 1)
}
CHANGELOG = [
diff --git a/backend/widget_catalog.py b/backend/widget_catalog.py
new file mode 100644
index 0000000..210d398
--- /dev/null
+++ b/backend/widget_catalog.py
@@ -0,0 +1,55 @@
+"""
+Öffentlicher Widget-Katalog (Dashboard-Lab / später Produkt-Dashboard).
+
+Single Source für: erlaubte IDs, Standard-Reihenfolge, Anzeige-Metadaten für API/GUI.
+Frontend-Komponenten registrieren dieselben IDs lokal (siehe widgetSystem/registerPilotLabWidgets).
+"""
+from __future__ import annotations
+
+from typing import Any, TypedDict
+
+
+class WidgetCatalogEntry(TypedDict):
+ id: str
+ title: str
+ description: str
+
+
+# Reihenfolge in der Liste = Standard-Layout (alle default_enabled: True im Default-Layout)
+WIDGET_CATALOG: list[WidgetCatalogEntry] = [
+ {
+ "id": "welcome",
+ "title": "Willkommen",
+ "description": "Begrüßung und Kurzkontext",
+ },
+ {
+ "id": "quick_capture",
+ "title": "Schnelleingabe",
+ "description": "Gewicht und Vitalwerte erfassen",
+ },
+ {
+ "id": "kpi_board",
+ "title": "KPI-Kacheln",
+ "description": "Referenzwerte, KF%, Kalorien",
+ },
+ {
+ "id": "body_overview",
+ "title": "Körper (Chart)",
+ "description": "Gewicht & Kennzahlen",
+ },
+ {
+ "id": "activity_overview",
+ "title": "Aktivität",
+ "description": "Training & Konsistenz",
+ },
+]
+
+ALLOWED_WIDGET_IDS: frozenset[str] = frozenset(e["id"] for e in WIDGET_CATALOG)
+
+
+def catalog_response() -> dict[str, Any]:
+ """Payload für GET /api/app/widgets/catalog."""
+ return {
+ "catalog_version": 1,
+ "widgets": list(WIDGET_CATALOG),
+ }
diff --git a/frontend/src/app/areas/dashboardLab/dashboardLabWidgets.js b/frontend/src/app/areas/dashboardLab/dashboardLabWidgets.js
deleted file mode 100644
index 13db16f..0000000
--- a/frontend/src/app/areas/dashboardLab/dashboardLabWidgets.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * Dashboard-Lab: Widget-Metadaten (IDs müssen mit backend/dashboard_layout_schema.py übereinstimmen).
- */
-export const DASHBOARD_LAB_WIDGET_META = [
- { id: 'welcome', label: 'Willkommen' },
- { id: 'quick_capture', label: 'Schnelleingabe' },
- { id: 'kpi_board', label: 'KPI-Kacheln' },
- { id: 'body_overview', label: 'Körper (Chart)' },
- { id: 'activity_overview', label: 'Aktivität' },
-]
diff --git a/frontend/src/pages/DashboardLabPage.jsx b/frontend/src/pages/DashboardLabPage.jsx
index 18ce9fe..ac09de2 100644
--- a/frontend/src/pages/DashboardLabPage.jsx
+++ b/frontend/src/pages/DashboardLabPage.jsx
@@ -2,45 +2,34 @@ import { useCallback, useEffect, useState } from 'react'
import { ChevronDown, ChevronUp, LayoutGrid } from 'lucide-react'
import { Link } from 'react-router-dom'
import { api, formatFastApiDetail } from '../utils/api'
-import { DASHBOARD_LAB_WIDGET_META } from '../app/areas/dashboardLab/dashboardLabWidgets'
-import PilotWelcome from '../components/pilot/PilotWelcome'
-import PilotQuickCapture from '../components/pilot/PilotQuickCapture'
-import PilotKpiBoard from '../components/pilot/PilotKpiBoard'
-import PilotBodySection from '../components/pilot/PilotBodySection'
-import PilotActivitySection from '../components/pilot/PilotActivitySection'
+import { WidgetRenderer } from '../widgetSystem/dashboardWidgetRegistry'
+import { ensurePilotLabWidgetsRegistered } from '../widgetSystem/registerPilotLabWidgets'
+import { moveWidget, toggleWidget } from '../widgetSystem/layoutEditor'
-const metaById = Object.fromEntries(DASHBOARD_LAB_WIDGET_META.map((m) => [m.id, m]))
-
-function moveWidget(layout, index, delta) {
- const next = [...layout.widgets]
- const j = index + delta
- if (j < 0 || j >= next.length) return layout
- ;[next[index], next[j]] = [next[j], next[index]]
- return { ...layout, widgets: next }
-}
-
-function toggleWidget(layout, index) {
- const next = layout.widgets.map((w, i) =>
- i === index ? { ...w, enabled: !w.enabled } : w
- )
- const anyOn = next.some((w) => w.enabled)
- if (!anyOn) return layout
- return { ...layout, widgets: next }
+function catalogMetaById(catalog) {
+ if (!catalog?.widgets?.length) return {}
+ return Object.fromEntries(catalog.widgets.map((w) => [w.id, w]))
}
export default function DashboardLabPage() {
+ ensurePilotLabWidgetsRegistered()
+
const [refreshTick, setRefreshTick] = useState(0)
- const bump = () => setRefreshTick((t) => t + 1)
+ const requestRefresh = () => setRefreshTick((t) => t + 1)
+ const [catalog, setCatalog] = useState(null)
const [bundle, setBundle] = useState(null)
const [layout, setLayout] = useState(null)
const [err, setErr] = useState(null)
const [busy, setBusy] = useState(false)
const [msg, setMsg] = useState(null)
+ const metaById = catalogMetaById(catalog)
+
const load = useCallback(async () => {
setErr(null)
try {
- const b = await api.getAppDashboardLayout()
+ const [cat, b] = await Promise.all([api.getAppWidgetsCatalog(), api.getAppDashboardLayout()])
+ setCatalog(cat)
setBundle(b)
setLayout(b.layout)
} catch (e) {
@@ -92,23 +81,6 @@ export default function DashboardLabPage() {
}
}
- const renderWidget = (id) => {
- switch (id) {
- case 'welcome':
- return
- Geschützte Route (Login erforderlich). Widget-Reihenfolge und Sichtbarkeit werden pro Profil in der - Datenbank gespeichert — getrennt vom Produktiv-Dashboard. Vergleich:{' '} + Widget-System (Iteration 1): Katalog vom Server, Registry im Frontend, Renderer für alle + Pilot-Module. Layout wird pro Profil persistiert — getrennt vom Produktiv-Dashboard. Vergleich:{' '} - Pilot-Übersicht (festes Layout) + Pilot-Übersicht (festes Standard-Layout) .
@@ -171,15 +141,11 @@ export default function DashboardLabPage() { Status: {bundle.custom ? 'individuell gespeichert' : 'Standard (nicht in DB)'} )} - {err && ( -{err}
- )} - {msg && ( -{msg}
- )} + {err &&{err}
} + {msg &&{msg}
}