feat: remove deprecated demo route and enhance dashboard widget registration
All checks were successful
Deploy Development / deploy (push) Successful in 56s
Build Test / pytest-backend (push) Successful in 5s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 17s

- Removed outdated visualization demo route and fixed demo layout in the frontend.
- Updated widget registration logic in `frontend/src/widgetSystem/registerDashboardWidgets.js` to ensure proper integration of core widgets.
- Adjusted documentation in `.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md` and comments in `backend/widget_catalog.py` to reflect changes.
- Added new dashboard widgets for activity and body overview, enhancing user experience and data visualization capabilities.
- Bumped application version to reflect these changes.
This commit is contained in:
Lars 2026-04-23 15:24:13 +02:00
parent 725e7ffe4b
commit ddc87ba5ae
18 changed files with 70 additions and 138 deletions

View File

@ -42,7 +42,7 @@ Kontext: **Dashboard-Lab** unter geschützten Endpoints `GET/PUT /api/app/...` (
1. **`backend/widget_catalog.py`** `WIDGET_CATALOG`: erlaubte Widget-IDs, Reihenfolge, Titel/Beschreibung für API und Default-Layout. 1. **`backend/widget_catalog.py`** `WIDGET_CATALOG`: erlaubte Widget-IDs, Reihenfolge, Titel/Beschreibung für API und Default-Layout.
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** `ensurePilotLabWidgetsRegistered()` in `frontend/src/widgetSystem/registerPilotLabWidgets.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. **Dashboard-Lab-UI** `frontend/src/pages/DashboardLabPage.jsx`: Umsortieren, Ein/Aus, Speichern; **zusätzliche** UI nur nötig, wenn das Widget konfigurierbare Felder braucht.
--- ---
@ -53,8 +53,8 @@ Kontext: **Dashboard-Lab** unter geschützten Endpoints `GET/PUT /api/app/...` (
|--------|--------|--------| |--------|--------|--------|
| 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 Standard-Lab **aktiv** sein soll. |
| C | `frontend/src/components/dashboard-widgets/MyWidget.jsx` (oder Pilot-Komponente) | 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/registerPilotLabWidgets.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. |
| F | `backend/version.py` | `MODULE_VERSIONS["app_dashboard"]` MINOR erhöhen und kurz kommentieren. | | F | `backend/version.py` | `MODULE_VERSIONS["app_dashboard"]` MINOR erhöhen und kurz kommentieren. |
| G | Build/Tests | `pytest` (z.B. `tests/test_dashboard_layout_schema.py`, `test_widget_catalog.py`); `npm run build` im `frontend`. | | G | Build/Tests | `pytest` (z.B. `tests/test_dashboard_layout_schema.py`, `test_widget_catalog.py`); `npm run build` im `frontend`. |
@ -159,5 +159,5 @@ Nach Speichern ruft die Seite `api.putAppDashboardLayout(layout)` auf; das Backe
| Layout-Pydantic | `backend/dashboard_layout_schema.py` | | Layout-Pydantic | `backend/dashboard_layout_schema.py` |
| 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` |
| Pilot/Lab-Registrierung | `frontend/src/widgetSystem/registerPilotLabWidgets.js` | | Dashboard-Widget-Registrierung | `frontend/src/widgetSystem/registerDashboardWidgets.js` |
| Lab-UI | `frontend/src/pages/DashboardLabPage.jsx` | | Lab-UI | `frontend/src/pages/DashboardLabPage.jsx` |

View File

@ -100,6 +100,11 @@ frontend/src/
**Branch:** develop **Branch:** develop
**Nächster Schritt:** Frontend Chart Integration → Testing → Prod Deploy v0.9i **Nächster Schritt:** Frontend Chart Integration → Testing → Prod Deploy v0.9i
### 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`.
- **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)
- **Agent-Leitfaden:** `.claude/docs/technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md` (Checkliste für neue Import-Module, Executor, Vorlagen, `source=csv`, SAVEPOINT-/Cursor-Regeln) - **Agent-Leitfaden:** `.claude/docs/technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md` (Checkliste für neue Import-Module, Executor, Vorlagen, `source=csv`, SAVEPOINT-/Cursor-Regeln)

