mitai-jinkendo/backend/data_layer/vital_signs_assessment.py
Lars 8cb5ad992f
All checks were successful
Deploy Development / deploy (push) Successful in 1m1s
Build Test / pytest-backend (push) Successful in 4s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 18s
feat: enhance recovery dashboard with vital signs analytics and visualization improvements
- Updated the `build_vital_signs_matrix_chart_payload` function to accept optional keys for omitting specific snapshot data, improving flexibility in data presentation.
- Enhanced the `build_recovery_dashboard_kpi_tiles` function to conditionally merge heart and autonomic tiles based on new parameters, refining the dashboard's insights.
- Integrated new analytics features in the `RecoveryDashboardOverview` component, including consolidated paragraphs for better narrative context and visual representation of trends.
- Improved the handling of vital signs data in the frontend, ensuring clearer messaging and enhanced user experience when displaying vital metrics.
2026-04-20 10:29:43 +02:00

157 lines
5.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Orientierende Zonen-Einschätzungen für Vitalwerte (Layer 1, Issue 53).
Keine Diagnose — typische Referenzbereiche für UI/Coaching.
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Set
from data_layer.utils import safe_float
Tone = str # good | warn | bad | neutral
def _item(
key: str,
label_de: str,
value_display: str,
tone: Tone,
zone_label_de: str,
hint_de: str,
sort_order: int,
) -> Dict[str, Any]:
return {
"key": key,
"label_de": label_de,
"value_display": value_display,
"tone": tone,
"zone_label_de": zone_label_de,
"hint_de": hint_de,
"sort_order": sort_order,
}
def assess_resting_hr(bpm: float) -> tuple:
if bpm < 50:
return (
"warn",
"Niedrig",
"Unter 50 bpm kann bei Sportlern normal sein — sonst ärztlich klären, wenn neu oder mit Beschwerden.",
)
if bpm < 60:
return ("good", "Günstig / athletisch", "Häufig bei gut trainierten Personen im unteren Normbereich.")
if bpm <= 100:
return ("good", "Im üblichen Normbereich", "Typischer Ruhepuls bei Erwachsenen oft ca. 60100 bpm.")
if bpm <= 110:
return ("warn", "Leicht erhöht", "Kann durch Stress, Krankheit, Koffein oder Untrainiertheit erhöht sein — Verlauf beobachten.")
return ("bad", "Deutlich erhöht", "Bei anhaltend hohem Ruhepuls medizinische Abklärung sinnvoll.")
def assess_hrv_ms(ms: float) -> tuple:
_ = ms
return (
"neutral",
"Individuell",
"HRV (ms) ist sehr personenabhängig; Aussagekraft vor allem im Vergleich zu deiner eigenen Basis/Trend.",
)
def assess_blood_pressure(systolic: float, diastolic: float) -> tuple:
sys_, dia = systolic, diastolic
if sys_ >= 180 or dia >= 110:
return ("bad", "Sehr hoch", "Sehr hohe Werte — bei Beschwerden oder neu aufgetreten ärztlich zeitnah abklären.")
if sys_ >= 140 or dia >= 90:
return (
"bad",
"Erhöht",
"Liegt in einem Bereich, der oft als Hypertonie eingestuft wird — Bestätigung und Beratung durch ärztliche Messung.",
)
if sys_ >= 130 or dia >= 85:
return ("warn", "Hochnormal", "Oberer Normal-/hochnormaler Bereich — Lebensstil und Verlauf beachten.")
if sys_ < 120 and dia < 80:
return ("good", "Optimal", "Liegt in einem oft als günstig beschriebenen Bereich (<120/80 mmHg).")
return ("good", "Normal", "Im gängigen Zielbereich für viele Erwachsene.")
def assess_spo2(pct: float) -> tuple:
if pct >= 97:
return ("good", "Günstig", "Sauerstoffsättigung im üblichen Zielbereich.")
if pct >= 95:
return ("good", "Unauffällig", "Häufig noch als normal eingestuft; Verlauf bei Atembeschwerden beobachten.")
if pct >= 90:
return ("warn", "Leicht vermindert", "Unter 95 % kann je nach Kontext relevant sein — bei Symptomen abklären.")
return ("bad", "Niedrig", "Niedrige SpO2 — bei anhaltend unter 90 % oder Beschwerden ärztlich vorstellen.")
def assess_respiratory_rate(rpm: float) -> tuple:
if 12 <= rpm <= 20:
return ("good", "Im üblichen Bereich", "Ruheatmung oft ca. 1220/min.")
if 10 <= rpm < 12 or 20 < rpm <= 24:
return ("warn", "Grenzbereich", "Leicht außerhalb des häufig zitierten Ruhebereichs — Kontext (Belastung, Stress) beachten.")
return ("bad", "Auffällig", "Deutlich außerhalb typischer Ruhewerte — bei Beschwerden medizinisch abklären.")
def assess_vo2_max(value: float) -> tuple:
_ = value
return (
"neutral",
"Orientativ",
"VO2max hängt stark von Alter, Geschlecht und Messmethode ab; Trends in der App sind aussagekräftiger als Einzelwerte.",
)
def build_vital_items_from_rows(
vitals_row: Optional[Dict[str, Any]],
bp_row: Optional[Dict[str, Any]],
omit_keys: Optional[Set[str]] = None,
) -> List[Dict[str, Any]]:
"""omit_keys: z. B. {'resting_hr','hrv'} wenn Einordnung zentral im Herz-/Autonomie-Block steht."""
skip = omit_keys or set()
items: List[Dict[str, Any]] = []
order = 0
if vitals_row:
rhr = vitals_row.get("resting_hr")
if rhr is not None and "resting_hr" not in skip:
v = safe_float(rhr)
t, z, h = assess_resting_hr(v)
items.append(_item("resting_hr", "Ruhepuls", f"{v:.0f} bpm", t, z, h, order))
order += 1
hrv = vitals_row.get("hrv")
if hrv is not None and "hrv" not in skip:
v = safe_float(hrv)
t, z, h = assess_hrv_ms(v)
items.append(_item("hrv", "HRV", f"{v:.0f} ms", t, z, h, order))
order += 1
vo2 = vitals_row.get("vo2_max")
if vo2 is not None:
v = safe_float(vo2)
t, z, h = assess_vo2_max(v)
items.append(_item("vo2_max", "VO2max", f"{v:.1f} ml/kg/min", t, z, h, order))
order += 1
spo2 = vitals_row.get("spo2")
if spo2 is not None:
v = safe_float(spo2)
t, z, h = assess_spo2(v)
items.append(_item("spo2", "SpO2", f"{v:.0f} %", t, z, h, order))
order += 1
rr = vitals_row.get("respiratory_rate")
if rr is not None:
v = safe_float(rr)
t, z, h = assess_respiratory_rate(v)
items.append(_item("respiratory_rate", "Atemfrequenz", f"{v:.0f} /min", t, z, h, order))
order += 1
if bp_row and bp_row.get("systolic") is not None and bp_row.get("diastolic") is not None:
sys_v = safe_float(bp_row["systolic"])
dia_v = safe_float(bp_row["diastolic"])
t, z, h = assess_blood_pressure(sys_v, dia_v)
items.append(_item("blood_pressure", "Blutdruck", f"{sys_v:.0f}/{dia_v:.0f} mmHg", t, z, h, order))
return items