From 2ea5f905c458bcf91e2313ced09ccf3c4901b370 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 11 Apr 2026 21:08:34 +0200 Subject: [PATCH] feat: Add new profile and time period placeholders in placeholder_resolver.py - Introduced functions to retrieve profile name, age, height, and gender for better placeholder resolution. - Added functions for displaying current date and time period labels (last 7, 30, and 90 days). - Updated PLACEHOLDER_MAP to utilize new functions for improved readability and maintainability. - Enhanced placeholder registrations in __init__.py to include new modules for sleep, vital metrics, and profile time periods. These changes enhance the flexibility and functionality of the placeholder system, allowing for more dynamic content generation. --- backend/placeholder_registrations/__init__.py | 12 + .../placeholder_registrations/_evidence.py | 19 + .../korrelationen.py | 96 +++++ .../phase_0b_meta_scores.py | 66 +++ .../phase_0b_ziele_fokus.py | 392 ++++++++++++++++++ .../profil_zeitraum.py | 139 +++++++ .../schlaf_erholung.py | 236 +++++++++++ .../placeholder_registrations/vitalwerte.py | 180 ++++++++ backend/placeholder_resolver.py | 161 ++++--- 9 files changed, 1233 insertions(+), 68 deletions(-) create mode 100644 backend/placeholder_registrations/_evidence.py create mode 100644 backend/placeholder_registrations/korrelationen.py create mode 100644 backend/placeholder_registrations/phase_0b_meta_scores.py create mode 100644 backend/placeholder_registrations/phase_0b_ziele_fokus.py create mode 100644 backend/placeholder_registrations/profil_zeitraum.py create mode 100644 backend/placeholder_registrations/schlaf_erholung.py create mode 100644 backend/placeholder_registrations/vitalwerte.py diff --git a/backend/placeholder_registrations/__init__.py b/backend/placeholder_registrations/__init__.py index 3d08e9f..a70185a 100644 --- a/backend/placeholder_registrations/__init__.py +++ b/backend/placeholder_registrations/__init__.py @@ -13,6 +13,12 @@ from . import body_metrics from . import body_extras from . import activity_metrics from . import activity_session_insights +from . import schlaf_erholung +from . import vitalwerte +from . import profil_zeitraum +from . import phase_0b_meta_scores +from . import phase_0b_ziele_fokus +from . import korrelationen __all__ = [ 'nutrition_part_a', @@ -23,4 +29,10 @@ __all__ = [ 'body_extras', 'activity_metrics', 'activity_session_insights', + 'schlaf_erholung', + 'vitalwerte', + 'profil_zeitraum', + 'phase_0b_meta_scores', + 'phase_0b_ziele_fokus', + 'korrelationen', ] diff --git a/backend/placeholder_registrations/_evidence.py b/backend/placeholder_registrations/_evidence.py new file mode 100644 index 0000000..0fcc430 --- /dev/null +++ b/backend/placeholder_registrations/_evidence.py @@ -0,0 +1,19 @@ +"""Gemeinsames Evidence-Tagging für Registry-Einträge.""" + +from placeholder_registry import EvidenceType, PlaceholderMetadata + +STANDARD_FIELDS = ( + "key", "category", "description", "resolver_module", "resolver_function", + "data_layer_module", "data_layer_function", "source_tables", "semantic_contract", + "unit", "time_window", "output_type", "placeholder_type", "format_hint", + "example_output", "minimum_data_requirements", "confidence_logic", + "missing_value_policy", "layer_1_decision", "layer_2a_decision", + "layer_2b_reuse_possible", "architecture_alignment", "issue_53_alignment", +) + + +def tag_standard_evidence(meta: PlaceholderMetadata) -> None: + for field in STANDARD_FIELDS: + meta.set_evidence(field, EvidenceType.CODE_DERIVED) + meta.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + meta.set_evidence("known_limitations", EvidenceType.MIXED) diff --git a/backend/placeholder_registrations/korrelationen.py b/backend/placeholder_registrations/korrelationen.py new file mode 100644 index 0000000..f7f4dc8 --- /dev/null +++ b/backend/placeholder_registrations/korrelationen.py @@ -0,0 +1,96 @@ +"""Registry: Korrelations- und Treiber-Metriken (Data Layer correlations).""" + +from placeholder_registry import ( + PlaceholderMetadata, + MissingValuePolicy, + OutputType, + PlaceholderType, + register_placeholder, +) +from ._evidence import tag_standard_evidence + +CAT = "Korrelationen" +MVP = lambda reason, disp: MissingValuePolicy( + available=False, value_raw=None, missing_reason=reason, legacy_display=disp +) + + +def register_korrelationen(): + for key, dl_fn, desc, tables, sem in [ + ( + "correlation_energy_weight_lag", + "calculate_lag_correlation", + "JSON: Lag-Korrelation Energiebilanz ↔ Gewicht", + ["nutrition_log", "weight_log"], + "correlations.calculate_lag_correlation(pid, 'energy', 'weight')", + ), + ( + "correlation_protein_lbm", + "calculate_lag_correlation", + "JSON: Lag-Korrelation Protein ↔ Magermasse", + ["nutrition_log", "weight_log", "caliper_log"], + "correlations.calculate_lag_correlation(pid, 'protein', 'lbm')", + ), + ( + "correlation_load_hrv", + "calculate_lag_correlation", + "JSON: Lag-Korrelation Trainingslast ↔ HRV", + ["activity_log", "vitals_baseline"], + "correlations.calculate_lag_correlation(pid, 'training_load', 'hrv')", + ), + ( + "correlation_load_rhr", + "calculate_lag_correlation", + "JSON: Lag-Korrelation Trainingslast ↔ Ruhepuls", + ["activity_log", "vitals_baseline"], + "correlations.calculate_lag_correlation(pid, 'training_load', 'rhr')", + ), + ( + "plateau_detected", + "calculate_plateau_detected", + "JSON: Platten-Erkennung (Gewicht/Körper)", + ["weight_log", "caliper_log"], + "correlations.calculate_plateau_detected", + ), + ( + "top_drivers", + "calculate_top_drivers", + "JSON: Top Treiber für Ziel-/Score-Variablen", + ["weight_log", "nutrition_log", "activity_log", "vitals_baseline", "sleep_log"], + "correlations.calculate_top_drivers", + ), + ]: + m = PlaceholderMetadata( + key=key, + category=CAT, + description=desc, + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_json", + data_layer_module="backend/data_layer/correlations.py", + data_layer_function=dl_fn, + source_tables=tables, + semantic_contract=sem, + business_meaning="Strukturierte Korrelationsausgabe für KI", + unit="JSON", + time_window="funktionsintern", + output_type=OutputType.JSON, + placeholder_type=PlaceholderType.RAW_DATA, + format_hint="JSON-String", + example_output="{}", + minimum_data_requirements="Ausreichend gekoppelte Zeitreihen", + quality_filter_policy=None, + confidence_logic="Wie correlations.*", + missing_value_policy=MVP("insufficient_data", "{}"), + known_limitations="Bei wenigen Daten leer oder wenig robust", + layer_1_decision=f"correlations.{dl_fn}", + layer_2a_decision="_safe_json", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + +register_korrelationen() diff --git a/backend/placeholder_registrations/phase_0b_meta_scores.py b/backend/placeholder_registrations/phase_0b_meta_scores.py new file mode 100644 index 0000000..fa15295 --- /dev/null +++ b/backend/placeholder_registrations/phase_0b_meta_scores.py @@ -0,0 +1,66 @@ +"""Registry: Meta-Scores (Phase 0b) — Ziel-Fortschritt und Datenqualität.""" + +from placeholder_registry import ( + PlaceholderMetadata, + MissingValuePolicy, + OutputType, + PlaceholderType, + register_placeholder, +) +from ._evidence import tag_standard_evidence + +CAT = "Scores (Phase 0b)" +MVP = lambda reason, disp: MissingValuePolicy( + available=False, value_raw=None, missing_reason=reason, legacy_display=disp +) + + +def register_phase_0b_meta_scores(): + for key, dl_fn, desc, unit in [ + ( + "goal_progress_score", + "calculate_goal_progress_score", + "Aggregierter Ziel-Fortschritt 0–100", + "0–100", + ), + ( + "data_quality_score", + "calculate_data_quality_score", + "Datenqualitäts-Score 0–100", + "0–100", + ), + ]: + m = PlaceholderMetadata( + key=key, + category=CAT, + description=desc, + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_int", + data_layer_module="backend/data_layer/scores.py", + data_layer_function=dl_fn, + source_tables=["goals", "weight_log", "nutrition_log", "activity_log", "profiles"], + semantic_contract=f"scores.{dl_fn} (siehe Data Layer).", + business_meaning="Meta-KPI für Prompt-Gewichtung", + unit=unit, + time_window="composite", + output_type=OutputType.NUMERIC, + placeholder_type=PlaceholderType.SCORE, + format_hint="Ganzzahl als String", + example_output="72", + minimum_data_requirements="Abhängig von Score-Implementierung", + quality_filter_policy=None, + confidence_logic="Wie calculate_* in scores.py", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations="Bei dünnen Daten weniger aussagekräftig", + layer_1_decision=f"scores.{dl_fn}", + layer_2a_decision="_safe_int", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0b", + issue_53_alignment="Layer 1", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + +register_phase_0b_meta_scores() diff --git a/backend/placeholder_registrations/phase_0b_ziele_fokus.py b/backend/placeholder_registrations/phase_0b_ziele_fokus.py new file mode 100644 index 0000000..1c1009e --- /dev/null +++ b/backend/placeholder_registrations/phase_0b_ziele_fokus.py @@ -0,0 +1,392 @@ +"""Registry: Ziele, Fokusbereiche, Kategorie-Scores und formatierte Listen (Phase 0b).""" + +from placeholder_registry import ( + PlaceholderMetadata, + MissingValuePolicy, + OutputType, + PlaceholderType, + register_placeholder, +) +from ._evidence import tag_standard_evidence + +CAT = "Ziele & Fokus (Phase 0b)" +MVP = lambda reason, disp: MissingValuePolicy( + available=False, value_raw=None, missing_reason=reason, legacy_display=disp +) + + +def register_phase_0b_ziele_fokus(): + # Top-Ziel / Top-Fokusbereich + m = PlaceholderMetadata( + key="top_goal_name", + category=CAT, + description="Name/Typ des höchstpriorisierten Ziels", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_str", + data_layer_module="backend/data_layer/scores.py", + data_layer_function="get_top_priority_goal", + source_tables=["goals"], + semantic_contract="Feld name oder goal_type aus get_top_priority_goal", + business_meaning="Priorisierung für KI-Empfehlungen", + unit="text", + time_window="aktuell", + output_type=OutputType.STRING, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="Kurztext", + example_output="Gewicht 80kg", + minimum_data_requirements="Mindestens ein aktives Ziel", + quality_filter_policy=None, + confidence_logic="scores.get_top_priority_goal", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations=None, + layer_1_decision="scores.get_top_priority_goal", + layer_2a_decision="_safe_str('top_goal_name')", + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0b", + issue_53_alignment="Layer 1", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + m = PlaceholderMetadata( + key="top_goal_progress_pct", + category=CAT, + description="Fortschritt Top-Ziel (%)", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_int", + data_layer_module="backend/data_layer/scores.py", + data_layer_function="get_top_priority_goal", + source_tables=["goals"], + semantic_contract="progress_pct aus get_top_priority_goal", + business_meaning="Priorisierung für KI-Empfehlungen", + unit="%", + time_window="aktuell", + output_type=OutputType.NUMERIC, + placeholder_type=PlaceholderType.SCORE, + format_hint="Ganzzahl", + example_output="65", + minimum_data_requirements="Mindestens ein aktives Ziel", + quality_filter_policy=None, + confidence_logic="scores.get_top_priority_goal", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations=None, + layer_1_decision="scores.get_top_priority_goal", + layer_2a_decision="_safe_int('top_goal_progress_pct')", + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0b", + issue_53_alignment="Layer 1", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + m = PlaceholderMetadata( + key="top_goal_status", + category=CAT, + description="Status-Label Top-Ziel", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_str", + data_layer_module="backend/data_layer/scores.py", + data_layer_function="get_top_priority_goal", + source_tables=["goals"], + semantic_contract="status aus get_top_priority_goal", + business_meaning="Priorisierung für KI-Empfehlungen", + unit="text", + time_window="aktuell", + output_type=OutputType.STRING, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="Kurztext", + example_output="active", + minimum_data_requirements="Mindestens ein aktives Ziel", + quality_filter_policy=None, + confidence_logic="scores.get_top_priority_goal", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations=None, + layer_1_decision="scores.get_top_priority_goal", + layer_2a_decision="_safe_str('top_goal_status')", + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0b", + issue_53_alignment="Layer 1", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + m = PlaceholderMetadata( + key="top_focus_area_name", + category=CAT, + description="Bezeichnung des gewichtet stärksten Fokusbereichs", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_str", + data_layer_module="backend/data_layer/scores.py", + data_layer_function="get_top_focus_area", + source_tables=["user_focus_area_weights", "focus_area_definitions"], + semantic_contract="label aus get_top_focus_area", + business_meaning="Priorisierung für KI-Empfehlungen", + unit="text", + time_window="aktuell", + output_type=OutputType.STRING, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="Kurztext", + example_output="Kraft", + minimum_data_requirements="Gewichtete Fokusbereiche", + quality_filter_policy=None, + confidence_logic="scores.get_top_focus_area", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations=None, + layer_1_decision="scores.get_top_focus_area", + layer_2a_decision="_safe_str('top_focus_area_name')", + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0b", + issue_53_alignment="Layer 1", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + m = PlaceholderMetadata( + key="top_focus_area_progress", + category=CAT, + description="Fortschritt Top-Fokusbereich (%)", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_int", + data_layer_module="backend/data_layer/scores.py", + data_layer_function="get_top_focus_area", + source_tables=["user_focus_area_weights", "focus_area_definitions", "goals"], + semantic_contract="progress aus get_top_focus_area", + business_meaning="Priorisierung für KI-Empfehlungen", + unit="%", + time_window="aktuell", + output_type=OutputType.NUMERIC, + placeholder_type=PlaceholderType.SCORE, + format_hint="Ganzzahl", + example_output="58", + minimum_data_requirements="Gewichtete Fokusbereiche", + quality_filter_policy=None, + confidence_logic="scores.get_top_focus_area", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations=None, + layer_1_decision="scores.get_top_focus_area", + layer_2a_decision="_safe_int('top_focus_area_progress')", + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0b", + issue_53_alignment="Layer 1", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + # Kategorie Progress / Weight (7 Kategorien) + for slug in ( + "körper", + "ernährung", + "aktivität", + "recovery", + "vitalwerte", + "mental", + "lebensstil", + ): + key_p = f"focus_cat_{slug}_progress" + key_w = f"focus_cat_{slug}_weight" + m_p = PlaceholderMetadata( + key=key_p, + category=CAT, + description=f"Aggregierter Fortschritt Kategorie „{slug}“ (%)", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_int", + data_layer_module="backend/data_layer/scores.py", + data_layer_function="calculate_category_progress", + source_tables=["goals", "focus_area_definitions", "user_focus_area_weights"], + semantic_contract=f"scores.calculate_category_progress(pid, '{slug}')", + business_meaning="Focus-Area-Kategorie-Score", + unit="%", + time_window="aktuell", + output_type=OutputType.NUMERIC, + placeholder_type=PlaceholderType.SCORE, + format_hint="Ganzzahl", + example_output="55", + minimum_data_requirements="Gewichtete Bereiche in Kategorie", + quality_filter_policy=None, + confidence_logic="scores.calculate_category_progress", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations=None, + layer_1_decision="scores.calculate_category_progress", + layer_2a_decision="_safe_int", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0b", + issue_53_alignment="Layer 1", + evidence={}, + ) + tag_standard_evidence(m_p) + register_placeholder(m_p) + + m_w = PlaceholderMetadata( + key=key_w, + category=CAT, + description=f"Nutzer-Gewichtung Kategorie „{slug}“ (Anteil 0–1)", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_float", + data_layer_module="backend/data_layer/scores.py", + data_layer_function="calculate_category_weight", + source_tables=["user_focus_area_weights", "focus_area_definitions"], + semantic_contract=f"scores.calculate_category_weight(pid, '{slug}')", + business_meaning="Kategorie-Gewichtung im Fokusmodell", + unit="0–1", + time_window="aktuell", + output_type=OutputType.NUMERIC, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="Dezimal", + example_output="0.25", + minimum_data_requirements="user_focus_area_weights", + quality_filter_policy=None, + confidence_logic="scores.calculate_category_weight", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations=None, + layer_1_decision="scores.calculate_category_weight", + layer_2a_decision="_safe_float", + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0b", + issue_53_alignment="Layer 1", + evidence={}, + ) + tag_standard_evidence(m_w) + register_placeholder(m_w) + + # Strukturierte Ziele / Fokus + for key, res_fn, dl_mod, dl_fn, desc, out, ptype in [ + ( + "active_goals_json", + "_safe_json", + "backend/goal_utils.py", + "get_active_goals", + "Aktive Ziele als JSON", + OutputType.JSON, + PlaceholderType.RAW_DATA, + ), + ( + "active_goals_md", + "_safe_str", + "backend/placeholder_resolver.py", + "_format_goals_as_markdown", + "Aktive Ziele als Markdown-Tabelle", + OutputType.TEXT_SUMMARY, + PlaceholderType.INTERPRETED, + ), + ( + "focus_areas_weighted_json", + "_safe_json", + "backend/placeholder_resolver.py", + "_get_focus_areas_weighted_json", + "Gewichtete Fokusbereiche mit Namen (JSON)", + OutputType.JSON, + PlaceholderType.RAW_DATA, + ), + ( + "focus_areas_weighted_md", + "_safe_str", + "backend/placeholder_resolver.py", + "_format_focus_areas_as_markdown", + "Gewichtete Fokusbereiche als Markdown", + OutputType.TEXT_SUMMARY, + PlaceholderType.INTERPRETED, + ), + ( + "focus_area_weights_json", + "_safe_json", + "backend/data_layer/scores.py", + "get_user_focus_weights", + "Rohe Gewichtungen key→Anteil (JSON)", + OutputType.JSON, + PlaceholderType.RAW_DATA, + ), + ]: + m = PlaceholderMetadata( + key=key, + category=CAT, + description=desc, + resolver_module="backend/placeholder_resolver.py", + resolver_function=res_fn, + data_layer_module=dl_mod, + data_layer_function=dl_fn, + source_tables=["goals", "focus_area_definitions", "user_focus_area_weights"], + semantic_contract=f"{dl_fn} (siehe Modul {dl_mod})", + business_meaning="Strukturierte Übersicht für Prompts", + unit="JSON" if out == OutputType.JSON else "markdown", + time_window="aktuell", + output_type=out, + placeholder_type=ptype, + format_hint="String aus Resolver", + example_output="[]" if out == OutputType.JSON else "—", + minimum_data_requirements="Ziele bzw. Fokusgewichte", + quality_filter_policy=None, + confidence_logic="Resolver + goal_utils / scores", + missing_value_policy=MVP("insufficient_data", "[]" if out == OutputType.JSON else "nicht verfügbar"), + known_limitations=None, + layer_1_decision=dl_fn, + layer_2a_decision=res_fn, + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0b", + issue_53_alignment="Layer 1", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + for key, res_fn, dl_fn, desc, ex in [ + ( + "top_3_focus_areas", + "_safe_str", + "_format_top_focus_areas", + "Top-3 Fokusbereiche als formatierter Text", + "1. Kraft …", + ), + ( + "top_3_goals_behind_schedule", + "_safe_str", + "_format_goals_behind", + "Bis zu drei Ziele hinter Zeitplan", + "—", + ), + ( + "top_3_goals_on_track", + "_safe_str", + "_format_goals_on_track", + "Bis zu drei Ziele im Plan", + "—", + ), + ]: + m = PlaceholderMetadata( + key=key, + category=CAT, + description=desc, + resolver_module="backend/placeholder_resolver.py", + resolver_function=res_fn, + data_layer_module="backend/goal_utils.py", + data_layer_function="get_active_goals", + source_tables=["goals", "focus_area_definitions"], + semantic_contract=f"Resolver {dl_fn}", + business_meaning="Kurzlisten für Coaching-Prompts", + unit="text", + time_window="aktuell", + output_type=OutputType.TEXT_SUMMARY, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="Freitext / Aufzählung", + example_output=ex, + minimum_data_requirements="Ziele / Fokusdaten", + quality_filter_policy=None, + confidence_logic=dl_fn, + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations=None, + layer_1_decision="goals + focus aggregation", + layer_2a_decision=dl_fn, + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0b", + issue_53_alignment="Layer 2a", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + +register_phase_0b_ziele_fokus() diff --git a/backend/placeholder_registrations/profil_zeitraum.py b/backend/placeholder_registrations/profil_zeitraum.py new file mode 100644 index 0000000..778c482 --- /dev/null +++ b/backend/placeholder_registrations/profil_zeitraum.py @@ -0,0 +1,139 @@ +""" +Registry: Profil-Stammdaten und statische Zeitraum-Labels für Prompts. +""" + +from placeholder_registry import ( + PlaceholderMetadata, + MissingValuePolicy, + OutputType, + PlaceholderType, + register_placeholder, +) +from ._evidence import tag_standard_evidence + +MVP = lambda reason, disp: MissingValuePolicy( + available=False, value_raw=None, missing_reason=reason, legacy_display=disp +) + + +def register_profil_zeitraum(): + cat_profil = "Profil" + for key, desc, res_fn, unit, ptype, out, hint, ex, sem in [ + ( + "name", + "Anzeigename aus profiles.name", + "get_profile_name", + "text", + PlaceholderType.ATOMIC, + OutputType.STRING, + "Kurzname", + "Max", + "profiles.name, Fallback „Nutzer“.", + ), + ( + "age", + "Alter in Jahren aus profiles.dob", + "get_profile_age_display", + "Jahre", + PlaceholderType.ATOMIC, + OutputType.STRING, + "Ganzzahl oder unbekannt", + "42", + "Berechnung aus Geburtsdatum; PostgreSQL date oder ISO-String.", + ), + ( + "height", + "Körpergröße (cm) aus profiles.height", + "get_profile_height_display", + "cm", + PlaceholderType.ATOMIC, + OutputType.STRING, + "Zahl oder unbekannt", + "180", + "profiles.height.", + ), + ( + "geschlecht", + "Geschlecht (männlich/weiblich) aus profiles.sex", + "get_profile_geschlecht_display", + "Kategorie", + PlaceholderType.ATOMIC, + OutputType.STRING, + "m/w-Mapping", + "männlich", + "sex == 'm' → männlich, sonst weiblich.", + ), + ]: + m = PlaceholderMetadata( + key=key, + category=cat_profil, + description=desc, + resolver_module="backend/placeholder_resolver.py", + resolver_function=res_fn, + data_layer_module=None, + data_layer_function=None, + source_tables=["profiles"], + semantic_contract=sem, + business_meaning="Profil-Kontext für KI-Prompts", + unit=unit, + time_window="latest profile row", + output_type=out, + placeholder_type=ptype, + format_hint=hint, + example_output=ex, + minimum_data_requirements="Profilzeile", + quality_filter_policy=None, + confidence_logic="Row vorhanden", + missing_value_policy=MVP("no_data", "unbekannt" if key != "name" else "Nutzer"), + known_limitations="Keine diversen Geschlechtsoptionen im Platzhalter", + layer_1_decision="profiles", + layer_2a_decision=res_fn, + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0b", + issue_53_alignment="Resolver", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + cat_zeit = "Zeitraum" + for key, desc, res_fn, sem, ex_out in [ + ("datum_heute", "Heutiges Datum (lokal)", "get_datum_heute", "datetime.now, Format dd.mm.yyyy", "11.04.2026"), + ("zeitraum_7d", "Label „letzte 7 Tage“", "get_zeitraum_label_7d", "Statisches UI/Prompt-Label", "letzte 7 Tage"), + ("zeitraum_30d", "Label „letzte 30 Tage“", "get_zeitraum_label_30d", "Statisches UI/Prompt-Label", "letzte 30 Tage"), + ("zeitraum_90d", "Label „letzte 90 Tage“", "get_zeitraum_label_90d", "Statisches UI/Prompt-Label", "letzte 90 Tage"), + ]: + m = PlaceholderMetadata( + key=key, + category=cat_zeit, + description=desc, + resolver_module="backend/placeholder_resolver.py", + resolver_function=res_fn, + data_layer_module=None, + data_layer_function=None, + source_tables=[], + semantic_contract=sem, + business_meaning="Zeitlicher Bezug im Prompt ohne Datenabfrage", + unit="label", + time_window="n/a", + output_type=OutputType.STRING, + placeholder_type=PlaceholderType.META, + format_hint="Kurzdeutsch", + example_output=ex_out, + minimum_data_requirements=None, + quality_filter_policy=None, + confidence_logic="Immer verfügbar", + missing_value_policy=None, + known_limitations="Kein kalender-basierter Datenfilter allein durch Platzhalter", + layer_1_decision="n/a", + layer_2a_decision=res_fn, + layer_2b_reuse_possible=False, + architecture_alignment="Phase 0b", + issue_53_alignment="Resolver", + evidence={}, + ) + tag_standard_evidence(m) + register_placeholder(m) + + +register_profil_zeitraum() diff --git a/backend/placeholder_registrations/schlaf_erholung.py b/backend/placeholder_registrations/schlaf_erholung.py new file mode 100644 index 0000000..bc9d422 --- /dev/null +++ b/backend/placeholder_registrations/schlaf_erholung.py @@ -0,0 +1,236 @@ +""" +Registry: Schlaf, Ruhetage, Recovery-Score, Schlaf-Metriken, Schlaf-Erholungs-Korrelation. +""" + +from placeholder_registry import ( + PlaceholderMetadata, + MissingValuePolicy, + EvidenceType, + OutputType, + PlaceholderType, + register_placeholder, +) + +CAT = "Schlaf & Erholung" +MVP = lambda reason, disp: MissingValuePolicy( + available=False, value_raw=None, missing_reason=reason, legacy_display=disp +) + + +def _tag(m: PlaceholderMetadata): + for f in ( + "key", "category", "description", "resolver_module", "resolver_function", + "data_layer_module", "data_layer_function", "source_tables", "semantic_contract", + "unit", "time_window", "output_type", "placeholder_type", "format_hint", + "example_output", "minimum_data_requirements", "confidence_logic", + "missing_value_policy", "layer_1_decision", "layer_2a_decision", + "layer_2b_reuse_possible", "architecture_alignment", "issue_53_alignment", + ): + m.set_evidence(f, EvidenceType.CODE_DERIVED) + m.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + m.set_evidence("known_limitations", EvidenceType.MIXED) + + +def register_schlaf_erholung(): + # ── formatierte Schlaf-/Ruhetage-Snapshots ─────────────────────────────── + m = PlaceholderMetadata( + key="sleep_avg_duration", + category=CAT, + description="Durchschnittliche Schlafdauer (Stunden), formatiert", + resolver_module="backend/placeholder_resolver.py", + resolver_function="get_sleep_avg_duration", + data_layer_module="backend/data_layer/recovery_metrics.py", + data_layer_function="get_sleep_duration_data", + source_tables=["sleep_log"], + semantic_contract="Mittel aus Schlafphasen im Fenster (siehe get_sleep_duration_data).", + business_meaning="KI-Kontext Schlafdauer", + unit="h (Anzeige mit Einheit)", + time_window="7d default im Resolver", + output_type=OutputType.STRING, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="z. B. 7.2h", + example_output="7.2h", + minimum_data_requirements="sleep_log im Fenster", + quality_filter_policy=None, + confidence_logic="data['confidence'] im Layer1", + missing_value_policy=MVP("no_data", "nicht verfügbar"), + known_limitations="Abhängig von Import/Qualität der Phasen", + layer_1_decision="recovery_metrics.get_sleep_duration_data", + layer_2a_decision="get_sleep_avg_duration", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + _tag(m) + register_placeholder(m) + + m = PlaceholderMetadata( + key="sleep_avg_quality", + category=CAT, + description="Schlafqualität (Deep+REM %), formatiert", + resolver_module="backend/placeholder_resolver.py", + resolver_function="get_sleep_avg_quality", + data_layer_module="backend/data_layer/recovery_metrics.py", + data_layer_function="get_sleep_quality_data", + source_tables=["sleep_log"], + semantic_contract="Anteil Deep+REM aus Segmenten (siehe get_sleep_quality_data).", + business_meaning="KI-Kontext Schlafqualität", + unit="%", + time_window="7d default", + output_type=OutputType.STRING, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="Prozent oder nicht verfügbar", + example_output="24%", + minimum_data_requirements="sleep_log mit Phasen", + quality_filter_policy=None, + confidence_logic="Layer-1-Confidence", + missing_value_policy=MVP("no_data", "nicht verfügbar"), + known_limitations="Segment-Schreibweise case-sensitiv normalisiert", + layer_1_decision="recovery_metrics.get_sleep_quality_data", + layer_2a_decision="get_sleep_avg_quality", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + _tag(m) + register_placeholder(m) + + m = PlaceholderMetadata( + key="rest_days_count", + category=CAT, + description="Anzahl dokumentierter Ruhetage (30d default)", + resolver_module="backend/placeholder_resolver.py", + resolver_function="get_rest_days_count", + data_layer_module="backend/data_layer/recovery_metrics.py", + data_layer_function="get_rest_days_data", + source_tables=["rest_days"], + semantic_contract="Count rest_days im Zeitraum", + business_meaning="Aktive/passive Erholungstags-Übersicht", + unit="count", + time_window="30d default", + output_type=OutputType.STRING, + placeholder_type=PlaceholderType.RAW_DATA, + format_hint="z. B. 2 Ruhetage", + example_output="2 Ruhetage", + minimum_data_requirements="rest_days", + quality_filter_policy=None, + confidence_logic="Immer Zählung, 0 möglich", + missing_value_policy=MVP("no_data", "0 Ruhetage"), + known_limitations="Nur explizit erfasste Ruhetage", + layer_1_decision="recovery_metrics.get_rest_days_data", + layer_2a_decision="get_rest_days_count", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + _tag(m) + register_placeholder(m) + + m = PlaceholderMetadata( + key="recovery_score", + category=CAT, + description="Recovery-Score 0–100 (v2, komposit)", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_int", + data_layer_module="backend/data_layer/recovery_metrics.py", + data_layer_function="calculate_recovery_score_v2", + source_tables=["sleep_log", "vitals_baseline", "activity_log"], + semantic_contract="Gewichteter Score aus Schlaf, Vitaltrends, optional Load (siehe Implementierung).", + business_meaning="Gesamt-Recovery-KPI für Prompts", + unit="0–100", + time_window="composite", + output_type=OutputType.NUMERIC, + placeholder_type=PlaceholderType.SCORE, + format_hint="Ganzzahl-String", + example_output="72", + minimum_data_requirements="Teilkomponenten je nach Gewichtung", + quality_filter_policy=None, + confidence_logic="Wie calculate_recovery_score_v2", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations="Abhängig von Datenabdeckung HF/HRV/Schlaf", + layer_1_decision="recovery_metrics.calculate_recovery_score_v2", + layer_2a_decision="_safe_int('recovery_score_v2')", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + _tag(m) + register_placeholder(m) + + for key, dl_fn, desc, unit, tbls, res_fn in [ + ("sleep_avg_duration_7d", "calculate_sleep_avg_duration_7d", "Durchschnittliche Schlafdauer 7d (h)", "h", ["sleep_log"], "_safe_float"), + ("sleep_debt_hours", "calculate_sleep_debt_hours", "Kumulative Schlafschuld (h)", "h", ["sleep_log"], "_safe_float"), + ("sleep_regularity_proxy", "calculate_sleep_regularity_proxy", "Schlaf-Regularität (Proxy)", "min", ["sleep_log"], "_safe_float"), + ("recent_load_balance_3d", "calculate_recent_load_balance_3d", "Load-Balance 3d (Score)", "score", ["activity_log"], "_safe_int"), + ("sleep_quality_7d", "calculate_sleep_quality_7d", "Schlafqualität 7d (0–100)", "0-100", ["sleep_log"], "_safe_int"), + ]: + m = PlaceholderMetadata( + key=key, + category=CAT, + description=desc, + resolver_module="backend/placeholder_resolver.py", + resolver_function=res_fn, + data_layer_module="backend/data_layer/recovery_metrics.py", + data_layer_function=dl_fn, + source_tables=tbls, + semantic_contract=f"Berechnung {dl_fn} in recovery_metrics.", + business_meaning="Erholungs-Detailmetrik", + unit=unit, + time_window="siehe Funktion", + output_type=OutputType.NUMERIC, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="numerischer String", + example_output="1.0", + minimum_data_requirements="wie Funktion", + quality_filter_policy=None, + confidence_logic="Funktionsintern", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations=None, + layer_1_decision=f"recovery_metrics.{dl_fn}", + layer_2a_decision="Resolver _safe_float/_safe_int", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + _tag(m) + register_placeholder(m) + + m = PlaceholderMetadata( + key="correlation_sleep_recovery", + category=CAT, + description="JSON: Korrelation Schlaf ↔ Recovery-Indikatoren", + resolver_module="backend/placeholder_resolver.py", + resolver_function="_safe_json", + data_layer_module="backend/data_layer/correlations.py", + data_layer_function="calculate_correlation_sleep_recovery", + source_tables=["sleep_log", "vitals_baseline", "activity_log"], + semantic_contract="Strukturierte Korrelationsauswertung (siehe correlations).", + business_meaning="KI: Zusammenhänge Schlaf und Erholung", + unit="JSON", + time_window="funktionsabhängig", + output_type=OutputType.JSON, + placeholder_type=PlaceholderType.RAW_DATA, + format_hint="JSON-String", + example_output="{}", + minimum_data_requirements="Ausreichend gekoppelte Datenpunkte", + quality_filter_policy=None, + confidence_logic="Wie correlation_metrics", + missing_value_policy=MVP("insufficient_data", "{}"), + known_limitations="Bei wenig Daten leer oder schwach", + layer_1_decision="correlations.calculate_correlation_sleep_recovery", + layer_2a_decision="_safe_json", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + _tag(m) + register_placeholder(m) + + +register_schlaf_erholung() diff --git a/backend/placeholder_registrations/vitalwerte.py b/backend/placeholder_registrations/vitalwerte.py new file mode 100644 index 0000000..79743e9 --- /dev/null +++ b/backend/placeholder_registrations/vitalwerte.py @@ -0,0 +1,180 @@ +""" +Registry: Baseline-Vitals (Ruhepuls, HRV, VO2 Max) und Abweichung vs. persönlicher Baseline. +""" + +from placeholder_registry import ( + PlaceholderMetadata, + MissingValuePolicy, + EvidenceType, + OutputType, + PlaceholderType, + register_placeholder, +) + +CAT = "Vitalwerte" +MVP = lambda reason, disp: MissingValuePolicy( + available=False, value_raw=None, missing_reason=reason, legacy_display=disp +) + + +def _tag(m: PlaceholderMetadata): + for f in ( + "key", "category", "description", "resolver_module", "resolver_function", + "data_layer_module", "data_layer_function", "source_tables", "semantic_contract", + "unit", "time_window", "output_type", "placeholder_type", "format_hint", + "example_output", "minimum_data_requirements", "confidence_logic", + "missing_value_policy", "layer_1_decision", "layer_2a_decision", + "layer_2b_reuse_possible", "architecture_alignment", "issue_53_alignment", + ): + m.set_evidence(f, EvidenceType.CODE_DERIVED) + m.set_evidence("business_meaning", EvidenceType.DRAFT_DERIVED) + m.set_evidence("known_limitations", EvidenceType.MIXED) + + +def register_vitalwerte(): + m = PlaceholderMetadata( + key="vitals_avg_hr", + category=CAT, + description="Durchschnittlicher Ruhepuls (7d), formatiert", + resolver_module="backend/placeholder_resolver.py", + resolver_function="get_vitals_avg_hr", + data_layer_module="backend/data_layer/health_metrics.py", + data_layer_function="get_resting_heart_rate_data", + source_tables=["vitals_baseline"], + semantic_contract="Mittel RHR aus vitals_baseline im Fenster (siehe health_metrics).", + business_meaning="KI-Kontext kardiovaskuläre Ruhelage", + unit="bpm (Anzeige mit Einheit)", + time_window="7d default im Resolver", + output_type=OutputType.STRING, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="z. B. 58 bpm", + example_output="58 bpm", + minimum_data_requirements="vitals_baseline im Fenster", + quality_filter_policy=None, + confidence_logic="data['confidence'] im Layer1", + missing_value_policy=MVP("no_data", "nicht verfügbar"), + known_limitations="Nur erfasste Morgen-Baseline-Messungen", + layer_1_decision="health_metrics.get_resting_heart_rate_data", + layer_2a_decision="get_vitals_avg_hr", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + _tag(m) + register_placeholder(m) + + m = PlaceholderMetadata( + key="vitals_avg_hrv", + category=CAT, + description="Durchschnittliche HRV (7d), formatiert", + resolver_module="backend/placeholder_resolver.py", + resolver_function="get_vitals_avg_hrv", + data_layer_module="backend/data_layer/health_metrics.py", + data_layer_function="get_heart_rate_variability_data", + source_tables=["vitals_baseline"], + semantic_contract="Mittel HRV aus vitals_baseline im Fenster.", + business_meaning="KI-Kontext autonome Regulation / Erholung", + unit="ms (Anzeige mit Einheit)", + time_window="7d default", + output_type=OutputType.STRING, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="z. B. 45 ms", + example_output="45 ms", + minimum_data_requirements="vitals_baseline mit HRV", + quality_filter_policy=None, + confidence_logic="data['confidence'] im Layer1", + missing_value_policy=MVP("no_data", "nicht verfügbar"), + known_limitations="Geräte-/Messprotokoll kann streuen", + layer_1_decision="health_metrics.get_heart_rate_variability_data", + layer_2a_decision="get_vitals_avg_hrv", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + _tag(m) + register_placeholder(m) + + m = PlaceholderMetadata( + key="vitals_vo2_max", + category=CAT, + description="Aktueller VO2 Max (letzte Messung), formatiert", + resolver_module="backend/placeholder_resolver.py", + resolver_function="get_vitals_vo2_max", + data_layer_module="backend/data_layer/health_metrics.py", + data_layer_function="get_vo2_max_data", + source_tables=["vitals_baseline"], + semantic_contract="Jüngster vo2_max aus vitals_baseline.", + business_meaning="Ausdauer-/Fitness-Kontext", + unit="ml/kg/min", + time_window="latest", + output_type=OutputType.STRING, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="eine Dezimalstelle + Einheit", + example_output="42.0 ml/kg/min", + minimum_data_requirements="mindestens eine VO2-Messung", + quality_filter_policy=None, + confidence_logic="data['confidence'] im Layer1", + missing_value_policy=MVP("no_data", "nicht verfügbar"), + known_limitations="Schätzung vs. Labortest je nach Quelle", + layer_1_decision="health_metrics.get_vo2_max_data", + layer_2a_decision="get_vitals_vo2_max", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + _tag(m) + register_placeholder(m) + + for key, dl_fn, desc, unit, res_fn in [ + ( + "hrv_vs_baseline_pct", + "calculate_hrv_vs_baseline_pct", + "HRV vs. persönlicher Baseline (%)", + "%", + "_safe_float", + ), + ( + "rhr_vs_baseline_pct", + "calculate_rhr_vs_baseline_pct", + "Ruhepuls vs. persönlicher Baseline (%)", + "%", + "_safe_float", + ), + ]: + m = PlaceholderMetadata( + key=key, + category=CAT, + description=desc, + resolver_module="backend/placeholder_resolver.py", + resolver_function=res_fn, + data_layer_module="backend/data_layer/recovery_metrics.py", + data_layer_function=dl_fn, + source_tables=["vitals_baseline"], + semantic_contract=f"Vergleich aktueller Wert zu Baseline (siehe {dl_fn}).", + business_meaning="Erholungs- und Belastungsindikator relativ zur Norm des Nutzers", + unit=unit, + time_window="funktionsintern", + output_type=OutputType.NUMERIC, + placeholder_type=PlaceholderType.INTERPRETED, + format_hint="numerischer Prozent-String", + example_output="5.2", + minimum_data_requirements="Ausreichend Baseline-Historie", + quality_filter_policy=None, + confidence_logic="Funktionsintern", + missing_value_policy=MVP("insufficient_data", "nicht verfügbar"), + known_limitations="Baseline braucht ausreichend Vorlauf", + layer_1_decision=f"recovery_metrics.{dl_fn}", + layer_2a_decision=f"Resolver {res_fn}", + layer_2b_reuse_possible=True, + architecture_alignment="Phase 0c", + issue_53_alignment="Layer 1", + evidence={}, + ) + _tag(m) + register_placeholder(m) + + +register_vitalwerte() diff --git a/backend/placeholder_resolver.py b/backend/placeholder_resolver.py index a2d8392..86d279c 100644 --- a/backend/placeholder_resolver.py +++ b/backend/placeholder_resolver.py @@ -9,7 +9,7 @@ This module now focuses on FORMATTING for AI consumption. """ import re from datetime import datetime, timedelta -from typing import Dict, List, Optional, Callable +from typing import Dict, List, Optional, Callable, Tuple from db import get_db, get_cursor, r2d # Phase 0c: Import data layer @@ -277,6 +277,43 @@ def calculate_age(dob) -> str: return "unbekannt" +def get_profile_name(profile_id: str) -> str: + """Profil-Platzhalter: Anzeigename (profiles.name).""" + return get_profile_data(profile_id).get('name', 'Nutzer') + + +def get_profile_age_display(profile_id: str) -> str: + """Profil-Platzhalter: Alter aus Geburtsdatum.""" + return calculate_age(get_profile_data(profile_id).get('dob')) + + +def get_profile_height_display(profile_id: str) -> str: + """Profil-Platzhalter: Körpergröße (cm) als String.""" + return str(get_profile_data(profile_id).get('height', 'unbekannt')) + + +def get_profile_geschlecht_display(profile_id: str) -> str: + """Profil-Platzhalter: Geschlecht aus profiles.sex (m/w).""" + return 'männlich' if get_profile_data(profile_id).get('sex') == 'm' else 'weiblich' + + +def get_datum_heute(_profile_id: str) -> str: + """Zeitraum-Platzhalter: heutiges Datum (dd.mm.yyyy).""" + return datetime.now().strftime('%d.%m.%Y') + + +def get_zeitraum_label_7d(_profile_id: str) -> str: + return 'letzte 7 Tage' + + +def get_zeitraum_label_30d(_profile_id: str) -> str: + return 'letzte 30 Tage' + + +def get_zeitraum_label_90d(_profile_id: str) -> str: + return 'letzte 90 Tage' + + def get_activity_detail(profile_id: str, days: int = 14) -> str: """ Get detailed activity log for analysis. @@ -1136,10 +1173,10 @@ def _format_goals_on_track(profile_id: str, n: int = 3) -> str: PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = { # Profil - '{{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', + '{{name}}': get_profile_name, + '{{age}}': get_profile_age_display, + '{{height}}': get_profile_height_display, + '{{geschlecht}}': get_profile_geschlecht_display, # Körper (21 Registry-Keys: body_metrics + body_extras — alles hier gebündelt) '{{weight_aktuell}}': get_latest_weight, @@ -1203,29 +1240,37 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = { '{{training_inter_session_gap_md}}': get_training_inter_session_gap_md, '{{training_sessions_recent_json}}': lambda pid: _safe_json('training_sessions_recent_json', pid), - # Schlaf & Erholung + # Schlaf & Erholung (10 Registry-Keys; recovery_score hier, nicht unter Meta Scores) '{{sleep_avg_duration}}': lambda pid: get_sleep_avg_duration(pid, 7), '{{sleep_avg_quality}}': lambda pid: get_sleep_avg_quality(pid, 7), '{{rest_days_count}}': lambda pid: get_rest_days_count(pid, 30), + '{{recovery_score}}': lambda pid: _safe_int('recovery_score_v2', pid), + '{{sleep_avg_duration_7d}}': lambda pid: _safe_float('sleep_avg_duration_7d', pid), + '{{sleep_debt_hours}}': lambda pid: _safe_float('sleep_debt_hours', pid), + '{{sleep_regularity_proxy}}': lambda pid: _safe_float('sleep_regularity_proxy', pid), + '{{recent_load_balance_3d}}': lambda pid: _safe_int('recent_load_balance_3d', pid), + '{{sleep_quality_7d}}': lambda pid: _safe_int('sleep_quality_7d', pid), + '{{correlation_sleep_recovery}}': lambda pid: _safe_json('correlation_sleep_recovery', pid), - # Vitalwerte + # Vitalwerte (5 Registry-Keys: Mittelwerte + vs. Baseline) '{{vitals_avg_hr}}': lambda pid: get_vitals_avg_hr(pid, 7), '{{vitals_avg_hrv}}': lambda pid: get_vitals_avg_hrv(pid, 7), '{{vitals_vo2_max}}': get_vitals_vo2_max, + '{{hrv_vs_baseline_pct}}': lambda pid: _safe_float('hrv_vs_baseline_pct', pid), + '{{rhr_vs_baseline_pct}}': lambda pid: _safe_float('rhr_vs_baseline_pct', pid), # Zeitraum - '{{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', + '{{datum_heute}}': get_datum_heute, + '{{zeitraum_7d}}': get_zeitraum_label_7d, + '{{zeitraum_30d}}': get_zeitraum_label_30d, + '{{zeitraum_90d}}': get_zeitraum_label_90d, # ======================================================================== # PHASE 0b: Goal-Aware Placeholders (Dynamic Focus Areas v2.0) # ======================================================================== - # --- Meta Scores (Ebene 1: Aggregierte Scores; body/nutrition/activity scores → jeweilige Kategorie) --- + # --- Meta Scores (Ebene 1; recovery_score → Schlaf & Erholung) --- '{{goal_progress_score}}': lambda pid: _safe_int('goal_progress_score', pid), - '{{recovery_score}}': lambda pid: _safe_int('recovery_score_v2', pid), '{{data_quality_score}}': lambda pid: _safe_int('data_quality_score', pid), # --- Top-Weighted Goals/Focus Areas (Ebene 2: statt Primary) --- @@ -1251,21 +1296,11 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = { '{{focus_cat_lebensstil_progress}}': lambda pid: _safe_int('focus_cat_lebensstil_progress', pid), '{{focus_cat_lebensstil_weight}}': lambda pid: _safe_float('focus_cat_lebensstil_weight', pid), - # --- Recovery Metrics (Recovery Score v2) --- - '{{hrv_vs_baseline_pct}}': lambda pid: _safe_float('hrv_vs_baseline_pct', pid), - '{{rhr_vs_baseline_pct}}': lambda pid: _safe_float('rhr_vs_baseline_pct', pid), - '{{sleep_avg_duration_7d}}': lambda pid: _safe_float('sleep_avg_duration_7d', pid), - '{{sleep_debt_hours}}': lambda pid: _safe_float('sleep_debt_hours', pid), - '{{sleep_regularity_proxy}}': lambda pid: _safe_float('sleep_regularity_proxy', pid), - '{{recent_load_balance_3d}}': lambda pid: _safe_int('recent_load_balance_3d', pid), - '{{sleep_quality_7d}}': lambda pid: _safe_int('sleep_quality_7d', pid), - # --- Correlation Metrics (C1-C7) --- '{{correlation_energy_weight_lag}}': lambda pid: _safe_json('correlation_energy_weight_lag', pid), '{{correlation_protein_lbm}}': lambda pid: _safe_json('correlation_protein_lbm', pid), '{{correlation_load_hrv}}': lambda pid: _safe_json('correlation_load_hrv', pid), '{{correlation_load_rhr}}': lambda pid: _safe_json('correlation_load_rhr', pid), - '{{correlation_sleep_recovery}}': lambda pid: _safe_json('correlation_sleep_recovery', pid), '{{plateau_detected}}': lambda pid: _safe_json('plateau_detected', pid), '{{top_drivers}}': lambda pid: _safe_json('top_drivers', pid), @@ -1378,9 +1413,42 @@ def get_available_placeholders(categories: Optional[List[str]] = None) -> Dict[s '{{rest_day_compliance}}', '{{vo2max_trend_28d}}', '{{activity_score}}', '{{training_frequency_by_type_md}}', '{{training_inter_session_gap_md}}', '{{training_sessions_recent_json}}', ], + 'schlaf': [ + '{{sleep_avg_duration}}', '{{sleep_avg_quality}}', '{{rest_days_count}}', + '{{recovery_score}}', + '{{sleep_avg_duration_7d}}', '{{sleep_debt_hours}}', '{{sleep_regularity_proxy}}', + '{{recent_load_balance_3d}}', '{{sleep_quality_7d}}', + '{{correlation_sleep_recovery}}', + ], + 'vitalwerte': [ + '{{vitals_avg_hr}}', '{{vitals_avg_hrv}}', '{{vitals_vo2_max}}', + '{{hrv_vs_baseline_pct}}', '{{rhr_vs_baseline_pct}}', + ], 'zeitraum': [ '{{datum_heute}}', '{{zeitraum_7d}}', '{{zeitraum_30d}}', '{{zeitraum_90d}}' - ] + ], + 'phase0b_meta': [ + '{{goal_progress_score}}', '{{data_quality_score}}', + ], + 'ziele_fokus': [ + '{{top_goal_name}}', '{{top_goal_progress_pct}}', '{{top_goal_status}}', + '{{top_focus_area_name}}', '{{top_focus_area_progress}}', + '{{focus_cat_körper_progress}}', '{{focus_cat_körper_weight}}', + '{{focus_cat_ernährung_progress}}', '{{focus_cat_ernährung_weight}}', + '{{focus_cat_aktivität_progress}}', '{{focus_cat_aktivität_weight}}', + '{{focus_cat_recovery_progress}}', '{{focus_cat_recovery_weight}}', + '{{focus_cat_vitalwerte_progress}}', '{{focus_cat_vitalwerte_weight}}', + '{{focus_cat_mental_progress}}', '{{focus_cat_mental_weight}}', + '{{focus_cat_lebensstil_progress}}', '{{focus_cat_lebensstil_weight}}', + '{{active_goals_json}}', '{{active_goals_md}}', + '{{focus_areas_weighted_json}}', '{{focus_areas_weighted_md}}', '{{focus_area_weights_json}}', + '{{top_3_focus_areas}}', '{{top_3_goals_behind_schedule}}', '{{top_3_goals_on_track}}', + ], + 'korrelationen': [ + '{{correlation_energy_weight_lag}}', '{{correlation_protein_lbm}}', + '{{correlation_load_hrv}}', '{{correlation_load_rhr}}', + '{{plateau_detected}}', '{{top_drivers}}', + ], } if not categories: @@ -1460,50 +1528,7 @@ def get_placeholder_catalog(profile_id: str) -> Dict[str, List[Dict[str, str]]]: }) # Legacy placeholders (not in registry yet) - legacy_placeholders = { - 'Profil': [ - ('name', 'Name des Nutzers'), - ('age', 'Alter in Jahren'), - ('height', 'Körpergröße in cm'), - ('geschlecht', 'Geschlecht'), - ], - 'Schlaf & Erholung': [ - ('sleep_avg_duration', 'Durchschn. Schlafdauer (7d)'), - ('sleep_avg_quality', 'Durchschn. Schlafqualität (7d)'), - ('rest_days_count', 'Anzahl Ruhetage (30d)'), - ('sleep_avg_duration_7d', 'Schlaf 7d (Stunden)'), - ('sleep_debt_hours', 'Schlafschuld (Stunden)'), - ('sleep_regularity_proxy', 'Schlaf-Regelmäßigkeit (Min Abweichung)'), - ('sleep_quality_7d', 'Schlafqualität 7d (0-100)'), - ], - 'Vitalwerte': [ - ('vitals_avg_hr', 'Durchschn. Ruhepuls (7d)'), - ('vitals_avg_hrv', 'Durchschn. HRV (7d)'), - ('vitals_vo2_max', 'Aktueller VO2 Max'), - ('hrv_vs_baseline_pct', 'HRV vs. Baseline (%)'), - ('rhr_vs_baseline_pct', 'RHR vs. Baseline (%)'), - ], - 'Scores (Phase 0b)': [ - ('goal_progress_score', 'Goal Progress Score (0-100)'), - ('recovery_score', 'Recovery Score (0-100)'), - ('data_quality_score', 'Data Quality Score (0-100)'), - ], - 'Focus Areas': [ - ('top_focus_area_name', 'Top Focus Area Name'), - ('top_focus_area_progress', 'Top Focus Area Progress (%)'), - ('focus_cat_körper_progress', 'Kategorie Körper - Progress (%)'), - ('focus_cat_körper_weight', 'Kategorie Körper - Gewichtung (%)'), - ('focus_cat_ernährung_progress', 'Kategorie Ernährung - Progress (%)'), - ('focus_cat_ernährung_weight', 'Kategorie Ernährung - Gewichtung (%)'), - ('focus_cat_aktivität_progress', 'Kategorie Aktivität - Progress (%)'), - ('focus_cat_aktivität_weight', 'Kategorie Aktivität - Gewichtung (%)'), - ], - 'Zeitraum': [ - ('datum_heute', 'Heutiges Datum'), - ('zeitraum_7d', '7-Tage-Zeitraum'), - ('zeitraum_30d', '30-Tage-Zeitraum'), - ], - } + legacy_placeholders: Dict[str, List[Tuple[str, str]]] = {} # Add legacy placeholders (skip if already in registry) for category, items in legacy_placeholders.items():