View File

@ -2,7 +2,7 @@
Öffentlicher Widget-Katalog (Dashboard-Lab / später Produkt-Dashboard). Öffentlicher Widget-Katalog (Dashboard-Lab / später Produkt-Dashboard).
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/registerPilotLabWidgets). Frontend-Komponenten registrieren dieselben IDs lokal (siehe widgetSystem/registerDashboardWidgets.js, Funktion ensureDashboardWidgetsRegistered).
""" """
from __future__ import annotations from __future__ import annotations

View File

@ -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 PilotVizPage from './pages/PilotVizPage'
import DashboardLabPage from './pages/DashboardLabPage' 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'
@ -271,7 +270,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="/pilot/viz" element={<PilotVizPage />} />
<Route path="/app/dashboard-lab" element={<DashboardLabPage />} /> <Route path="/app/dashboard-lab" element={<DashboardLabPage />} />
</Routes> </Routes>
</main> </main>

View File

@ -8,9 +8,9 @@ import {
BODY_CHART_DAYS_DEFAULT, BODY_CHART_DAYS_DEFAULT,
normalizeBodyChartDays, normalizeBodyChartDays,
} from '../../widgetSystem/bodyChartDays' } from '../../widgetSystem/bodyChartDays'
import PilotRuleCard from './PilotRuleCard' import DashboardRuleCard from './DashboardRuleCard'
export default function PilotActivitySection({ refreshTick = 0, chartDays = BODY_CHART_DAYS_DEFAULT }) { export default function ActivityOverviewWidget({ refreshTick = 0, chartDays = BODY_CHART_DAYS_DEFAULT }) {
const periodDays = normalizeBodyChartDays(chartDays) const periodDays = normalizeBodyChartDays(chartDays)
const { activeProfile } = useProfile() const { activeProfile } = useProfile()
const globalQualityLevel = activeProfile?.quality_filter_level const globalQualityLevel = activeProfile?.quality_filter_level
@ -124,7 +124,7 @@ export default function PilotActivitySection({ refreshTick = 0, chartDays = BODY
</Link> </Link>
</p> </p>
) : ( ) : (
actRules.map((item, i) => <PilotRuleCard key={i} item={item} />) actRules.map((item, i) => <DashboardRuleCard key={i} item={item} />)
)} )}
</div> </div>
</div> </div>

View File

@ -15,14 +15,14 @@ import dayjs from 'dayjs'
import { api } from '../../utils/api' import { api } from '../../utils/api'
import { useProfile } from '../../context/ProfileContext' import { useProfile } from '../../context/ProfileContext'
import { getInterpretation } from '../../utils/interpret' import { getInterpretation } from '../../utils/interpret'
import { rollingAvg, fmtDate } from '../../pilot/pilotChartUtils' import { rollingAvg, fmtDate } from '../../widgetSystem/dashboardChartUtils'
import { import {
BODY_CHART_DAYS_DEFAULT, BODY_CHART_DAYS_DEFAULT,
normalizeBodyChartDays, normalizeBodyChartDays,
} from '../../widgetSystem/bodyChartDays' } from '../../widgetSystem/bodyChartDays'
import PilotRuleCard from './PilotRuleCard' import DashboardRuleCard from './DashboardRuleCard'
export default function PilotBodySection({ refreshTick = 0, chartDays = BODY_CHART_DAYS_DEFAULT }) { export default function BodyOverviewWidget({ refreshTick = 0, chartDays = BODY_CHART_DAYS_DEFAULT }) {
const windowDays = normalizeBodyChartDays(chartDays) const windowDays = normalizeBodyChartDays(chartDays)
const { activeProfile } = useProfile() const { activeProfile } = useProfile()
const [weights, setWeights] = useState([]) const [weights, setWeights] = useState([])
@ -221,7 +221,7 @@ export default function PilotBodySection({ refreshTick = 0, chartDays = BODY_CHA
Körperfett, Magermasse (FFMI), BMI gleiche Logik wie auf der Verlauf-Seite (Körper). Körperfett, Magermasse (FFMI), BMI gleiche Logik wie auf der Verlauf-Seite (Körper).
</p> </p>
{rules.map((item, i) => ( {rules.map((item, i) => (
<PilotRuleCard key={i} item={item} /> <DashboardRuleCard key={i} item={item} />
))} ))}
</div> </div>
)} )}

