# Issue #53: Phase 0c - Multi-Layer Data Architecture **Status:** ✅ COMPLETED **Priorität:** High (Strategic) **Aufwand:** 20-27h (5-7 Tage bei 4h/Tag) **Erstellt:** 28. März 2026 **Abgeschlossen:** 28. März 2026 **Abhängigkeiten:** Phase 0a ✅, Phase 0b ✅ **Completion Summary:** - ✅ Data Layer: 97 functions migrated to `data_layer/` (6 modules) - ✅ Chart Endpoints: 20 new endpoints implemented (E1-E5, A1-A8, R1-R5, C1-C4) - ✅ Single Source of Truth: All calculations in data_layer, used by KI + Charts - ✅ Commits: 7 systematic commits (6 module migrations + 1 chart expansion) - ✅ charts.py: 329 → 2246 lines (+1917 lines) --- ## Executive Summary **Ziel:** Refactoring der Datenarchitektur von monolithischer Platzhalter-Logik zu einer dreischichtigen Architektur mit klarer Separation of Concerns. **Motivation:** - Aktuell sind Datenermittlung, Berechnungslogik und Formatierung in `placeholder_resolver.py` vermischt - Keine Wiederverwendbarkeit für Charts, Diagramme, API-Endpoints - Jede neue Visualisierung erfordert Duplikation der Berechnungslogik - Schwer testbar, schwer erweiterbar **Lösung:** ``` ┌────────────────────────────────────────────────┐ │ Layer 1: DATA LAYER (neu) │ │ - Pure data retrieval + calculation logic │ │ - Returns: Structured data (dict/list/float) │ │ - No formatting, no strings │ │ - Testable, reusable │ └──────────────────┬─────────────────────────────┘ │ ┌───────────┴──────────┐ │ │ ▼ ▼ ┌──────────────┐ ┌─────────────────────┐ │ Layer 2a: │ │ Layer 2b: │ │ KI LAYER │ │ VISUALIZATION LAYER │ │ (refactored) │ │ (new) │ └──────────────┘ └─────────────────────┘ ``` --- ## Phase 0b Achievements (Blaupause für Phase 0c) ### Was wurde in Phase 0b implementiert? ✅ **Datum:** 28. März 2026 (früher Chat) **Commits:** 20+ Commits mit "Phase 0b" prefix #### 1. Platzhalter-Funktionen (in placeholder_resolver.py) **Körper-Metriken:** ```python def _get_body_progress_score(profile_id: str, goal_mode: str) -> dict: """ Berechnet goal-mode-abhängigen Body Progress Score. Aktuelle Implementierung (Phase 0b): - SQL queries direkt in Funktion - Berechnungslogik inline - Returns: dict mit score + components Phase 0c Migration: → Wird zu: data_layer.body_metrics.get_body_progress_data() → Placeholder nutzt dann nur noch: data['score'] """ ``` **Fokus-Bereiche:** ```python def _get_active_goals_json(profile_id: str) -> str: """ Returns: JSON string mit aktiven Zielen Phase 0c Migration: → Data Layer: data_layer.goals.get_active_goals() → list[dict] → KI Layer: json.dumps(data) → str """ def _get_focus_areas_json(profile_id: str) -> str: """ Returns: JSON string mit gewichteten Focus Areas Phase 0c Migration: → Data Layer: data_layer.goals.get_weighted_focus_areas() → list[dict] → KI Layer: json.dumps(data) → str """ ``` **Ernährungs-Metriken:** ```python def _get_nutrition_metrics(profile_id: str, days: int) -> dict: """ Berechnet Protein/kg, Adherence, etc. Phase 0c Migration: → Wird zu: data_layer.nutrition_metrics.get_protein_adequacy_data() → Zusätzliche Metriken in: get_energy_balance_data(), get_macro_distribution_data() """ ``` #### 2. Score-System (goal_utils.py) ```python # backend/goal_utils.py def map_focus_to_score_components(profile_id: str) -> dict: """ Maps gewichtete Focus Areas zu Score-Komponenten. Returns: { "body": 0.30, "nutrition": 0.25, "training": 0.20, "recovery": 0.15, "health": 0.10 } Phase 0c: BLEIBT in goal_utils.py → Ist Score-Gewichtung, nicht Datenermittlung """ def get_active_goals(profile_id: str) -> list[dict]: """ Holt alle aktiven Ziele mit vollständigen Daten. Phase 0c Migration: → Wird zu: data_layer.goals.get_active_goals() → goal_utils.py importiert dann aus data_layer """ ``` #### 3. Bug Fixes (Learnings für Phase 0c) **Decimal → Float Conversion:** ```python # Problem: PostgreSQL Decimal-Type nicht JSON-serializable # Lösung: Explizite Konvertierung # ALT (Phase 0b Bug): protein_g = row['protein'] # Decimal object return {"protein": protein_g} # JSON error # FIX (Phase 0b): protein_g = float(row['protein']) if row['protein'] else 0.0 return {"protein": protein_g} # OK ``` **Column Name Consistency:** ```python # Problem: Inkonsistente Spaltennamen # Lösung: Immer aus Schema prüfen # ALT (Phase 0b Bug): SELECT bf_jpl FROM caliper_log # Spalte existiert nicht # FIX (Phase 0b): SELECT body_fat_pct FROM caliper_log # Korrekt ``` **Dict Access Safety:** ```python # Problem: KeyError bei fehlenden Daten # Lösung: .get() mit defaults # ALT (Phase 0b Bug): sleep_quality = sleep_data['quality'] # KeyError wenn leer # FIX (Phase 0b): sleep_quality = sleep_data.get('quality', 0.0) # Safe ``` --- ## Phase 0c: Detaillierte Spezifikation ### Ziele 1. ✅ **Single Source of Truth:** Jede Berechnung nur einmal implementiert 2. ✅ **Wiederverwendbarkeit:** Gleiche Daten für KI + Charts + API 3. ✅ **Testbarkeit:** Data Layer isoliert testbar 4. ✅ **Erweiterbarkeit:** Neue Features ohne Code-Duplikation 5. ✅ **Performance:** Caching auf Data Layer Ebene möglich ### Nicht-Ziele (Scope Grenzen) ❌ **NICHT in Phase 0c:** - Neue Charts im Frontend implementieren (nur Backend-Endpoints) - Frontend Chart-Komponenten (kommt in Phase 1) - Caching-Layer (kommt später) - API-Dokumentation mit Swagger (kommt später) --- ## Implementierungs-Plan ### Step 1: Data Layer Module erstellen (8-10h) **Verzeichnisstruktur:** ``` backend/ ├── data_layer/ # NEU │ ├── __init__.py # Exports all functions │ ├── body_metrics.py # Gewicht, FM, LBM, Umfänge, BF% │ ├── nutrition_metrics.py # Kalorien, Protein, Makros, Adherence │ ├── activity_metrics.py # Volumen, Qualität, Monotony, Abilities │ ├── recovery_metrics.py # RHR, HRV, Sleep, Recovery Score │ ├── health_metrics.py # BP, VO2Max, SpO2, Health Stability │ ├── goals.py # Active goals, progress, projections │ ├── correlations.py # Lag-Korrelationen, Plateau Detection │ └── utils.py # Shared: confidence, baseline, outliers ├── placeholder_resolver.py # REFACTORED (nutzt data_layer) ├── goal_utils.py # REFACTORED (nutzt data_layer.goals) └── routers/ └── charts.py # NEU (nutzt data_layer) ``` #### Module 1: body_metrics.py **Pfad:** `backend/data_layer/body_metrics.py` **Funktionen:** ```python """ Body composition metrics and weight trend analysis. All functions return structured data (dict/list) without formatting. Use these for both AI placeholders AND chart endpoints. """ from typing import Optional from datetime import date, timedelta from db import get_db, get_cursor def get_weight_trend_data( profile_id: str, days: int = 90, include_projections: bool = True ) -> dict: """ Weight trend with rolling medians, slopes, and goal projections. Args: profile_id: User profile ID days: Number of days to analyze (default 90) include_projections: Include goal projection calculations Returns: { "raw_values": [(date, weight), ...], "rolling_median_7d": [(date, value), ...], "slope_7d": float, # kg per week "slope_28d": float, "slope_90d": float, "confidence": str, # "high"/"medium"/"low"/"insufficient" "data_points": int, "first_date": date, "last_date": date, "first_value": float, "last_value": float, "delta": float, "projection": { # Only if include_projections=True "target_weight": float, "current_rate": float, "estimated_days": int, "estimated_date": date } | None } Confidence Rules (from utils.py): - "high": >= 60 points (90d) or >= 18 points (28d) or >= 4 points (7d) - "medium": >= 40 points (90d) or >= 12 points (28d) or >= 3 points (7d) - "low": < thresholds above but some data - "insufficient": < 3 points total Migration from Phase 0b: - OLD: _get_weight_trend_slope() in placeholder_resolver.py (inline SQL) - NEW: This function (reusable) - KI Layer: resolve_weight_28d_trend_slope() → f"{data['slope_28d']:.2f} kg/Woche" - Chart: GET /api/charts/weight-trend → return data """ # Implementation here def get_body_composition_data( profile_id: str, days: int = 90 ) -> dict: """ Fat mass, lean mass, body fat percentage trends. Returns: { "dates": [date, ...], "weight": [float, ...], "body_fat_pct": [float, ...], "fat_mass": [float, ...], "lean_mass": [float, ...], "fm_delta_7d": float, "fm_delta_28d": float, "fm_delta_90d": float, "lbm_delta_7d": float, "lbm_delta_28d": float, "lbm_delta_90d": float, "recomposition_score": int, # 0-100 "confidence": str, "data_points": int } Recomposition Score Logic: - FM↓ + LBM↑ = 100 (perfect) - FM↓ + LBM= = 80 (good) - FM= + LBM↑ = 70 (ok) - FM↓ + LBM↓ = depends on ratio - FM↑ + LBM↓ = 0 (worst) Migration from Phase 0b: - OLD: Part of _get_body_progress_score() (mixed with scoring) - NEW: This function (pure data) - Score calculation stays in goal_utils.py """ # Implementation here def get_circumference_summary( profile_id: str, days: int = 90 ) -> dict: """ Circumference measurements with best-of-each strategy. Returns: { "measurements": { "c_neck": {"value": float, "date": date, "age_days": int}, "c_chest": {"value": float, "date": date, "age_days": int}, "c_waist": {"value": float, "date": date, "age_days": int}, "c_hips": {"value": float, "date": date, "age_days": int}, "c_thigh_l": {"value": float, "date": date, "age_days": int}, "c_thigh_r": {"value": float, "date": date, "age_days": int}, "c_bicep_l": {"value": float, "date": date, "age_days": int}, "c_bicep_r": {"value": float, "date": date, "age_days": int} }, "ratios": { "waist_to_hip": float, # WHR - Bauchfettverteilung "waist_to_height": float # WHtR - Gesundheitsrisiko }, "confidence": str, "data_points": int } Best-of-Each Logic: - Pro Messpunkt: Neuester Wert innerhalb days - WHR: waist / hips (< 0.90 men, < 0.85 women = low risk) - WHtR: waist / height_cm (< 0.50 = low risk) Migration from Phase 0b: - OLD: resolve_circ_summary() in placeholder_resolver.py - NEW: This function """ # Implementation here ``` #### Module 2: nutrition_metrics.py **Pfad:** `backend/data_layer/nutrition_metrics.py` **Funktionen:** ```python """ Nutrition analysis: calories, protein, macros, adherence. """ def get_protein_adequacy_data( profile_id: str, days: int = 28, goal_mode: Optional[str] = None ) -> dict: """ Protein intake vs. target (goal_mode-dependent). Returns: { "daily_values": [(date, protein_g, target_g), ...], "avg_protein_g": float, "avg_protein_per_kg": float, "avg_protein_per_kg_lbm": float, "target_protein_g": float, "target_protein_per_kg": float, "adherence_pct": float, # % of days >= 90% of target "adherence_score": int, # 0-100 "goal_mode": str, "current_weight": float, "lean_body_mass": float, "confidence": str, "data_points": int } Target Protein per Goal Mode: - "strength": 2.0-2.2 g/kg - "weight_loss": 1.8-2.0 g/kg - "recomposition": 2.0-2.2 g/kg - "endurance": 1.4-1.6 g/kg - "health": 1.2-1.6 g/kg Adherence Score: - 100: >= 95% of days meet target - 80: >= 80% of days meet target - 60: >= 60% of days meet target - <60: proportional Migration from Phase 0b: - OLD: _get_nutrition_metrics() in placeholder_resolver.py - NEW: This function """ # Implementation here def get_energy_balance_data( profile_id: str, days: int = 28 ) -> dict: """ Calorie intake vs. expenditure, deficit/surplus calculations. Returns: { "daily_values": [(date, intake_kcal, activity_kcal, net), ...], "avg_intake": float, "avg_activity_kcal": float, "avg_net": float, # intake - activity "estimated_bmr": float, "energy_availability": float, # (intake - activity) / LBM "deficit_surplus_avg": float, # negative = deficit "confidence": str, "data_points": int, "red_s_warning": bool # True if EA < 30 kcal/kg LBM } Energy Availability: - EA = (intake - activity) / LBM (kg) - < 30 kcal/kg LBM = RED-S risk (Relative Energy Deficiency in Sport) - 30-45 = moderate risk - > 45 = adequate Migration: - NEW function (was part of Phase 0b scope, moved to 0c) """ # Implementation here def get_macro_distribution_data( profile_id: str, days: int = 28 ) -> dict: """ Macronutrient distribution and balance. Returns: { "avg_kcal": float, "avg_protein_g": float, "avg_carbs_g": float, "avg_fat_g": float, "pct_protein": float, # % of total kcal "pct_carbs": float, "pct_fat": float, "balance_score": int, # 0-100, goal_mode-dependent "confidence": str, "data_points": int } Balance Score (example for strength goal): - Protein: 25-35% = 100, outside = penalty - Carbs: 40-50% = 100, outside = penalty - Fat: 20-30% = 100, outside = penalty """ # Implementation here ``` #### Module 3: activity_metrics.py **Pfad:** `backend/data_layer/activity_metrics.py` **Funktionen:** ```python """ Training volume, quality, monotony, ability balance. """ def get_training_volume_data( profile_id: str, weeks: int = 4 ) -> dict: """ Training volume per week, distribution by type. Returns: { "weekly_totals": [ { "week_start": date, "duration_min": int, "kcal": int, "sessions": int, "avg_quality": float }, ... ], "by_type": { "strength": {"duration": int, "sessions": int, "kcal": int}, "cardio": {"duration": int, "sessions": int, "kcal": int}, ... }, "total_duration": int, "total_sessions": int, "avg_quality": float, # 1.0-5.0 "monotony": float, # < 2.0 = gut "strain": float, # kumulativ "confidence": str, "data_points": int } Monotony Calculation: - monotony = avg_daily_duration / std_dev_daily_duration - < 1.5 = hohe Variation (gut) - 1.5-2.0 = moderate Variation - > 2.0 = niedrige Variation (Risiko Plateau/Übertraining) Strain Calculation: - strain = total_duration * monotony - Hohe Strain + hohe Monotony = Übertraining-Risiko """ # Implementation here def get_activity_quality_distribution( profile_id: str, days: int = 28 ) -> dict: """ Quality label distribution and trends. Returns: { "distribution": { "excellent": int, # count "very_good": int, "good": int, "acceptable": int, "poor": int }, "avg_quality": float, # 1.0-5.0 "quality_trend": str, # "improving"/"stable"/"declining" "high_quality_pct": float, # % excellent + very_good "confidence": str, "data_points": int } Quality Trend: - Compare first_half_avg vs. second_half_avg - > 0.2 difference = improving/declining - <= 0.2 = stable """ # Implementation here def get_ability_balance_data( profile_id: str, weeks: int = 4 ) -> dict: """ Balance across 5 ability dimensions (from training_types). Returns: { "abilities": { "strength": float, # normalized 0-1 "cardio": float, "mobility": float, "coordination": float, "mental": float }, "balance_score": int, # 0-100 "imbalances": [ {"ability": str, "severity": str, "recommendation": str}, ... ], "confidence": str, "data_points": int } Balance Score: - Perfect balance (all ~0.20) = 100 - Moderate imbalance (one dominant) = 70-80 - Severe imbalance (one > 0.50) = < 50 Migration: - NEW function (was part of Phase 0b scope, moved to 0c) """ # Implementation here ``` #### Module 4: recovery_metrics.py **Pfad:** `backend/data_layer/recovery_metrics.py` **Funktionen:** ```python """ Recovery score, sleep analysis, vitals baselines. """ def get_recovery_score_data( profile_id: str, days: int = 7 ) -> dict: """ Composite recovery score from RHR, HRV, sleep, rest days. Returns: { "score": int, # 0-100 "components": { "rhr": { "value": float, "baseline_7d": float, "deviation_pct": float, "score": int # 0-100 }, "hrv": { "value": float, "baseline_7d": float, "deviation_pct": float, "score": int }, "sleep": { "duration_h": float, "quality_pct": float, # Deep+REM / total "score": int }, "rest_compliance": { "rest_days": int, "recommended": int, "score": int } }, "trend": str, # "improving"/"stable"/"declining" "confidence": str, "data_points": int } Component Weights: - RHR: 30% - HRV: 30% - Sleep: 30% - Rest Compliance: 10% Score Calculations: RHR Score: - Below baseline by >5% = 100 - At baseline ±5% = 80 - Above baseline by 5-10% = 50 - Above baseline by >10% = 20 HRV Score: - Above baseline by >10% = 100 - At baseline ±10% = 80 - Below baseline by 10-20% = 50 - Below baseline by >20% = 20 Sleep Score: - Duration >= 7h AND quality >= 75% = 100 - Duration >= 6h AND quality >= 65% = 80 - Duration >= 5h OR quality >= 50% = 50 - Else = 20 Rest Compliance: - rest_days >= recommended = 100 - rest_days >= recommended - 1 = 70 - Else = proportional Migration from Phase 0b: - OLD: Part of health_stability_score (mixed logic) - NEW: This function (focused on recovery only) """ # Implementation here def get_sleep_regularity_data( profile_id: str, days: int = 28 ) -> dict: """ Sleep regularity index and patterns. Returns: { "regularity_score": int, # 0-100 "avg_duration_h": float, "std_dev_duration": float, "avg_bedtime": str, # "23:15" (HH:MM) "std_dev_bedtime_min": float, "sleep_debt_h": float, # cumulative vs. 7h target "confidence": str, "data_points": int } Regularity Score: - Based on consistency of duration and bedtime - Low std_dev = high score - Formula: 100 - (std_dev_duration * 10 + std_dev_bedtime_min / 6) """ # Implementation here def get_vitals_baseline_data( profile_id: str, days: int = 7 ) -> dict: """ Baseline vitals: RHR, HRV, VO2Max, SpO2, respiratory rate. Returns: { "rhr": { "current": float, "baseline_7d": float, "baseline_28d": float, "trend": str # "improving"/"stable"/"declining" }, "hrv": { "current": float, "baseline_7d": float, "baseline_28d": float, "trend": str }, "vo2_max": { "current": float, "baseline_28d": float, "trend": str }, "spo2": { "current": float, "baseline_7d": float }, "respiratory_rate": { "current": float, "baseline_7d": float }, "confidence": str, "data_points": int } Trend Calculation: - Compare current vs. baseline - RHR: lower = improving - HRV: higher = improving - VO2Max: higher = improving """ # Implementation here ``` #### Module 5: health_metrics.py **Pfad:** `backend/data_layer/health_metrics.py` **Funktionen:** ```python """ Blood pressure, health stability score, risk indicators. """ def get_blood_pressure_data( profile_id: str, days: int = 28 ) -> dict: """ Blood pressure trends and risk classification. Returns: { "measurements": [ { "date": date, "systolic": int, "diastolic": int, "pulse": int, "context": str, "classification": str # WHO/ISH }, ... ], "avg_systolic": float, "avg_diastolic": float, "avg_pulse": float, "risk_level": str, # "normal"/"elevated"/"hypertension_stage_1"/... "measurements_by_context": dict, "confidence": str, "data_points": int } WHO/ISH Classification: - Normal: <120/<80 - Elevated: 120-129/<80 - Hypertension Stage 1: 130-139/80-89 - Hypertension Stage 2: >=140/>=90 """ # Implementation here def get_health_stability_score( profile_id: str, days: int = 28 ) -> dict: """ Overall health stability across multiple dimensions. Returns: { "score": int, # 0-100 "components": { "vitals_stability": int, # RHR, HRV, BP variance "sleep_regularity": int, "activity_consistency": int, "nutrition_adherence": int, "recovery_quality": int }, "risk_indicators": [ {"type": str, "severity": str, "message": str}, ... ], "confidence": str } Risk Indicators: - RED-S: energy_availability < 30 - Overtraining: high strain + low recovery - BP Risk: avg systolic >= 130 - Sleep Debt: cumulative > 10h - HRV Drop: < baseline by >20% Migration: - NEW function (was part of Phase 0b scope, moved to 0c) """ # Implementation here ``` #### Module 6: goals.py **Pfad:** `backend/data_layer/goals.py` **Funktionen:** ```python """ Goal tracking, progress, projections. """ def get_active_goals(profile_id: str) -> list[dict]: """ All active goals with full details. Returns: [ { "id": str, "goal_type": str, "name": str, "target_value": float, "target_date": date | None, "current_value": float, "start_value": float, "start_date": date, "progress_pct": float, "status": str, "is_primary": bool, "created_at": date, "focus_contributions": [ {"focus_area": str, "weight": float}, ... ] }, ... ] Migration from Phase 0b: - OLD: goal_utils.get_active_goals() - NEW: This function (moved to data_layer) - goal_utils.py imports from here """ # Implementation here def get_goal_progress_data( profile_id: str, goal_id: str ) -> dict: """ Detailed progress tracking for a single goal. Returns: { "goal": dict, # Full goal object "history": [ {"date": date, "value": float}, ... ], "progress_pct": float, "time_progress_pct": float, # (elapsed / total) * 100 "deviation": float, # actual - expected (time-based) "projection": { "estimated_completion": date, "linear_rate": float, "confidence": str } | None, "is_behind_schedule": bool, "is_on_track": bool } Time-Based Tracking (from Phase 0b Enhancement, 28.03.2026): - expected_progress = (elapsed_days / total_days) * 100 - deviation = actual_progress - expected_progress - Negative = behind schedule - Positive = ahead of schedule Auto-Population (from Phase 0b Enhancement, 28.03.2026): - start_value automatically populated from first historical measurement - start_date adjusted to actual measurement date """ # Implementation here def get_weighted_focus_areas(profile_id: str) -> list[dict]: """ User's weighted focus areas. Returns: [ { "key": str, "name": str, "category": str, "weight": float, # 0-100 "active_goals": int # count }, ... ] Migration from Phase 0b: - OLD: Part of placeholder resolution - NEW: This function (clean data) """ # Implementation here ``` #### Module 7: correlations.py **Pfad:** `backend/data_layer/correlations.py` **Funktionen:** ```python """ Lag-based correlations, plateau detection. """ def get_correlation_data( profile_id: str, metric_a: str, metric_b: str, days: int = 90, max_lag: int = 7 ) -> dict: """ Lag-based correlation between two metrics. Args: metric_a: e.g., "calorie_deficit" metric_b: e.g., "weight_change" max_lag: Maximum lag in days to test Returns: { "correlation": float, # Pearson r at best lag "best_lag": int, # Days of lag "p_value": float, "confidence": str, "paired_points": int, "interpretation": str # "strong"/"moderate"/"weak"/"none" } Confidence Rules: - "high": >= 28 paired points - "medium": >= 21 paired points - "low": >= 14 paired points - "insufficient": < 14 paired points Interpretation: - |r| > 0.7: "strong" - |r| > 0.5: "moderate" - |r| > 0.3: "weak" - |r| <= 0.3: "none" Migration: - NEW function (was Phase 0b scope, moved to 0c) """ # Implementation here def detect_plateau( profile_id: str, metric: str, days: int = 28 ) -> dict: """ Detect if metric has plateaued despite expected change. Returns: { "is_plateau": bool, "metric": str, "duration_days": int, "expected_change": float, "actual_change": float, "confidence": str, "possible_causes": [str, ...] } Plateau Criteria: - Weight: < 0.2kg change in 28d despite calorie deficit - Strength: No PR in 42d despite training - VO2Max: < 1% change in 90d despite cardio training Possible Causes: - "metabolic_adaptation" (weight) - "insufficient_stimulus" (strength/cardio) - "overtraining" (all) - "nutrition_inadequate" (strength) """ # Implementation here ``` #### Module 8: utils.py **Pfad:** `backend/data_layer/utils.py` **Funktionen:** ```python """ Shared utilities: confidence scoring, baseline calculations, outlier detection. """ def calculate_confidence( data_points: int, days_requested: int, metric_type: str = "general" ) -> str: """ Determine confidence level based on data availability. Args: data_points: Number of actual data points days_requested: Number of days in analysis window metric_type: "general" | "correlation" | "trend" Returns: "high" | "medium" | "low" | "insufficient" Rules: General (days_requested): 7d: high >= 4, medium >= 3, low >= 2 28d: high >= 18, medium >= 12, low >= 8 90d: high >= 60, medium >= 40, low >= 25 Correlation: high >= 28, medium >= 21, low >= 14 Trend: high >= (days * 0.7), medium >= (days * 0.5), low >= (days * 0.3) """ # Implementation here def calculate_baseline( values: list[float], method: str = "median" ) -> float: """ Calculate baseline value. Args: values: List of measurements method: "median" | "mean" | "trimmed_mean" Returns: Baseline value (float) Trimmed Mean: - Remove top/bottom 10% of values - Calculate mean of remaining - More robust than mean, less aggressive than median """ # Implementation here def detect_outliers( values: list[float], method: str = "iqr" ) -> list[int]: """ Detect outlier indices. Args: values: List of measurements method: "iqr" | "zscore" | "mad" Returns: List of outlier indices IQR Method (recommended): - Q1 = 25th percentile - Q3 = 75th percentile - IQR = Q3 - Q1 - Outliers: < Q1 - 1.5*IQR OR > Q3 + 1.5*IQR """ # Implementation here def calculate_linear_regression( x: list[float], y: list[float] ) -> dict: """ Simple linear regression. Returns: { "slope": float, "intercept": float, "r_squared": float, "p_value": float } """ # Implementation here def serialize_dates(obj): """ Convert date/datetime objects to ISO strings for JSON serialization. (Already exists in routers/goals.py - move here for reusability) Migration from Phase 0b Enhancement (28.03.2026): - Learned from bug: Python date objects don't auto-serialize - Solution: Recursive conversion to ISO strings """ # Implementation here ``` --- ### Step 2: Placeholder Resolver Refactoring (3-4h) **Pfad:** `backend/placeholder_resolver.py` **Ziel:** Von ~1100 Zeilen zu ~400 Zeilen durch Nutzung des Data Layer. **Muster (für alle Platzhalter):** ```python # ── ALTE IMPLEMENTIERUNG (Phase 0b) ────────────────────────────── def resolve_weight_28d_trend_slope(profile_id: str) -> str: """Returns kg/Woche slope for KI prompts""" with get_db() as conn: cur = get_cursor(conn) # 30 Zeilen SQL queries cur.execute(""" SELECT date, weight FROM weight_log WHERE profile_id = %s AND date >= NOW() - INTERVAL '28 days' ORDER BY date """, (profile_id,)) rows = cur.fetchall() if len(rows) < 18: return "Nicht genug Daten" # 15 Zeilen Berechnungslogik x = [(row[0] - rows[0][0]).days for row in rows] y = [row[1] for row in rows] n = len(x) sum_x = sum(x) sum_y = sum(y) sum_xy = sum(xi * yi for xi, yi in zip(x, y)) sum_x2 = sum(xi ** 2 for xi in x) slope = (n * sum_xy - sum_x * sum_y) / (n * sum_x2 - sum_x ** 2) slope_per_week = slope * 7 # Formatierung return f"{slope_per_week:.2f} kg/Woche" # ── NEUE IMPLEMENTIERUNG (Phase 0c) ────────────────────────────── from data_layer.body_metrics import get_weight_trend_data def resolve_weight_28d_trend_slope(profile_id: str) -> str: """Returns kg/Woche slope for KI prompts""" data = get_weight_trend_data(profile_id, days=28) if data['confidence'] == 'insufficient': return "Nicht genug Daten" return f"{data['slope_28d']:.2f} kg/Woche" ``` **Alle zu refactorierenden Platzhalter:** ```python # KÖRPER resolve_weight_28d_trend_slope() → get_weight_trend_data() resolve_weight_7d_rolling_median() → get_weight_trend_data() resolve_fm_28d_delta() → get_body_composition_data() resolve_lbm_28d_delta() → get_body_composition_data() resolve_recomposition_score() → get_body_composition_data() resolve_circ_summary() → get_circumference_summary() # ERNÄHRUNG resolve_protein_g_per_kg() → get_protein_adequacy_data() resolve_protein_adequacy() → get_protein_adequacy_data() resolve_nutrition_adherence_score() → get_protein_adequacy_data() resolve_energy_balance() → get_energy_balance_data() # AKTIVITÄT resolve_training_volume_28d() → get_training_volume_data() resolve_activity_quality_avg() → get_activity_quality_distribution() resolve_activity_monotony() → get_training_volume_data() # RECOVERY resolve_recovery_score() → get_recovery_score_data() resolve_sleep_regularity() → get_sleep_regularity_data() resolve_sleep_debt_hours() → get_sleep_regularity_data() # GOALS (JSON Platzhalter) resolve_active_goals() → get_active_goals() + json.dumps() resolve_focus_areas() → get_weighted_focus_areas() + json.dumps() # HEALTH resolve_bp_avg() → get_blood_pressure_data() resolve_vitals_baseline() → get_vitals_baseline_data() ``` **Platzhalter-Mapping aktualisieren:** ```python # backend/placeholder_resolver.py PLACEHOLDER_FUNCTIONS = { # ... existing placeholders ... # Phase 0c: Refactored to use data_layer "weight_28d_trend_slope": resolve_weight_28d_trend_slope, "weight_7d_rolling_median": resolve_weight_7d_rolling_median, "fm_28d_delta": resolve_fm_28d_delta, # ... etc. } ``` --- ### Step 3: Charts Router erstellen (6-8h) **Pfad:** `backend/routers/charts.py` **Struktur:** ```python """ Chart data endpoints for frontend visualizations. All endpoints use data_layer functions and return structured JSON compatible with Chart.js / Recharts. Implements charts from konzept_diagramme_auswertungen_v2.md: - K1-K10: Body charts - E1-E4: Nutrition charts - A1-A5: Activity charts - V1-V3: Vitals charts - R1-R2: Recovery charts """ from fastapi import APIRouter, Depends, Query from auth import require_auth from data_layer.body_metrics import ( get_weight_trend_data, get_body_composition_data, get_circumference_summary ) from data_layer.nutrition_metrics import ( get_protein_adequacy_data, get_energy_balance_data, get_macro_distribution_data ) from data_layer.activity_metrics import ( get_training_volume_data, get_activity_quality_distribution, get_ability_balance_data ) from data_layer.recovery_metrics import ( get_recovery_score_data, get_sleep_regularity_data, get_vitals_baseline_data ) from data_layer.health_metrics import ( get_blood_pressure_data, get_health_stability_score ) from data_layer.correlations import ( get_correlation_data, detect_plateau ) router = APIRouter(prefix="/api/charts", tags=["charts"]) # ── BODY CHARTS (K1-K10) ──────────────────────────────────────── @router.get("/weight-trend") def weight_trend_chart( days: int = Query(90, ge=7, le=365), session: dict = Depends(require_auth) ): """ K1: Weight Trend + Goal Projection Returns Chart.js compatible data structure. """ pid = session['profile_id'] data = get_weight_trend_data(pid, days=days) return { "chart_type": "line", "data": { "labels": [str(d[0]) for d in data['raw_values']], "datasets": [ { "label": "Rohwerte", "data": [d[1] for d in data['raw_values']], "type": "scatter", "backgroundColor": "rgba(29, 158, 117, 0.5)", "borderColor": "rgba(29, 158, 117, 0.5)", "pointRadius": 4 }, { "label": "7d Trend (Median)", "data": [d[1] for d in data['rolling_median_7d']], "type": "line", "borderColor": "#1D9E75", "borderWidth": 3, "fill": False, "pointRadius": 0 } ] }, "metadata": { "slope_7d": data['slope_7d'], "slope_28d": data['slope_28d'], "slope_90d": data['slope_90d'], "confidence": data['confidence'], "projection": data['projection'] }, "options": { "title": "Gewichtstrend + Zielprojektion", "yAxisLabel": "Gewicht (kg)", "xAxisLabel": "Datum" } } @router.get("/body-composition") def body_composition_chart( days: int = Query(90, ge=7, le=365), session: dict = Depends(require_auth) ): """ K2: Fat Mass / Lean Mass Trend """ pid = session['profile_id'] data = get_body_composition_data(pid, days=days) return { "chart_type": "line", "data": { "labels": [str(d) for d in data['dates']], "datasets": [ { "label": "Fettmasse (kg)", "data": data['fat_mass'], "borderColor": "#D85A30", "backgroundColor": "rgba(216, 90, 48, 0.1)", "fill": True }, { "label": "Magermasse (kg)", "data": data['lean_mass'], "borderColor": "#1D9E75", "backgroundColor": "rgba(29, 158, 117, 0.1)", "fill": True } ] }, "metadata": { "fm_delta_28d": data['fm_delta_28d'], "lbm_delta_28d": data['lbm_delta_28d'], "recomposition_score": data['recomposition_score'], "confidence": data['confidence'] }, "options": { "title": "Körperkomposition", "yAxisLabel": "Masse (kg)" } } # ── NUTRITION CHARTS (E1-E4) ──────────────────────────────────── @router.get("/protein-adequacy") def protein_adequacy_chart( days: int = Query(28, ge=7, le=90), session: dict = Depends(require_auth) ): """ E1: Protein Intake vs. Target """ pid = session['profile_id'] data = get_protein_adequacy_data(pid, days=days) return { "chart_type": "line", "data": { "labels": [str(d[0]) for d in data['daily_values']], "datasets": [ { "label": "Protein (g)", "data": [d[1] for d in data['daily_values']], "type": "bar", "backgroundColor": "rgba(29, 158, 117, 0.7)" }, { "label": "Ziel (g)", "data": [d[2] for d in data['daily_values']], "type": "line", "borderColor": "#D85A30", "borderDash": [5, 5], "fill": False } ] }, "metadata": { "avg_protein_g": data['avg_protein_g'], "target_protein_g": data['target_protein_g'], "adherence_pct": data['adherence_pct'], "adherence_score": data['adherence_score'], "confidence": data['confidence'] } } @router.get("/energy-balance") def energy_balance_chart( days: int = Query(28, ge=7, le=90), session: dict = Depends(require_auth) ): """ E2: Energy Balance (Intake - Activity) """ pid = session['profile_id'] data = get_energy_balance_data(pid, days=days) return { "chart_type": "line", "data": { "labels": [str(d[0]) for d in data['daily_values']], "datasets": [ { "label": "Aufnahme (kcal)", "data": [d[1] for d in data['daily_values']], "borderColor": "#1D9E75", "fill": False }, { "label": "Verbrauch (kcal)", "data": [d[2] for d in data['daily_values']], "borderColor": "#D85A30", "fill": False }, { "label": "Netto (kcal)", "data": [d[3] for d in data['daily_values']], "borderColor": "#666", "borderDash": [5, 5], "fill": False } ] }, "metadata": { "avg_net": data['avg_net'], "energy_availability": data['energy_availability'], "red_s_warning": data['red_s_warning'], "confidence": data['confidence'] } } # ── ACTIVITY CHARTS (A1-A5) ───────────────────────────────────── @router.get("/training-volume") def training_volume_chart( weeks: int = Query(4, ge=1, le=12), session: dict = Depends(require_auth) ): """ A1: Training Volume per Week """ pid = session['profile_id'] data = get_training_volume_data(pid, weeks=weeks) return { "chart_type": "bar", "data": { "labels": [str(w['week_start']) for w in data['weekly_totals']], "datasets": [ { "label": "Dauer (min)", "data": [w['duration_min'] for w in data['weekly_totals']], "backgroundColor": "rgba(29, 158, 117, 0.7)" } ] }, "metadata": { "by_type": data['by_type'], "avg_quality": data['avg_quality'], "monotony": data['monotony'], "strain": data['strain'], "confidence": data['confidence'] } } @router.get("/ability-balance") def ability_balance_chart( weeks: int = Query(4, ge=1, le=12), session: dict = Depends(require_auth) ): """ A5: Ability Balance Radar """ pid = session['profile_id'] data = get_ability_balance_data(pid, weeks=weeks) return { "chart_type": "radar", "data": { "labels": ["Kraft", "Ausdauer", "Mobilität", "Koordination", "Mental"], "datasets": [ { "label": "Aktuelle Balance", "data": [ data['abilities']['strength'], data['abilities']['cardio'], data['abilities']['mobility'], data['abilities']['coordination'], data['abilities']['mental'] ], "backgroundColor": "rgba(29, 158, 117, 0.2)", "borderColor": "#1D9E75", "pointBackgroundColor": "#1D9E75" } ] }, "metadata": { "balance_score": data['balance_score'], "imbalances": data['imbalances'], "confidence": data['confidence'] } } # ── RECOVERY CHARTS (R1-R2) ───────────────────────────────────── @router.get("/recovery-score") def recovery_score_chart( days: int = Query(7, ge=7, le=28), session: dict = Depends(require_auth) ): """ R1: Recovery Score Breakdown """ pid = session['profile_id'] data = get_recovery_score_data(pid, days=days) return { "chart_type": "bar_horizontal", "data": { "labels": ["RHR", "HRV", "Sleep", "Rest Compliance"], "datasets": [ { "label": "Score", "data": [ data['components']['rhr']['score'], data['components']['hrv']['score'], data['components']['sleep']['score'], data['components']['rest_compliance']['score'] ], "backgroundColor": [ "#1D9E75", "#1D9E75", "#1D9E75", "#1D9E75" ] } ] }, "metadata": { "total_score": data['score'], "trend": data['trend'], "confidence": data['confidence'] } } # ── VITALS CHARTS (V1-V3) ─────────────────────────────────────── @router.get("/blood-pressure") def blood_pressure_chart( days: int = Query(28, ge=7, le=90), session: dict = Depends(require_auth) ): """ V1: Blood Pressure Trend """ pid = session['profile_id'] data = get_blood_pressure_data(pid, days=days) return { "chart_type": "line", "data": { "labels": [str(m['date']) for m in data['measurements']], "datasets": [ { "label": "Systolisch (mmHg)", "data": [m['systolic'] for m in data['measurements']], "borderColor": "#D85A30", "fill": False }, { "label": "Diastolisch (mmHg)", "data": [m['diastolic'] for m in data['measurements']], "borderColor": "#1D9E75", "fill": False } ] }, "metadata": { "avg_systolic": data['avg_systolic'], "avg_diastolic": data['avg_diastolic'], "risk_level": data['risk_level'], "confidence": data['confidence'] } } # ── CORRELATIONS ──────────────────────────────────────────────── @router.get("/correlation") def correlation_chart( metric_a: str = Query(..., description="e.g., 'calorie_deficit'"), metric_b: str = Query(..., description="e.g., 'weight_change'"), days: int = Query(90, ge=28, le=365), session: dict = Depends(require_auth) ): """ Lag-based correlation between two metrics. """ pid = session['profile_id'] data = get_correlation_data(pid, metric_a, metric_b, days=days) return { "chart_type": "scatter", "data": { # Scatter plot data would go here # (implementation depends on metric types) }, "metadata": { "correlation": data['correlation'], "best_lag": data['best_lag'], "p_value": data['p_value'], "interpretation": data['interpretation'], "confidence": data['confidence'] } } @router.get("/plateau-detection") def plateau_detection( metric: str = Query(..., description="e.g., 'weight', 'vo2max'"), days: int = Query(28, ge=14, le=90), session: dict = Depends(require_auth) ): """ Detect if metric has plateaued. """ pid = session['profile_id'] data = detect_plateau(pid, metric, days=days) return { "is_plateau": data['is_plateau'], "metric": data['metric'], "duration_days": data['duration_days'], "expected_change": data['expected_change'], "actual_change": data['actual_change'], "possible_causes": data['possible_causes'], "confidence": data['confidence'] } ``` **Router in main.py registrieren:** ```python # backend/main.py from routers import charts # NEU # ... existing routers ... app.include_router(charts.router) # NEU ``` --- ### Step 4: goal_utils.py Refactoring (1h) **Pfad:** `backend/goal_utils.py` **Änderungen:** ```python # ALT: def get_active_goals(profile_id: str) -> list[dict]: # 50 Zeilen SQL + Logik ... # NEU: from data_layer.goals import get_active_goals as _get_active_goals def get_active_goals(profile_id: str) -> list[dict]: """ Wrapper for backwards compatibility. Phase 0c: Delegates to data_layer.goals.get_active_goals() """ return _get_active_goals(profile_id) # map_focus_to_score_components() BLEIBT HIER # → Ist Score-Gewichtung, nicht Datenermittlung ``` --- ### Step 5: Testing (2-3h) **Test-Strategie:** #### Unit Tests für Data Layer **Pfad:** `backend/tests/test_data_layer.py` (NEU) ```python import pytest from data_layer.body_metrics import get_weight_trend_data from data_layer.utils import calculate_confidence def test_weight_trend_data_sufficient(): """Test with sufficient data points""" data = get_weight_trend_data("test_profile_1", days=28) assert data['confidence'] in ['high', 'medium', 'low', 'insufficient'] assert 'raw_values' in data assert 'slope_28d' in data assert len(data['raw_values']) >= 0 def test_weight_trend_data_insufficient(): """Test with insufficient data points""" data = get_weight_trend_data("profile_no_data", days=28) assert data['confidence'] == 'insufficient' def test_confidence_calculation(): """Test confidence scoring logic""" assert calculate_confidence(20, 28, "general") == "high" assert calculate_confidence(15, 28, "general") == "medium" assert calculate_confidence(5, 28, "general") == "low" assert calculate_confidence(2, 28, "general") == "insufficient" # ... weitere tests ... ``` #### Integration Tests **Pfad:** `backend/tests/test_charts_api.py` (NEU) ```python import pytest from fastapi.testclient import TestClient from main import app client = TestClient(app) def test_weight_trend_chart_endpoint(auth_token): """Test weight trend chart endpoint""" response = client.get( "/api/charts/weight-trend?days=90", headers={"X-Auth-Token": auth_token} ) assert response.status_code == 200 data = response.json() assert 'chart_type' in data assert data['chart_type'] == 'line' assert 'data' in data assert 'metadata' in data assert 'confidence' in data['metadata'] # ... weitere tests ... ``` #### Manual Testing Checklist ``` Data Layer: [ ] get_weight_trend_data() mit verschiedenen days-Parametern [ ] get_body_composition_data() mit realen Profil-Daten [ ] get_protein_adequacy_data() mit goal_mode Variationen [ ] get_recovery_score_data() mit/ohne vollständige Vitals [ ] Confidence scoring bei verschiedenen Datenmengen [ ] Outlier detection funktioniert korrekt [ ] Baseline calculations korrekt KI Layer (Refactored): [ ] Alle bestehenden Platzhalter funktionieren weiter [ ] Keine Regression in KI-Prompt-Outputs [ ] {{active_goals}} und {{focus_areas}} JSON korrekt Charts API: [ ] Alle 10+ Chart-Endpoints erreichbar [ ] JSON-Struktur Chart.js-kompatibel [ ] Metadata vollständig [ ] Fehlerbehandlung bei fehlenden Daten [ ] Auth funktioniert (require_auth) Performance: [ ] Keine N+1 Queries [ ] Response Times < 500ms [ ] Kein Memory Leak bei großen Datenmengen ``` --- ### Step 6: Dokumentation (1-2h) #### 1. Architecture Documentation **Pfad:** `.claude/docs/technical/DATA_LAYER_ARCHITECTURE.md` (NEU) ```markdown # Data Layer Architecture ## Overview Three-layer architecture for data retrieval, calculation, and presentation. ## Layers ### Layer 1: Data Layer (`backend/data_layer/`) - **Purpose:** Pure data retrieval + calculation logic - **Returns:** Structured data (dict/list/float) - **No formatting:** No strings, no KI-specific formatting - **Testable:** Unit tests for each function - **Reusable:** Used by both KI layer and visualization layer ### Layer 2a: KI Layer (`backend/placeholder_resolver.py`) - **Purpose:** Format data for KI prompts - **Input:** Data from data_layer - **Output:** Formatted strings - **Example:** `"0.23 kg/Woche"`, `"78/100"`, JSON strings ### Layer 2b: Visualization Layer (`backend/routers/charts.py`) - **Purpose:** Provide data for frontend charts - **Input:** Data from data_layer - **Output:** Chart.js compatible JSON - **Example:** `{"chart_type": "line", "data": {...}, "metadata": {...}}` ## Function Naming Conventions - Data Layer: `get__data()` → returns dict - KI Layer: `resolve_()` → returns str - Charts: `_chart()` → returns dict (Chart.js format) ## Migration from Phase 0b All placeholder functions in `placeholder_resolver.py` that contained inline SQL queries and calculations have been moved to `data_layer/`. The placeholder functions now simply call data_layer functions and format the result for KI consumption. ... ``` #### 2. API Documentation **Pfad:** `docs/api/CHARTS_API.md` (NEU) ```markdown # Charts API Reference ## Base URL `/api/charts` ## Authentication All endpoints require authentication via `X-Auth-Token` header. ## Endpoints ### Body Charts #### GET /charts/weight-trend Weight trend with goal projections. **Parameters:** - `days` (query, int, optional): Analysis window (default: 90, range: 7-365) **Response:** ```json { "chart_type": "line", "data": { "labels": ["2026-01-01", "2026-01-02", ...], "datasets": [...] }, "metadata": { "slope_28d": 0.23, "confidence": "high", ... } } ``` ... ``` #### 3. Update CLAUDE.md **Pfad:** `CLAUDE.md` ```markdown ### Phase 0c Completion (29-30.03.2026) 🏗️ - ✅ **Multi-Layer Data Architecture:** - Data Layer: 8 modules, 50+ functions - KI Layer: Refactored placeholder_resolver.py - Visualization Layer: charts.py router - ✅ **Charts API:** 10+ endpoints für Diagramme - ✅ **Separation of Concerns:** Single Source of Truth - ✅ **Testing:** Unit tests für Data Layer - ✅ **Dokumentation:** Architecture + API docs **Betroffene Dateien:** - `backend/data_layer/*` - NEU (8 Module) - `backend/routers/charts.py` - NEU - `backend/placeholder_resolver.py` - REFACTORED - `backend/goal_utils.py` - REFACTORED ``` --- ## Acceptance Criteria Phase 0c ist abgeschlossen, wenn: ### Funktional - ✅ Alle 50+ Data Layer Funktionen implementiert - ✅ Alle bestehenden Platzhalter funktionieren weiter (keine Regression) - ✅ Mindestens 10 Chart-Endpoints verfügbar - ✅ goal_utils.py nutzt data_layer.goals - ✅ Alle Charts liefern Chart.js-kompatible Daten ### Technisch - ✅ Keine Code-Duplikation zwischen KI Layer und Charts - ✅ Data Layer hat Unit Tests (>80% coverage für utils.py) - ✅ Confidence scoring funktioniert korrekt - ✅ Outlier detection funktioniert - ✅ Alle Decimal → Float Conversions korrekt ### Qualität - ✅ Keine SQL queries in placeholder_resolver.py - ✅ Keine SQL queries in routers/charts.py - ✅ Alle Funktionen haben Type Hints - ✅ Alle Funktionen haben Docstrings - ✅ Migrations laufen erfolgreich ### Dokumentation - ✅ DATA_LAYER_ARCHITECTURE.md erstellt - ✅ CHARTS_API.md erstellt - ✅ CLAUDE.md aktualisiert - ✅ Dieses Issue-Dokument vollständig --- ## Common Pitfalls (Learnings from Phase 0b) ### 1. Decimal → Float Conversion ```python # ❌ WRONG: protein = row['protein'] # Decimal object # ✅ CORRECT: protein = float(row['protein']) if row['protein'] else 0.0 ``` ### 2. Date Serialization ```python # ❌ WRONG: return {"date": date_obj} # Not JSON serializable # ✅ CORRECT: from data_layer.utils import serialize_dates return serialize_dates({"date": date_obj}) ``` ### 3. Dict Access Safety ```python # ❌ WRONG: value = data['key'] # KeyError if missing # ✅ CORRECT: value = data.get('key', default_value) ``` ### 4. Column Name Consistency ```python # ❌ WRONG (assumed name): SELECT bf_jpl FROM caliper_log # ✅ CORRECT (check schema): SELECT body_fat_pct FROM caliper_log ``` ### 5. Confidence Calculation ```python # ✅ ALWAYS use utils.calculate_confidence() # DON'T hardcode confidence logic ``` ### 6. SQL Query Structure ```python # ✅ Use parameter binding: cur.execute("SELECT * FROM t WHERE id = %s", (id,)) # ❌ NEVER string concatenation: cur.execute(f"SELECT * FROM t WHERE id = {id}") ``` --- ## Timeline **Geschätzte Dauer:** 20-27h (5-7 Tage bei 4h/Tag) | Tag | Aufgabe | Stunden | |-----|---------|---------| | 1-2 | Data Layer Module 1-4 (body, nutrition, activity, recovery) | 6-8h | | 3 | Data Layer Module 5-8 (health, goals, correlations, utils) | 4-5h | | 4 | Placeholder Resolver Refactoring | 3-4h | | 5 | Charts Router (10+ endpoints) | 6-8h | | 6 | goal_utils.py Refactoring + Testing | 3-4h | | 7 | Dokumentation + Final Testing | 2-3h | **Total:** 24-32h (realistisch: 5-7 Tage) --- ## Next Steps After Phase 0c **Phase 1: Frontend Charts (2-3 Wochen)** - Chart-Komponenten in React implementieren - Integration der Charts API - Dashboard-Layout mit Charts **Phase 2: Caching Layer** - Redis für häufige Abfragen - Cache invalidation strategy **Phase 3: Advanced Analytics** - Machine Learning für Projektionen - Anomaly Detection mit ML - Personalisierte Empfehlungen --- **Erstellt:** 28. März 2026 **Autor:** Claude Sonnet 4.5 **Status:** Ready for Implementation **Gitea Issue:** #53 (zu erstellen)