|
|
|
@ -7,6 +7,7 @@ Used for prompt templates and preview functionality.
|
|
|
|
Phase 0c: Refactored to use data_layer for structured data.
|
|
|
|
Phase 0c: Refactored to use data_layer for structured data.
|
|
|
|
This module now focuses on FORMATTING for AI consumption.
|
|
|
|
This module now focuses on FORMATTING for AI consumption.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
|
|
|
|
import json
|
|
|
|
import re
|
|
|
|
import re
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from typing import Dict, List, Optional, Callable, Tuple
|
|
|
|
from typing import Dict, List, Optional, Callable, Tuple
|
|
|
|
@ -57,6 +58,32 @@ def get_profile_data(profile_id: str) -> Dict:
|
|
|
|
return r2d(cur.fetchone()) if cur.rowcount > 0 else {}
|
|
|
|
return r2d(cur.fetchone()) if cur.rowcount > 0 else {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pv_unavailable(reason: str, detail: Optional[str] = None) -> str:
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
Standard-Antwort wenn kein Platzhalter-Wert lieferbar ist.
|
|
|
|
|
|
|
|
Grund ist für Nutzer und KI lesbar (ggf. Alternativen im Text).
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
r = (reason or "Keine auswertbaren Daten").strip()
|
|
|
|
|
|
|
|
if detail:
|
|
|
|
|
|
|
|
d = str(detail).strip()
|
|
|
|
|
|
|
|
if d:
|
|
|
|
|
|
|
|
return f"nicht verfügbar — {r} ({d})"
|
|
|
|
|
|
|
|
return f"nicht verfügbar — {r}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def pv_unavailable_json(reason: str, detail: Optional[str] = None) -> str:
|
|
|
|
|
|
|
|
"""Strukturierte JSON-Antwort statt leeres {} (für KI / Clients)."""
|
|
|
|
|
|
|
|
payload: Dict[str, object] = {
|
|
|
|
|
|
|
|
"_available": False,
|
|
|
|
|
|
|
|
"_reason": (reason or "Keine auswertbaren Daten").strip(),
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if detail:
|
|
|
|
|
|
|
|
d = str(detail).strip()
|
|
|
|
|
|
|
|
if d:
|
|
|
|
|
|
|
|
payload["_detail"] = d
|
|
|
|
|
|
|
|
return json.dumps(payload, ensure_ascii=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_latest_weight(profile_id: str) -> Optional[str]:
|
|
|
|
def get_latest_weight(profile_id: str) -> Optional[str]:
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Get latest weight entry.
|
|
|
|
Get latest weight entry.
|
|
|
|
@ -67,7 +94,10 @@ def get_latest_weight(profile_id: str) -> Optional[str]:
|
|
|
|
data = get_latest_weight_data(profile_id)
|
|
|
|
data = get_latest_weight_data(profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Kein aktuelles Gewicht",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')}, data_points={data.get('data_points',0)}",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return f"{data['weight']:.1f} kg"
|
|
|
|
return f"{data['weight']:.1f} kg"
|
|
|
|
|
|
|
|
|
|
|
|
@ -82,7 +112,10 @@ def get_weight_trend(profile_id: str, days: int = 28) -> str:
|
|
|
|
data = get_weight_trend_data(profile_id, days)
|
|
|
|
data = get_weight_trend_data(profile_id, days)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht genug Daten"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Gewichtstrend nicht ermittelbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')}, Fenster={days} Tage",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
direction = data['direction']
|
|
|
|
direction = data['direction']
|
|
|
|
delta = data['delta']
|
|
|
|
delta = data['delta']
|
|
|
|
@ -105,7 +138,10 @@ def get_latest_bf(profile_id: str) -> Optional[str]:
|
|
|
|
data = get_body_composition_data(profile_id)
|
|
|
|
data = get_body_composition_data(profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Körperfett nicht ermittelbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')} (keine ausreichenden Caliper-/Kompositionsdaten)",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return f"{data['body_fat_pct']:.1f}%"
|
|
|
|
return f"{data['body_fat_pct']:.1f}%"
|
|
|
|
|
|
|
|
|
|
|
|
@ -120,7 +156,10 @@ def get_nutrition_avg(profile_id: str, field: str, days: int = 30) -> str:
|
|
|
|
data = get_nutrition_average_data(profile_id, days)
|
|
|
|
data = get_nutrition_average_data(profile_id, days)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Ernährungsmittelwert nicht ermittelbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')}, Feld={field}, Fenster={days} Tage",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# Map field names to data keys
|
|
|
|
# Map field names to data keys
|
|
|
|
field_map = {
|
|
|
|
field_map = {
|
|
|
|
@ -224,7 +263,10 @@ def get_protein_ziel_low(profile_id: str) -> str:
|
|
|
|
data = get_protein_targets_data(profile_id)
|
|
|
|
data = get_protein_targets_data(profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Proteinziel unten nicht ermittelbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')} (Gewicht/Profil für g/kg)",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return f"{int(data['protein_target_low'])}"
|
|
|
|
return f"{int(data['protein_target_low'])}"
|
|
|
|
|
|
|
|
|
|
|
|
@ -239,7 +281,10 @@ def get_protein_ziel_high(profile_id: str) -> str:
|
|
|
|
data = get_protein_targets_data(profile_id)
|
|
|
|
data = get_protein_targets_data(profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Proteinziel oben nicht ermittelbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')} (Gewicht/Profil für g/kg)",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return f"{int(data['protein_target_high'])}"
|
|
|
|
return f"{int(data['protein_target_high'])}"
|
|
|
|
|
|
|
|
|
|
|
|
@ -423,7 +468,12 @@ def get_sleep_avg_duration(profile_id: str, days: int = 7) -> str:
|
|
|
|
data = get_sleep_duration_data(profile_id, days)
|
|
|
|
data = get_sleep_duration_data(profile_id, days)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Schlafdauer (formatiert) nicht aus sleep_segments ableitbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')}, "
|
|
|
|
|
|
|
|
f"nächte_mit_segmenten={data.get('nights_with_data', 0)}/{data.get('days_analyzed', days)}; "
|
|
|
|
|
|
|
|
f"Hinweis: {{sleep_avg_duration_7d}} nutzt duration_minutes und kann trotzdem Werte liefern",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return f"{data['avg_duration_hours']:.1f}h"
|
|
|
|
return f"{data['avg_duration_hours']:.1f}h"
|
|
|
|
|
|
|
|
|
|
|
|
@ -438,7 +488,11 @@ def get_sleep_avg_quality(profile_id: str, days: int = 7) -> str:
|
|
|
|
data = get_sleep_quality_data(profile_id, days)
|
|
|
|
data = get_sleep_quality_data(profile_id, days)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Schlafqualität (Deep+REM) nicht aus sleep_segments ableitbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')}, "
|
|
|
|
|
|
|
|
f"nächte_analysiert={data.get('nights_analyzed', 0)}/{data.get('days_analyzed', days)}",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return f"{data['quality_score']:.0f}% (Deep+REM)"
|
|
|
|
return f"{data['quality_score']:.0f}% (Deep+REM)"
|
|
|
|
|
|
|
|
|
|
|
|
@ -464,7 +518,10 @@ def get_vitals_avg_hr(profile_id: str, days: int = 7) -> str:
|
|
|
|
data = get_resting_heart_rate_data(profile_id, days)
|
|
|
|
data = get_resting_heart_rate_data(profile_id, days)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Ruhepuls-Schnitt nicht ermittelbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')}, Fenster={days} Tage (vitals_baseline)",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return f"{data['avg_rhr']} bpm"
|
|
|
|
return f"{data['avg_rhr']} bpm"
|
|
|
|
|
|
|
|
|
|
|
|
@ -479,7 +536,10 @@ def get_vitals_avg_hrv(profile_id: str, days: int = 7) -> str:
|
|
|
|
data = get_heart_rate_variability_data(profile_id, days)
|
|
|
|
data = get_heart_rate_variability_data(profile_id, days)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"HRV-Schnitt nicht ermittelbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')}, Fenster={days} Tage (vitals_baseline)",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return f"{data['avg_hrv']} ms"
|
|
|
|
return f"{data['avg_hrv']} ms"
|
|
|
|
|
|
|
|
|
|
|
|
@ -494,11 +554,156 @@ def get_vitals_vo2_max(profile_id: str) -> str:
|
|
|
|
data = get_vo2_max_data(profile_id)
|
|
|
|
data = get_vo2_max_data(profile_id)
|
|
|
|
|
|
|
|
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
if data['confidence'] == 'insufficient':
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"VO2max nicht ermittelbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')} (vitals_baseline)",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
return f"{data['vo2_max']:.1f} ml/kg/min"
|
|
|
|
return f"{data['vo2_max']:.1f} ml/kg/min"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Begründungen wenn Layer-2-Berechnung None liefert (für KI / Export)
|
|
|
|
|
|
|
|
_DEFAULT_NUMERIC_UNAVAILABLE = (
|
|
|
|
|
|
|
|
"Numerische Berechnung liefert keinen Wert (Daten unzureichend oder Schwellen nicht erreicht)"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
_DEFAULT_STR_UNAVAILABLE = (
|
|
|
|
|
|
|
|
"Kein Wert ermittelbar (Daten unzureichend oder Schwellen nicht erreicht)"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
_DEFAULT_JSON_UNAVAILABLE = (
|
|
|
|
|
|
|
|
"Keine strukturierten JSON-Daten ermittelbar (Berechnung liefert None)"
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SAFE_INT_NONE_REASON: Dict[str, str] = {
|
|
|
|
|
|
|
|
"goal_progress_score": (
|
|
|
|
|
|
|
|
"Aggregierter Ziel-Fortschritt nicht berechenbar; Alternativen: {{active_goals_md}}, "
|
|
|
|
|
|
|
|
"{{data_quality_score}}"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"body_progress_score": "Körper-Fortschritts-Score nicht berechenbar",
|
|
|
|
|
|
|
|
"nutrition_score": (
|
|
|
|
|
|
|
|
"Ernährungs-Score nicht berechenbar (z. B. keine gewichteten Ernährungs-Fokusbereiche oder zu wenig Log-Daten)"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"activity_score": (
|
|
|
|
|
|
|
|
"Aktivitäts-Score nicht berechenbar (z. B. Score-Schwellen oder fehlende abilities-Zuordnung in activity_log)"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"recovery_score_v2": "Recovery-Score v2 nicht berechenbar (Schlaf/Vitals/Last)",
|
|
|
|
|
|
|
|
"data_quality_score": "Datenqualitäts-Score nicht berechenbar",
|
|
|
|
|
|
|
|
"top_goal_progress_pct": (
|
|
|
|
|
|
|
|
"Fortschritt % des Top-Ziels nicht ermittelbar (progress_pct fehlt oder Ziel nicht quantifizierbar); "
|
|
|
|
|
|
|
|
"Alternative: {{active_goals_json}}"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"top_focus_area_progress": "Fortschritt % des Top-Fokusbereichs nicht ermittelbar",
|
|
|
|
|
|
|
|
"focus_cat_körper_progress": "Kategorie-Fortschritt „Körper“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_ernährung_progress": "Kategorie-Fortschritt „Ernährung“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_aktivität_progress": "Kategorie-Fortschritt „Aktivität“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_recovery_progress": "Kategorie-Fortschritt „Recovery“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_vitalwerte_progress": "Kategorie-Fortschritt „Vitalwerte“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_mental_progress": "Kategorie-Fortschritt „Mental“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_lebensstil_progress": "Kategorie-Fortschritt „Lebensstil“ nicht berechenbar",
|
|
|
|
|
|
|
|
"training_minutes_week": "Trainingsminuten/Woche nicht berechenbar",
|
|
|
|
|
|
|
|
"training_frequency_7d": "Trainingseinheiten (7 Tage) nicht berechenbar",
|
|
|
|
|
|
|
|
"quality_sessions_pct": "Anteil Qualitätssessions nicht berechenbar",
|
|
|
|
|
|
|
|
"ability_balance_strength": (
|
|
|
|
|
|
|
|
"Fähigkeiten-Balance Kraft nicht berechenbar (zu wenig abilities-Daten in activity_log)"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"ability_balance_endurance": (
|
|
|
|
|
|
|
|
"Fähigkeiten-Balance Ausdauer nicht berechenbar (abilities in activity_log)"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"ability_balance_mental": (
|
|
|
|
|
|
|
|
"Fähigkeiten-Balance Mental nicht berechenbar (abilities in activity_log)"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"ability_balance_coordination": (
|
|
|
|
|
|
|
|
"Fähigkeiten-Balance Koordination nicht berechenbar (abilities in activity_log)"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"ability_balance_mobility": (
|
|
|
|
|
|
|
|
"Fähigkeiten-Balance Mobilität nicht berechenbar (abilities in activity_log)"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"proxy_internal_load_7d": "Interne Last (7 Tage) nicht berechenbar",
|
|
|
|
|
|
|
|
"strain_score": "Strain-Score nicht berechenbar",
|
|
|
|
|
|
|
|
"rest_day_compliance": "Ruhetag-Compliance nicht berechenbar",
|
|
|
|
|
|
|
|
"protein_adequacy_28d": "Protein-Adequacy (28 Tage) nicht berechenbar",
|
|
|
|
|
|
|
|
"macro_consistency_score": "Makro-Konsistenz-Score nicht berechenbar",
|
|
|
|
|
|
|
|
"recent_load_balance_3d": "Load-Balance (3 Tage) nicht berechenbar",
|
|
|
|
|
|
|
|
"sleep_quality_7d": "Schlafqualität 7 Tage nicht berechenbar",
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SAFE_FLOAT_NONE_REASON: Dict[str, str] = {
|
|
|
|
|
|
|
|
"weight_7d_median": (
|
|
|
|
|
|
|
|
"Gewichts-Median 7 Tage: mindestens 4 Messungen im Fenster erforderlich"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"weight_28d_slope": (
|
|
|
|
|
|
|
|
"Gewichts-Trend 28 Tage: zu wenige Messpunkte (ca. 60 % Tagesabdeckung im Fenster)"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"weight_90d_slope": (
|
|
|
|
|
|
|
|
"Gewichts-Trend 90 Tage: zu wenige Messpunkte (ca. 60 % Tagesabdeckung im Fenster)"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"fm_28d_change": "Fettmasse-Änderung 28 Tage nicht berechenbar (Serie Caliper/Gewicht)",
|
|
|
|
|
|
|
|
"lbm_28d_change": "Magermasse-Änderung 28 Tage nicht berechenbar (Serie Caliper/Gewicht)",
|
|
|
|
|
|
|
|
"waist_28d_delta": "Taillen-Delta 28 Tage nicht berechenbar (zwei auswertbare Messungen nötig)",
|
|
|
|
|
|
|
|
"hip_28d_delta": "Hüft-Delta 28 Tage nicht berechenbar",
|
|
|
|
|
|
|
|
"chest_28d_delta": "Brust-Delta 28 Tage nicht berechenbar",
|
|
|
|
|
|
|
|
"arm_28d_delta": "Arm-Delta 28 Tage nicht berechenbar",
|
|
|
|
|
|
|
|
"thigh_28d_delta": "Oberschenkel-Delta 28 Tage nicht berechenbar",
|
|
|
|
|
|
|
|
"waist_hip_ratio": "Taille-Hüfte-Verhältnis nicht berechenbar",
|
|
|
|
|
|
|
|
"energy_balance_7d": (
|
|
|
|
|
|
|
|
"Energiebilanz 7 Tage nicht berechenbar (Intake oder TDEE/Gewicht fehlt)"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"protein_g_per_kg": "Protein g/kg nicht berechenbar",
|
|
|
|
|
|
|
|
"monotony_score": "Monotonie-Score nicht berechenbar",
|
|
|
|
|
|
|
|
"vo2max_trend_28d": "VO2max-Trend 28 Tage nicht berechenbar",
|
|
|
|
|
|
|
|
"hrv_vs_baseline_pct": "HRV vs. Baseline nicht berechenbar (Baseline/Historie)",
|
|
|
|
|
|
|
|
"rhr_vs_baseline_pct": "Ruhepuls vs. Baseline nicht berechenbar",
|
|
|
|
|
|
|
|
"sleep_avg_duration_7d": "Schlafdauer 7 Tage nicht berechenbar (duration_minutes in sleep_log)",
|
|
|
|
|
|
|
|
"sleep_debt_hours": "Schlafschuld nicht berechenbar (mindestens ~10 Nächte mit Dauer)",
|
|
|
|
|
|
|
|
"sleep_regularity_proxy": "Schlaf-Regularität nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_körper_weight": "Kategorie-Gewichtung „Körper“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_ernährung_weight": "Kategorie-Gewichtung „Ernährung“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_aktivität_weight": "Kategorie-Gewichtung „Aktivität“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_recovery_weight": "Kategorie-Gewichtung „Recovery“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_vitalwerte_weight": "Kategorie-Gewichtung „Vitalwerte“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_mental_weight": "Kategorie-Gewichtung „Mental“ nicht berechenbar",
|
|
|
|
|
|
|
|
"focus_cat_lebensstil_weight": "Kategorie-Gewichtung „Lebensstil“ nicht berechenbar",
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SAFE_STR_NONE_REASON: Dict[str, str] = {
|
|
|
|
|
|
|
|
"top_goal_name": "Kein priorisiertes Ziel ermittelbar",
|
|
|
|
|
|
|
|
"top_goal_status": "Status des Top-Ziels nicht ermittelbar",
|
|
|
|
|
|
|
|
"top_focus_area_name": "Kein Top-Fokusbereich ermittelbar",
|
|
|
|
|
|
|
|
"recomposition_quadrant": "Rekompositions-Quadrant nicht berechenbar (FM/LBM-Serie)",
|
|
|
|
|
|
|
|
"energy_deficit_surplus": "Defizit/Überschuss-Status nicht berechenbar",
|
|
|
|
|
|
|
|
"protein_days_in_target": "Protein-Tage im Ziel nicht berechenbar",
|
|
|
|
|
|
|
|
"intake_volatility": "Intake-Volatilität nicht berechenbar",
|
|
|
|
|
|
|
|
"active_goals_md": "Aktive Ziele (Markdown) nicht darstellbar",
|
|
|
|
|
|
|
|
"focus_areas_weighted_md": "Fokusbereiche (Markdown) nicht darstellbar",
|
|
|
|
|
|
|
|
"top_3_focus_areas": "Top-3-Fokusbereiche nicht darstellbar",
|
|
|
|
|
|
|
|
"top_3_goals_behind_schedule": "Ziele „hinter Zeitplan“ nicht darstellbar",
|
|
|
|
|
|
|
|
"top_3_goals_on_track": "Ziele „im Plan“ nicht darstellbar",
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_SAFE_JSON_NONE_REASON: Dict[str, str] = {
|
|
|
|
|
|
|
|
"training_sessions_recent_json": "Keine Session-Daten für JSON-Zeitfenster",
|
|
|
|
|
|
|
|
"correlation_energy_weight_lag": (
|
|
|
|
|
|
|
|
"Korrelation Energiebilanz zu Gewicht: zu wenige gekoppelte Datenpunkte"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"correlation_protein_lbm": (
|
|
|
|
|
|
|
|
"Korrelation Protein zu Magermasse: zu wenige gekoppelte Datenpunkte"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"correlation_load_hrv": (
|
|
|
|
|
|
|
|
"Korrelation Trainingslast zu HRV: zu wenige gekoppelte Datenpunkte"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"correlation_load_rhr": (
|
|
|
|
|
|
|
|
"Korrelation Trainingslast zu Ruhepuls: zu wenige gekoppelte Datenpunkte"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"correlation_sleep_recovery": (
|
|
|
|
|
|
|
|
"Korrelation Schlaf zu Recovery: zu wenige gekoppelte Datenpunkte"
|
|
|
|
|
|
|
|
),
|
|
|
|
|
|
|
|
"plateau_detected": "Plateau-Erkennung: keine auswertbare Serie",
|
|
|
|
|
|
|
|
"top_drivers": "Top-Treiber: keine auswertbare Korrelationsbasis",
|
|
|
|
|
|
|
|
"active_goals_json": "Aktive Ziele als JSON nicht ermittelbar",
|
|
|
|
|
|
|
|
"focus_areas_weighted_json": "Gewichtete Fokusbereiche JSON nicht ermittelbar",
|
|
|
|
|
|
|
|
"focus_area_weights_json": "Fokus-Gewichtungen JSON nicht ermittelbar",
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Phase 0b Calculation Engine Integration ──────────────────────────────────
|
|
|
|
# ── Phase 0b Calculation Engine Integration ──────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
def _safe_int(func_name: str, profile_id: str) -> str:
|
|
|
|
def _safe_int(func_name: str, profile_id: str) -> str:
|
|
|
|
@ -510,7 +715,7 @@ def _safe_int(func_name: str, profile_id: str) -> str:
|
|
|
|
profile_id: Profile ID
|
|
|
|
profile_id: Profile ID
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Returns:
|
|
|
|
String representation of integer value or 'nicht verfügbar'
|
|
|
|
String representation of integer value oder pv_unavailable-Text mit Grund
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
import traceback
|
|
|
|
import traceback
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
@ -554,14 +759,24 @@ def _safe_int(func_name: str, profile_id: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
func = func_map.get(func_name)
|
|
|
|
func = func_map.get(func_name)
|
|
|
|
if not func:
|
|
|
|
if not func:
|
|
|
|
return 'nicht verfügbar'
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Ungültiger Platzhalter (keine numerische Berechnung registriert)",
|
|
|
|
|
|
|
|
func_name,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = func(profile_id)
|
|
|
|
result = func(profile_id)
|
|
|
|
return str(int(result)) if result is not None else 'nicht verfügbar'
|
|
|
|
if result is None:
|
|
|
|
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
_SAFE_INT_NONE_REASON.get(func_name, _DEFAULT_NUMERIC_UNAVAILABLE),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return str(int(result))
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
print(f"[ERROR] _safe_int({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
|
|
|
print(f"[ERROR] _safe_int({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
|
|
|
traceback.print_exc()
|
|
|
|
traceback.print_exc()
|
|
|
|
return 'nicht verfügbar'
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Berechnungsfehler bei numerischem Platzhalter",
|
|
|
|
|
|
|
|
f"{type(e).__name__}: {e}",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _safe_float(func_name: str, profile_id: str, decimals: int = 1) -> str:
|
|
|
|
def _safe_float(func_name: str, profile_id: str, decimals: int = 1) -> str:
|
|
|
|
@ -574,7 +789,7 @@ def _safe_float(func_name: str, profile_id: str, decimals: int = 1) -> str:
|
|
|
|
decimals: Number of decimal places
|
|
|
|
decimals: Number of decimal places
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Returns:
|
|
|
|
String representation of float value or 'nicht verfügbar'
|
|
|
|
String representation of float value oder pv_unavailable-Text mit Grund
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
import traceback
|
|
|
|
import traceback
|
|
|
|
try:
|
|
|
|
try:
|
|
|
|
@ -612,14 +827,24 @@ def _safe_float(func_name: str, profile_id: str, decimals: int = 1) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
func = func_map.get(func_name)
|
|
|
|
func = func_map.get(func_name)
|
|
|
|
if not func:
|
|
|
|
if not func:
|
|
|
|
return 'nicht verfügbar'
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Ungültiger Platzhalter (keine Float-Berechnung registriert)",
|
|
|
|
|
|
|
|
func_name,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = func(profile_id)
|
|
|
|
result = func(profile_id)
|
|
|
|
return f"{result:.{decimals}f}" if result is not None else 'nicht verfügbar'
|
|
|
|
if result is None:
|
|
|
|
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
_SAFE_FLOAT_NONE_REASON.get(func_name, _DEFAULT_NUMERIC_UNAVAILABLE),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return f"{result:.{decimals}f}"
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
print(f"[ERROR] _safe_float({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
|
|
|
print(f"[ERROR] _safe_float({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
|
|
|
traceback.print_exc()
|
|
|
|
traceback.print_exc()
|
|
|
|
return 'nicht verfügbar'
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Berechnungsfehler bei Float-Platzhalter",
|
|
|
|
|
|
|
|
f"{type(e).__name__}: {e}",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _safe_str(func_name: str, profile_id: str) -> str:
|
|
|
|
def _safe_str(func_name: str, profile_id: str) -> str:
|
|
|
|
@ -648,14 +873,24 @@ def _safe_str(func_name: str, profile_id: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
func = func_map.get(func_name)
|
|
|
|
func = func_map.get(func_name)
|
|
|
|
if not func:
|
|
|
|
if not func:
|
|
|
|
return 'nicht verfügbar'
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Ungültiger Platzhalter (keine String-Berechnung registriert)",
|
|
|
|
|
|
|
|
func_name,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = func(profile_id)
|
|
|
|
result = func(profile_id)
|
|
|
|
return str(result) if result is not None else 'nicht verfügbar'
|
|
|
|
if result is None:
|
|
|
|
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
_SAFE_STR_NONE_REASON.get(func_name, _DEFAULT_STR_UNAVAILABLE),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
return str(result)
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
print(f"[ERROR] _safe_str({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
|
|
|
print(f"[ERROR] _safe_str({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
|
|
|
traceback.print_exc()
|
|
|
|
traceback.print_exc()
|
|
|
|
return 'nicht verfügbar'
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Berechnungsfehler bei Text-Platzhalter",
|
|
|
|
|
|
|
|
f"{type(e).__name__}: {e}",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _safe_json(func_name: str, profile_id: str) -> str:
|
|
|
|
def _safe_json(func_name: str, profile_id: str) -> str:
|
|
|
|
@ -684,11 +919,16 @@ def _safe_json(func_name: str, profile_id: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
func = func_map.get(func_name)
|
|
|
|
func = func_map.get(func_name)
|
|
|
|
if not func:
|
|
|
|
if not func:
|
|
|
|
return '{}'
|
|
|
|
return pv_unavailable_json(
|
|
|
|
|
|
|
|
"Ungültiger Platzhalter (keine JSON-Berechnung registriert)",
|
|
|
|
|
|
|
|
func_name,
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
result = func(profile_id)
|
|
|
|
result = func(profile_id)
|
|
|
|
if result is None:
|
|
|
|
if result is None:
|
|
|
|
return '{}'
|
|
|
|
return pv_unavailable_json(
|
|
|
|
|
|
|
|
_SAFE_JSON_NONE_REASON.get(func_name, _DEFAULT_JSON_UNAVAILABLE),
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# If already string, return it; otherwise convert to JSON
|
|
|
|
# If already string, return it; otherwise convert to JSON
|
|
|
|
if isinstance(result, str):
|
|
|
|
if isinstance(result, str):
|
|
|
|
@ -698,7 +938,10 @@ def _safe_json(func_name: str, profile_id: str) -> str:
|
|
|
|
except Exception as e:
|
|
|
|
except Exception as e:
|
|
|
|
print(f"[ERROR] _safe_json({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
|
|
|
print(f"[ERROR] _safe_json({func_name}, {profile_id}): {type(e).__name__}: {e}")
|
|
|
|
traceback.print_exc()
|
|
|
|
traceback.print_exc()
|
|
|
|
return '{}'
|
|
|
|
return pv_unavailable_json(
|
|
|
|
|
|
|
|
"Berechnungsfehler bei JSON-Platzhalter",
|
|
|
|
|
|
|
|
f"{type(e).__name__}: {e}",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_active_goals_json(profile_id: str) -> str:
|
|
|
|
def _get_active_goals_json(profile_id: str) -> str:
|
|
|
|
@ -840,8 +1083,11 @@ def _format_top_focus_areas(profile_id: str, n: int = 3) -> str:
|
|
|
|
lines.append(f"{i}. {name} ({weight}%)")
|
|
|
|
lines.append(f"{i}. {name} ({weight}%)")
|
|
|
|
|
|
|
|
|
|
|
|
return ', '.join(lines)
|
|
|
|
return ', '.join(lines)
|
|
|
|
except Exception:
|
|
|
|
except Exception as e:
|
|
|
|
return 'nicht verfügbar'
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Top-Fokusbereiche nicht darstellbar",
|
|
|
|
|
|
|
|
f"{type(e).__name__}: {e}",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _format_goals_behind(profile_id: str, n: int = 3) -> str:
|
|
|
|
def _format_goals_behind(profile_id: str, n: int = 3) -> str:
|
|
|
|
@ -1005,7 +1251,10 @@ def _format_goals_behind(profile_id: str, n: int = 3) -> str:
|
|
|
|
print(f"[ERROR] _format_goals_behind: {e}")
|
|
|
|
print(f"[ERROR] _format_goals_behind: {e}")
|
|
|
|
import traceback
|
|
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
|
|
traceback.print_exc()
|
|
|
|
return 'nicht verfügbar'
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Ziele „hinter Zeitplan“ nicht darstellbar",
|
|
|
|
|
|
|
|
f"{type(e).__name__}: {e}",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _format_goals_on_track(profile_id: str, n: int = 3) -> str:
|
|
|
|
def _format_goals_on_track(profile_id: str, n: int = 3) -> str:
|
|
|
|
@ -1166,7 +1415,10 @@ def _format_goals_on_track(profile_id: str, n: int = 3) -> str:
|
|
|
|
print(f"[ERROR] _format_goals_on_track: {e}")
|
|
|
|
print(f"[ERROR] _format_goals_on_track: {e}")
|
|
|
|
import traceback
|
|
|
|
import traceback
|
|
|
|
traceback.print_exc()
|
|
|
|
traceback.print_exc()
|
|
|
|
return 'nicht verfügbar'
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"Ziele „im Plan“ nicht darstellbar",
|
|
|
|
|
|
|
|
f"{type(e).__name__}: {e}",
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── Placeholder Registry ──────────────────────────────────────────────────────
|
|
|
|
# ── Placeholder Registry ──────────────────────────────────────────────────────
|
|
|
|
@ -1321,7 +1573,11 @@ def calculate_bmi(profile_id: str) -> str:
|
|
|
|
data = get_bmi_data(profile_id)
|
|
|
|
data = get_bmi_data(profile_id)
|
|
|
|
bmi = data.get("bmi")
|
|
|
|
bmi = data.get("bmi")
|
|
|
|
if bmi is None:
|
|
|
|
if bmi is None:
|
|
|
|
return "nicht verfügbar"
|
|
|
|
return pv_unavailable(
|
|
|
|
|
|
|
|
"BMI nicht berechenbar",
|
|
|
|
|
|
|
|
f"confidence={data.get('confidence')}; benötigt Profil-Größe (cm) und letztes Gewicht "
|
|
|
|
|
|
|
|
f"(weight_log); height_cm={data.get('height_cm')}, weight_kg={data.get('weight_kg')}",
|
|
|
|
|
|
|
|
)
|
|
|
|
return f"{bmi:.1f}"
|
|
|
|
return f"{bmi:.1f}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|