View File

@ -2,7 +2,7 @@ import { useState } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react' import { ChevronDown, ChevronUp } from 'lucide-react'
import { getStatusColor, getStatusBg } from '../../utils/interpret' import { getStatusColor, getStatusBg } from '../../utils/interpret'
export default function PilotRuleCard({ item }) { export default function DashboardRuleCard({ item }) {
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
const color = getStatusColor(item.status) const color = getStatusColor(item.status)
return ( return (

View File

@ -38,7 +38,7 @@ function buildAutoTileIds(refTiles, hasBf, hasKcal) {
* @param {{ refreshTick?: number, kpiConfig?: Record<string, unknown> }} props * @param {{ refreshTick?: number, kpiConfig?: Record<string, unknown> }} props
* kpiConfig.tiles: geordnete Kachel-ids; fehlend = automatische Belegung (wie bisher). * kpiConfig.tiles: geordnete Kachel-ids; fehlend = automatische Belegung (wie bisher).
*/ */
export default function PilotKpiBoard({ refreshTick = 0, kpiConfig }) { export default function KpiBoardWidget({ refreshTick = 0, kpiConfig }) {
const manualOrder = useMemo(() => kpiTileOrderFromConfig(kpiConfig), [kpiConfig]) const manualOrder = useMemo(() => kpiTileOrderFromConfig(kpiConfig), [kpiConfig])
const { activeProfile } = useProfile() const { activeProfile } = useProfile()

View File

@ -9,7 +9,7 @@ import { api } from '../../utils/api'
* @param {{ onSaved?: () => void, captureConfig?: Record<string, unknown> }} props * @param {{ onSaved?: () => void, captureConfig?: Record<string, unknown> }} props
* captureConfig: show_weight, show_resting_hr, show_hrv, show_vo2_max (false = ausblenden; fehlend = true) * captureConfig: show_weight, show_resting_hr, show_hrv, show_vo2_max (false = ausblenden; fehlend = true)
*/ */
export default function PilotQuickCapture({ onSaved, captureConfig }) { export default function QuickCaptureWidget({ onSaved, captureConfig }) {
const cfgRaw = captureConfig && typeof captureConfig === 'object' ? captureConfig : {} const cfgRaw = captureConfig && typeof captureConfig === 'object' ? captureConfig : {}
const showWeight = cfgRaw.show_weight !== false const showWeight = cfgRaw.show_weight !== false
const showRestingHr = cfgRaw.show_resting_hr !== false const showRestingHr = cfgRaw.show_resting_hr !== false
@ -200,7 +200,7 @@ export default function PilotQuickCapture({ onSaved, captureConfig }) {
{showRestingHr && ( {showRestingHr && (
<div> <div>
<label <label
htmlFor="pqc-resting-hr" htmlFor="qcw-resting-hr"
className="form-label" className="form-label"
style={{ display: 'block', marginBottom: 4, fontSize: 11, fontWeight: 600, color: 'var(--text2)' }} style={{ display: 'block', marginBottom: 4, fontSize: 11, fontWeight: 600, color: 'var(--text2)' }}
> >
@ -208,7 +208,7 @@ export default function PilotQuickCapture({ onSaved, captureConfig }) {
<span style={{ fontWeight: 400, color: 'var(--text3)' }}> (bpm)</span> <span style={{ fontWeight: 400, color: 'var(--text3)' }}> (bpm)</span>
</label> </label>
<input <input
id="pqc-resting-hr" id="qcw-resting-hr"
type="number" type="number"
className="form-input" className="form-input"
style={{ width: '100%' }} style={{ width: '100%' }}
@ -221,7 +221,7 @@ export default function PilotQuickCapture({ onSaved, captureConfig }) {
{showHrv && ( {showHrv && (
<div> <div>
<label <label
htmlFor="pqc-hrv" htmlFor="qcw-hrv"
className="form-label" className="form-label"
style={{ display: 'block', marginBottom: 4, fontSize: 11, fontWeight: 600, color: 'var(--text2)' }} style={{ display: 'block', marginBottom: 4, fontSize: 11, fontWeight: 600, color: 'var(--text2)' }}
> >
@ -229,7 +229,7 @@ export default function PilotQuickCapture({ onSaved, captureConfig }) {
<span style={{ fontWeight: 400, color: 'var(--text3)' }}> (ms)</span> <span style={{ fontWeight: 400, color: 'var(--text3)' }}> (ms)</span>
</label> </label>
<input <input
id="pqc-hrv" id="qcw-hrv"
type="number" type="number"
className="form-input" className="form-input"
style={{ width: '100%' }} style={{ width: '100%' }}
@ -242,14 +242,14 @@ export default function PilotQuickCapture({ onSaved, captureConfig }) {
{showVo2 && ( {showVo2 && (
<div> <div>
<label <label
htmlFor="pqc-vo2" htmlFor="qcw-vo2"
className="form-label" className="form-label"
style={{ display: 'block', marginBottom: 4, fontSize: 11, fontWeight: 600, color: 'var(--text2)' }} style={{ display: 'block', marginBottom: 4, fontSize: 11, fontWeight: 600, color: 'var(--text2)' }}
> >
VO₂max VO₂max
</label> </label>
<input <input
id="pqc-vo2" id="qcw-vo2"
type="number" type="number"
className="form-input" className="form-input"
style={{ width: '100%' }} style={{ width: '100%' }}

View File

@ -4,7 +4,7 @@ import { useProfile } from '../../context/ProfileContext'
dayjs.locale('de') dayjs.locale('de')
export default function PilotWelcome() { export default function WelcomeWidget() {
const { activeProfile } = useProfile() const { activeProfile } = useProfile()
return ( return (
<div className="card section-gap" style={{ marginBottom: 16 }}> <div className="card section-gap" style={{ marginBottom: 16 }}>
@ -12,7 +12,7 @@ export default function PilotWelcome() {
Hallo, {activeProfile?.name || 'Nutzer'} 👋 Hallo, {activeProfile?.name || 'Nutzer'} 👋
</h2> </h2>
<p style={{ fontSize: 12, color: 'var(--text3)', margin: '6px 0 0' }}> <p style={{ fontSize: 12, color: 'var(--text3)', margin: '6px 0 0' }}>
{dayjs().format('dddd, DD. MMMM YYYY')} · Pilot-Übersicht {dayjs().format('dddd, DD. MMMM YYYY')} · Übersicht
</p> </p>
</div> </div>
) )

View File

@ -5,7 +5,7 @@ import { api } from '../utils/api'
import { useProfile } from '../context/ProfileContext' import { useProfile } from '../context/ProfileContext'
import TrialBanner from '../components/TrialBanner' import TrialBanner from '../components/TrialBanner'
import EmailVerificationBanner from '../components/EmailVerificationBanner' import EmailVerificationBanner from '../components/EmailVerificationBanner'
import { ensurePilotLabWidgetsRegistered } from '../widgetSystem/registerPilotLabWidgets' import { ensureDashboardWidgetsRegistered } from '../widgetSystem/registerDashboardWidgets'
import { WidgetRenderer } from '../widgetSystem/dashboardWidgetRegistry' import { WidgetRenderer } from '../widgetSystem/dashboardWidgetRegistry'
function catalogMetaById(catalog) { function catalogMetaById(catalog) {
@ -27,7 +27,7 @@ export default function Dashboard() {
const requestRefresh = () => setRefreshTick((t) => t + 1) const requestRefresh = () => setRefreshTick((t) => t + 1)
useEffect(() => { useEffect(() => {
ensurePilotLabWidgetsRegistered() ensureDashboardWidgetsRegistered()
}, []) }, [])
useEffect(() => { useEffect(() => {

View File

@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ChevronDown, ChevronUp, GripVertical, LayoutDashboard, Plus, Search, X } from 'lucide-react' import { ChevronDown, ChevronUp, GripVertical, LayoutDashboard, Plus, Search, X } from 'lucide-react'
import { api, formatFastApiDetail } from '../utils/api' import { api, formatFastApiDetail } from '../utils/api'
import { ensurePilotLabWidgetsRegistered } from '../widgetSystem/registerPilotLabWidgets' import { ensureDashboardWidgetsRegistered } from '../widgetSystem/registerDashboardWidgets'
import { import {
BODY_CHART_DAYS_DEFAULT, BODY_CHART_DAYS_DEFAULT,
BODY_CHART_DAYS_MAX, BODY_CHART_DAYS_MAX,
@ -46,7 +46,7 @@ function catalogMetaById(catalog) {
* @param {{ adminMode?: boolean }} [props] * @param {{ adminMode?: boolean }} [props]
*/ */
export default function DashboardConfigurePage({ adminMode = false } = {}) { export default function DashboardConfigurePage({ adminMode = false } = {}) {
ensurePilotLabWidgetsRegistered() ensureDashboardWidgetsRegistered()
const [bundle, setBundle] = useState(null) const [bundle, setBundle] = useState(null)
const [adminFromDatabase, setAdminFromDatabase] = useState(null) const [adminFromDatabase, setAdminFromDatabase] = useState(null)

View File

@ -3,7 +3,7 @@ import { ChevronDown, ChevronUp, LayoutGrid } from 'lucide-react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { api, formatFastApiDetail } from '../utils/api' import { api, formatFastApiDetail } from '../utils/api'
import { WidgetRenderer } from '../widgetSystem/dashboardWidgetRegistry' import { WidgetRenderer } from '../widgetSystem/dashboardWidgetRegistry'
import { ensurePilotLabWidgetsRegistered } from '../widgetSystem/registerPilotLabWidgets' import { ensureDashboardWidgetsRegistered } from '../widgetSystem/registerDashboardWidgets'
import { import {
BODY_CHART_DAYS_DEFAULT, BODY_CHART_DAYS_DEFAULT,
BODY_CHART_DAYS_MAX, BODY_CHART_DAYS_MAX,
@ -38,7 +38,7 @@ function catalogMetaById(catalog) {
} }
export default function DashboardLabPage() { export default function DashboardLabPage() {
ensurePilotLabWidgetsRegistered() ensureDashboardWidgetsRegistered()
const [refreshTick, setRefreshTick] = useState(0) const [refreshTick, setRefreshTick] = useState(0)
const requestRefresh = () => setRefreshTick((t) => t + 1) const requestRefresh = () => setRefreshTick((t) => t + 1)
@ -198,13 +198,11 @@ export default function DashboardLabPage() {
<p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}> <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.{' '} Widget-System: Katalog, Registry, Renderer; optional pro Widget <code>config</code> (z.B.{' '}
<strong>Körper</strong> / <strong>Aktivität</strong>: Zeitraum 790 Tage; <strong>KPI</strong>: Kacheln <strong>Körper</strong> / <strong>Aktivität</strong>: Zeitraum 790 Tage; <strong>KPI</strong>: Kacheln
wählen &amp; sortieren). Layout pro Profil in der DB wählen &amp; sortieren). Layout pro Profil in der DB dieselben Widgets wie auf der{' '}
getrennt vom Produktiv-Dashboard. <Link to="/" style={{ color: 'var(--accent)' }}>
Vergleich:{' '} Produkt-Übersicht
<Link to="/pilot/viz" style={{ color: 'var(--accent)' }}>
Pilot-Übersicht (festes Standard-Layout)
</Link> </Link>
. , hier mit Editor und API-Fokus.
</p> </p>
</div> </div>

View File

@ -1,45 +0,0 @@
import { useState } from 'react'
import { FlaskConical } from 'lucide-react'
import { Link } from 'react-router-dom'
import { WidgetRenderer } from '../widgetSystem/dashboardWidgetRegistry'
import { ensurePilotLabWidgetsRegistered } from '../widgetSystem/registerPilotLabWidgets'
import { DEFAULT_LAB_LAYOUT } from '../widgetSystem/defaultLabLayout'
/**
* Pilot-Übersicht nach Product-Spec (festes Standard-Layout).
* Nutzt dasselbe Widget-Rendering wie /app/dashboard-lab.
*/
export default function PilotVizPage() {
ensurePilotLabWidgetsRegistered()
const [refreshTick, setRefreshTick] = useState(0)
const requestRefresh = () => setRefreshTick((t) => t + 1)
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 }}>
<FlaskConical size={26} color="var(--accent)" />
Pilot: Übersicht
</h1>
<p style={{ fontSize: 13, color: 'var(--text2)', lineHeight: 1.6, marginTop: 8 }}>
Konfigurierbare Ziel-Übersicht (Test). Produktives Dashboard und Verlauf unverändert. Nach Speichern von
Gewicht oder Vitalwerten werden KPIs und Körperbereich neu geladen.
</p>
</div>
<WidgetRenderer
layout={DEFAULT_LAB_LAYOUT}
refreshTick={refreshTick}
requestRefresh={requestRefresh}
/>
</div>
)
}

View File

@ -463,37 +463,28 @@ export default function SettingsPage() {
style={{ borderStyle: 'dashed', borderColor: 'var(--border2)', background: 'var(--surface2)' }} style={{ borderStyle: 'dashed', borderColor: 'var(--border2)', background: 'var(--surface2)' }}
> >
<div className="card-title" style={{ fontSize: 14 }}> <div className="card-title" style={{ fontSize: 14 }}>
Pilot: Visualisierungs-Module Entwickler: Dashboard-Layout (API)
</div> </div>
<p style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 12, lineHeight: 1.5 }}> <p style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 12, lineHeight: 1.5 }}>
Ziel-Übersicht-Pilot: Schnelleingabe, KPIs, Körper-Chart, Aktivität. Die reguläre Übersicht konfigurierst du Experimentelles Layout-Lab mit Katalog und API (getrennt von der regulären Übersicht). Die produktive Kachelansicht
unter <strong>Übersicht anpassen</strong> oben. steuerst du über <strong>Übersicht anpassen</strong> oben.
</p> </p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <Link
<Link to="/app/dashboard-lab"
to="/pilot/viz" className="btn btn-secondary btn-full"
className="btn btn-secondary btn-full" style={{
style={{ textAlign: 'center', textDecoration: 'none', boxSizing: 'border-box' }} textAlign: 'center',
> textDecoration: 'none',
Pilot öffnen boxSizing: 'border-box',
</Link> display: 'flex',
<Link alignItems: 'center',
to="/app/dashboard-lab" justifyContent: 'center',
className="btn btn-secondary btn-full" gap: 8,
style={{ }}
textAlign: 'center', >
textDecoration: 'none', <LayoutGrid size={18} />
boxSizing: 'border-box', Dashboard-Lab öffnen
display: 'flex', </Link>
alignItems: 'center',
justifyContent: 'center',
gap: 8,
}}
>
<LayoutGrid size={18} />
Dashboard-Lab (Layout API)
</Link>
</div>
</div> </div>
{/* Auth actions */} {/* Auth actions */}

View File

@ -1,15 +0,0 @@
/**
* Standard-Layout v1 (nur Pilot `/pilot/viz` ohne API).
* API-Nutzer: default_layout aus Backend (alle Katalog-IDs; aktiv = DEFAULT_LAB_WIDGET_IDS).
* Diese Datei: kompakte feste 5 Widgets für den Pilot nicht automatisch alle P1-Widgets.
*/
export const DEFAULT_LAB_LAYOUT = {
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 },
],
}

View File

@ -1,11 +1,11 @@
/** /**
* Pilot/Lab-Widgets registrieren. IDs müssen zu backend/widget_catalog.WIDGET_CATALOG passen. * Dashboard-Widget-Registry: Katalog-IDs aus backend/widget_catalog.WIDGET_CATALOG React-Komponenten.
*/ */
import PilotWelcome from '../components/pilot/PilotWelcome' import WelcomeWidget from '../components/dashboard-widgets-legacy/WelcomeWidget'
import PilotQuickCapture from '../components/pilot/PilotQuickCapture' import QuickCaptureWidget from '../components/dashboard-widgets-legacy/QuickCaptureWidget'
import PilotKpiBoard from '../components/pilot/PilotKpiBoard' import KpiBoardWidget from '../components/dashboard-widgets-legacy/KpiBoardWidget'
import PilotBodySection from '../components/pilot/PilotBodySection' import BodyOverviewWidget from '../components/dashboard-widgets-legacy/BodyOverviewWidget'
import PilotActivitySection from '../components/pilot/PilotActivitySection' import ActivityOverviewWidget from '../components/dashboard-widgets-legacy/ActivityOverviewWidget'
import DashboardGreetingWidget from '../components/dashboard-widgets/DashboardGreetingWidget' import DashboardGreetingWidget from '../components/dashboard-widgets/DashboardGreetingWidget'
import QuickWeightTodayWidget from '../components/dashboard-widgets/QuickWeightTodayWidget' import QuickWeightTodayWidget from '../components/dashboard-widgets/QuickWeightTodayWidget'
import BodyStatStripWidget from '../components/dashboard-widgets/BodyStatStripWidget' import BodyStatStripWidget from '../components/dashboard-widgets/BodyStatStripWidget'
@ -34,18 +34,18 @@ import { registerDashboardWidget } from './dashboardWidgetRegistry'
let _registered = false let _registered = false
export function ensurePilotLabWidgetsRegistered() { export function ensureDashboardWidgetsRegistered() {
if (_registered) return if (_registered) return
_registered = true _registered = true
registerDashboardWidget({ registerDashboardWidget({
id: 'welcome', id: 'welcome',
Component: PilotWelcome, Component: WelcomeWidget,
mapProps: () => ({}), mapProps: () => ({}),
}) })
registerDashboardWidget({ registerDashboardWidget({
id: 'quick_capture', id: 'quick_capture',
Component: PilotQuickCapture, Component: QuickCaptureWidget,
mapProps: (ctx) => ({ mapProps: (ctx) => ({
onSaved: ctx.requestRefresh, onSaved: ctx.requestRefresh,
captureConfig: ctx.layoutEntry?.config || {}, captureConfig: ctx.layoutEntry?.config || {},
@ -53,7 +53,7 @@ export function ensurePilotLabWidgetsRegistered() {
}) })
registerDashboardWidget({ registerDashboardWidget({
id: 'kpi_board', id: 'kpi_board',
Component: PilotKpiBoard, Component: KpiBoardWidget,
mapProps: (ctx) => ({ mapProps: (ctx) => ({
refreshTick: ctx.refreshTick, refreshTick: ctx.refreshTick,
kpiConfig: ctx.layoutEntry?.config || {}, kpiConfig: ctx.layoutEntry?.config || {},
@ -61,7 +61,7 @@ export function ensurePilotLabWidgetsRegistered() {
}) })
registerDashboardWidget({ registerDashboardWidget({
id: 'body_overview', id: 'body_overview',
Component: PilotBodySection, Component: BodyOverviewWidget,
mapProps: (ctx) => ({ mapProps: (ctx) => ({
refreshTick: ctx.refreshTick, refreshTick: ctx.refreshTick,
chartDays: normalizeBodyChartDays(ctx.layoutEntry?.config?.chart_days), chartDays: normalizeBodyChartDays(ctx.layoutEntry?.config?.chart_days),
@ -77,7 +77,7 @@ export function ensurePilotLabWidgetsRegistered() {
}) })
registerDashboardWidget({ registerDashboardWidget({
id: 'activity_overview', id: 'activity_overview',
Component: PilotActivitySection, Component: ActivityOverviewWidget,
mapProps: (ctx) => ({ mapProps: (ctx) => ({
refreshTick: ctx.refreshTick, refreshTick: ctx.refreshTick,
chartDays: normalizeBodyChartDays(ctx.layoutEntry?.config?.chart_days), chartDays: normalizeBodyChartDays(ctx.layoutEntry?.config?.chart_days),
@ -193,6 +193,6 @@ export function ensurePilotLabWidgetsRegistered() {
} }
/** @internal Nur für Tests */ /** @internal Nur für Tests */
export function __resetPilotLabRegistrationForTests() { export function __resetDashboardWidgetRegistrationForTests() {
_registered = false _registered = false
} }