refactor: rename Dashboard-Lab-Widgets to Dashboard-Widgets and update related documentation
- Renamed references from "Dashboard-Lab-Widgets" to "Dashboard-Widgets" across documentation and codebase for consistency. - Removed the deprecated Dashboard-Lab page and integrated its functionality into the new Dashboard-Widgets layout. - Updated widget registration and configuration handling to reflect the new naming convention. - Adjusted documentation in `.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md` and other related files to ensure clarity on the updated structure. - Bumped application version to reflect these changes.
This commit is contained in:
parent
ddc87ba5ae
commit
141df021c1
|
|
@ -52,7 +52,7 @@ _Dieser Ordner `.claude/docs/` ist per `.gitignore`-Ausnahme **versioniert** (Sp
|
||||||
|--------|-------------|-------------------|
|
|--------|-------------|-------------------|
|
||||||
| Data Layer / Charts (Phase 0c) | `functional/DATA_ARCHITECTURE.md`, `technical/DATA_LAYER_EXTENSION_GUIDE.md` | `backend/data_layer/`, `backend/routers/charts.py` |
|
| Data Layer / Charts (Phase 0c) | `functional/DATA_ARCHITECTURE.md`, `technical/DATA_LAYER_EXTENSION_GUIDE.md` | `backend/data_layer/`, `backend/routers/charts.py` |
|
||||||
| Platzhalter / Registry | `technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md`, `technical/PLACEHOLDER_DEVELOPMENT_GUIDE.md` | `backend/placeholder_registrations/`, `backend/placeholder_resolver.py` |
|
| Platzhalter / Registry | `technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md`, `technical/PLACEHOLDER_DEVELOPMENT_GUIDE.md` | `backend/placeholder_registrations/`, `backend/placeholder_resolver.py` |
|
||||||
| Dashboard-Lab-Widgets | `technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md` | Widget-Katalog + Registrierung (siehe Guide) |
|
| Dashboard-Widgets | `technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md` | Widget-Katalog + Registrierung (siehe Guide) |
|
||||||
| Training Profiler / Resolver | `technical/TRAINING_PROFILE_RESOLVER_LAYER1.md`, `functional/TRAINING_TYPE_PROFILES.md` | Resolver-Module wie im Guide genannt |
|
| Training Profiler / Resolver | `technical/TRAINING_PROFILE_RESOLVER_LAYER1.md`, `functional/TRAINING_TYPE_PROFILES.md` | Resolver-Module wie im Guide genannt |
|
||||||
| Universal CSV Import | `technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md` | `backend/csv_parser/`, `routers/csv_import.py`, `routers/admin_csv_templates.py` |
|
| Universal CSV Import | `technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md` | `backend/csv_parser/`, `routers/csv_import.py`, `routers/admin_csv_templates.py` |
|
||||||
| Aktivität Produktionsreife | `technical/ACTIVITY_PRODUCTION_ARCHITECTURE_AND_PHASES.md` (+ EAV-Guide) | `backend/data_layer/activity_session_metrics.py`, `activity_metrics.py`, CSV-Orchestrierung |
|
| Aktivität Produktionsreife | `technical/ACTIVITY_PRODUCTION_ARCHITECTURE_AND_PHASES.md` (+ EAV-Guide) | `backend/data_layer/activity_session_metrics.py`, `activity_metrics.py`, CSV-Orchestrierung |
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
# Dashboard-Lab-Widgets – Anleitung für Coding-Agenten
|
# Dashboard-Widgets – Anleitung für Coding-Agenten
|
||||||
|
|
||||||
Ziel: Ein neues Dashboard-Widget **end-to-end** korrekt einbinden (Backend-Katalog, Validierung, API-Layout, Frontend-Registrierung, optional Lab-Editor für `config`).
|
Ziel: Ein neues Dashboard-Widget **end-to-end** korrekt einbinden (Backend-Katalog, Validierung, API-Layout, Frontend-Registrierung, optional Editor für `config` in **Übersicht anpassen**).
|
||||||
Kontext: **Dashboard-Lab** unter geschützten Endpoints `GET/PUT /api/app/...` (siehe `backend/routers/app_dashboard.py`). Layout liegt pro Profil in `profiles.dashboard_layout` (JSON).
|
Kontext: Geschützte Endpoints `GET/PUT /api/app/...` (siehe `backend/routers/app_dashboard.py`). Layout liegt pro Profil in `profiles.dashboard_layout` (JSON). Nutzer-Oberfläche: `frontend/src/pages/DashboardConfigurePage.jsx` (Route z. B. `/settings/dashboard-layout`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ Kontext: **Dashboard-Lab** unter geschützten Endpoints `GET/PUT /api/app/...` (
|
||||||
| Anforderung | Beschreibung |
|
| Anforderung | Beschreibung |
|
||||||
|-------------|--------------|
|
|-------------|--------------|
|
||||||
| **A1 – Zentrale Auflösung** | Backend ermittelt pro Profil (effektiver Tier + Restrictions), welche Widget-IDs **erlaubt** sind – idealerweise in **einer** Stelle (Erweiterung des Katalog-Endpoints oder dedizierter Entitlements-Teil der Response). Intern: `check_feature_access` und später ggf. Mapping Widget-ID → Feature-ID(n) / Cluster. |
|
| **A1 – Zentrale Auflösung** | Backend ermittelt pro Profil (effektiver Tier + Restrictions), welche Widget-IDs **erlaubt** sind – idealerweise in **einer** Stelle (Erweiterung des Katalog-Endpoints oder dedizierter Entitlements-Teil der Response). Intern: `check_feature_access` und später ggf. Mapping Widget-ID → Feature-ID(n) / Cluster. |
|
||||||
| **A2 – Nutzer-Konfigurator** | Im Dashboard-Lab (und jedem späteren Layout-Konfigurator): Widgets **ohne Berechtigung nicht anbieten** (ausgeblendet oder gar nicht in der Liste). Alle **erlaubten** Widgets bleiben wie heute wählbar. |
|
| **A2 – Nutzer-Konfigurator** | Im Layout-Konfigurator (**Übersicht anpassen**): Widgets **ohne Berechtigung nicht anbieten** (ausgeblendet oder gar nicht in der Liste). Alle **erlaubten** Widgets bleiben wie heute wählbar. |
|
||||||
| **A3 – Layout-Persistenz** | `PUT /api/app/dashboard-layout`: Layout darf **keine** nicht erlaubten Widgets dauerhaft speichern – entweder **ablehnen** (422) oder **beim Speichern entfernen/deaktivieren** (Policy festlegen und dokumentieren). Verhindert „gespeichert, aber nie sichtbar“-Zombies. |
|
| **A3 – Layout-Persistenz** | `PUT /api/app/dashboard-layout`: Layout darf **keine** nicht erlaubten Widgets dauerhaft speichern – entweder **ablehnen** (422) oder **beim Speichern entfernen/deaktivieren** (Policy festlegen und dokumentieren). Verhindert „gespeichert, aber nie sichtbar“-Zombies. |
|
||||||
| **A4 – API-/Datenschutz** | Sichtbarkeit im UI reicht nicht: Endpoints, die **Inhalte** für gated Widgets liefern (Charts, KI, …), müssen weiterhin wie heute **eigenständig** über Features abgesichert sein (`check_feature_access`, 403). |
|
| **A4 – API-/Datenschutz** | Sichtbarkeit im UI reicht nicht: Endpoints, die **Inhalte** für gated Widgets liefern (Charts, KI, …), müssen weiterhin wie heute **eigenständig** über Features abgesichert sein (`check_feature_access`, 403). |
|
||||||
|
|
||||||
|
|
@ -43,7 +43,7 @@ Kontext: **Dashboard-Lab** unter geschützten Endpoints `GET/PUT /api/app/...` (
|
||||||
2. **`backend/dashboard_layout_schema.py`** – `DashboardLayoutPayload`: jede Zeile hat `id`, `enabled`, optional `config`. IDs müssen in `ALLOWED_WIDGET_IDS` sein (aus dem Katalog abgeleitet).
|
2. **`backend/dashboard_layout_schema.py`** – `DashboardLayoutPayload`: jede Zeile hat `id`, `enabled`, optional `config`. IDs müssen in `ALLOWED_WIDGET_IDS` sein (aus dem Katalog abgeleitet).
|
||||||
3. **`backend/dashboard_widget_config.py`** – `validate_widget_entry_config`: **nur** Widgets in `WIDGETS_ALLOWING_CONFIG` dürfen **nicht-leere** `config` haben; Keys werden streng validiert (unbekannte Keys → Fehler).
|
3. **`backend/dashboard_widget_config.py`** – `validate_widget_entry_config`: **nur** Widgets in `WIDGETS_ALLOWING_CONFIG` dürfen **nicht-leere** `config` haben; Keys werden streng validiert (unbekannte Keys → Fehler).
|
||||||
4. **Frontend** – `ensureDashboardWidgetsRegistered()` in `frontend/src/widgetSystem/registerDashboardWidgets.js`: verbindet jede Katalog-ID mit einer React-Komponente und mappt `ctx.layoutEntry.config` auf Props.
|
4. **Frontend** – `ensureDashboardWidgetsRegistered()` in `frontend/src/widgetSystem/registerDashboardWidgets.js`: verbindet jede Katalog-ID mit einer React-Komponente und mappt `ctx.layoutEntry.config` auf Props.
|
||||||
5. **Dashboard-Lab-UI** – `frontend/src/pages/DashboardLabPage.jsx`: Umsortieren, Ein/Aus, Speichern; **zusätzliche** UI nur nötig, wenn das Widget konfigurierbare Felder braucht.
|
5. **Layout-Editor (Produkt)** – `frontend/src/pages/DashboardConfigurePage.jsx`: Umsortieren, Ein/Aus, Speichern; **zusätzliche** UI nur nötig, wenn das Widget konfigurierbare Felder braucht.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ Kontext: **Dashboard-Lab** unter geschützten Endpoints `GET/PUT /api/app/...` (
|
||||||
| Schritt | Datei | Aktion |
|
| Schritt | Datei | Aktion |
|
||||||
|--------|--------|--------|
|
|--------|--------|--------|
|
||||||
| A | `backend/widget_catalog.py` | Neuen Eintrag `{ "id", "title", "description" }` in `WIDGET_CATALOG` einfügen (Reihenfolge = Default-Reihenfolge im Layout). Optional `"requires_feature": "<features.id>"` für Tarif-Gating (`dashboard_widget_entitlements`). |
|
| A | `backend/widget_catalog.py` | Neuen Eintrag `{ "id", "title", "description" }` in `WIDGET_CATALOG` einfügen (Reihenfolge = Default-Reihenfolge im Layout). Optional `"requires_feature": "<features.id>"` für Tarif-Gating (`dashboard_widget_entitlements`). |
|
||||||
| B | `backend/widget_catalog.py` | Optional: ID zu `DEFAULT_LAB_WIDGET_IDS` hinzufügen, wenn es im Standard-Lab **aktiv** sein soll. |
|
| B | `backend/widget_catalog.py` | Optional: ID zu `DEFAULT_LAB_WIDGET_IDS` hinzufügen, wenn es im Server-Standardlayout **aktiv** sein soll (Feld `lab_default_layout` in der Layout-API). |
|
||||||
| C | `frontend/src/components/dashboard-widgets/MyWidget.jsx` (oder Legacy-Widget unter `dashboard-widgets-legacy/`) | React-Komponente implementieren; typischerweise `refreshTick` aus `mapProps` nutzen, um Daten neu zu laden. |
|
| C | `frontend/src/components/dashboard-widgets/MyWidget.jsx` (oder Legacy-Widget unter `dashboard-widgets-legacy/`) | React-Komponente implementieren; typischerweise `refreshTick` aus `mapProps` nutzen, um Daten neu zu laden. |
|
||||||
| D | `frontend/src/widgetSystem/registerDashboardWidgets.js` | `import` + `registerDashboardWidget({ id, Component, mapProps })` – `id` **exakt** wie im Katalog. |
|
| D | `frontend/src/widgetSystem/registerDashboardWidgets.js` | `import` + `registerDashboardWidget({ id, Component, mapProps })` – `id` **exakt** wie im Katalog. |
|
||||||
| E | `backend/tests/test_widget_catalog.py` | Läuft implizit mit; bei Strukturänderungen Katalog-Tests beachten. |
|
| E | `backend/tests/test_widget_catalog.py` | Läuft implizit mit; bei Strukturänderungen Katalog-Tests beachten. |
|
||||||
|
|
@ -110,11 +110,11 @@ mapProps: (ctx) => ({
|
||||||
|
|
||||||
**Abgleich mit Chart-Zeitraum:** Für `chart_days` existiert `frontend/src/widgetSystem/bodyChartDays.js` (`BODY_CHART_DAYS_MIN/MAX`, `normalizeBodyChartDays`). Entweder in `mapProps` normalisieren (wie `body_overview`) oder rohen Wert durchreichen und in der Widget-Komponente normalisieren (wie `nutrition_detail_charts` / `TrendKcalWeightWidget`) – **beides** ist im Projekt vertreten; wichtig ist Konsistenz mit der Backend-Grenze 7–90.
|
**Abgleich mit Chart-Zeitraum:** Für `chart_days` existiert `frontend/src/widgetSystem/bodyChartDays.js` (`BODY_CHART_DAYS_MIN/MAX`, `normalizeBodyChartDays`). Entweder in `mapProps` normalisieren (wie `body_overview`) oder rohen Wert durchreichen und in der Widget-Komponente normalisieren (wie `nutrition_detail_charts` / `TrendKcalWeightWidget`) – **beides** ist im Projekt vertreten; wichtig ist Konsistenz mit der Backend-Grenze 7–90.
|
||||||
|
|
||||||
### 3.4 Dashboard-Lab-Editor (`DashboardLabPage.jsx`)
|
### 3.4 Layout-Editor (`DashboardConfigurePage.jsx`)
|
||||||
|
|
||||||
Ohne UI-Änderung bleibt `config` beim Nutzer `{}` – konfigurierbare Widgets brauchen **Editor-Controls**:
|
Ohne UI-Änderung bleibt `config` beim Nutzer `{}` – konfigurierbare Widgets brauchen **Editor-Controls**:
|
||||||
|
|
||||||
- **Einfaches Zahlfeld `chart_days`:** Eintrag in `CHART_DAYS_WIDGET_IDS` (Set oben in der Datei) + bestehendes Label/`aria-label`-Pattern für die Zeitraum-Zeile erweitern (siehe `body_overview`, `nutrition_detail_charts`).
|
- **Einfaches Zahlfeld `chart_days`:** Eintrag in `CHART_DAYS_WIDGET_IDS` (Set oben in `DashboardConfigurePage.jsx`) + bestehendes Label/`aria-label`-Pattern für die Zeitraum-Zeile erweitern (siehe `body_overview`, `nutrition_detail_charts`).
|
||||||
- **Strukturierte Config (Listen, mehrere Booleans):** Eigenes Editor-Komponenten-File nach Vorbild `KpiBoardConfigEditor.jsx` / `QuickCaptureConfigEditor.jsx` einbinden und `setLayout` + `normalizeLayoutForEditor` wie bei den bestehenden Blöcken verwenden.
|
- **Strukturierte Config (Listen, mehrere Booleans):** Eigenes Editor-Komponenten-File nach Vorbild `KpiBoardConfigEditor.jsx` / `QuickCaptureConfigEditor.jsx` einbinden und `setLayout` + `normalizeLayoutForEditor` wie bei den bestehenden Blöcken verwenden.
|
||||||
|
|
||||||
Nach Speichern ruft die Seite `api.putAppDashboardLayout(layout)` auf; das Backend validiert über `DashboardLayoutPayload` → `validate_widget_entry_config`.
|
Nach Speichern ruft die Seite `api.putAppDashboardLayout(layout)` auf; das Backend validiert über `DashboardLayoutPayload` → `validate_widget_entry_config`.
|
||||||
|
|
@ -137,7 +137,7 @@ Nach Speichern ruft die Seite `api.putAppDashboardLayout(layout)` auf; das Backe
|
||||||
## 5. API zum Prüfen
|
## 5. API zum Prüfen
|
||||||
|
|
||||||
- `GET /api/app/widgets/catalog` – Katalog inkl. `allowed` je Widget (Auth + `X-Profile-Id` wie andere App-Endpoints).
|
- `GET /api/app/widgets/catalog` – Katalog inkl. `allowed` je Widget (Auth + `X-Profile-Id` wie andere App-Endpoints).
|
||||||
- `GET /api/app/dashboard-layout` – `layout` (effektiv, bereinigt), `custom`, `product_default_layout` (Übersichts-Standard), `lab_default_layout` (Dashboard-Lab-Standard).
|
- `GET /api/app/dashboard-layout` – `layout` (effektiv, bereinigt), `custom`, `product_default_layout` (Übersichts-Standard), `lab_default_layout` (Servertemplate für Editor/Reset; Feldname historisch).
|
||||||
- `PUT /api/app/dashboard-layout` – Body `{ "version": 1, "widgets": [ ... ] }` (unerlaubte Widgets werden auf `enabled: false` gesetzt).
|
- `PUT /api/app/dashboard-layout` – Body `{ "version": 1, "widgets": [ ... ] }` (unerlaubte Widgets werden auf `enabled: false` gesetzt).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
@ -160,4 +160,4 @@ Nach Speichern ruft die Seite `api.putAppDashboardLayout(layout)` auf; das Backe
|
||||||
| HTTP | `backend/routers/app_dashboard.py` |
|
| HTTP | `backend/routers/app_dashboard.py` |
|
||||||
| Registry + Render | `frontend/src/widgetSystem/dashboardWidgetRegistry.jsx` |
|
| Registry + Render | `frontend/src/widgetSystem/dashboardWidgetRegistry.jsx` |
|
||||||
| Dashboard-Widget-Registrierung | `frontend/src/widgetSystem/registerDashboardWidgets.js` |
|
| Dashboard-Widget-Registrierung | `frontend/src/widgetSystem/registerDashboardWidgets.js` |
|
||||||
| Lab-UI | `frontend/src/pages/DashboardLabPage.jsx` |
|
| Layout-Editor (Nutzer) | `frontend/src/pages/DashboardConfigurePage.jsx` |
|
||||||
|
|
|
||||||
|
|
@ -455,15 +455,15 @@ NIEMALS gegen mitai.jinkendo.de
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Dashboard-Lab-Widgets und Feature-System
|
## 10. Dashboard-Widgets und Feature-System
|
||||||
|
|
||||||
**Kontext:** Dashboard-Widgets (`backend/widget_catalog.py`, Lab unter `/api/app/...`) und das **Subscription-/Feature-Modell** (`features`, `tier_limits`, `check_feature_access` in `backend/auth.py`) sind **getrennte Schichten**, müssen aber bei tariffrelevanten Widgets **verknüpft** werden.
|
**Kontext:** Dashboard-Widgets (`backend/widget_catalog.py`, API unter `/api/app/...`) und das **Subscription-/Feature-Modell** (`features`, `tier_limits`, `check_feature_access` in `backend/auth.py`) sind **getrennte Schichten**, müssen aber bei tariffrelevanten Widgets **verknüpft** werden.
|
||||||
|
|
||||||
**Bindend:**
|
**Bindend:**
|
||||||
|
|
||||||
1. **Keine fest codierten Tier-Namen** für Widget-Rechte – Tiers und Limits kommen aus der DB.
|
1. **Keine fest codierten Tier-Namen** für Widget-Rechte – Tiers und Limits kommen aus der DB.
|
||||||
2. **Komplexität** (Module aus, Unter-Stufen, KI vs. Standard) liegt in der **Feature-/Subscription-Logik**, nicht verteilt in Widget-Komponenten.
|
2. **Komplexität** (Module aus, Unter-Stufen, KI vs. Standard) liegt in der **Feature-/Subscription-Logik**, nicht verteilt in Widget-Komponenten.
|
||||||
3. **Nutzer-Konfigurator** (z. B. Dashboard-Lab): Widgets **ohne** passende Berechtigung **nicht anzeigen**; alle erlaubten Widgets bleiben verfügbar.
|
3. **Nutzer-Konfigurator** (**Übersicht anpassen** / `DashboardConfigurePage`): Widgets **ohne** passende Berechtigung **nicht anzeigen**; alle erlaubten Widgets bleiben verfügbar.
|
||||||
4. **Backend** liefert die effektive Erlaubnis (z. B. über erweiterten Katalog oder Entitlements), und **validiert beim Speichern** des Layouts, dass keine unerlaubten Widget-IDs persistiert werden (Policy: ablehnen oder strippen – einheitlich halten).
|
4. **Backend** liefert die effektive Erlaubnis (z. B. über erweiterten Katalog oder Entitlements), und **validiert beim Speichern** des Layouts, dass keine unerlaubten Widget-IDs persistiert werden (Policy: ablehnen oder strippen – einheitlich halten).
|
||||||
5. **Daten/API:** Zusätzlich zur UI-Filterung müssen die **inhaltsliefernden Endpoints** weiterhin über `check_feature_access` geschützt sein (kein Leck über direkte API-Aufrufe).
|
5. **Daten/API:** Zusätzlich zur UI-Filterung müssen die **inhaltsliefernden Endpoints** weiterhin über `check_feature_access` geschützt sein (kein Leck über direkte API-Aufrufe).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
> | **Gitea-Landkarte (lokal gepflegt)** | **`.claude/docs/GITEA_ISSUES_INDEX.md`** |
|
> | **Gitea-Landkarte (lokal gepflegt)** | **`.claude/docs/GITEA_ISSUES_INDEX.md`** |
|
||||||
> | **Universal CSV Import** (neues Modul / Executor / Vorlagen) | **`.claude/docs/technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md`** |
|
> | **Universal CSV Import** (neues Modul / Executor / Vorlagen) | **`.claude/docs/technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md`** |
|
||||||
> | **GUI / IA / Admin / Nav / PWA-Leiste** | **`docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md`** |
|
> | **GUI / IA / Admin / Nav / PWA-Leiste** | **`docs/issues/GUI_IA_ADMIN_NAV_2026-04-05.md`** |
|
||||||
> | **Dashboard-Lab-Widgets** (Katalog, Registrierung, `config`) | **`.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md`** |
|
> | **Dashboard-Widgets** (Katalog, Registrierung, `config`) | **`.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md`** |
|
||||||
> | **Agent-Einstieg** | **`.claude/README.md`** |
|
> | **Agent-Einstieg** | **`.claude/README.md`** |
|
||||||
> | **Activity Session Metrics (EAV, Attributprofile)** | **`.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md`** |
|
> | **Activity Session Metrics (EAV, Attributprofile)** | **`.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md`** |
|
||||||
|
|
||||||
|
|
@ -102,7 +102,7 @@ frontend/src/
|
||||||
|
|
||||||
### Updates (23.04.2026 - Dashboard: veraltete Demo-Route entfernt, klare Produkt-Registry)
|
### Updates (23.04.2026 - Dashboard: veraltete Demo-Route entfernt, klare Produkt-Registry)
|
||||||
|
|
||||||
- **Frontend:** Veraltete Visualisierungs-Demo-Route und festes Demo-Layout entfernt; Widget-Registrierung in `frontend/src/widgetSystem/registerDashboardWidgets.js` (`ensureDashboardWidgetsRegistered`). Kern-Widgets unter `frontend/src/components/dashboard-widgets-legacy/`. Chart-Hilfen in `frontend/src/widgetSystem/dashboardChartUtils.js`.
|
- **Frontend:** Veraltete Visualisierungs-Demo-Route und festes Demo-Layout entfernt; Widget-Registrierung in `frontend/src/widgetSystem/registerDashboardWidgets.js` (`ensureDashboardWidgetsRegistered`). Kern-Widgets unter `frontend/src/components/dashboard-widgets-legacy/`. Chart-Hilfen in `frontend/src/widgetSystem/dashboardChartUtils.js`. Experimentelles Layout-Lab entfernt; Konfiguration nur noch **Übersicht anpassen** (`DashboardConfigurePage`).
|
||||||
- **Doku:** `.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md` und Kommentar in `backend/widget_catalog.py` angepasst.
|
- **Doku:** `.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md` und Kommentar in `backend/widget_catalog.py` angepasst.
|
||||||
|
|
||||||
### Updates (09.04.2026 - Universal CSV Import, Prod-Migration abgeschlossen)
|
### Updates (09.04.2026 - Universal CSV Import, Prod-Migration abgeschlossen)
|
||||||
|
|
@ -896,7 +896,7 @@ Bottom-Padding Mobile: 80px (Navigation)
|
||||||
|Auth-Flow|`.claude/library/AUTH.md`|Sicherheit + Sessions|
|
|Auth-Flow|`.claude/library/AUTH.md`|Sicherheit + Sessions|
|
||||||
|API-Referenz|`.claude/library/API\_REFERENCE.md`|Alle Endpoints|
|
|API-Referenz|`.claude/library/API\_REFERENCE.md`|Alle Endpoints|
|
||||||
|Datenbankschema|`.claude/library/DATABASE.md`|Tabellen + Beziehungen|
|
|Datenbankschema|`.claude/library/DATABASE.md`|Tabellen + Beziehungen|
|
||||||
|Dashboard-Lab-Widgets|`.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md`|Katalog, Validierung, Frontend-Registry, konfigurierbare `config`|
|
|Dashboard-Widgets|`.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md`|Katalog, Validierung, Frontend-Registry, konfigurierbare `config`|
|
||||||
|Projekt-Doku (Git)|`docs/README.md` + `docs/issues/`|Issue-Specs, Reviews, Platzhalter-Governance, Status-Snapshots|
|
|Projekt-Doku (Git)|`docs/README.md` + `docs/issues/`|Issue-Specs, Reviews, Platzhalter-Governance, Status-Snapshots|
|
||||||
|
|
||||||
> Library-Dateien werden mit `/document` generiert und nach größeren
|
> Library-Dateien werden mit `/document` generiert und nach größeren
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Dashboard-Layout v1: Validierung, Produkt-Standard (Übersicht) und Lab-Standard.
|
Dashboard-Layout v1: Validierung, Produkt-Standard (Übersicht) und Servertemplate (`lab_default_layout_dict`).
|
||||||
|
|
||||||
Erlaubte Widget-IDs und Reihenfolge: widget_catalog.WIDGET_CATALOG.
|
Erlaubte Widget-IDs und Reihenfolge: widget_catalog.WIDGET_CATALOG.
|
||||||
"""
|
"""
|
||||||
|
|
@ -32,7 +32,7 @@ __all__ = [
|
||||||
|
|
||||||
|
|
||||||
def lab_default_layout_dict() -> dict[str, Any]:
|
def lab_default_layout_dict() -> dict[str, Any]:
|
||||||
"""Standard für Dashboard-Lab (Experimentier-Widgets)."""
|
"""Serverseitiges Standardlayout (DEFAULT_LAB_WIDGET_IDS); API-Feld `lab_default_layout`, u. a. für Editor/Reset."""
|
||||||
on = DEFAULT_LAB_WIDGET_IDS
|
on = DEFAULT_LAB_WIDGET_IDS
|
||||||
return {
|
return {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ from routers import workflow_questions # Phase 1 Workflow Engine - Question Cat
|
||||||
from routers import workflows # Phase 2 Workflow Engine - Execution
|
from routers import workflows # Phase 2 Workflow Engine - Execution
|
||||||
from routers import reference_values # Persönliche Referenzwerte (Profil)
|
from routers import reference_values # Persönliche Referenzwerte (Profil)
|
||||||
from routers import admin_reference_value_types # Admin: Referenzwert-Typen
|
from routers import admin_reference_value_types # Admin: Referenzwert-Typen
|
||||||
from routers import app_dashboard # Geschützter App-Bereich: Dashboard-Lab Layout
|
from routers import app_dashboard # Geschützter App-Bereich: Dashboard-Layout + Widget-Katalog
|
||||||
from routers import csv_import, admin_csv_templates # Issue #21 Universal CSV Parser
|
from routers import csv_import, admin_csv_templates # Issue #21 Universal CSV Parser
|
||||||
from routers import admin_training_parameters, admin_activity_attribute_profiles # EAV session metrics
|
from routers import admin_training_parameters, admin_activity_attribute_profiles # EAV session metrics
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Geschützter App-Bereich: Dashboard-Lab Layout (kein Produktiv-Dashboard).
|
Geschützter App-Bereich: Dashboard-Layout und Widget-Katalog.
|
||||||
|
|
||||||
/api/app/dashboard-layout — nur mit Session + aktivem Profil (X-Profile-Id).
|
/api/app/dashboard-layout — nur mit Session + aktivem Profil (X-Profile-Id).
|
||||||
"""
|
"""
|
||||||
|
|
@ -20,7 +20,7 @@ from db import get_cursor, get_db
|
||||||
from routers.profiles import get_pid
|
from routers.profiles import get_pid
|
||||||
from system_dashboard_product_default import get_product_default_base_dict
|
from system_dashboard_product_default import get_product_default_base_dict
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/app", tags=["app-dashboard-lab"])
|
router = APIRouter(prefix="/api/app", tags=["app-dashboard"])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/widgets/catalog")
|
@router.get("/widgets/catalog")
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
"""
|
"""
|
||||||
Öffentlicher Widget-Katalog (Dashboard-Lab / später Produkt-Dashboard).
|
Öffentlicher Widget-Katalog (konfigurierbare Übersicht / API).
|
||||||
|
|
||||||
Single Source für: erlaubte IDs, Standard-Reihenfolge, Anzeige-Metadaten für API/GUI.
|
Single Source für: erlaubte IDs, Standard-Reihenfolge, Anzeige-Metadaten für API/GUI.
|
||||||
Frontend-Komponenten registrieren dieselben IDs lokal (siehe widgetSystem/registerDashboardWidgets.js, Funktion ensureDashboardWidgetsRegistered).
|
Frontend-Komponenten registrieren dieselben IDs lokal (siehe widgetSystem/registerDashboardWidgets.js, Funktion ensureDashboardWidgetsRegistered).
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import Analysis from './pages/Analysis'
|
||||||
import SettingsPage from './pages/SettingsPage'
|
import SettingsPage from './pages/SettingsPage'
|
||||||
import SettingsShell from './layouts/SettingsShell'
|
import SettingsShell from './layouts/SettingsShell'
|
||||||
import ProfileReferenceValuesPage from './pages/ProfileReferenceValuesPage'
|
import ProfileReferenceValuesPage from './pages/ProfileReferenceValuesPage'
|
||||||
import DashboardLabPage from './pages/DashboardLabPage'
|
|
||||||
import DashboardConfigurePage from './pages/DashboardConfigurePage'
|
import DashboardConfigurePage from './pages/DashboardConfigurePage'
|
||||||
import GuidePage from './pages/GuidePage'
|
import GuidePage from './pages/GuidePage'
|
||||||
import AdminTierLimitsPage from './pages/AdminTierLimitsPage'
|
import AdminTierLimitsPage from './pages/AdminTierLimitsPage'
|
||||||
|
|
@ -270,7 +269,6 @@ function AppShell() {
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="/workflow-editor/:id" element={<WorkflowEditorPage/>}/>
|
<Route path="/workflow-editor/:id" element={<WorkflowEditorPage/>}/>
|
||||||
<Route path="/subscription" element={<SubscriptionPage/>}/>
|
<Route path="/subscription" element={<SubscriptionPage/>}/>
|
||||||
<Route path="/app/dashboard-lab" element={<DashboardLabPage />} />
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -130,8 +130,9 @@ export default function QuickCaptureWidget({ onSaved, captureConfig }) {
|
||||||
<div className="card section-gap">
|
<div className="card section-gap">
|
||||||
<div className="card-title">Schnelleingabe (heute)</div>
|
<div className="card-title">Schnelleingabe (heute)</div>
|
||||||
<p style={{ fontSize: 13, color: 'var(--text3)', margin: 0 }}>
|
<p style={{ fontSize: 13, color: 'var(--text3)', margin: 0 }}>
|
||||||
Für dieses Widget sind keine Eingabebereiche aktiviert. Im Dashboard-Lab die Sichtbarkeit prüfen
|
Für dieses Widget sind keine Eingabebereiche aktiviert. Unter{' '}
|
||||||
oder <Link to="/vitals">Vitalwerte-Seite</Link> nutzen.
|
<Link to="/settings/dashboard-layout">Übersicht anpassen</Link> die Schnelleingabe-Konfiguration prüfen oder{' '}
|
||||||
|
<Link to="/vitals">Vitalwerte-Seite</Link> nutzen.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,512 +0,0 @@
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
|
||||||
import { ChevronDown, ChevronUp, LayoutGrid } from 'lucide-react'
|
|
||||||
import { Link } from 'react-router-dom'
|
|
||||||
import { api, formatFastApiDetail } from '../utils/api'
|
|
||||||
import { WidgetRenderer } from '../widgetSystem/dashboardWidgetRegistry'
|
|
||||||
import { ensureDashboardWidgetsRegistered } from '../widgetSystem/registerDashboardWidgets'
|
|
||||||
import {
|
|
||||||
BODY_CHART_DAYS_DEFAULT,
|
|
||||||
BODY_CHART_DAYS_MAX,
|
|
||||||
BODY_CHART_DAYS_MIN,
|
|
||||||
normalizeBodyChartDays,
|
|
||||||
} from '../widgetSystem/bodyChartDays'
|
|
||||||
import KpiBoardConfigEditor from '../widgetSystem/KpiBoardConfigEditor'
|
|
||||||
import QuickCaptureConfigEditor from '../widgetSystem/QuickCaptureConfigEditor'
|
|
||||||
import BodyHistoryVizConfigEditor from '../widgetSystem/BodyHistoryVizConfigEditor'
|
|
||||||
import NutritionHistoryVizConfigEditor from '../widgetSystem/NutritionHistoryVizConfigEditor'
|
|
||||||
import FitnessHistoryVizConfigEditor from '../widgetSystem/FitnessHistoryVizConfigEditor'
|
|
||||||
import RecoveryHistoryVizConfigEditor from '../widgetSystem/RecoveryHistoryVizConfigEditor'
|
|
||||||
import HistoryOverviewVizConfigEditor from '../widgetSystem/HistoryOverviewVizConfigEditor'
|
|
||||||
import { moveWidget, normalizeLayoutForEditor, toggleWidget } from '../widgetSystem/layoutEditor'
|
|
||||||
|
|
||||||
/** Widgets mit optionalem config.chart_days (7–90), gleiche UX im Editor */
|
|
||||||
const CHART_DAYS_WIDGET_IDS = new Set([
|
|
||||||
'body_overview',
|
|
||||||
'body_history_viz',
|
|
||||||
'activity_overview',
|
|
||||||
'nutrition_detail_charts',
|
|
||||||
'nutrition_history_viz',
|
|
||||||
'fitness_history_viz',
|
|
||||||
'recovery_history_viz',
|
|
||||||
'history_overview_viz',
|
|
||||||
'recovery_charts_panel',
|
|
||||||
])
|
|
||||||
|
|
||||||
function catalogMetaById(catalog) {
|
|
||||||
if (!catalog?.widgets?.length) return {}
|
|
||||||
return Object.fromEntries(catalog.widgets.map((w) => [w.id, w]))
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function DashboardLabPage() {
|
|
||||||
ensureDashboardWidgetsRegistered()
|
|
||||||
|
|
||||||
const [refreshTick, setRefreshTick] = useState(0)
|
|
||||||
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)
|
|
||||||
/** Pro Widget-ID: Rohstring während der Eingabe (Tippen ohne sofortiges Clampen) */
|
|
||||||
const [chartDaysDraftByWidgetId, setChartDaysDraftByWidgetId] = useState({})
|
|
||||||
|
|
||||||
const metaById = catalogMetaById(catalog)
|
|
||||||
|
|
||||||
const isWidgetCatalogAllowed = useCallback(
|
|
||||||
(widgetId) => {
|
|
||||||
const m = metaById[widgetId]
|
|
||||||
if (m == null) return true
|
|
||||||
return m.allowed !== false
|
|
||||||
},
|
|
||||||
[metaById],
|
|
||||||
)
|
|
||||||
|
|
||||||
const visibleEditorIndices = useMemo(
|
|
||||||
() =>
|
|
||||||
layout?.widgets?.map((_, i) => i).filter((i) => isWidgetCatalogAllowed(layout.widgets[i].id)) ?? [],
|
|
||||||
[layout, isWidgetCatalogAllowed],
|
|
||||||
)
|
|
||||||
|
|
||||||
const layoutForPreview = useMemo(
|
|
||||||
() =>
|
|
||||||
layout
|
|
||||||
? {
|
|
||||||
...layout,
|
|
||||||
widgets: layout.widgets.map((w) => ({
|
|
||||||
...w,
|
|
||||||
enabled: w.enabled && isWidgetCatalogAllowed(w.id),
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
[layout, isWidgetCatalogAllowed],
|
|
||||||
)
|
|
||||||
|
|
||||||
const commitChartDaysDraftToLayout = useCallback((draftStr, baseLayout, widgetId) => {
|
|
||||||
const clamped = normalizeBodyChartDays(
|
|
||||||
draftStr === '' || draftStr == null ? BODY_CHART_DAYS_DEFAULT : draftStr
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
...baseLayout,
|
|
||||||
widgets: baseLayout.widgets.map((x) =>
|
|
||||||
x.id !== widgetId ? x : { ...x, config: { ...x.config, chart_days: clamped } }
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
|
||||||
setErr(null)
|
|
||||||
try {
|
|
||||||
const [cat, b] = await Promise.all([api.getAppWidgetsCatalog(), api.getAppDashboardLayout()])
|
|
||||||
setCatalog(cat)
|
|
||||||
setBundle(b)
|
|
||||||
setChartDaysDraftByWidgetId({})
|
|
||||||
setLayout(normalizeLayoutForEditor(b.layout))
|
|
||||||
} catch (e) {
|
|
||||||
setErr(formatFastApiDetail(null, e.message))
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load()
|
|
||||||
}, [load])
|
|
||||||
|
|
||||||
const save = async () => {
|
|
||||||
if (!layout) return
|
|
||||||
let toSave = layout
|
|
||||||
const draftEntries = Object.entries(chartDaysDraftByWidgetId)
|
|
||||||
if (draftEntries.length) {
|
|
||||||
for (const [wid, val] of draftEntries) {
|
|
||||||
toSave = normalizeLayoutForEditor(commitChartDaysDraftToLayout(val, toSave, wid))
|
|
||||||
}
|
|
||||||
setLayout(toSave)
|
|
||||||
setChartDaysDraftByWidgetId({})
|
|
||||||
}
|
|
||||||
setBusy(true)
|
|
||||||
setMsg(null)
|
|
||||||
setErr(null)
|
|
||||||
try {
|
|
||||||
await api.putAppDashboardLayout(toSave)
|
|
||||||
setMsg('Layout gespeichert.')
|
|
||||||
await load()
|
|
||||||
} catch (e) {
|
|
||||||
setErr(formatFastApiDetail(null, e.message))
|
|
||||||
} finally {
|
|
||||||
setBusy(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reset = async () => {
|
|
||||||
if (!confirm('Persönliches Layout löschen und Standard wiederherstellen?')) return
|
|
||||||
setBusy(true)
|
|
||||||
setMsg(null)
|
|
||||||
setErr(null)
|
|
||||||
try {
|
|
||||||
const r = await api.resetAppDashboardLayout()
|
|
||||||
setChartDaysDraftByWidgetId({})
|
|
||||||
setLayout(normalizeLayoutForEditor(r.layout))
|
|
||||||
setMsg('Auf Standard zurückgesetzt.')
|
|
||||||
await load()
|
|
||||||
} catch (e) {
|
|
||||||
setErr(formatFastApiDetail(null, e.message))
|
|
||||||
} finally {
|
|
||||||
setBusy(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const applyDefaultLocal = () => {
|
|
||||||
if (bundle?.lab_default_layout) {
|
|
||||||
setChartDaysDraftByWidgetId({})
|
|
||||||
setLayout(normalizeLayoutForEditor(structuredClone(bundle.lab_default_layout)))
|
|
||||||
setMsg('Lab-Standard geladen (noch nicht gespeichert).')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (err && !layout) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 24, maxWidth: 640, margin: '0 auto' }}>
|
|
||||||
<p style={{ color: '#D85A30' }}>{err}</p>
|
|
||||||
<button type="button" className="btn btn-secondary" onClick={load}>
|
|
||||||
Erneut laden
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!layout) {
|
|
||||||
return (
|
|
||||||
<div style={{ padding: 48, textAlign: 'center' }}>
|
|
||||||
<div className="spinner" style={{ width: 32, height: 32, margin: '0 auto' }} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ paddingBottom: 96, textAlign: 'left', maxWidth: 920, margin: '0 auto' }}>
|
|
||||||
<div style={{ marginBottom: 20 }}>
|
|
||||||
<Link
|
|
||||||
to="/settings"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
style={{ display: 'inline-flex', marginBottom: 12, textDecoration: 'none' }}
|
|
||||||
>
|
|
||||||
← Einstellungen
|
|
||||||
</Link>
|
|
||||||
<h1 className="page-title" style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
||||||
<LayoutGrid size={26} color="var(--accent)" />
|
|
||||||
App-Bereich: Dashboard-Lab
|
|
||||||
</h1>
|
|
||||||
<p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}>
|
|
||||||
Widget-System: Katalog, Registry, Renderer; optional pro Widget <code>config</code> (z. B.{' '}
|
|
||||||
<strong>Körper</strong> / <strong>Aktivität</strong>: Zeitraum 7–90 Tage; <strong>KPI</strong>: Kacheln
|
|
||||||
wählen & sortieren). Layout pro Profil in der DB — dieselben Widgets wie auf der{' '}
|
|
||||||
<Link to="/" style={{ color: 'var(--accent)' }}>
|
|
||||||
Produkt-Übersicht
|
|
||||||
</Link>
|
|
||||||
, hier mit Editor und API-Fokus.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="card"
|
|
||||||
style={{
|
|
||||||
marginBottom: 20,
|
|
||||||
borderStyle: 'dashed',
|
|
||||||
borderColor: 'var(--border2)',
|
|
||||||
background: 'var(--surface2)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="card-title" style={{ fontSize: 14 }}>
|
|
||||||
Layout (v1)
|
|
||||||
</div>
|
|
||||||
{bundle && (
|
|
||||||
<p style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 12 }}>
|
|
||||||
Status: {bundle.custom ? 'individuell gespeichert' : 'Standard (nicht in DB)'}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{err && <p style={{ fontSize: 12, color: '#D85A30', marginBottom: 8 }}>{err}</p>}
|
|
||||||
{msg && <p style={{ fontSize: 12, color: 'var(--accent)', marginBottom: 8 }}>{msg}</p>}
|
|
||||||
<ul style={{ listStyle: 'none', padding: 0, margin: '0 0 12px' }}>
|
|
||||||
{visibleEditorIndices.map((i) => {
|
|
||||||
const w = layout.widgets[i]
|
|
||||||
const label = metaById[w.id]?.title || w.id
|
|
||||||
const chartDaysVal =
|
|
||||||
w.config?.chart_days != null
|
|
||||||
? normalizeBodyChartDays(w.config.chart_days)
|
|
||||||
: BODY_CHART_DAYS_DEFAULT
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={w.id}
|
|
||||||
style={{
|
|
||||||
padding: '8px 0',
|
|
||||||
borderBottom: '1px solid var(--border)',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
gap: 8,
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<label style={{ display: 'flex', alignItems: 'center', gap: 8, flex: '1 1 140px' }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={w.enabled}
|
|
||||||
onChange={() => setLayout((L) => toggleWidget(L, i))}
|
|
||||||
/>
|
|
||||||
<span style={{ fontSize: 14 }}>{label}</span>
|
|
||||||
</label>
|
|
||||||
<div style={{ display: 'flex', gap: 6 }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
style={{ padding: '6px 10px' }}
|
|
||||||
aria-label="Nach oben"
|
|
||||||
onClick={() => setLayout((L) => moveWidget(L, i, -1))}
|
|
||||||
>
|
|
||||||
<ChevronUp size={18} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary"
|
|
||||||
style={{ padding: '6px 10px' }}
|
|
||||||
aria-label="Nach unten"
|
|
||||||
onClick={() => setLayout((L) => moveWidget(L, i, 1))}
|
|
||||||
>
|
|
||||||
<ChevronDown size={18} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{w.id === 'quick_capture' && (
|
|
||||||
<QuickCaptureConfigEditor
|
|
||||||
config={w.config || {}}
|
|
||||||
onChange={(next) =>
|
|
||||||
setLayout((L) =>
|
|
||||||
normalizeLayoutForEditor({
|
|
||||||
...L,
|
|
||||||
widgets: L.widgets.map((x, j) => {
|
|
||||||
if (j !== i) return x
|
|
||||||
const cfg = { ...(x.config || {}) }
|
|
||||||
for (const k of ['show_weight', 'show_resting_hr', 'show_hrv', 'show_vo2_max']) {
|
|
||||||
delete cfg[k]
|
|
||||||
}
|
|
||||||
Object.assign(cfg, next)
|
|
||||||
return { ...x, config: cfg }
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{w.id === 'kpi_board' && (
|
|
||||||
<KpiBoardConfigEditor
|
|
||||||
tiles={Object.prototype.hasOwnProperty.call(w.config || {}, 'tiles') ? w.config.tiles : undefined}
|
|
||||||
onChange={(next) =>
|
|
||||||
setLayout((L) =>
|
|
||||||
normalizeLayoutForEditor({
|
|
||||||
...L,
|
|
||||||
widgets: L.widgets.map((x, j) => {
|
|
||||||
if (j !== i) return x
|
|
||||||
const cfg = { ...(x.config || {}) }
|
|
||||||
if (next === undefined) {
|
|
||||||
delete cfg.tiles
|
|
||||||
} else {
|
|
||||||
cfg.tiles = next
|
|
||||||
}
|
|
||||||
return { ...x, config: cfg }
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{CHART_DAYS_WIDGET_IDS.has(w.id) && (
|
|
||||||
<div style={{ marginTop: 10, marginLeft: 28 }}>
|
|
||||||
<label style={{ fontSize: 12, color: 'var(--text2)', display: 'block', marginBottom: 4 }}>
|
|
||||||
{w.id === 'body_overview'
|
|
||||||
? 'Körper-Chart'
|
|
||||||
: w.id === 'body_history_viz'
|
|
||||||
? 'Körper (Verlauf-Bundle)'
|
|
||||||
: w.id === 'activity_overview'
|
|
||||||
? 'Aktivität (Verteilung & Konsistenz)'
|
|
||||||
: w.id === 'nutrition_detail_charts'
|
|
||||||
? 'Ernährung — Charts'
|
|
||||||
: w.id === 'nutrition_history_viz'
|
|
||||||
? 'Ernährung (Verlauf-Bundle)'
|
|
||||||
: w.id === 'fitness_history_viz'
|
|
||||||
? 'Fitness (Verlauf-Bundle)'
|
|
||||||
: w.id === 'history_overview_viz'
|
|
||||||
? 'Gesamtübersicht (Verlauf-Bundle)'
|
|
||||||
: w.id === 'recovery_history_viz'
|
|
||||||
? 'Erholung (Verlauf-Bundle)'
|
|
||||||
: 'Erholung — Charts'}{' '}
|
|
||||||
— Zeitraum (Tage): {BODY_CHART_DAYS_MIN}–{BODY_CHART_DAYS_MAX}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
autoComplete="off"
|
|
||||||
className="form-input"
|
|
||||||
style={{ maxWidth: 120 }}
|
|
||||||
aria-label={
|
|
||||||
w.id === 'body_overview'
|
|
||||||
? 'Körper-Chart Zeitraum in Tagen'
|
|
||||||
: w.id === 'body_history_viz'
|
|
||||||
? 'Körper Verlauf-Bundle Zeitraum in Tagen'
|
|
||||||
: w.id === 'activity_overview'
|
|
||||||
? 'Aktivität Zeitraum in Tagen'
|
|
||||||
: w.id === 'nutrition_detail_charts'
|
|
||||||
? 'Ernährungs-Charts Zeitraum in Tagen'
|
|
||||||
: w.id === 'nutrition_history_viz'
|
|
||||||
? 'Ernährung Verlauf-Bundle Zeitraum in Tagen'
|
|
||||||
: w.id === 'fitness_history_viz'
|
|
||||||
? 'Fitness Verlauf-Bundle Zeitraum in Tagen'
|
|
||||||
: w.id === 'history_overview_viz'
|
|
||||||
? 'Gesamtübersicht Verlauf-Bundle Zeitraum in Tagen'
|
|
||||||
: w.id === 'recovery_history_viz'
|
|
||||||
? 'Erholung Verlauf-Bundle Zeitraum in Tagen'
|
|
||||||
: 'Erholungs-Charts Zeitraum in Tagen'
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
chartDaysDraftByWidgetId[w.id] !== undefined
|
|
||||||
? chartDaysDraftByWidgetId[w.id]
|
|
||||||
: String(chartDaysVal)
|
|
||||||
}
|
|
||||||
onFocus={() =>
|
|
||||||
setChartDaysDraftByWidgetId((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[w.id]: String(chartDaysVal),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
onChange={(e) =>
|
|
||||||
setChartDaysDraftByWidgetId((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[w.id]: e.target.value,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
onBlur={(e) => {
|
|
||||||
const raw = e.target.value
|
|
||||||
setLayout((L) =>
|
|
||||||
normalizeLayoutForEditor(commitChartDaysDraftToLayout(raw, L, w.id))
|
|
||||||
)
|
|
||||||
setChartDaysDraftByWidgetId((prev) => {
|
|
||||||
const next = { ...prev }
|
|
||||||
delete next[w.id]
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter') e.currentTarget.blur()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{w.id === 'body_history_viz' && (
|
|
||||||
<BodyHistoryVizConfigEditor
|
|
||||||
config={w.config || {}}
|
|
||||||
onChange={(next) =>
|
|
||||||
setLayout((L) =>
|
|
||||||
normalizeLayoutForEditor({
|
|
||||||
...L,
|
|
||||||
widgets: L.widgets.map((x, j) => {
|
|
||||||
if (j !== i) return x
|
|
||||||
if (Object.keys(next).length === 0) return { ...x, config: {} }
|
|
||||||
return { ...x, config: { ...(x.config || {}), ...next } }
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{w.id === 'nutrition_history_viz' && (
|
|
||||||
<NutritionHistoryVizConfigEditor
|
|
||||||
config={w.config || {}}
|
|
||||||
onChange={(next) =>
|
|
||||||
setLayout((L) =>
|
|
||||||
normalizeLayoutForEditor({
|
|
||||||
...L,
|
|
||||||
widgets: L.widgets.map((x, j) => {
|
|
||||||
if (j !== i) return x
|
|
||||||
if (Object.keys(next).length === 0) return { ...x, config: {} }
|
|
||||||
return { ...x, config: { ...(x.config || {}), ...next } }
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{w.id === 'fitness_history_viz' && (
|
|
||||||
<FitnessHistoryVizConfigEditor
|
|
||||||
config={w.config || {}}
|
|
||||||
onChange={(next) =>
|
|
||||||
setLayout((L) =>
|
|
||||||
normalizeLayoutForEditor({
|
|
||||||
...L,
|
|
||||||
widgets: L.widgets.map((x, j) => {
|
|
||||||
if (j !== i) return x
|
|
||||||
if (Object.keys(next).length === 0) return { ...x, config: {} }
|
|
||||||
return { ...x, config: { ...(x.config || {}), ...next } }
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{w.id === 'recovery_history_viz' && (
|
|
||||||
<RecoveryHistoryVizConfigEditor
|
|
||||||
config={w.config || {}}
|
|
||||||
onChange={(next) =>
|
|
||||||
setLayout((L) =>
|
|
||||||
normalizeLayoutForEditor({
|
|
||||||
...L,
|
|
||||||
widgets: L.widgets.map((x, j) => {
|
|
||||||
if (j !== i) return x
|
|
||||||
if (Object.keys(next).length === 0) return { ...x, config: {} }
|
|
||||||
return { ...x, config: { ...(x.config || {}), ...next } }
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{w.id === 'history_overview_viz' && (
|
|
||||||
<HistoryOverviewVizConfigEditor
|
|
||||||
config={w.config || {}}
|
|
||||||
onChange={(next) =>
|
|
||||||
setLayout((L) =>
|
|
||||||
normalizeLayoutForEditor({
|
|
||||||
...L,
|
|
||||||
widgets: L.widgets.map((x, j) => {
|
|
||||||
if (j !== i) return x
|
|
||||||
if (Object.keys(next).length === 0) return { ...x, config: {} }
|
|
||||||
return { ...x, config: { ...(x.config || {}), ...next } }
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
||||||
<button type="button" className="btn btn-primary" disabled={busy} onClick={save}>
|
|
||||||
Speichern
|
|
||||||
</button>
|
|
||||||
<button type="button" className="btn btn-secondary" disabled={busy} onClick={reset}>
|
|
||||||
Zurücksetzen (DB)
|
|
||||||
</button>
|
|
||||||
<button type="button" className="btn btn-secondary" disabled={busy} onClick={applyDefaultLocal}>
|
|
||||||
Standard in Editor laden
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{layoutForPreview && (
|
|
||||||
<WidgetRenderer layout={layoutForPreview} refreshTick={refreshTick} requestRefresh={requestRefresh} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { Save, Download, Upload, Check, LogOut, Key, BarChart3, Target, LayoutGrid, LayoutDashboard } from 'lucide-react'
|
import { Save, Download, Upload, Check, LogOut, Key, BarChart3, Target, LayoutDashboard } from 'lucide-react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { useProfile } from '../context/ProfileContext'
|
import { useProfile } from '../context/ProfileContext'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
|
@ -458,35 +458,6 @@ export default function SettingsPage() {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
|
||||||
className="card section-gap"
|
|
||||||
style={{ borderStyle: 'dashed', borderColor: 'var(--border2)', background: 'var(--surface2)' }}
|
|
||||||
>
|
|
||||||
<div className="card-title" style={{ fontSize: 14 }}>
|
|
||||||
Entwickler: Dashboard-Layout (API)
|
|
||||||
</div>
|
|
||||||
<p style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 12, lineHeight: 1.5 }}>
|
|
||||||
Experimentelles Layout-Lab mit Katalog und API (getrennt von der regulären Übersicht). Die produktive Kachelansicht
|
|
||||||
steuerst du über <strong>Übersicht anpassen</strong> oben.
|
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
to="/app/dashboard-lab"
|
|
||||||
className="btn btn-secondary btn-full"
|
|
||||||
style={{
|
|
||||||
textAlign: 'center',
|
|
||||||
textDecoration: 'none',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 8,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<LayoutGrid size={18} />
|
|
||||||
Dashboard-Lab öffnen
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Auth actions */}
|
{/* Auth actions */}
|
||||||
<div className="card section-gap">
|
<div className="card section-gap">
|
||||||
<div className="card-title">🔐 Konto</div>
|
<div className="card-title">🔐 Konto</div>
|
||||||
|
|
|
||||||
|
|
@ -79,7 +79,7 @@ export const api = {
|
||||||
getProfile: () => req('/profile'),
|
getProfile: () => req('/profile'),
|
||||||
updateActiveProfile:(d)=> req('/profile', jput(d)),
|
updateActiveProfile:(d)=> req('/profile', jput(d)),
|
||||||
|
|
||||||
// App-Bereich: Dashboard-Lab (Layout JSON, Issue #65) + Widget-Katalog
|
// App-Bereich: konfigurierbares Dashboard (Layout JSON) + Widget-Katalog
|
||||||
getAppWidgetsCatalog: () => req('/app/widgets/catalog'),
|
getAppWidgetsCatalog: () => req('/app/widgets/catalog'),
|
||||||
getAppDashboardLayout: () => req('/app/dashboard-layout'),
|
getAppDashboardLayout: () => req('/app/dashboard-layout'),
|
||||||
putAppDashboardLayout: (layout) => req('/app/dashboard-layout', jput(layout)),
|
putAppDashboardLayout: (layout) => req('/app/dashboard-layout', jput(layout)),
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/**
|
/**
|
||||||
* Sichtbarkeit der Teile im Schnelleingabe-Widget (Dashboard-Lab).
|
* Sichtbarkeit der Teile im Schnelleingabe-Widget (Übersicht anpassen).
|
||||||
* Default: alle sichtbar (leeres config).
|
* Default: alle sichtbar (leeres config).
|
||||||
*/
|
*/
|
||||||
const KEYS = [
|
const KEYS = [
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user