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` |
|
||||
| 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 |
|
||||
| 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 |
|
||||
|
|
|
|||
|
|
@ -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`).
|
||||
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).
|
||||
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: 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 |
|
||||
|-------------|--------------|
|
||||
| **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. |
|
||||
| **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).
|
||||
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.
|
||||
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 |
|
||||
|--------|--------|--------|
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
|
@ -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.
|
||||
|
||||
### 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**:
|
||||
|
||||
- **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.
|
||||
|
||||
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
|
||||
|
||||
- `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).
|
||||
|
||||
---
|
||||
|
|
@ -160,4 +160,4 @@ Nach Speichern ruft die Seite `api.putAppDashboardLayout(layout)` auf; das Backe
|
|||
| HTTP | `backend/routers/app_dashboard.py` |
|
||||
| Registry + Render | `frontend/src/widgetSystem/dashboardWidgetRegistry.jsx` |
|
||||
| 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:**
|
||||
|
||||
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.
|
||||
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).
|
||||
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`** |
|
||||
> | **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`** |
|
||||
> | **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`** |
|
||||
> | **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)
|
||||
|
||||
- **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.
|
||||
|
||||
### 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|
|
||||
|API-Referenz|`.claude/library/API\_REFERENCE.md`|Alle Endpoints|
|
||||
|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|
|
||||
|
||||
> 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.
|
||||
"""
|
||||
|
|
@ -32,7 +32,7 @@ __all__ = [
|
|||
|
||||
|
||||
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
|
||||
return {
|
||||
"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 reference_values # Persönliche Referenzwerte (Profil)
|
||||
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 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).
|
||||
"""
|
||||
|
|
@ -20,7 +20,7 @@ from db import get_cursor, get_db
|
|||
from routers.profiles import get_pid
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
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 SettingsShell from './layouts/SettingsShell'
|
||||
import ProfileReferenceValuesPage from './pages/ProfileReferenceValuesPage'
|
||||
import DashboardLabPage from './pages/DashboardLabPage'
|
||||
import DashboardConfigurePage from './pages/DashboardConfigurePage'
|
||||
import GuidePage from './pages/GuidePage'
|
||||
import AdminTierLimitsPage from './pages/AdminTierLimitsPage'
|
||||
|
|
@ -270,7 +269,6 @@ function AppShell() {
|
|||
</Route>
|
||||
<Route path="/workflow-editor/:id" element={<WorkflowEditorPage/>}/>
|
||||
<Route path="/subscription" element={<SubscriptionPage/>}/>
|
||||
<Route path="/app/dashboard-lab" element={<DashboardLabPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -130,8 +130,9 @@ export default function QuickCaptureWidget({ onSaved, captureConfig }) {
|
|||
<div className="card section-gap">
|
||||
<div className="card-title">Schnelleingabe (heute)</div>
|
||||
<p style={{ fontSize: 13, color: 'var(--text3)', margin: 0 }}>
|
||||
Für dieses Widget sind keine Eingabebereiche aktiviert. Im Dashboard-Lab die Sichtbarkeit prüfen
|
||||
oder <Link to="/vitals">Vitalwerte-Seite</Link> nutzen.
|
||||
Für dieses Widget sind keine Eingabebereiche aktiviert. Unter{' '}
|
||||
<Link to="/settings/dashboard-layout">Übersicht anpassen</Link> die Schnelleingabe-Konfiguration prüfen oder{' '}
|
||||
<Link to="/vitals">Vitalwerte-Seite</Link> nutzen.
|
||||
</p>
|
||||
</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 { 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 { useProfile } from '../context/ProfileContext'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
|
|
@ -458,35 +458,6 @@ export default function SettingsPage() {
|
|||
</Link>
|
||||
</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 */}
|
||||
<div className="card section-gap">
|
||||
<div className="card-title">🔐 Konto</div>
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export const api = {
|
|||
getProfile: () => req('/profile'),
|
||||
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'),
|
||||
getAppDashboardLayout: () => req('/app/dashboard-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).
|
||||
*/
|
||||
const KEYS = [
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user