diff --git a/backend/dashboard_widget_config.py b/backend/dashboard_widget_config.py index 432df73..57c6a58 100644 --- a/backend/dashboard_widget_config.py +++ b/backend/dashboard_widget_config.py @@ -16,8 +16,15 @@ WIDGETS_ALLOWING_CONFIG: frozenset[str] = frozenset({ "body_overview", "activity_overview", "kpi_board", + "quick_capture", "trend_kcal_weight", - "training_type_distribution", +}) + +_QUICK_CAPTURE_KEYS: frozenset[str] = frozenset({ + "show_weight", + "show_resting_hr", + "show_hrv", + "show_vo2_max", }) _KPI_TILE_FIXED: frozenset[str] = frozenset({"body_fat", "avg_kcal"}) @@ -47,14 +54,34 @@ def validate_widget_entry_config(widget_id: str, raw: Any) -> dict[str, Any]: return _validate_chart_days_only(raw, label="activity_overview") if widget_id == "kpi_board": return _validate_kpi_board_config(raw) + if widget_id == "quick_capture": + return _validate_quick_capture_config(raw) if widget_id == "trend_kcal_weight": return _validate_chart_days_only(raw, label="trend_kcal_weight") - if widget_id == "training_type_distribution": - return _validate_distribution_days_only(raw) raise ValueError(f"Widget {widget_id}: keine Konfiguration unterstützt") +def _validate_quick_capture_config(raw: dict[str, Any]) -> dict[str, Any]: + label = "quick_capture" + unknown = set(raw) - _QUICK_CAPTURE_KEYS + if unknown: + raise ValueError(f"{label}: unbekannte config-Felder: {sorted(unknown)}") + out: dict[str, bool] = {} + for k in _QUICK_CAPTURE_KEYS: + if k not in raw: + continue + v = raw[k] + if not isinstance(v, bool): + raise ValueError(f"{label}: {k} muss boolean sein") + out[k] = v + merged = {k: True for k in _QUICK_CAPTURE_KEYS} + merged.update(out) + if not any(merged.values()): + raise ValueError(f"{label}: mindestens ein Bereich muss sichtbar sein (show_*)") + return out + + def _kpi_tile_id_valid(tid: str) -> bool: if tid in _KPI_TILE_FIXED: return True @@ -130,15 +157,3 @@ def _validate_chart_days_only(raw: dict[str, Any], *, label: str) -> dict[str, A return {"chart_days": v} -def _validate_distribution_days_only(raw: dict[str, Any]) -> dict[str, Any]: - label = "training_type_distribution" - allowed = frozenset({"distribution_days"}) - unknown = set(raw) - allowed - if unknown: - raise ValueError(f"{label}: unbekannte config-Felder: {sorted(unknown)}") - if "distribution_days" not in raw: - return {} - v = _parse_chart_days(raw["distribution_days"], label) - if v < 7 or v > 120: - raise ValueError(f"{label}: distribution_days muss zwischen 7 und 120 liegen") - return {"distribution_days": v} diff --git a/backend/tests/test_dashboard_widget_config.py b/backend/tests/test_dashboard_widget_config.py index 875238b..f2287fc 100644 --- a/backend/tests/test_dashboard_widget_config.py +++ b/backend/tests/test_dashboard_widget_config.py @@ -45,6 +45,32 @@ def test_kpi_board_tiles(): validate_widget_entry_config("kpi_board", {"extra": 1}) +def test_quick_capture_visibility(): + assert validate_widget_entry_config("quick_capture", {}) == {} + assert validate_widget_entry_config("quick_capture", {"show_weight": False}) == {"show_weight": False} + full = { + "show_weight": True, + "show_resting_hr": False, + "show_hrv": True, + "show_vo2_max": False, + } + assert validate_widget_entry_config("quick_capture", full) == full + with pytest.raises(ValueError): + validate_widget_entry_config("quick_capture", {"show_weight": "yes"}) + with pytest.raises(ValueError): + validate_widget_entry_config( + "quick_capture", + { + "show_weight": False, + "show_resting_hr": False, + "show_hrv": False, + "show_vo2_max": False, + }, + ) + with pytest.raises(ValueError): + validate_widget_entry_config("quick_capture", {"extra": 1}) + + def test_trend_kcal_weight_chart_days(): assert validate_widget_entry_config("trend_kcal_weight", {}) == {} assert validate_widget_entry_config("trend_kcal_weight", {"chart_days": 30}) == {"chart_days": 30} @@ -52,17 +78,6 @@ def test_trend_kcal_weight_chart_days(): validate_widget_entry_config("trend_kcal_weight", {"chart_days": 6}) -def test_training_type_distribution_days(): - assert validate_widget_entry_config("training_type_distribution", {}) == {} - assert validate_widget_entry_config( - "training_type_distribution", {"distribution_days": 28} - ) == {"distribution_days": 28} - with pytest.raises(ValueError): - validate_widget_entry_config("training_type_distribution", {"distribution_days": 5}) - with pytest.raises(ValueError): - validate_widget_entry_config("training_type_distribution", {"distribution_days": 200}) - - def test_kpi_board_legacy_chart_days_dropped(): """Nur chart_days (Alt-Layouts) → automatische Kachelwahl, kein Ø-Kal-Fenster mehr.""" assert validate_widget_entry_config("kpi_board", {"chart_days": 14}) == {} diff --git a/backend/version.py b/backend/version.py index 379d111..e2cb67f 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.6.0", # P1 Produkt-Widgets im Katalog + Default nur Kern-5 aktiv + "app_dashboard": "1.6.2", # quick_capture: Sichtbarkeit show_* konfigurierbar } CHANGELOG = [ diff --git a/backend/widget_catalog.py b/backend/widget_catalog.py index 80fbda6..b0c8f9e 100644 --- a/backend/widget_catalog.py +++ b/backend/widget_catalog.py @@ -25,7 +25,7 @@ WIDGET_CATALOG: list[WidgetCatalogEntry] = [ { "id": "quick_capture", "title": "Schnelleingabe", - "description": "Gewicht und Vitalwerte erfassen", + "description": "Gewicht + Baseline-Vitals; optional show_weight / show_resting_hr / show_hrv / show_vo2_max (false = aus)", }, { "id": "kpi_board", @@ -40,7 +40,7 @@ WIDGET_CATALOG: list[WidgetCatalogEntry] = [ { "id": "activity_overview", "title": "Aktivität", - "description": "Training & Konsistenz (optional: config chart_days 7–90)", + "description": "Trainingstyp-Verteilung (Kuchen) + Konsistenz — Zeitraum über config chart_days 7–90", }, { "id": "dashboard_greeting", @@ -82,11 +82,6 @@ WIDGET_CATALOG: list[WidgetCatalogEntry] = [ "title": "Erholung", "description": "Schlaf-Widget & Ruhetage", }, - { - "id": "training_type_distribution", - "title": "Training Verteilung", - "description": "Kuchen Trainingstypen (optional config distribution_days 7–120, Default 28)", - }, { "id": "goals_focus_teaser", "title": "Ziele Teaser", diff --git a/frontend/src/components/dashboard-widgets/TrainingTypeDistributionWidget.jsx b/frontend/src/components/dashboard-widgets/TrainingTypeDistributionWidget.jsx deleted file mode 100644 index 2ab05ac..0000000 --- a/frontend/src/components/dashboard-widgets/TrainingTypeDistributionWidget.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useNavigate } from 'react-router-dom' -import TrainingTypeDistribution from '../TrainingTypeDistribution' - -/** - * @param {{ refreshTick?: number, distributionDays?: number }} props - */ -export default function TrainingTypeDistributionWidget({ refreshTick = 0, distributionDays = 28 }) { - const nav = useNavigate() - const days = Math.max(7, Math.min(120, Number(distributionDays) || 28)) - - return ( -
+ Für dieses Widget sind keine Eingabebereiche aktiviert. Im Dashboard-Lab die Sichtbarkeit prüfen + oder Vitalwerte-Seite nutzen. +
+- Gewicht separat; Vitalwerte typischerweise gemeinsam.{' '} - - Volle Vitalwerte-Seite → - -
+ {(showWeight || showVitalsBlock) && ( ++ {showWeight && showVitalsBlock && 'Gewicht separat; Vitalwerte typischerweise gemeinsam. '} + {showWeight && !showVitalsBlock && 'Gewicht für heute. '} + {!showWeight && showVitalsBlock && 'Baseline-Vitalwerte für heute. '} + + Volle Vitalwerte-Seite → + +
+ )}