423 lines
16 KiB
Markdown
423 lines
16 KiB
Markdown
# Phase 0c: Placeholder Migration Analysis
|
||
|
||
**Erstellt:** 28. März 2026
|
||
**Zweck:** Analyse welche Platzhalter zu Data Layer migriert werden müssen
|
||
|
||
---
|
||
|
||
## Gesamt-Übersicht
|
||
|
||
**Aktuelle Platzhalter:** 116
|
||
**Nach Phase 0c Migration:**
|
||
- ✅ **Bleiben einfach (kein Data Layer):** 8 Platzhalter
|
||
- 🔄 **Gehen zu Data Layer:** 108 Platzhalter
|
||
|
||
---
|
||
|
||
## Kategorisierung: BLEIBEN EINFACH (8 Platzhalter)
|
||
|
||
Diese Platzhalter bleiben im KI Layer (placeholder_resolver.py) weil sie:
|
||
- Keine Berechnungen durchführen
|
||
- Keine Daten-Aggregation benötigen
|
||
- Einfache Getter oder Konstanten sind
|
||
|
||
### Zeitraum (4 Platzhalter)
|
||
```python
|
||
'{{datum_heute}}': lambda pid: datetime.now().strftime('%d.%m.%Y')
|
||
'{{zeitraum_7d}}': lambda pid: 'letzte 7 Tage'
|
||
'{{zeitraum_30d}}': lambda pid: 'letzte 30 Tage'
|
||
'{{zeitraum_90d}}': lambda pid: 'letzte 90 Tage'
|
||
```
|
||
**Begründung:** Konstanten oder einfache Datum-Formatierung. Kein Data Layer nötig.
|
||
|
||
### Profil - Basis (4 Platzhalter)
|
||
```python
|
||
'{{name}}': lambda pid: get_profile_data(pid).get('name', 'Nutzer')
|
||
'{{age}}': lambda pid: calculate_age(get_profile_data(pid).get('dob'))
|
||
'{{height}}': lambda pid: str(get_profile_data(pid).get('height', 'unbekannt'))
|
||
'{{geschlecht}}': lambda pid: 'männlich' if get_profile_data(pid).get('sex') == 'm' else 'weiblich'
|
||
```
|
||
**Begründung:** Direkte Getter aus profiles Tabelle. Keine Aggregation.
|
||
|
||
---
|
||
|
||
## GEHEN ZU DATA LAYER (108 Platzhalter)
|
||
|
||
### 1. Körper (20 Platzhalter) → `data_layer.body_metrics`
|
||
|
||
#### Basis-Metriken (8):
|
||
```python
|
||
'{{weight_aktuell}}' → get_weight_trend_data()['last_value']
|
||
'{{weight_trend}}' → get_weight_trend_data() (formatiert)
|
||
'{{kf_aktuell}}' → get_body_composition_data()['body_fat_pct'][-1]
|
||
'{{bmi}}' → get_body_composition_data() (berechnet)
|
||
'{{caliper_summary}}' → get_caliper_summary_data()
|
||
'{{circ_summary}}' → get_circumference_summary()
|
||
'{{goal_weight}}' → get_active_goals() (filtered)
|
||
'{{goal_bf_pct}}' → get_active_goals() (filtered)
|
||
```
|
||
|
||
#### Phase 0b - Advanced Body (12):
|
||
```python
|
||
'{{weight_7d_median}}' → get_weight_trend_data()['rolling_median_7d'][-1]
|
||
'{{weight_28d_slope}}' → get_weight_trend_data()['slope_28d']
|
||
'{{weight_90d_slope}}' → get_weight_trend_data()['slope_90d']
|
||
'{{fm_28d_change}}' → get_body_composition_data()['fm_delta_28d']
|
||
'{{lbm_28d_change}}' → get_body_composition_data()['lbm_delta_28d']
|
||
'{{waist_28d_delta}}' → get_circumference_summary()['changes']['waist_28d']
|
||
'{{hip_28d_delta}}' → get_circumference_summary()['changes']['hip_28d']
|
||
'{{chest_28d_delta}}' → get_circumference_summary()['changes']['chest_28d']
|
||
'{{arm_28d_delta}}' → get_circumference_summary()['changes']['arm_28d']
|
||
'{{thigh_28d_delta}}' → get_circumference_summary()['changes']['thigh_28d']
|
||
'{{waist_hip_ratio}}' → get_circumference_summary()['ratios']['waist_to_hip']
|
||
'{{recomposition_quadrant}}'→ get_body_composition_data()['recomposition_score']
|
||
```
|
||
|
||
**Data Layer Funktionen benötigt:**
|
||
- `get_weight_trend_data(profile_id, days=90)`
|
||
- `get_body_composition_data(profile_id, days=90)`
|
||
- `get_circumference_summary(profile_id, days=90)`
|
||
- `get_caliper_summary_data(profile_id, days=90)`
|
||
|
||
---
|
||
|
||
### 2. Ernährung (14 Platzhalter) → `data_layer.nutrition_metrics`
|
||
|
||
#### Basis-Metriken (7):
|
||
```python
|
||
'{{kcal_avg}}' → get_energy_balance_data()['avg_intake']
|
||
'{{protein_avg}}' → get_protein_adequacy_data()['avg_protein_g']
|
||
'{{carb_avg}}' → get_macro_distribution_data()['avg_carbs_g']
|
||
'{{fat_avg}}' → get_macro_distribution_data()['avg_fat_g']
|
||
'{{nutrition_days}}' → get_energy_balance_data()['data_points']
|
||
'{{protein_ziel_low}}' → get_protein_adequacy_data()['target_protein_g'] (low)
|
||
'{{protein_ziel_high}}' → get_protein_adequacy_data()['target_protein_g'] (high)
|
||
```
|
||
|
||
#### Phase 0b - Advanced Nutrition (7):
|
||
```python
|
||
'{{energy_balance_7d}}' → get_energy_balance_data()['avg_net']
|
||
'{{energy_deficit_surplus}}'→ get_energy_balance_data()['deficit_surplus_avg']
|
||
'{{protein_g_per_kg}}' → get_protein_adequacy_data()['avg_protein_per_kg']
|
||
'{{protein_days_in_target}}'→ get_protein_adequacy_data()['adherence_pct']
|
||
'{{protein_adequacy_28d}}' → get_protein_adequacy_data()['adherence_score']
|
||
'{{macro_consistency_score}}'→ get_macro_distribution_data()['balance_score']
|
||
'{{intake_volatility}}' → get_macro_distribution_data()['variability']
|
||
```
|
||
|
||
**Data Layer Funktionen benötigt:**
|
||
- `get_protein_adequacy_data(profile_id, days=28, goal_mode=None)`
|
||
- `get_energy_balance_data(profile_id, days=28)`
|
||
- `get_macro_distribution_data(profile_id, days=28)`
|
||
|
||
---
|
||
|
||
### 3. Training (16 Platzhalter) → `data_layer.activity_metrics`
|
||
|
||
#### Basis-Metriken (3):
|
||
```python
|
||
'{{activity_summary}}' → get_training_volume_data()['weekly_totals'] (formatted)
|
||
'{{activity_detail}}' → get_training_volume_data()['by_type'] (formatted)
|
||
'{{trainingstyp_verteilung}}'→ get_activity_quality_distribution()
|
||
```
|
||
|
||
#### Phase 0b - Advanced Activity (13):
|
||
```python
|
||
'{{training_minutes_week}}' → get_training_volume_data()['weekly_totals'][0]['duration_min']
|
||
'{{training_frequency_7d}}' → get_training_volume_data()['weekly_totals'][0]['sessions']
|
||
'{{quality_sessions_pct}}' → get_activity_quality_distribution()['high_quality_pct']
|
||
'{{ability_balance_strength}}' → get_ability_balance_data()['abilities']['strength']
|
||
'{{ability_balance_endurance}}'→ get_ability_balance_data()['abilities']['cardio']
|
||
'{{ability_balance_mental}}' → get_ability_balance_data()['abilities']['mental']
|
||
'{{ability_balance_coordination}}'→ get_ability_balance_data()['abilities']['coordination']
|
||
'{{ability_balance_mobility}}' → get_ability_balance_data()['abilities']['mobility']
|
||
'{{proxy_internal_load_7d}}'→ get_training_volume_data()['strain']
|
||
'{{monotony_score}}' → get_training_volume_data()['monotony']
|
||
'{{strain_score}}' → get_training_volume_data()['strain']
|
||
'{{rest_day_compliance}}' → get_recovery_score_data()['components']['rest_compliance']['score']
|
||
'{{vo2max_trend_28d}}' → get_vitals_baseline_data()['vo2_max']['trend']
|
||
```
|
||
|
||
**Data Layer Funktionen benötigt:**
|
||
- `get_training_volume_data(profile_id, weeks=4)`
|
||
- `get_activity_quality_distribution(profile_id, days=28)`
|
||
- `get_ability_balance_data(profile_id, weeks=4)`
|
||
|
||
---
|
||
|
||
### 4. Schlaf & Erholung (10 Platzhalter) → `data_layer.recovery_metrics`
|
||
|
||
#### Basis-Metriken (3):
|
||
```python
|
||
'{{sleep_avg_duration}}' → get_sleep_regularity_data()['avg_duration_h']
|
||
'{{sleep_avg_quality}}' → get_sleep_regularity_data()['avg_quality']
|
||
'{{rest_days_count}}' → get_recovery_score_data()['components']['rest_compliance']['rest_days']
|
||
```
|
||
|
||
#### Phase 0b - Advanced Recovery (7):
|
||
```python
|
||
'{{hrv_vs_baseline_pct}}' → get_vitals_baseline_data()['hrv']['deviation_pct']
|
||
'{{rhr_vs_baseline_pct}}' → get_vitals_baseline_data()['rhr']['deviation_pct']
|
||
'{{sleep_avg_duration_7d}}' → get_sleep_regularity_data()['avg_duration_h']
|
||
'{{sleep_debt_hours}}' → get_sleep_regularity_data()['sleep_debt_h']
|
||
'{{sleep_regularity_proxy}}'→ get_sleep_regularity_data()['regularity_score']
|
||
'{{recent_load_balance_3d}}'→ get_recovery_score_data()['load_balance']
|
||
'{{sleep_quality_7d}}' → get_sleep_regularity_data()['avg_quality']
|
||
```
|
||
|
||
**Data Layer Funktionen benötigt:**
|
||
- `get_recovery_score_data(profile_id, days=7)`
|
||
- `get_sleep_regularity_data(profile_id, days=28)`
|
||
- `get_vitals_baseline_data(profile_id, days=7)`
|
||
|
||
---
|
||
|
||
### 5. Vitalwerte (3 Platzhalter) → `data_layer.health_metrics`
|
||
|
||
```python
|
||
'{{vitals_avg_hr}}' → get_vitals_baseline_data()['rhr']['current']
|
||
'{{vitals_avg_hrv}}' → get_vitals_baseline_data()['hrv']['current']
|
||
'{{vitals_vo2_max}}' → get_vitals_baseline_data()['vo2_max']['current']
|
||
```
|
||
|
||
**Data Layer Funktionen benötigt:**
|
||
- `get_vitals_baseline_data(profile_id, days=7)` (bereits in recovery)
|
||
|
||
---
|
||
|
||
### 6. Scores (6 Platzhalter) → Diverse Module
|
||
|
||
```python
|
||
'{{goal_progress_score}}' → get_goal_progress_data() → goals.py
|
||
'{{body_progress_score}}' → get_body_composition_data() → body_metrics.py
|
||
'{{nutrition_score}}' → get_protein_adequacy_data() → nutrition_metrics.py
|
||
'{{activity_score}}' → get_training_volume_data() → activity_metrics.py
|
||
'{{recovery_score}}' → get_recovery_score_data()['score'] → recovery_metrics.py
|
||
'{{data_quality_score}}' → get_data_quality_score() → utils.py (NEW)
|
||
```
|
||
|
||
**Hinweis:** Scores nutzen bestehende Data Layer Funktionen, nur Formatierung nötig.
|
||
|
||
---
|
||
|
||
### 7. Top Goals/Focus (5 Platzhalter) → `data_layer.goals`
|
||
|
||
```python
|
||
'{{top_goal_name}}' → get_active_goals()[0]['name']
|
||
'{{top_goal_progress_pct}}' → get_active_goals()[0]['progress_pct']
|
||
'{{top_goal_status}}' → get_active_goals()[0]['status']
|
||
'{{top_focus_area_name}}' → get_weighted_focus_areas()[0]['name']
|
||
'{{top_focus_area_progress}}'→ get_weighted_focus_areas()[0]['progress']
|
||
```
|
||
|
||
**Data Layer Funktionen benötigt:**
|
||
- `get_active_goals(profile_id)` (already exists from Phase 0b)
|
||
- `get_weighted_focus_areas(profile_id)` (already exists from Phase 0b)
|
||
|
||
---
|
||
|
||
### 8. Category Scores (14 Platzhalter) → Formatierung nur
|
||
|
||
```python
|
||
'{{focus_cat_körper_progress}}' → _format_from_aggregated_data()
|
||
'{{focus_cat_körper_weight}}' → _format_from_aggregated_data()
|
||
'{{focus_cat_ernährung_progress}}' → _format_from_aggregated_data()
|
||
'{{focus_cat_ernährung_weight}}' → _format_from_aggregated_data()
|
||
# ... (7 Kategorien × 2 = 14 total)
|
||
```
|
||
|
||
**Hinweis:** Diese nutzen bereits aggregierte Daten aus Phase 0b.
|
||
**Migration:** Nur KI Layer Formatierung, Data Layer nicht nötig (Daten kommen aus anderen Funktionen).
|
||
|
||
---
|
||
|
||
### 9. Korrelationen (7 Platzhalter) → `data_layer.correlations`
|
||
|
||
```python
|
||
'{{correlation_energy_weight_lag}}' → get_correlation_data(pid, 'energy', 'weight')
|
||
'{{correlation_protein_lbm}}' → get_correlation_data(pid, 'protein', 'lbm')
|
||
'{{correlation_load_hrv}}' → get_correlation_data(pid, 'load', 'hrv')
|
||
'{{correlation_load_rhr}}' → get_correlation_data(pid, 'load', 'rhr')
|
||
'{{correlation_sleep_recovery}}' → get_correlation_data(pid, 'sleep', 'recovery')
|
||
'{{plateau_detected}}' → detect_plateau(pid, 'weight')
|
||
'{{top_drivers}}' → get_top_drivers(pid)
|
||
```
|
||
|
||
**Data Layer Funktionen benötigt:**
|
||
- `get_correlation_data(profile_id, metric_a, metric_b, days=90, max_lag=7)`
|
||
- `detect_plateau(profile_id, metric, days=28)`
|
||
- `get_top_drivers(profile_id)` (NEW - identifies top correlations)
|
||
|
||
---
|
||
|
||
### 10. JSON/Markdown (8 Platzhalter) → Formatierung nur
|
||
|
||
```python
|
||
'{{active_goals_json}}' → json.dumps(get_active_goals(pid))
|
||
'{{active_goals_md}}' → format_as_markdown(get_active_goals(pid))
|
||
'{{focus_areas_weighted_json}}' → json.dumps(get_weighted_focus_areas(pid))
|
||
'{{focus_areas_weighted_md}}' → format_as_markdown(get_weighted_focus_areas(pid))
|
||
'{{focus_area_weights_json}}' → json.dumps(get_focus_area_weights(pid))
|
||
'{{top_3_focus_areas}}' → format_top_3(get_weighted_focus_areas(pid))
|
||
'{{top_3_goals_behind_schedule}}' → format_goals_behind(get_active_goals(pid))
|
||
'{{top_3_goals_on_track}}' → format_goals_on_track(get_active_goals(pid))
|
||
```
|
||
|
||
**Hinweis:** Diese nutzen bereits existierende Data Layer Funktionen.
|
||
**Migration:** Nur KI Layer Formatierung (json.dumps, markdown, etc.).
|
||
|
||
---
|
||
|
||
## Data Layer Funktionen - Zusammenfassung
|
||
|
||
### Neue Funktionen zu erstellen (Phase 0c):
|
||
|
||
#### body_metrics.py (4 Funktionen):
|
||
- ✅ `get_weight_trend_data()`
|
||
- ✅ `get_body_composition_data()`
|
||
- ✅ `get_circumference_summary()`
|
||
- ✅ `get_caliper_summary_data()`
|
||
|
||
#### nutrition_metrics.py (3 Funktionen):
|
||
- ✅ `get_protein_adequacy_data()`
|
||
- ✅ `get_energy_balance_data()`
|
||
- ✅ `get_macro_distribution_data()`
|
||
|
||
#### activity_metrics.py (3 Funktionen):
|
||
- ✅ `get_training_volume_data()`
|
||
- ✅ `get_activity_quality_distribution()`
|
||
- ✅ `get_ability_balance_data()`
|
||
|
||
#### recovery_metrics.py (2 Funktionen):
|
||
- ✅ `get_recovery_score_data()`
|
||
- ✅ `get_sleep_regularity_data()`
|
||
|
||
#### health_metrics.py (2 Funktionen):
|
||
- ✅ `get_vitals_baseline_data()`
|
||
- ✅ `get_blood_pressure_data()` (aus Spec)
|
||
|
||
#### goals.py (3 Funktionen):
|
||
- ✅ `get_active_goals()` (exists from Phase 0b)
|
||
- ✅ `get_weighted_focus_areas()` (exists from Phase 0b)
|
||
- ✅ `get_goal_progress_data()` (aus Spec)
|
||
|
||
#### correlations.py (3 Funktionen):
|
||
- ✅ `get_correlation_data()`
|
||
- ✅ `detect_plateau()`
|
||
- 🆕 `get_top_drivers()` (NEW - not in spec)
|
||
|
||
#### utils.py (Shared):
|
||
- ✅ `calculate_confidence()`
|
||
- ✅ `calculate_baseline()`
|
||
- ✅ `detect_outliers()`
|
||
- ✅ `aggregate_data()`
|
||
- ✅ `serialize_dates()`
|
||
- 🆕 `get_data_quality_score()` (NEW)
|
||
|
||
**Total neue Funktionen:** 20 (aus Spec) + 2 (zusätzlich) = **22 Data Layer Funktionen**
|
||
|
||
---
|
||
|
||
## Migration-Aufwand pro Kategorie
|
||
|
||
| Kategorie | Platzhalter | Data Layer Funcs | Aufwand | Priorität |
|
||
|-----------|-------------|------------------|---------|-----------|
|
||
| Körper | 20 | 4 | 3-4h | High |
|
||
| Ernährung | 14 | 3 | 2-3h | High |
|
||
| Training | 16 | 3 | 3-4h | Medium |
|
||
| Recovery | 10 | 2 | 2-3h | Medium |
|
||
| Vitalwerte | 3 | 1 (shared) | 0.5h | Low |
|
||
| Scores | 6 | 0 (use others) | 1h | Low |
|
||
| Goals/Focus | 5 | 0 (exists) | 0.5h | Low |
|
||
| Categories | 14 | 0 (formatting) | 1h | Low |
|
||
| Korrelationen | 7 | 3 | 2-3h | Medium |
|
||
| JSON/Markdown | 8 | 0 (formatting) | 0.5h | Low |
|
||
| **TOTAL** | **108** | **22** | **16-22h** | - |
|
||
|
||
---
|
||
|
||
## KI Layer Refactoring-Muster
|
||
|
||
**VORHER (Phase 0b):**
|
||
```python
|
||
def get_latest_weight(profile_id: str) -> str:
|
||
"""Returns latest weight with SQL + formatting"""
|
||
with get_db() as conn:
|
||
cur = get_cursor(conn)
|
||
cur.execute("""
|
||
SELECT weight FROM weight_log
|
||
WHERE profile_id = %s
|
||
ORDER BY date DESC LIMIT 1
|
||
""", (profile_id,))
|
||
row = cur.fetchone()
|
||
if not row:
|
||
return "nicht verfügbar"
|
||
return f"{row['weight']:.1f} kg"
|
||
|
||
PLACEHOLDER_MAP = {
|
||
'{{weight_aktuell}}': get_latest_weight,
|
||
}
|
||
```
|
||
|
||
**NACHHER (Phase 0c):**
|
||
```python
|
||
from data_layer.body_metrics import get_weight_trend_data
|
||
|
||
def resolve_weight_aktuell(profile_id: str) -> str:
|
||
"""Returns latest weight (formatted for KI)"""
|
||
data = get_weight_trend_data(profile_id, days=7)
|
||
|
||
if data['confidence'] == 'insufficient':
|
||
return "nicht verfügbar"
|
||
|
||
return f"{data['last_value']:.1f} kg"
|
||
|
||
PLACEHOLDER_MAP = {
|
||
'{{weight_aktuell}}': resolve_weight_aktuell,
|
||
}
|
||
```
|
||
|
||
**Reduzierung:** Von ~15 Zeilen (SQL + Logic) zu ~7 Zeilen (Call + Format)
|
||
|
||
---
|
||
|
||
## Erwartetes Ergebnis nach Phase 0c
|
||
|
||
### Zeilen-Reduktion:
|
||
- **placeholder_resolver.py:**
|
||
- Vorher: ~1200 Zeilen
|
||
- Nachher: ~400 Zeilen (67% Reduktion)
|
||
|
||
### Code-Qualität:
|
||
- ✅ Keine SQL queries in placeholder_resolver.py
|
||
- ✅ Keine Berechnungslogik in placeholder_resolver.py
|
||
- ✅ Nur Formatierung für KI-Consumption
|
||
|
||
### Wiederverwendbarkeit:
|
||
- ✅ 22 Data Layer Funktionen nutzbar für:
|
||
- KI Layer (108 Platzhalter)
|
||
- Charts Layer (10+ Charts)
|
||
- API Endpoints (beliebig erweiterbar)
|
||
|
||
---
|
||
|
||
## Checkliste: Migration pro Platzhalter
|
||
|
||
Für jeden der **108 Platzhalter**:
|
||
|
||
```
|
||
[ ] Data Layer Funktion existiert
|
||
[ ] KI Layer ruft Data Layer Funktion auf
|
||
[ ] Formatierung für KI korrekt
|
||
[ ] Fehlerbehandlung (insufficient data)
|
||
[ ] Test: Platzhalter liefert gleichen Output wie vorher
|
||
[ ] In PLACEHOLDER_MAP registriert
|
||
[ ] Dokumentiert
|
||
```
|
||
|
||
---
|
||
|
||
**Erstellt:** 28. März 2026
|
||
**Status:** Ready for Phase 0c Implementation
|
||
**Nächster Schritt:** Data Layer Funktionen implementieren (Start mit utils.py)
|