feat: Add new profile and time period placeholders in placeholder_resolver.py
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 16s

- 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.
This commit is contained in:
Lars 2026-04-11 21:08:34 +02:00
parent e9e094c6a4
commit 2ea5f905c4
9 changed files with 1233 additions and 68 deletions

View File

@ -13,6 +13,12 @@ from . import body_metrics
from . import body_extras from . import body_extras
from . import activity_metrics from . import activity_metrics
from . import activity_session_insights 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__ = [ __all__ = [
'nutrition_part_a', 'nutrition_part_a',
@ -23,4 +29,10 @@ __all__ = [
'body_extras', 'body_extras',
'activity_metrics', 'activity_metrics',
'activity_session_insights', 'activity_session_insights',
'schlaf_erholung',
'vitalwerte',
'profil_zeitraum',
'phase_0b_meta_scores',
'phase_0b_ziele_fokus',
'korrelationen',
] ]

View File

@ -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)

View File

@ -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()

View File

@ -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 0100",
"0100",
),
(
"data_quality_score",
"calculate_data_quality_score",
"Datenqualitäts-Score 0100",
"0100",
),
]:
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()

View File

@ -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 01)",
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="01",
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()

View File

@ -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()

View File

@ -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 0100 (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="0100",
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 (0100)", "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()

View File

@ -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()

View File

@ -9,7 +9,7 @@ This module now focuses on FORMATTING for AI consumption.
""" """
import re import re
from datetime import datetime, timedelta 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 from db import get_db, get_cursor, r2d
# Phase 0c: Import data layer # Phase 0c: Import data layer
@ -277,6 +277,43 @@ def calculate_age(dob) -> str:
return "unbekannt" 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: def get_activity_detail(profile_id: str, days: int = 14) -> str:
""" """
Get detailed activity log for analysis. 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]] = { PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = {
# Profil # Profil
'{{name}}': lambda pid: get_profile_data(pid).get('name', 'Nutzer'), '{{name}}': get_profile_name,
'{{age}}': lambda pid: calculate_age(get_profile_data(pid).get('dob')), '{{age}}': get_profile_age_display,
'{{height}}': lambda pid: str(get_profile_data(pid).get('height', 'unbekannt')), '{{height}}': get_profile_height_display,
'{{geschlecht}}': lambda pid: 'männlich' if get_profile_data(pid).get('sex') == 'm' else 'weiblich', '{{geschlecht}}': get_profile_geschlecht_display,
# Körper (21 Registry-Keys: body_metrics + body_extras — alles hier gebündelt) # Körper (21 Registry-Keys: body_metrics + body_extras — alles hier gebündelt)
'{{weight_aktuell}}': get_latest_weight, '{{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_inter_session_gap_md}}': get_training_inter_session_gap_md,
'{{training_sessions_recent_json}}': lambda pid: _safe_json('training_sessions_recent_json', pid), '{{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_duration}}': lambda pid: get_sleep_avg_duration(pid, 7),
'{{sleep_avg_quality}}': lambda pid: get_sleep_avg_quality(pid, 7), '{{sleep_avg_quality}}': lambda pid: get_sleep_avg_quality(pid, 7),
'{{rest_days_count}}': lambda pid: get_rest_days_count(pid, 30), '{{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_hr}}': lambda pid: get_vitals_avg_hr(pid, 7),
'{{vitals_avg_hrv}}': lambda pid: get_vitals_avg_hrv(pid, 7), '{{vitals_avg_hrv}}': lambda pid: get_vitals_avg_hrv(pid, 7),
'{{vitals_vo2_max}}': get_vitals_vo2_max, '{{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 # Zeitraum
'{{datum_heute}}': lambda pid: datetime.now().strftime('%d.%m.%Y'), '{{datum_heute}}': get_datum_heute,
'{{zeitraum_7d}}': lambda pid: 'letzte 7 Tage', '{{zeitraum_7d}}': get_zeitraum_label_7d,
'{{zeitraum_30d}}': lambda pid: 'letzte 30 Tage', '{{zeitraum_30d}}': get_zeitraum_label_30d,
'{{zeitraum_90d}}': lambda pid: 'letzte 90 Tage', '{{zeitraum_90d}}': get_zeitraum_label_90d,
# ======================================================================== # ========================================================================
# PHASE 0b: Goal-Aware Placeholders (Dynamic Focus Areas v2.0) # 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), '{{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), '{{data_quality_score}}': lambda pid: _safe_int('data_quality_score', pid),
# --- Top-Weighted Goals/Focus Areas (Ebene 2: statt Primary) --- # --- 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_progress}}': lambda pid: _safe_int('focus_cat_lebensstil_progress', pid),
'{{focus_cat_lebensstil_weight}}': lambda pid: _safe_float('focus_cat_lebensstil_weight', 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 Metrics (C1-C7) ---
'{{correlation_energy_weight_lag}}': lambda pid: _safe_json('correlation_energy_weight_lag', pid), '{{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_protein_lbm}}': lambda pid: _safe_json('correlation_protein_lbm', pid),
'{{correlation_load_hrv}}': lambda pid: _safe_json('correlation_load_hrv', pid), '{{correlation_load_hrv}}': lambda pid: _safe_json('correlation_load_hrv', pid),
'{{correlation_load_rhr}}': lambda pid: _safe_json('correlation_load_rhr', 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), '{{plateau_detected}}': lambda pid: _safe_json('plateau_detected', pid),
'{{top_drivers}}': lambda pid: _safe_json('top_drivers', 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}}', '{{rest_day_compliance}}', '{{vo2max_trend_28d}}', '{{activity_score}}',
'{{training_frequency_by_type_md}}', '{{training_inter_session_gap_md}}', '{{training_sessions_recent_json}}', '{{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': [ 'zeitraum': [
'{{datum_heute}}', '{{zeitraum_7d}}', '{{zeitraum_30d}}', '{{zeitraum_90d}}' '{{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: 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 (not in registry yet)
legacy_placeholders = { legacy_placeholders: Dict[str, List[Tuple[str, str]]] = {}
'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'),
],
}
# Add legacy placeholders (skip if already in registry) # Add legacy placeholders (skip if already in registry)
for category, items in legacy_placeholders.items(): for category, items in legacy_placeholders.items():