feat: Enhance training parameters handling and documentation
- Introduced new fields for descriptions in training parameters, improving clarity for AI context in `training_sessions_recent_json`.
- Added a glossary placeholder `{{training_parameters_glossary_md}}` to provide a Markdown table of active training parameters, including names and descriptions.
- Updated the `placeholder_resolver.py` and `activity_metrics.py` to support the new glossary functionality.
- Enhanced the `AdminActivityAttributeProfilesPage` to allow input for descriptions in both German and English, ensuring better context for metrics.
- Revised tests to validate the inclusion of description fields in parameter schema merges and metrics handling.
This commit is contained in:
parent
c3be745efa
commit
bc8e9fb7fa
|
|
@ -57,6 +57,8 @@
|
||||||
- Platzhalter / Charts, die Session-Details brauchen: **nur** diese Layer-1-Helfer erweitern oder aufrufen (z. B. `activity_metrics.get_training_sessions_recent_weeks_data` nutzt `enrich_sessions_with_metrics`).
|
- Platzhalter / Charts, die Session-Details brauchen: **nur** diese Layer-1-Helfer erweitern oder aufrufen (z. B. `activity_metrics.get_training_sessions_recent_weeks_data` nutzt `enrich_sessions_with_metrics`).
|
||||||
- Router: `get_db`, `get_cursor`, Auth; Business-Validierung delegieren an `activity_session_metrics`.
|
- Router: `get_db`, `get_cursor`, Auth; Business-Validierung delegieren an `activity_session_metrics`.
|
||||||
|
|
||||||
|
**KI-Kontext:** In `training_sessions_recent_json` enthält jedes Element von `session_metrics` neben `key`/`value` die Felder `name_de`, `name_en`, `description_de`, `description_en` (aus dem effektiven Schema). Für nicht selbsterklärende Keys soll im Katalog `training_parameters.description_*` gepflegt werden (Admin). Ergänzend liefert der Platzhalter `{{training_parameters_glossary_md}}` die gesamte aktive Parameter-Legende als Markdown-Tabelle (`get_training_parameters_ki_glossary_data` → `get_training_parameters_glossary_md`).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. API (Ist / geplant)
|
## 4. API (Ist / geplant)
|
||||||
|
|
@ -137,6 +139,7 @@ Abdeckung: reine Merge-Logik (`merge_parameter_schema_rows`), Validierung (`_val
|
||||||
- Migration 004/014: `training_types`, `activity_log`-Erweiterungen
|
- Migration 004/014: `training_types`, `activity_log`-Erweiterungen
|
||||||
- Pattern Admin-Katalog: `routers/admin_reference_value_types.py`
|
- Pattern Admin-Katalog: `routers/admin_reference_value_types.py`
|
||||||
- Platzhalter Session-JSON: `data_layer/activity_metrics.py` → `get_training_sessions_recent_weeks_data`
|
- Platzhalter Session-JSON: `data_layer/activity_metrics.py` → `get_training_sessions_recent_weeks_data`
|
||||||
|
- KI-Legende: `get_training_parameters_ki_glossary_data`, Platzhalter `{{training_parameters_glossary_md}}`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,7 @@ frontend/src/
|
||||||
|
|
||||||
- **Agent-Guide:** `.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md` (Prod: nur additive Migration **054**; Layer1 `data_layer/activity_session_metrics.py`).
|
- **Agent-Guide:** `.claude/docs/technical/ACTIVITY_SESSION_METRICS_EAV_AGENT_GUIDE.md` (Prod: nur additive Migration **054**; Layer1 `data_layer/activity_session_metrics.py`).
|
||||||
- **DB:** `training_category_parameter`, `training_type_parameter`, `activity_session_metrics`; `activity_log.started_at` / `ended_at` (nullable).
|
- **DB:** `training_category_parameter`, `training_type_parameter`, `activity_session_metrics`; `activity_log.started_at` / `ended_at` (nullable).
|
||||||
- **API:** Admin `/api/admin/training-parameters`, `/api/admin/training-category-parameters`, `/api/admin/training-type-parameters`; Nutzer `GET /api/activity/{id}`, `PUT /api/activity/{id}/metrics`; Platzhalter-Pfad `training_sessions_recent_json` liefert pro Session `session_metrics` (wenn befüllt).
|
- **API:** Admin `/api/admin/training-parameters`, `/api/admin/training-category-parameters`, `/api/admin/training-type-parameters`; Nutzer `GET /api/activity/{id}`, `PUT /api/activity/{id}/metrics`; Platzhalter-Pfad `training_sessions_recent_json` liefert pro Session `session_metrics` inkl. `name_*` / `description_*`; **`{{training_parameters_glossary_md}}`** = Markdown-Legende aller aktiven Parameter (KI).
|
||||||
- **Frontend:** Admin `/admin/activity-attribute-profiles`; Aktivität → Verlauf → Bearbeiten: Profil-Kennwerte; `api.js` ergänzt.
|
- **Frontend:** Admin `/admin/activity-attribute-profiles`; Aktivität → Verlauf → Bearbeiten: Profil-Kennwerte; `api.js` ergänzt.
|
||||||
|
|
||||||
### Updates (16.04.2026 - Aktivität Phase A abgeschlossen, Phase B gestartet)
|
### Updates (16.04.2026 - Aktivität Phase A abgeschlossen, Phase B gestartet)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ Functions:
|
||||||
- get_training_frequency_by_type_data(): Häufigkeit & Intensität pro activity_type
|
- get_training_frequency_by_type_data(): Häufigkeit & Intensität pro activity_type
|
||||||
- get_training_inter_session_gap_data(): Pausen zwischen Einheiten (Stunden)
|
- get_training_inter_session_gap_data(): Pausen zwischen Einheiten (Stunden)
|
||||||
- get_training_sessions_recent_weeks_data(): Wochen-JSON für KI-Kontext
|
- get_training_sessions_recent_weeks_data(): Wochen-JSON für KI-Kontext
|
||||||
|
- get_training_parameters_ki_glossary_data(): Parameter-Katalog (Feld, Namen, Beschreibungen) für KI
|
||||||
|
|
||||||
All functions return structured data (dict) without formatting.
|
All functions return structured data (dict) without formatting.
|
||||||
Use placeholder_resolver.py for formatted strings for AI.
|
Use placeholder_resolver.py for formatted strings for AI.
|
||||||
|
|
@ -1179,3 +1180,32 @@ def get_training_sessions_recent_weeks_data(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_training_parameters_ki_glossary_data(profile_id: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Alle aktiven ``training_parameters`` für KI-Kontext (z. B. neben ``training_sessions_recent_json``).
|
||||||
|
|
||||||
|
Enthält technischen key, name_de/name_en, description_de/description_en, data_type, unit, category.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
profile_id: Reserviert für spätere Einschränkung (z. B. nur im Profil vorkommende Keys);
|
||||||
|
aktuell ungenutzt, Signatur bleibt für Platzhalter-Resolver.
|
||||||
|
"""
|
||||||
|
_ = profile_id
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT key, name_de, name_en, description_de, description_en,
|
||||||
|
data_type, unit, category
|
||||||
|
FROM training_parameters
|
||||||
|
WHERE is_active = true
|
||||||
|
ORDER BY category, key
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
rows = [r2d(r) for r in cur.fetchall()]
|
||||||
|
return {
|
||||||
|
"parameters": rows,
|
||||||
|
"meta": {"count": len(rows), "scope": "global_active_catalog"},
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,8 @@ def merge_parameter_schema_rows(
|
||||||
"key": r["key"],
|
"key": r["key"],
|
||||||
"name_de": r["name_de"],
|
"name_de": r["name_de"],
|
||||||
"name_en": r["name_en"],
|
"name_en": r["name_en"],
|
||||||
|
"description_de": r.get("description_de"),
|
||||||
|
"description_en": r.get("description_en"),
|
||||||
"param_category": r["param_category"],
|
"param_category": r["param_category"],
|
||||||
"data_type": r["data_type"],
|
"data_type": r["data_type"],
|
||||||
"unit": r["unit"],
|
"unit": r["unit"],
|
||||||
|
|
@ -90,6 +92,8 @@ def merge_parameter_schema_rows(
|
||||||
"key": r["key"],
|
"key": r["key"],
|
||||||
"name_de": r["name_de"],
|
"name_de": r["name_de"],
|
||||||
"name_en": r["name_en"],
|
"name_en": r["name_en"],
|
||||||
|
"description_de": r.get("description_de"),
|
||||||
|
"description_en": r.get("description_en"),
|
||||||
"param_category": r["param_category"],
|
"param_category": r["param_category"],
|
||||||
"data_type": r["data_type"],
|
"data_type": r["data_type"],
|
||||||
"unit": r["unit"],
|
"unit": r["unit"],
|
||||||
|
|
@ -133,7 +137,9 @@ def resolve_activity_attribute_schema(
|
||||||
tcp.sort_order AS cat_sort,
|
tcp.sort_order AS cat_sort,
|
||||||
tcp.required AS cat_required,
|
tcp.required AS cat_required,
|
||||||
tcp.ui_group AS cat_ui_group,
|
tcp.ui_group AS cat_ui_group,
|
||||||
tp.key, tp.name_de, tp.name_en, tp.category AS param_category,
|
tp.key, tp.name_de, tp.name_en,
|
||||||
|
tp.description_de, tp.description_en,
|
||||||
|
tp.category AS param_category,
|
||||||
tp.data_type, tp.unit, tp.validation_rules, tp.source_field
|
tp.data_type, tp.unit, tp.validation_rules, tp.source_field
|
||||||
FROM training_category_parameter tcp
|
FROM training_category_parameter tcp
|
||||||
JOIN training_parameters tp ON tp.id = tcp.training_parameter_id
|
JOIN training_parameters tp ON tp.id = tcp.training_parameter_id
|
||||||
|
|
@ -151,7 +157,9 @@ def resolve_activity_attribute_schema(
|
||||||
ttp.sort_order AS typ_sort,
|
ttp.sort_order AS typ_sort,
|
||||||
ttp.required AS typ_required,
|
ttp.required AS typ_required,
|
||||||
ttp.ui_group AS typ_ui_group,
|
ttp.ui_group AS typ_ui_group,
|
||||||
tp.key, tp.name_de, tp.name_en, tp.category AS param_category,
|
tp.key, tp.name_de, tp.name_en,
|
||||||
|
tp.description_de, tp.description_en,
|
||||||
|
tp.category AS param_category,
|
||||||
tp.data_type, tp.unit, tp.validation_rules, tp.source_field
|
tp.data_type, tp.unit, tp.validation_rules, tp.source_field
|
||||||
FROM training_type_parameter ttp
|
FROM training_type_parameter ttp
|
||||||
JOIN training_parameters tp ON tp.id = ttp.training_parameter_id
|
JOIN training_parameters tp ON tp.id = ttp.training_parameter_id
|
||||||
|
|
@ -164,6 +172,16 @@ def resolve_activity_attribute_schema(
|
||||||
return merge_parameter_schema_rows(category_rows, type_rows)
|
return merge_parameter_schema_rows(category_rows, type_rows)
|
||||||
|
|
||||||
|
|
||||||
|
def _metric_human_labels(schema_row: Mapping[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""Bezeichnung + Kurzbeschreibung aus training_parameters (KI / Export)."""
|
||||||
|
return {
|
||||||
|
"name_de": schema_row.get("name_de"),
|
||||||
|
"name_en": schema_row.get("name_en"),
|
||||||
|
"description_de": schema_row.get("description_de"),
|
||||||
|
"description_en": schema_row.get("description_en"),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _validation_rules_dict(raw: Any) -> Dict[str, Any]:
|
def _validation_rules_dict(raw: Any) -> Dict[str, Any]:
|
||||||
if isinstance(raw, dict):
|
if isinstance(raw, dict):
|
||||||
return raw
|
return raw
|
||||||
|
|
@ -357,6 +375,7 @@ def merge_column_backed_and_eav_metrics(
|
||||||
"data_type": dt,
|
"data_type": dt,
|
||||||
"unit": unit,
|
"unit": unit,
|
||||||
"value": val,
|
"value": val,
|
||||||
|
**_metric_human_labels(s),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
used_column = True
|
used_column = True
|
||||||
|
|
@ -377,6 +396,7 @@ def merge_column_backed_and_eav_metrics(
|
||||||
"data_type": dt,
|
"data_type": dt,
|
||||||
"unit": unit,
|
"unit": unit,
|
||||||
"value": val,
|
"value": val,
|
||||||
|
**_metric_human_labels(s),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
keys_handled.add(k)
|
keys_handled.add(k)
|
||||||
|
|
@ -395,6 +415,7 @@ def merge_column_backed_and_eav_metrics(
|
||||||
"data_type": dt,
|
"data_type": dt,
|
||||||
"unit": unit,
|
"unit": unit,
|
||||||
"value": val,
|
"value": val,
|
||||||
|
**_metric_human_labels(s),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
keys_handled.add(k)
|
keys_handled.add(k)
|
||||||
|
|
@ -403,7 +424,9 @@ def merge_column_backed_and_eav_metrics(
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if k in eav_by_key:
|
if k in eav_by_key:
|
||||||
merged.append(dict(eav_by_key[k]))
|
row = dict(eav_by_key[k])
|
||||||
|
row.update(_metric_human_labels(s))
|
||||||
|
merged.append(row)
|
||||||
keys_handled.add(k)
|
keys_handled.add(k)
|
||||||
|
|
||||||
merged.sort(key=lambda x: x["key"])
|
merged.sort(key=lambda x: x["key"])
|
||||||
|
|
@ -699,6 +722,15 @@ def enrich_sessions_with_metrics(cur, sessions: List[Dict[str, Any]]) -> None:
|
||||||
eav_list = by_act.get(aid, [])
|
eav_list = by_act.get(aid, [])
|
||||||
merged = merge_column_backed_and_eav_metrics(header, schema, eav_list)
|
merged = merge_column_backed_and_eav_metrics(header, schema, eav_list)
|
||||||
s["session_metrics"] = [
|
s["session_metrics"] = [
|
||||||
{"key": m["key"], "data_type": m["data_type"], "unit": m["unit"], "value": m["value"]}
|
{
|
||||||
|
"key": m["key"],
|
||||||
|
"data_type": m["data_type"],
|
||||||
|
"unit": m["unit"],
|
||||||
|
"value": m["value"],
|
||||||
|
"name_de": m.get("name_de"),
|
||||||
|
"name_en": m.get("name_en"),
|
||||||
|
"description_de": m.get("description_de"),
|
||||||
|
"description_en": m.get("description_en"),
|
||||||
|
}
|
||||||
for m in merged
|
for m in merged
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,8 @@ def register_activity_session_insights():
|
||||||
"duration_min, kcal_active, hr_avg, hr_max, rpe, training_category, training_type_name, "
|
"duration_min, kcal_active, hr_avg, hr_max, rpe, training_category, training_type_name, "
|
||||||
"session_metrics[]. "
|
"session_metrics[]. "
|
||||||
"session_metrics: effektive Liste nach merge_column_backed_and_eav_metrics — Einträge mit "
|
"session_metrics: effektive Liste nach merge_column_backed_and_eav_metrics — Einträge mit "
|
||||||
"training_parameter_id, key, data_type, unit, value; nur Parameter aus Attributschema "
|
"training_parameter_id, key, data_type, unit, value, name_de/name_en, description_de/description_en; "
|
||||||
|
"nur Parameter aus Attributschema "
|
||||||
"(training_category_parameter + training_type_parameter Overrides), keys sortiert. "
|
"(training_category_parameter + training_type_parameter Overrides), keys sortiert. "
|
||||||
"Kanon Lesen: activity_log-Spalte vor EAV bei Konflikt. "
|
"Kanon Lesen: activity_log-Spalte vor EAV bei Konflikt. "
|
||||||
"meta: weeks_requested, days_loaded, session_count, confidence. "
|
"meta: weeks_requested, days_loaded, session_count, confidence. "
|
||||||
|
|
@ -170,6 +171,7 @@ def register_activity_session_insights():
|
||||||
"session_metrics oft [] (kein Typ, kein Profil, keine gespeicherten Werte). "
|
"session_metrics oft [] (kein Typ, kein Profil, keine gespeicherten Werte). "
|
||||||
"Anzahl und Namen der Metrik-Keys sind instanz-/adminabhängig — JSON nicht als festes Schema "
|
"Anzahl und Namen der Metrik-Keys sind instanz-/adminabhängig — JSON nicht als festes Schema "
|
||||||
"für Downstream-Parsing harter Logik verwenden. "
|
"für Downstream-Parsing harter Logik verwenden. "
|
||||||
|
"Für KI-Semantik zusätzlich {{training_parameters_glossary_md}} (gesamter aktiver Katalog) in den Prompt legen. "
|
||||||
"Composite-Parameter (JSON in EAV) noch nicht im MVP expandiert; ggf. Roh-value_text in späterer Phase."
|
"Composite-Parameter (JSON in EAV) noch nicht im MVP expandiert; ggf. Roh-value_text in späterer Phase."
|
||||||
),
|
),
|
||||||
layer_1_decision="activity_metrics.get_training_sessions_recent_weeks_data",
|
layer_1_decision="activity_metrics.get_training_sessions_recent_weeks_data",
|
||||||
|
|
@ -192,5 +194,61 @@ def register_activity_session_insights():
|
||||||
_ev(pj, "known_limitations", EvidenceType.MIXED)
|
_ev(pj, "known_limitations", EvidenceType.MIXED)
|
||||||
register_placeholder(pj)
|
register_placeholder(pj)
|
||||||
|
|
||||||
|
md_gloss = PlaceholderMetadata(
|
||||||
|
key="training_parameters_glossary_md",
|
||||||
|
category="Aktivität",
|
||||||
|
description=(
|
||||||
|
"Markdown-Tabelle: alle aktiven training_parameters (key, DE/EN, Beschreibungen, Typ, Einheit, Kategorie). "
|
||||||
|
"Ergänzung zu training_sessions_recent_json für KI (Bedeutung dynamischer Metrik-Keys)."
|
||||||
|
),
|
||||||
|
resolver_module="backend/placeholder_resolver.py",
|
||||||
|
resolver_function="get_training_parameters_glossary_md",
|
||||||
|
data_layer_module="backend/data_layer/activity_metrics.py",
|
||||||
|
data_layer_function="get_training_parameters_ki_glossary_data",
|
||||||
|
source_tables=["training_parameters"],
|
||||||
|
semantic_contract=(
|
||||||
|
"SELECT auf training_parameters WHERE is_active; sortiert category, key. "
|
||||||
|
"profile_id-Parameter im Resolver reserviert, aktuell globaler Katalog."
|
||||||
|
),
|
||||||
|
business_meaning="KI: Legende zu session_metrics-Keys und Custom-Parametern",
|
||||||
|
unit="Markdown",
|
||||||
|
time_window="n/a (Katalog-Snapshot)",
|
||||||
|
output_type=OutputType.TEXT_SUMMARY,
|
||||||
|
placeholder_type=PlaceholderType.INTERPRETED,
|
||||||
|
format_hint="GitHub-Flavored Markdown-Tabelle",
|
||||||
|
example_output="| Feld (key) | DE | EN | Beschreibung DE | … |",
|
||||||
|
minimum_data_requirements="Optional leer → Kurztext statt Tabelle",
|
||||||
|
quality_filter_policy=None,
|
||||||
|
confidence_logic="Immer verfügbar wenn DB erreichbar",
|
||||||
|
missing_value_policy=MissingValuePolicy(
|
||||||
|
available=False,
|
||||||
|
value_raw=None,
|
||||||
|
missing_reason="no_data",
|
||||||
|
legacy_display="Keine aktiven Trainingsparameter im Katalog.",
|
||||||
|
),
|
||||||
|
known_limitations=(
|
||||||
|
"Keine profil-spezifische Einschränkung auf tatsächlich genutzte Keys (V2). "
|
||||||
|
"Tabellen können bei großem Katalog lang werden."
|
||||||
|
),
|
||||||
|
layer_1_decision="activity_metrics.get_training_parameters_ki_glossary_data",
|
||||||
|
layer_2a_decision="get_training_parameters_glossary_md",
|
||||||
|
layer_2b_reuse_possible=True,
|
||||||
|
architecture_alignment="Phase 0c",
|
||||||
|
issue_53_alignment="Layer 2a",
|
||||||
|
evidence={},
|
||||||
|
)
|
||||||
|
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",
|
||||||
|
):
|
||||||
|
_ev(md_gloss, f)
|
||||||
|
_ev(md_gloss, "business_meaning", EvidenceType.DRAFT_DERIVED)
|
||||||
|
_ev(md_gloss, "known_limitations", EvidenceType.MIXED)
|
||||||
|
register_placeholder(md_gloss)
|
||||||
|
|
||||||
|
|
||||||
register_activity_session_insights()
|
register_activity_session_insights()
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ from data_layer.activity_metrics import (
|
||||||
get_training_frequency_by_type_data,
|
get_training_frequency_by_type_data,
|
||||||
get_training_inter_session_gap_data,
|
get_training_inter_session_gap_data,
|
||||||
get_training_sessions_recent_weeks_data,
|
get_training_sessions_recent_weeks_data,
|
||||||
|
get_training_parameters_ki_glossary_data,
|
||||||
)
|
)
|
||||||
from data_layer.recovery_metrics import (
|
from data_layer.recovery_metrics import (
|
||||||
get_sleep_duration_data,
|
get_sleep_duration_data,
|
||||||
|
|
@ -426,7 +427,8 @@ def get_activity_detail(profile_id: str, days: int = 14) -> str:
|
||||||
k, v = m.get("key"), m.get("value")
|
k, v = m.get("key"), m.get("value")
|
||||||
if k is None or v is None:
|
if k is None or v is None:
|
||||||
continue
|
continue
|
||||||
eav_parts.append(f"{k}={v}")
|
label = m.get("name_de") or m.get("name_en") or k
|
||||||
|
eav_parts.append(f"{label} ({k})={v}")
|
||||||
eav_str = f" | EAV: {'; '.join(eav_parts)}" if eav_parts else ""
|
eav_str = f" | EAV: {'; '.join(eav_parts)}" if eav_parts else ""
|
||||||
lines.append(
|
lines.append(
|
||||||
f"{activity['date']}: {activity['activity_type']} "
|
f"{activity['date']}: {activity['activity_type']} "
|
||||||
|
|
@ -456,6 +458,45 @@ def get_trainingstyp_verteilung(profile_id: str, days: int = 14) -> str:
|
||||||
return ", ".join(parts)
|
return ", ".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def get_training_parameters_glossary_md(profile_id: str) -> str:
|
||||||
|
"""
|
||||||
|
Markdown-Tabelle: alle aktiven training_parameters (key, Namen, Beschreibungen, Typ, Einheit).
|
||||||
|
Für KI neben session_metrics / training_sessions_recent_json.
|
||||||
|
"""
|
||||||
|
data = get_training_parameters_ki_glossary_data(profile_id)
|
||||||
|
params = data.get("parameters") or []
|
||||||
|
if not params:
|
||||||
|
return "Keine aktiven Trainingsparameter im Katalog."
|
||||||
|
|
||||||
|
def cell(x: object) -> str:
|
||||||
|
if x is None:
|
||||||
|
return "—"
|
||||||
|
return str(x).replace("|", "·").replace("\n", " ").strip()[:400]
|
||||||
|
|
||||||
|
lines = [
|
||||||
|
"| Feld (key) | DE | EN | Beschreibung DE | Beschreibung EN | Typ | Einheit | Kategorie |",
|
||||||
|
"|---|---|---|---|---|---|---|---|",
|
||||||
|
]
|
||||||
|
for p in params:
|
||||||
|
lines.append(
|
||||||
|
"| "
|
||||||
|
+ " | ".join(
|
||||||
|
[
|
||||||
|
cell(p.get("key")),
|
||||||
|
cell(p.get("name_de")),
|
||||||
|
cell(p.get("name_en")),
|
||||||
|
cell(p.get("description_de")),
|
||||||
|
cell(p.get("description_en")),
|
||||||
|
cell(p.get("data_type")),
|
||||||
|
cell(p.get("unit")),
|
||||||
|
cell(p.get("category")),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
+ " |"
|
||||||
|
)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def get_training_frequency_by_type_md(profile_id: str, days: int = 28) -> str:
|
def get_training_frequency_by_type_md(profile_id: str, days: int = 28) -> str:
|
||||||
"""
|
"""
|
||||||
Markdown-Tabelle: pro Trainingsart (Roh-Label) Ø Sessions/Woche, Dauer, kcal, HF, RPE, kcal/min.
|
Markdown-Tabelle: pro Trainingsart (Roh-Label) Ø Sessions/Woche, Dauer, kcal, HF, RPE, kcal/min.
|
||||||
|
|
@ -1545,6 +1586,7 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = {
|
||||||
'{{training_frequency_by_type_md}}': get_training_frequency_by_type_md,
|
'{{training_frequency_by_type_md}}': get_training_frequency_by_type_md,
|
||||||
'{{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),
|
||||||
|
'{{training_parameters_glossary_md}}': get_training_parameters_glossary_md,
|
||||||
|
|
||||||
# Schlaf & Erholung (10 Registry-Keys; recovery_score hier, nicht unter Meta Scores)
|
# 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),
|
||||||
|
|
@ -1749,6 +1791,7 @@ def get_available_placeholders(categories: Optional[List[str]] = None) -> Dict[s
|
||||||
'{{proxy_internal_load_7d}}', '{{monotony_score}}', '{{strain_score}}',
|
'{{proxy_internal_load_7d}}', '{{monotony_score}}', '{{strain_score}}',
|
||||||
'{{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}}',
|
||||||
|
'{{training_parameters_glossary_md}}',
|
||||||
],
|
],
|
||||||
'schlaf': [
|
'schlaf': [
|
||||||
'{{sleep_avg_duration}}', '{{sleep_avg_quality}}', '{{rest_days_count}}',
|
'{{sleep_avg_duration}}', '{{sleep_avg_quality}}', '{{rest_days_count}}',
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,52 @@ def test_merge_type_overrides_required_and_sort():
|
||||||
assert merged[0]["required"] is True
|
assert merged[0]["required"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_parameter_schema_includes_descriptions():
|
||||||
|
cat = [
|
||||||
|
{
|
||||||
|
"training_parameter_id": 1,
|
||||||
|
"cat_sort": 0,
|
||||||
|
"cat_required": False,
|
||||||
|
"cat_ui_group": None,
|
||||||
|
"key": "custom_w",
|
||||||
|
"name_de": "Leistung",
|
||||||
|
"name_en": "Watts",
|
||||||
|
"description_de": "Mittlere 5-Min-Leistung",
|
||||||
|
"description_en": "5 min average power",
|
||||||
|
"param_category": "performance",
|
||||||
|
"data_type": "integer",
|
||||||
|
"unit": "W",
|
||||||
|
"validation_rules": {},
|
||||||
|
"source_field": None,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
merged = merge_parameter_schema_rows(cat, [])
|
||||||
|
assert merged[0]["description_de"] == "Mittlere 5-Min-Leistung"
|
||||||
|
assert merged[0]["description_en"] == "5 min average power"
|
||||||
|
|
||||||
|
|
||||||
|
def test_merge_column_backed_includes_human_labels_from_schema():
|
||||||
|
schema = [
|
||||||
|
{
|
||||||
|
"training_parameter_id": 1,
|
||||||
|
"key": "watts",
|
||||||
|
"data_type": "integer",
|
||||||
|
"unit": "W",
|
||||||
|
"validation_rules": {},
|
||||||
|
"source_field": "avg_power",
|
||||||
|
"name_de": "Leistung",
|
||||||
|
"name_en": "Power",
|
||||||
|
"description_de": "Gerätewert",
|
||||||
|
"description_en": "Device reading",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
out = merge_column_backed_and_eav_metrics({"avg_power": 200}, schema, [])
|
||||||
|
assert len(out) == 1
|
||||||
|
assert out[0]["value"] == 200
|
||||||
|
assert out[0]["name_de"] == "Leistung"
|
||||||
|
assert out[0]["description_en"] == "Device reading"
|
||||||
|
|
||||||
|
|
||||||
def test_merge_type_adds_parameter_not_in_category():
|
def test_merge_type_adds_parameter_not_in_category():
|
||||||
typ = [_ttp_row(7, "cadence", typ_sort=1, typ_required=True, data_type="integer")]
|
typ = [_ttp_row(7, "cadence", typ_sort=1, typ_required=True, data_type="integer")]
|
||||||
merged = merge_parameter_schema_rows([], typ)
|
merged = merge_parameter_schema_rows([], typ)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,8 @@ const emptyParamForm = () => ({
|
||||||
key: '',
|
key: '',
|
||||||
name_de: '',
|
name_de: '',
|
||||||
name_en: '',
|
name_en: '',
|
||||||
|
description_de: '',
|
||||||
|
description_en: '',
|
||||||
category: 'physical',
|
category: 'physical',
|
||||||
data_type: 'float',
|
data_type: 'float',
|
||||||
unit: '',
|
unit: '',
|
||||||
|
|
@ -130,6 +132,8 @@ export default function AdminActivityAttributeProfilesPage() {
|
||||||
key: paramForm.key.trim().toLowerCase(),
|
key: paramForm.key.trim().toLowerCase(),
|
||||||
name_de: paramForm.name_de.trim(),
|
name_de: paramForm.name_de.trim(),
|
||||||
name_en: paramForm.name_en.trim(),
|
name_en: paramForm.name_en.trim(),
|
||||||
|
description_de: paramForm.description_de.trim() || null,
|
||||||
|
description_en: paramForm.description_en.trim() || null,
|
||||||
category: paramForm.category,
|
category: paramForm.category,
|
||||||
data_type: paramForm.data_type,
|
data_type: paramForm.data_type,
|
||||||
unit: paramForm.unit.trim() || null,
|
unit: paramForm.unit.trim() || null,
|
||||||
|
|
@ -153,6 +157,8 @@ export default function AdminActivityAttributeProfilesPage() {
|
||||||
await api.adminUpdateTrainingParameter(editParam.id, {
|
await api.adminUpdateTrainingParameter(editParam.id, {
|
||||||
name_de: editParam.name_de.trim(),
|
name_de: editParam.name_de.trim(),
|
||||||
name_en: editParam.name_en.trim(),
|
name_en: editParam.name_en.trim(),
|
||||||
|
description_de: editParam.description_de?.trim() || null,
|
||||||
|
description_en: editParam.description_en?.trim() || null,
|
||||||
category: editParam.category,
|
category: editParam.category,
|
||||||
data_type: editParam.data_type,
|
data_type: editParam.data_type,
|
||||||
unit: editParam.unit?.trim() || null,
|
unit: editParam.unit?.trim() || null,
|
||||||
|
|
@ -302,6 +308,11 @@ export default function AdminActivityAttributeProfilesPage() {
|
||||||
Nach Migration <strong>055</strong> werden Standard-Parameter allen Kategorien zugeordnet und vorhandene{' '}
|
Nach Migration <strong>055</strong> werden Standard-Parameter allen Kategorien zugeordnet und vorhandene{' '}
|
||||||
<code>activity_log</code>-Spalten idempotent nach EAV gespiegelt (sofern noch keine EAV-Zeile existiert).
|
<code>activity_log</code>-Spalten idempotent nach EAV gespiegelt (sofern noch keine EAV-Zeile existiert).
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong>KI:</strong> Bei eigenen / unklaren Metriken kurze <strong>Beschreibung DE/EN</strong> im Katalog
|
||||||
|
pflegen — sie erscheinen in Export/Platzhalter-Kontext (<code>training_sessions_recent_json</code>,{' '}
|
||||||
|
<code>{'{{training_parameters_glossary_md}}'}</code>).
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -394,6 +405,23 @@ export default function AdminActivityAttributeProfilesPage() {
|
||||||
onChange={(e) => setParamForm((f) => ({ ...f, name_en: e.target.value }))}
|
onChange={(e) => setParamForm((f) => ({ ...f, name_en: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Beschreibung DE / EN (optional, für KI)</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Was bedeutet der Wert? Einheit/Skala?"
|
||||||
|
value={paramForm.description_de}
|
||||||
|
onChange={(e) => setParamForm((f) => ({ ...f, description_de: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
rows={2}
|
||||||
|
placeholder="Short meaning for prompts / EN users"
|
||||||
|
value={paramForm.description_en}
|
||||||
|
onChange={(e) => setParamForm((f) => ({ ...f, description_en: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-label">Gruppe / Datentyp</label>
|
<label className="form-label">Gruppe / Datentyp</label>
|
||||||
<select
|
<select
|
||||||
|
|
@ -468,6 +496,21 @@ export default function AdminActivityAttributeProfilesPage() {
|
||||||
onChange={(e) => setEditParam((p) => ({ ...p, name_en: e.target.value }))}
|
onChange={(e) => setEditParam((p) => ({ ...p, name_en: e.target.value }))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Beschreibung DE / EN (optional, für KI)</label>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
rows={2}
|
||||||
|
value={editParam.description_de || ''}
|
||||||
|
onChange={(e) => setEditParam((p) => ({ ...p, description_de: e.target.value }))}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
className="form-input"
|
||||||
|
rows={2}
|
||||||
|
value={editParam.description_en || ''}
|
||||||
|
onChange={(e) => setEditParam((p) => ({ ...p, description_en: e.target.value }))}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div className="form-row">
|
<div className="form-row">
|
||||||
<label className="form-label">Gruppe / Typ</label>
|
<label className="form-label">Gruppe / Typ</label>
|
||||||
<select
|
<select
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user