diff --git a/backend/dashboard_widget_config.py b/backend/dashboard_widget_config.py index 80f9b00..d164759 100644 --- a/backend/dashboard_widget_config.py +++ b/backend/dashboard_widget_config.py @@ -11,7 +11,7 @@ from typing import Any MAX_WIDGET_CONFIG_JSON_BYTES = 1024 -WIDGETS_ALLOWING_CONFIG: frozenset[str] = frozenset({"body_overview"}) +WIDGETS_ALLOWING_CONFIG: frozenset[str] = frozenset({"body_overview", "activity_overview"}) def _config_json_size_bytes(config: dict[str, Any]) -> int: @@ -32,33 +32,35 @@ def validate_widget_entry_config(widget_id: str, raw: Any) -> dict[str, Any]: raise ValueError(f"Widget {widget_id}: keine Konfiguration unterstützt") if widget_id == "body_overview": - return _validate_body_overview_config(raw) + return _validate_chart_days_only(raw, label="body_overview") + if widget_id == "activity_overview": + return _validate_chart_days_only(raw, label="activity_overview") raise ValueError(f"Widget {widget_id}: keine Konfiguration unterstützt") -def _validate_body_overview_config(raw: dict[str, Any]) -> dict[str, Any]: +def _parse_chart_days(v: Any, label: str) -> int: + if isinstance(v, bool): + raise ValueError(f"{label}: chart_days muss ganze Zahl sein") + if isinstance(v, float): + if not math.isfinite(v): + raise ValueError(f"{label}: chart_days muss ganze Zahl sein") + if abs(v - round(v)) > 1e-9: + raise ValueError(f"{label}: chart_days muss ganze Zahl sein") + return int(round(v)) + if isinstance(v, int): + return v + raise ValueError(f"{label}: chart_days muss ganze Zahl sein") + + +def _validate_chart_days_only(raw: dict[str, Any], *, label: str) -> dict[str, Any]: allowed = frozenset({"chart_days"}) unknown = set(raw) - allowed if unknown: - raise ValueError(f"body_overview: unbekannte config-Felder: {sorted(unknown)}") - out: dict[str, Any] = {} + raise ValueError(f"{label}: unbekannte config-Felder: {sorted(unknown)}") if "chart_days" not in raw: - return out - v = raw["chart_days"] - if isinstance(v, bool): - raise ValueError("body_overview: chart_days muss ganze Zahl sein") - if isinstance(v, float): - if not math.isfinite(v): - raise ValueError("body_overview: chart_days muss ganze Zahl sein") - if abs(v - round(v)) > 1e-9: - raise ValueError("body_overview: chart_days muss ganze Zahl sein") - v = int(round(v)) - elif isinstance(v, int): - pass - else: - raise ValueError("body_overview: chart_days muss ganze Zahl sein") + return {} + v = _parse_chart_days(raw["chart_days"], label) if v < 7 or v > 90: - raise ValueError("body_overview: chart_days muss zwischen 7 und 90 liegen") - out["chart_days"] = v - return out + raise ValueError(f"{label}: chart_days muss zwischen 7 und 90 liegen") + return {"chart_days": v} diff --git a/backend/tests/test_dashboard_widget_config.py b/backend/tests/test_dashboard_widget_config.py index ba6f7fe..39180dc 100644 --- a/backend/tests/test_dashboard_widget_config.py +++ b/backend/tests/test_dashboard_widget_config.py @@ -24,6 +24,17 @@ def test_body_unknown_key(): validate_widget_entry_config("body_overview", {"chart_days": 30, "extra": 1}) +def test_activity_chart_days(): + assert validate_widget_entry_config("activity_overview", {"chart_days": 14}) == {"chart_days": 14} + with pytest.raises(ValueError): + validate_widget_entry_config("activity_overview", {"chart_days": 5}) + + +def test_kpi_config_rejected(): + with pytest.raises(ValueError): + validate_widget_entry_config("kpi_board", {"chart_days": 30}) + + def test_layout_payload_with_chart_days_roundtrip(): p = DashboardLayoutPayload.model_validate( { diff --git a/backend/version.py b/backend/version.py index 5c0e287..5f465cd 100644 --- a/backend/version.py +++ b/backend/version.py @@ -30,7 +30,7 @@ MODULE_VERSIONS = { "importdata": "1.0.0", "membership": "2.1.0", "workflow": "0.6.0", # Phase 4: End Node Template Engine - "app_dashboard": "1.2.0", # Widget-Config (body_overview.chart_days) + Validierung + "app_dashboard": "1.3.0", # activity_overview.chart_days + WidgetErrorBoundary } CHANGELOG = [ diff --git a/backend/widget_catalog.py b/backend/widget_catalog.py index 5c67966..6152496 100644 --- a/backend/widget_catalog.py +++ b/backend/widget_catalog.py @@ -40,7 +40,7 @@ WIDGET_CATALOG: list[WidgetCatalogEntry] = [ { "id": "activity_overview", "title": "Aktivität", - "description": "Training & Konsistenz", + "description": "Training & Konsistenz (optional: config chart_days 7–90)", }, ] diff --git a/frontend/src/components/pilot/PilotActivitySection.jsx b/frontend/src/components/pilot/PilotActivitySection.jsx index fe3199d..f934d05 100644 --- a/frontend/src/components/pilot/PilotActivitySection.jsx +++ b/frontend/src/components/pilot/PilotActivitySection.jsx @@ -4,11 +4,14 @@ import dayjs from 'dayjs' import { api } from '../../utils/api' import { useProfile } from '../../context/ProfileContext' import TrainingTypeDistribution from '../TrainingTypeDistribution' +import { + BODY_CHART_DAYS_DEFAULT, + normalizeBodyChartDays, +} from '../../widgetSystem/bodyChartDays' import PilotRuleCard from './PilotRuleCard' -const PERIOD = 30 - -export default function PilotActivitySection({ refreshTick = 0 }) { +export default function PilotActivitySection({ refreshTick = 0, chartDays = BODY_CHART_DAYS_DEFAULT }) { + const periodDays = normalizeBodyChartDays(chartDays) const { activeProfile } = useProfile() const globalQualityLevel = activeProfile?.quality_filter_level const [activities, setActivities] = useState([]) @@ -18,7 +21,8 @@ export default function PilotActivitySection({ refreshTick = 0 }) { let cancelled = false ;(async () => { try { - const a = await api.listActivity(120) + const fetchDays = Math.max(120, periodDays + 60) + const a = await api.listActivity(fetchDays) if (!cancelled) setActivities(Array.isArray(a) ? a : []) } catch { if (!cancelled) setActivities([]) @@ -29,15 +33,15 @@ export default function PilotActivitySection({ refreshTick = 0 }) { return () => { cancelled = true } - }, [refreshTick, globalQualityLevel]) + }, [refreshTick, globalQualityLevel, periodDays]) - const cutoff = dayjs().subtract(PERIOD, 'day').format('YYYY-MM-DD') + const cutoff = dayjs().subtract(periodDays, 'day').format('YYYY-MM-DD') const filtA = (activities || []).filter((d) => d.date >= cutoff) const daysWithAct = new Set(filtA.map((a) => a.date)).size const totalDays = filtA.length > 0 - ? Math.min(PERIOD, dayjs().diff(dayjs(filtA[filtA.length - 1]?.date), 'day') + 1) + ? Math.min(periodDays, dayjs().diff(dayjs(filtA[filtA.length - 1]?.date), 'day') + 1) : 0 const consistency = totalDays > 0 ? Math.round((daysWithAct / totalDays) * 100) : 0 @@ -46,7 +50,7 @@ export default function PilotActivitySection({ refreshTick = 0 }) { status: consistency >= 70 ? 'good' : consistency >= 40 ? 'warn' : 'bad', icon: '📅', category: 'Konsistenz', - title: `${consistency}% aktive Tage (${daysWithAct}/${Math.min(PERIOD, totalDays || PERIOD)} Tage)`, + title: `${consistency}% aktive Tage (${daysWithAct}/${Math.min(periodDays, totalDays || periodDays)} Tage)`, detail: consistency >= 70 ? 'Ausgezeichnete Regelmäßigkeit.' @@ -77,7 +81,7 @@ export default function PilotActivitySection({ refreshTick = 0 }) { >
- Trainingstyp-Verteilung {PERIOD} Tage · Bewertung Konsistenz wie im Verlauf + Trainingstyp-Verteilung {periodDays} Tage · Bewertung Konsistenz wie im Verlauf
@@ -102,7 +106,7 @@ export default function PilotActivitySection({ refreshTick = 0 }) {
Widget-System: Katalog, Registry, Renderer; optional pro Widget config (z. B.{' '}
- Körper-Chart 7–90 Tage). Layout pro Profil in der DB — getrennt vom Produktiv-Dashboard.
+ Körper und Aktivität: Zeitraum 7–90 Tage). Layout pro Profil in der DB —
+ getrennt vom Produktiv-Dashboard.
Vergleich:{' '}
Pilot-Übersicht (festes Standard-Layout)
@@ -221,10 +230,11 @@ export default function DashboardLabPage() {
+ {this.props.widgetId}
+
+ {msg}
+
+