feat: implement missing placeholder functions (sleep, vitals, rest)
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s

Implementiert 6 fehlende Platzhalter-Funktionen die im Katalog waren
aber keine Berechnung hatten.

Neue Funktionen:
- get_sleep_avg_duration(7d) → "7.5h"
- get_sleep_avg_quality(7d) → "65% (Deep+REM)"
- get_rest_days_count(30d) → "5 Ruhetage"
- get_vitals_avg_hr(7d) → "58 bpm"
- get_vitals_avg_hrv(7d) → "45 ms"
- get_vitals_vo2_max() → "42.5 ml/kg/min"

Datenquellen:
- sleep_log (JSONB segments mit Deep/REM/Light/Awake)
- rest_days (Kraft/Cardio/Entspannung)
- vitals_baseline (resting_hr, hrv, vo2_max)

Jetzt in PLACEHOLDER_MAP registriert → sofort nutzbar.

Fixes: Platzhalter-Export zeigt jetzt alle Werte (statt "nicht verfügbar")

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-26 10:14:17 +01:00
parent 555ff62b56
commit a9114bc40a

View File

@ -290,6 +290,138 @@ def get_trainingstyp_verteilung(profile_id: str, days: int = 14) -> str:
return ", ".join(parts)
def get_sleep_avg_duration(profile_id: str, days: int = 7) -> str:
"""Calculate average sleep duration in hours."""
with get_db() as conn:
cur = get_cursor(conn)
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
cur.execute(
"""SELECT sleep_segments FROM sleep_log
WHERE profile_id=%s AND date >= %s
ORDER BY date DESC""",
(profile_id, cutoff)
)
rows = cur.fetchall()
if not rows:
return "nicht verfügbar"
total_minutes = 0
for row in rows:
segments = row['sleep_segments']
if segments:
# Sum duration_min from all segments
for seg in segments:
total_minutes += seg.get('duration_min', 0)
if total_minutes == 0:
return "nicht verfügbar"
avg_hours = total_minutes / len(rows) / 60
return f"{avg_hours:.1f}h"
def get_sleep_avg_quality(profile_id: str, days: int = 7) -> str:
"""Calculate average sleep quality (Deep+REM %)."""
with get_db() as conn:
cur = get_cursor(conn)
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
cur.execute(
"""SELECT sleep_segments FROM sleep_log
WHERE profile_id=%s AND date >= %s
ORDER BY date DESC""",
(profile_id, cutoff)
)
rows = cur.fetchall()
if not rows:
return "nicht verfügbar"
total_quality = 0
count = 0
for row in rows:
segments = row['sleep_segments']
if segments:
deep_rem_min = sum(s.get('duration_min', 0) for s in segments if s.get('stage') in ['Deep', 'REM'])
total_min = sum(s.get('duration_min', 0) for s in segments)
if total_min > 0:
quality_pct = (deep_rem_min / total_min) * 100
total_quality += quality_pct
count += 1
if count == 0:
return "nicht verfügbar"
avg_quality = total_quality / count
return f"{avg_quality:.0f}% (Deep+REM)"
def get_rest_days_count(profile_id: str, days: int = 30) -> str:
"""Count rest days in the given period."""
with get_db() as conn:
cur = get_cursor(conn)
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
cur.execute(
"""SELECT COUNT(DISTINCT date) as count FROM rest_days
WHERE profile_id=%s AND date >= %s""",
(profile_id, cutoff)
)
row = cur.fetchone()
count = row['count'] if row else 0
return f"{count} Ruhetage"
def get_vitals_avg_hr(profile_id: str, days: int = 7) -> str:
"""Calculate average resting heart rate."""
with get_db() as conn:
cur = get_cursor(conn)
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
cur.execute(
"""SELECT AVG(resting_hr) as avg FROM vitals_baseline
WHERE profile_id=%s AND date >= %s AND resting_hr IS NOT NULL""",
(profile_id, cutoff)
)
row = cur.fetchone()
if row and row['avg']:
return f"{int(row['avg'])} bpm"
return "nicht verfügbar"
def get_vitals_avg_hrv(profile_id: str, days: int = 7) -> str:
"""Calculate average heart rate variability."""
with get_db() as conn:
cur = get_cursor(conn)
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
cur.execute(
"""SELECT AVG(hrv) as avg FROM vitals_baseline
WHERE profile_id=%s AND date >= %s AND hrv IS NOT NULL""",
(profile_id, cutoff)
)
row = cur.fetchone()
if row and row['avg']:
return f"{int(row['avg'])} ms"
return "nicht verfügbar"
def get_vitals_vo2_max(profile_id: str) -> str:
"""Get latest VO2 Max value."""
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"""SELECT vo2_max FROM vitals_baseline
WHERE profile_id=%s AND vo2_max IS NOT NULL
ORDER BY date DESC LIMIT 1""",
(profile_id,)
)
row = cur.fetchone()
if row and row['vo2_max']:
return f"{row['vo2_max']:.1f} ml/kg/min"
return "nicht verfügbar"
# ── Placeholder Registry ──────────────────────────────────────────────────────
PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = {
@ -323,6 +455,16 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = {
'{{activity_detail}}': get_activity_detail,
'{{trainingstyp_verteilung}}': get_trainingstyp_verteilung,
# Schlaf & Erholung
'{{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),
# Vitalwerte
'{{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,
# Zeitraum
'{{datum_heute}}': lambda pid: datetime.now().strftime('%d.%m.%Y'),
'{{zeitraum_7d}}': lambda pid: 'letzte 7 Tage',