mitai-jinkendo/.claude/docs/technical/V9D_PHASE2_VITALS_SLEEP.md

1258 lines
46 KiB
Markdown
Raw Normal View History

# Technische Spezifikation: v9d Phase 2 – Vitalwerte & Schlaf
**Version:** 1.1
**Status:** Review-Änderungen eingearbeitet
**Datum:** 22.03.2026
**Basis:** TRAINING_TYPES.md + SLEEP_MODULE.md
**Änderungen v1.1:**
- Wochenplanung → DB-persistiert (weekly_goals Tabelle)
- Schlafphasen-Segmente → JSONB-Spalte hinzugefügt (sleep_segments)
- Erholungsstatus-Gewichtung → 50% HRV / 30% Ruhepuls / 20% Training
- Schlafschulden-Zeitraum → 14 Tage (statt 7)
- Ruhepuls-Warnung → beide Baselines (7d + 30d)
- Vitalwerte-Seite → eigener Nav-Eintrag
- Implementierungs-Reihenfolge → Schlaf zuerst (2b → 2c → 2d → 2a → 2e)
---
## 1. Entscheidungen zu offenen Fragen
### 1.1 TRAINING_TYPES.md
**Q1: HRV-Erfassung – eigene Tabelle oder Spalte in vitals_log?**
✅ **Entscheidung:** Spalte in `vitals_log` (nullable)
**Begründung:** HRV und Ruhepuls werden beide morgens gemessen, gehören konzeptionell zusammen. Separate Tabelle würde Joins und Korrelationen komplizieren.
**Q2: Wochenplanung – DB persistieren oder Session-Only?**
✅ **Entscheidung:** DB-persistiert (Tabelle `weekly_goals`)
**Begründung:** localStorage geht bei Browser-Wechsel verloren. Einfaches Soll/Ist-Tracking für Wochenplanung wird in Phase 2 implementiert. Erweiterte Ziele-Features kommen in v9f.
**Q3: HF-Kurven aus Apple Health oder nur Avg/Max?**
✅ **Entscheidung:** Nur Avg/Max für Phase 2
**Begründung:** HF-Kurven (Zeit × HF) benötigen komplexe Zeitserien-Struktur (JSONB oder separate Tabelle). Avg/Max deckt 80% der Use Cases ab. Kurven kommen in v9h (Connectoren).
### 1.2 SLEEP_MODULE.md
**Q4: sleep_log oder Erweiterung von activity_log?**
✅ **Entscheidung:** Separate Tabelle `sleep_log`
**Begründung:** Schlaf ist konzeptionell keine Aktivität. Unterschiedliche Felder (bedtime, wake_time, quality, phases) passen nicht in activity_log Schema. Separate Tabelle ermöglicht saubere Korrelations-Queries.
**Q5: Schlafphasen – separate Spalten oder JSONB?**
✅ **Entscheidung:** Beides – Summen-Spalten + JSONB für Segmente
**Begründung:**
- **Summen-Spalten** (`deep_minutes`, `rem_minutes`, `light_minutes`, `awake_minutes`): Einfach zu querien, performant für Aggregationen
- **JSONB-Spalte** (`sleep_segments`, nullable): Rohdaten der Phasen-Übergänge aus Apple Health Import
- Format: `[{"phase": "deep", "start": "23:44", "duration_min": 42}, ...]`
- Nur bei Import befüllt, bei manueller Eingabe NULL
- Basis für spätere Analyse (Zykluslänge, Unterbrechungsmuster) in v9e/f
**Q6: Morgendlicher Check-in – Frontend oder Backend?**
✅ **Entscheidung:** Frontend-State
**Begründung:** Frontend prüft beim Dashboard-Load ob Schlaf-Eintrag für letzte Nacht existiert (`GET /api/sleep?date={yesterday}`). Kein Backend-State nötig, keine zusätzliche Komplexität.
**Q7: Korrelationsberechnung – Backend oder Frontend?**
✅ **Entscheidung:** Backend (Python)
**Begründung:** Konsistent mit bestehenden Correlation-Endpoints (`/api/nutrition/correlations`). Python ist besser für statistisches Computing (numpy). Frontend rendert nur Ergebnis.
**Q8: Apple Health CSV-Format für Schlaf?**
✅ **Analysiert:** Apple Health Schlaf-Export hat folgendes Format:
```csv
Start,End,Duration (hr),Value,Source
2026-03-14 22:44:23,2026-03-14 23:00:19,0.266,Kern,Apple Watch von Lars
2026-03-14 23:00:19,2026-03-14 23:12:16,0.199,REM,Apple Watch von Lars
2026-03-14 23:12:16,2026-03-14 23:34:40,0.373,Kern,Apple Watch von Lars
2026-03-14 23:34:40,2026-03-14 23:44:38,0.166,Tief,Apple Watch von Lars
```
**Import-Logik:**
- Value-Mapping: `Kern` → light_minutes, `REM` → rem_minutes, `Tief` → deep_minutes, `Wach` → awake_minutes
- `Schlafend` (initial entry) wird ignoriert, nur Phasen werden gezählt
- Aggregation nach Datum: Alle Segmente einer Nacht gruppieren (Einschlafzeit = erster Start, Aufwachzeit = letztes End)
- Duration: Summe aller Phasen in Minuten (`Duration (hr) * 60`)
- Datum-Zuordnung: Wenn Schlaf über Mitternacht geht → Datum der Aufwachzeit verwenden
- **Segmente:** Raw-Daten werden zusätzlich in `sleep_segments` JSONB gespeichert für spätere Analyse
- Qualität: Bei Import nicht verfügbar (NULL), User kann nachträglich setzen
---
## 2. Datenbankschema
### 2.1 Neue Tabellen
#### `rest_days`
```sql
CREATE TABLE rest_days (
id SERIAL PRIMARY KEY,
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
date DATE NOT NULL,
rest_config JSONB NOT NULL,
note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_rest_day_per_profile UNIQUE(profile_id, date)
);
CREATE INDEX idx_rest_days_profile_date ON rest_days(profile_id, date DESC);
```
**JSONB Schema (`rest_config`):**
```typescript
interface RestConfig {
focus: 'muscle_recovery' | 'cardio_recovery' | 'mental_rest' | 'deload' | 'injury'
rest_from: string[] // Training type IDs (z.B. ['strength', 'hiit'])
allows: string[] // Erlaubte Aktivitäten (z.B. ['cardio_low', 'meditation'])
intensity_max?: number // Max HF% (1-100), optional
note?: string // Zusätzliche Info, optional
}
```
**Beispiele:**
```json
// Krafttraining-Ruhetag
{
"focus": "muscle_recovery",
"rest_from": ["strength", "hiit"],
"allows": ["cardio_low", "meditation", "mobility"],
"intensity_max": 60
}
// Kompletter Entspannungstag
{
"focus": "mental_rest",
"rest_from": ["strength", "cardio", "hiit", "power"],
"allows": ["meditation", "walk"],
"intensity_max": 50
}
// Deload-Woche
{
"focus": "deload",
"rest_from": [],
"allows": ["strength", "cardio", "mobility"],
"intensity_max": 70,
"note": "Deload Week 4 - 70% Volumen"
}
```
**Erklärung:**
- **Flexibles Modell** statt binärem `full_rest`/`active_recovery`
- **Kontext-spezifische Ruhetage**: Ruhe von Kraft, aber Cardio erlaubt
- **Integration mit Wochenplanung**: Regelbasierte Validierung möglich
- **Intensitätsbegrenzung**: Max HF% für erlaubte Aktivitäten
- UNIQUE Constraint verhindert Duplikate pro Tag
#### `vitals_log`
```sql
CREATE TABLE vitals_log (
id SERIAL PRIMARY KEY,
profile_id VARCHAR(36) NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
date DATE NOT NULL,
resting_hr INTEGER CHECK (resting_hr > 0 AND resting_hr < 200),
hrv INTEGER CHECK (hrv > 0), -- nullable, in Millisekunden
note TEXT,
source VARCHAR(20) DEFAULT 'manual' CHECK (source IN ('manual', 'apple_health', 'garmin')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_vitals_per_day UNIQUE(profile_id, date)
);
CREATE INDEX idx_vitals_profile_date ON vitals_log(profile_id, date DESC);
```
**Erklärung:**
- `resting_hr` = Ruhepuls in bpm (Schläge pro Minute)
- `hrv` = Herzratenvariabilität in ms (optional, nicht alle Geräte messen das)
- Upsert-Logik: Bei Reimport überschreiben wenn source = 'manual' Vorrang hat
#### `weekly_goals`
```sql
CREATE TABLE weekly_goals (
id SERIAL PRIMARY KEY,
profile_id VARCHAR(36) NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
week_start DATE NOT NULL, -- Montag der Woche (ISO Week)
goals JSONB NOT NULL, -- {"strength": 3, "cardio": 2, "mobility": 1, "rest": 1}
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_weekly_goal_per_profile UNIQUE(profile_id, week_start)
);
CREATE INDEX idx_weekly_goals_profile_week ON weekly_goals(profile_id, week_start DESC);
```
**Erklärung:**
- `week_start` = Montag der Woche (z.B. 2026-03-17 für KW 12)
- `goals` = JSONB mit Soll-Werten pro Trainingstyp-Kategorie
- Beispiel: `{"strength": 3, "cardio": 2, "mobility": 1, "rest": 1}` = 3x Kraft, 2x Cardio, 1x Mobility, 1 Ruhetag
- Flexible Struktur, kann um weitere Kategorien erweitert werden
- Dashboard zeigt Soll/Ist-Vergleich für aktuelle Woche
#### `sleep_log`
```sql
CREATE TABLE sleep_log (
id SERIAL PRIMARY KEY,
profile_id VARCHAR(36) NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
date DATE NOT NULL, -- Datum der Nacht (= Aufwachdatum)
bedtime TIME, -- Einschlafzeit (nullable, z.B. 23:15)
wake_time TIME, -- Aufwachzeit (nullable, z.B. 06:45)
duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0), -- Gesamtschlafdauer
quality INTEGER CHECK (quality >= 1 AND quality <= 5), -- nullable, subjektiv 1-5
wake_count INTEGER CHECK (wake_count >= 0), -- nullable, wie oft aufgewacht
deep_minutes INTEGER CHECK (deep_minutes >= 0), -- Tiefschlaf
rem_minutes INTEGER CHECK (rem_minutes >= 0), -- REM-Schlaf
light_minutes INTEGER CHECK (light_minutes >= 0), -- Leichtschlaf (Kern)
awake_minutes INTEGER CHECK (awake_minutes >= 0), -- Wachphasen im Bett
sleep_segments JSONB, -- Rohdaten: [{"phase": "deep", "start": "23:44", "duration_min": 42}, ...]
note TEXT,
source VARCHAR(20) DEFAULT 'manual' CHECK (source IN ('manual', 'apple_health', 'garmin')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_sleep_per_day UNIQUE(profile_id, date)
);
CREATE INDEX idx_sleep_profile_date ON sleep_log(profile_id, date DESC);
```
**Erklärung:**
- `date` = Datum der Nacht (wichtig: Aufwachdatum, nicht Einschlafdatum!)
- `bedtime` + `wake_time` = nullable, nur wenn bekannt
- `duration_minutes` = Pflichtfeld, kann aus bedtime/wake_time berechnet oder direkt eingegeben werden
- Phasen-Summen (deep/rem/light/awake_minutes) = nullable (nur wenn Smartwatch-Daten vorhanden)
- `sleep_segments` = JSONB nullable, enthält Rohdaten der Phasen-Übergänge aus Import
- Format: `[{"phase": "deep|rem|light|awake", "start": "HH:MM", "duration_min": N}, ...]`
- Nur bei Import befüllt, bei manueller Eingabe NULL
- Basis für spätere Zyklen-Analyse (v9e/f)
### 2.2 Erweiterte Tabellen
#### `activity_log` – Neue Spalten für Herzfrequenz
```sql
ALTER TABLE activity_log
ADD COLUMN avg_hr INTEGER CHECK (avg_hr > 0 AND avg_hr < 250),
ADD COLUMN max_hr INTEGER CHECK (max_hr > 0 AND max_hr < 250);
```
**Erklärung:**
- `avg_hr` = Durchschnittliche Herzfrequenz während Training (bpm)
- `max_hr` = Maximale Herzfrequenz während Training (bpm)
- Beide nullable (manuelle Einträge haben oft keine HF-Daten)
#### `profiles` – HF-Max für Zonen-Berechnung
```sql
ALTER TABLE profiles
ADD COLUMN hf_max INTEGER CHECK (hf_max > 0 AND hf_max < 250),
ADD COLUMN sleep_goal_minutes INTEGER DEFAULT 450 CHECK (sleep_goal_minutes > 0); -- 7h 30min
```
**Erklärung:**
- `hf_max` = Maximale Herzfrequenz (nullable, Standard: 220 - Alter)
- `sleep_goal_minutes` = Schlafziel in Minuten (Standard: 450 = 7h 30min)
---
## 3. Migrationen
### Migration 008: Vitalwerte & Ruhetage (initial)
**Datei:** `backend/migrations/008_vitals_rest_days.sql`
**Status:** Deployed to production (mit einfachem type-Schema)
```sql
-- Rest Days (initial simple schema)
CREATE TABLE IF NOT EXISTS rest_days (
id SERIAL PRIMARY KEY,
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
date DATE NOT NULL,
type VARCHAR(20) NOT NULL CHECK (type IN ('full_rest', 'active_recovery')),
note TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_rest_day_per_profile UNIQUE(profile_id, date)
);
CREATE INDEX idx_rest_days_profile_date ON rest_days(profile_id, date DESC);
```
### Migration 010: Rest Days Refactoring zu JSONB
**Datei:** `backend/migrations/010_rest_days_jsonb.sql`
**Status:** In Entwicklung
```sql
-- Refactor rest_days to JSONB config for flexible rest day types
ALTER TABLE rest_days
DROP COLUMN IF EXISTS type;
ALTER TABLE rest_days
ADD COLUMN rest_config JSONB NOT NULL DEFAULT '{}'::jsonb;
-- Validation function for rest_config
CREATE OR REPLACE FUNCTION validate_rest_config(config JSONB) RETURNS BOOLEAN AS $$
BEGIN
-- Must have focus field
IF NOT (config ? 'focus') THEN
RETURN FALSE;
END IF;
-- rest_from must be array if present
IF (config ? 'rest_from') AND jsonb_typeof(config->'rest_from') != 'array' THEN
RETURN FALSE;
END IF;
-- allows must be array if present
IF (config ? 'allows') AND jsonb_typeof(config->'allows') != 'array' THEN
RETURN FALSE;
END IF;
RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
-- Add check constraint
ALTER TABLE rest_days
ADD CONSTRAINT valid_rest_config CHECK (validate_rest_config(rest_config));
COMMENT ON COLUMN rest_days.rest_config IS 'JSONB: {focus, rest_from[], allows[], intensity_max, note}';
```
-- Vitals (Resting HR + HRV)
CREATE TABLE IF NOT EXISTS vitals_log (
id SERIAL PRIMARY KEY,
profile_id VARCHAR(36) NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
date DATE NOT NULL,
resting_hr INTEGER CHECK (resting_hr > 0 AND resting_hr < 200),
hrv INTEGER CHECK (hrv > 0),
note TEXT,
source VARCHAR(20) DEFAULT 'manual' CHECK (source IN ('manual', 'apple_health', 'garmin')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_vitals_per_day UNIQUE(profile_id, date)
);
CREATE INDEX idx_vitals_profile_date ON vitals_log(profile_id, date DESC);
-- Extend activity_log for heart rate data
ALTER TABLE activity_log
ADD COLUMN IF NOT EXISTS avg_hr INTEGER CHECK (avg_hr > 0 AND avg_hr < 250),
ADD COLUMN IF NOT EXISTS max_hr INTEGER CHECK (max_hr > 0 AND max_hr < 250);
-- Extend profiles for HF max and sleep goal
ALTER TABLE profiles
ADD COLUMN IF NOT EXISTS hf_max INTEGER CHECK (hf_max > 0 AND hf_max < 250),
ADD COLUMN IF NOT EXISTS sleep_goal_minutes INTEGER DEFAULT 450 CHECK (sleep_goal_minutes > 0);
-- Weekly Goals (Soll/Ist Wochenplanung)
CREATE TABLE IF NOT EXISTS weekly_goals (
id SERIAL PRIMARY KEY,
profile_id VARCHAR(36) NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
week_start DATE NOT NULL,
goals JSONB NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_weekly_goal_per_profile UNIQUE(profile_id, week_start)
);
CREATE INDEX idx_weekly_goals_profile_week ON weekly_goals(profile_id, week_start DESC);
```
### Migration 009: Schlaf-Modul
**Datei:** `backend/migrations/009_sleep_log.sql`
```sql
CREATE TABLE IF NOT EXISTS sleep_log (
id SERIAL PRIMARY KEY,
profile_id VARCHAR(36) NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
date DATE NOT NULL,
bedtime TIME,
wake_time TIME,
duration_minutes INTEGER NOT NULL CHECK (duration_minutes > 0),
quality INTEGER CHECK (quality >= 1 AND quality <= 5),
wake_count INTEGER CHECK (wake_count >= 0),
deep_minutes INTEGER CHECK (deep_minutes >= 0),
rem_minutes INTEGER CHECK (rem_minutes >= 0),
light_minutes INTEGER CHECK (light_minutes >= 0),
awake_minutes INTEGER CHECK (awake_minutes >= 0),
sleep_segments JSONB, -- Rohdaten: [{"phase": "deep", "start": "23:44", "duration_min": 42}, ...]
note TEXT,
source VARCHAR(20) DEFAULT 'manual' CHECK (source IN ('manual', 'apple_health', 'garmin')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT unique_sleep_per_day UNIQUE(profile_id, date)
);
CREATE INDEX idx_sleep_profile_date ON sleep_log(profile_id, date DESC);
```
---
## 4. Backend API-Endpoints
### 4.1 Rest Days (`routers/rest_days.py`)
```python
router = APIRouter(prefix="/api/rest-days", tags=["rest-days"])
# CRUD
GET /api/rest-days?limit=90 # List rest days (last 90 days)
POST /api/rest-days # Create rest day (with config validation)
PUT /api/rest-days/{id} # Update rest day
DELETE /api/rest-days/{id} # Delete rest day
# Stats & Validation
GET /api/rest-days/stats # Count per week, last 4 weeks
POST /api/rest-days/validate-activity # Check if activity conflicts with rest day
```
**Request/Response Models:**
```python
class RestConfig(BaseModel):
focus: Literal['muscle_recovery', 'cardio_recovery', 'mental_rest', 'deload', 'injury']
rest_from: list[str] # Training type IDs
allows: list[str] # Allowed activity type IDs
intensity_max: int | None = None # Max HF% (1-100)
note: str = ""
class RestDayCreate(BaseModel):
date: str # YYYY-MM-DD
rest_config: RestConfig
note: str = ""
class RestDayResponse(BaseModel):
id: int
profile_id: str
date: str
rest_config: dict # JSONB as dict
note: str
created_at: str
class ActivityConflictResponse(BaseModel):
conflict: bool
severity: Literal['warning', 'info', 'none']
message: str
```
**Validation Logic:**
```python
def check_rest_day_conflict(profile_id: str, date: str, activity_type: str) -> ActivityConflictResponse:
"""
Check if activity conflicts with rest day configuration.
Returns:
- conflict=True, severity='warning': Activity is in rest_from
- conflict=True, severity='info': Activity not in allows list
- conflict=False: No conflict
"""
rest_day = get_rest_day(profile_id, date)
if not rest_day:
return {"conflict": False, "severity": "none", "message": ""}
config = rest_day["rest_config"]
# Check if activity is in rest_from
if activity_type in config.get("rest_from", []):
return {
"conflict": True,
"severity": "warning",
"message": f"Ruhetag für {activity_type}. Trotzdem erfassen?"
}
# Check if activity is allowed
if config.get("allows") and activity_type not in config.get("allows", []):
return {
"conflict": True,
"severity": "info",
"message": "Aktivität nicht in erlaubten Aktivitäten."
}
return {"conflict": False, "severity": "none", "message": ""}
```
### 4.2 Vitals (`routers/vitals.py`)
```python
router = APIRouter(prefix="/api/vitals", tags=["vitals"])
# CRUD
GET /api/vitals?limit=90 # List vitals (last 90 days)
POST /api/vitals # Upsert (by date)
PUT /api/vitals/{id} # Update
DELETE /api/vitals/{id} # Delete
GET /api/vitals/by-date/{date} # Get entry for specific date
# Stats & Trends
GET /api/vitals/trend?days=30 # Resting HR trend (avg, baseline, warnings)
GET /api/vitals/hrv-baseline?days=30 # HRV baseline + deviation
```
**Request/Response Models:**
```python
class VitalsCreate(BaseModel):
date: str # YYYY-MM-DD
resting_hr: int
hrv: int | None = None
note: str = ""
source: Literal['manual', 'apple_health', 'garmin'] = 'manual'
class VitalsResponse(BaseModel):
id: int
profile_id: str
date: str
resting_hr: int
hrv: int | None
note: str
source: str
created_at: str
updated_at: str
class VitalsTrendResponse(BaseModel):
current_hr: int | None # letzter Eintrag
avg_hr_7d: float # 7-Tage-Durchschnitt
baseline_hr_30d: float # 30-Tage-Baseline
is_elevated_7d: bool # >+5 bpm über 7d-Baseline (akut)
is_elevated_30d: bool # >+5 bpm über 30d-Baseline (Trend)
warning_7d: str | None # "Ruhepuls heute erhöht – Ruhetag?"
warning_30d: str | None # "Ruhepuls im Aufwärtstrend – Übertraining?"
```
### 4.3 Sleep (`routers/sleep.py`)
```python
router = APIRouter(prefix="/api/sleep", tags=["sleep"])
# CRUD
GET /api/sleep?limit=90 # List sleep entries
POST /api/sleep # Create/Upsert sleep entry
PUT /api/sleep/{id} # Update
DELETE /api/sleep/{id} # Delete
GET /api/sleep/by-date/{date} # Get entry for specific date
# Import
POST /api/sleep/import-csv # Apple Health sleep CSV import
# Stats & Trends
GET /api/sleep/stats?days=7 # 7-day average (duration, quality)
GET /api/sleep/trend?days=30 # Duration + quality trend
GET /api/sleep/phases?days=30 # Phase distribution (deep, REM, light, awake)
GET /api/sleep/debt?days=14 # Sleep debt calculation (14 days default)
GET /api/sleep/optimal-window # Optimal bedtime window (min 14 entries)
# Correlations
GET /api/sleep/correlations/resting-hr # Sleep ↔ Resting HR correlation
GET /api/sleep/correlations/training # Sleep ↔ Training performance
GET /api/sleep/correlations/weight # Sleep ↔ Weight (weekly)
```
**Request/Response Models:**
```python
class SleepCreate(BaseModel):
date: str # YYYY-MM-DD
bedtime: str | None = None # HH:MM
wake_time: str | None = None # HH:MM
duration_minutes: int
quality: int | None = None # 1-5
wake_count: int | None = None
deep_minutes: int | None = None
rem_minutes: int | None = None
light_minutes: int | None = None
awake_minutes: int | None = None
note: str = ""
source: Literal['manual', 'apple_health', 'garmin'] = 'manual'
class SleepResponse(BaseModel):
id: int
profile_id: str
date: str
bedtime: str | None
wake_time: str | None
duration_minutes: int
duration_formatted: str # "7h 30min"
quality: int | None
wake_count: int | None
deep_minutes: int | None
rem_minutes: int | None
light_minutes: int | None
awake_minutes: int | None
sleep_segments: list[dict] | None # [{"phase": "deep", "start": "23:44", "duration_min": 42}, ...]
sleep_efficiency: float | None # calculated if phases present
deep_percent: float | None
rem_percent: float | None
note: str
source: str
created_at: str
class SleepStatsResponse(BaseModel):
avg_duration_minutes: float
avg_quality: float | None
total_nights: int
nights_below_goal: int
sleep_goal_minutes: int # from profile
class SleepDebtResponse(BaseModel):
sleep_debt_minutes: int # positive = deficit, negative = surplus
sleep_debt_formatted: str # "+2h 15min" or "0 – kein Defizit"
days_analyzed: int
sleep_goal_minutes: int
```
**CSV Import Logic (`/api/sleep/import-csv`):**
```python
def import_sleep_csv(file: UploadFile, profile_id: str):
"""
Import Apple Health sleep CSV with phases.
CSV Format:
Start,End,Duration (hr),Value,Source
2026-03-14 22:44:23,2026-03-14 23:00:19,0.266,Kern,Apple Watch
Logic:
1. Group by date (use wake_time date as date key)
2. Sum durations per phase type
3. Map German labels: Kern→light, REM→rem, Tief→deep, Wach→awake
4. Ignore "Schlafend" entries (initial marker, no phase info)
5. Calculate total duration_minutes
6. bedtime = min(Start), wake_time = max(End)
7. Build sleep_segments JSONB: [{"phase": "deep", "start": "23:44", "duration_min": 42}, ...]
8. Upsert by (profile_id, date)
"""
phases_map = {
'Kern': 'light',
'REM': 'rem',
'Tief': 'deep',
'Wach': 'awake',
'Schlafend': None # ignore
}
# Parse CSV, group by date
# For each segment (not "Schlafend"):
# - Extract start time (HH:MM from Start timestamp)
# - Calculate duration_min (Duration (hr) * 60)
# - Map phase using phases_map
# - Append to segments list: {"phase": phase, "start": start_time, "duration_min": duration}
#
# Sum durations by phase for aggregated columns (deep_minutes, etc.)
# Store segments array in sleep_segments JSONB column
#
# Upsert:
# INSERT ... (sleep_segments = json_segments)
# ON CONFLICT (profile_id, date) DO UPDATE ...
return {"imported": count, "updated": updated_count}
```
### 4.4 Activity Extension (HF Zones)
**Extend:** `routers/activity.py`
```python
# New endpoint
GET /api/activity/hr-zones?days=30 # HF zones distribution
class ActivityCreateExtended(BaseModel):
# ... existing fields
avg_hr: int | None = None
max_hr: int | None = None
def calculate_hr_zone(avg_hr: int, hf_max: int) -> int:
"""
Calculate HR zone (1-5) based on avg_hr and hf_max.
Zones:
1: 50-60% HFmax (Regeneration)
2: 60-70% HFmax (Grundlagenausdauer)
3: 70-80% HFmax (Aerobe Ausdauer)
4: 80-90% HFmax (Anaerobe Schwelle)
5: 90-100% HFmax (Maximale Intensität)
"""
percent = (avg_hr / hf_max) * 100
if percent < 60: return 1
elif percent < 70: return 2
elif percent < 80: return 3
elif percent < 90: return 4
else: return 5
class HRZonesResponse(BaseModel):
zone_1_count: int
zone_2_count: int
zone_3_count: int
zone_4_count: int
zone_5_count: int
total_activities: int
hf_max_used: int # which hf_max was used for calculation
```
### 4.5 Recovery Status
**New endpoint in:** `routers/stats.py`
```python
GET /api/stats/recovery-status # Recovery status (traffic light)
class RecoveryStatusResponse(BaseModel):
status: Literal['good', 'partial', 'poor'] # 🟢 🟡 🔴
color: str # '#1D9E75', '#E67E22', '#D85A30'
recommendation: str # "Intensives Training möglich" / "Leicht" / "Ruhetag"
factors: dict # breakdown: hrv_status, resting_hr_status, training_load
def calculate_recovery_status(profile_id: str) -> RecoveryStatusResponse:
"""
Calculate recovery status based on:
1. HRV deviation from baseline (30d) - weight: 50%
2. Resting HR trend (7d vs 30d baseline) - weight: 30%
3. Training load last 3 days - weight: 20%
Logic:
- HRV < 10% below baseline → poor
- Resting HR > +5 bpm above baseline → poor
- >3 intense trainings in last 3 days → poor
- Weighted score calculation:
score = (hrv_score * 0.5) + (hr_score * 0.3) + (training_score * 0.2)
- score >= 0.7 → good (🟢)
- score >= 0.4 → partial (🟡)
- score < 0.4 → poor (🔴)
"""
```
---
## 5. Frontend-Komponenten
### 5.1 Navigation
**Erweitere:** `frontend/src/App.jsx`
```javascript
// Add to nav links
const links = [
{ to: '/', icon: <LayoutDashboard size={20}/>, label: 'Übersicht' },
{ to: '/capture', icon: <PlusSquare size={20}/>, label: 'Erfassen' },
{ to: '/history', icon: <TrendingUp size={20}/>, label: 'Verlauf' },
{ to: '/sleep', icon: <Moon size={20}/>, label: 'Schlaf' }, // NEU
{ to: '/vitals', icon: <Heart size={20}/>, label: 'Vital' }, // NEU
{ to: '/analysis', icon: <BarChart2 size={20}/>, label: 'Analyse' },
{ to: '/settings', icon: <Settings size={20}/>, label: 'Einst.' },
]
// Add routes
<Route path="/sleep" element={<SleepPage/>}/>
<Route path="/vitals" element={<VitalsPage/>}/>
<Route path="/rest-days" element={<RestDaysPage/>}/>
```
### 5.2 Neue Seiten
#### `frontend/src/pages/SleepPage.jsx`
**Struktur:**
```
┌─────────────────────────────────────┐
│ Schlaf 🌙 │
├─────────────────────────────────────┤
│ [ Letzte Nacht erfassen ] (Button) │
├─────────────────────────────────────┤
│ 📊 Übersicht (letzte 7 Tage) │
│ Ø 7h 12min | Qualität 3.8/5 │
├─────────────────────────────────────┤
│ 📈 Schlafdauer-Trend (Chart) │
│ - Linie: Tägliche Dauer │
│ - Referenzlinie: Schlafziel │
│ - 7-Tage-Durchschnitt │
├─────────────────────────────────────┤
│ 📊 Schlafphasen (Stacked Bar) │
│ - Tief | REM | Leicht | Wach │
├─────────────────────────────────────┤
│ 💤 Schlafschulden │
│ +2h 15min (letzte 7 Tage) │
├─────────────────────────────────────┤
│ 🔗 Korrelationen │
│ - Schlaf ↔ Ruhepuls │
│ - Schlaf ↔ Training │
│ - Schlaf ↔ Gewicht │
└─────────────────────────────────────┘
Modal: Schlaf erfassen
┌─────────────────────────────────────┐
│ Schlaf erfassen │
├─────────────────────────────────────┤
│ Datum: [22.03.2026] │
│ Schlafdauer: [7] h [30] min │
│ Qualität: ★★★★☆ │
│ Notiz: [...] │
├─────────────────────────────────────┤
│ [ Detailansicht ] (Link) │
├─────────────────────────────────────┤
│ [ Speichern ] [ Abbrechen ] │
└─────────────────────────────────────┘
Detailansicht:
│ Eingeschlafen: [23:15] │
│ Aufgewacht: [06:45] │
│ Aufwachungen: [1] │
│ Tiefschlaf: [95] min │
│ REM-Schlaf: [110] min │
│ Leichtschlaf: [230] min │
│ Wach im Bett: [15] min │
```
**Features:**
- Schnelleingabe (3 Felder) vs. Detaileingabe (11 Felder)
- Chart-Zeitraum wählbar: 7 / 30 / 90 Tage
- CSV-Import-Button (Apple Health)
- Inline-Edit bestehender Einträge
#### `frontend/src/pages/VitalsPage.jsx`
**Struktur:**
```
┌─────────────────────────────────────┐
│ Vitalwerte 💓 │
├─────────────────────────────────────┤
│ [ Heute erfassen ] (Button) │
├─────────────────────────────────────┤
│ 📊 Aktuell │
│ Ruhepuls: 58 bpm │
│ HRV: 52 ms │
│ Trend: ✅ Stabil │
├─────────────────────────────────────┤
│ 📈 Ruhepuls-Trend (Chart) │
│ - Linie: Täglicher Wert │
│ - Baseline: 30-Tage-Ø │
│ - Warnzone: >+5 bpm │
├─────────────────────────────────────┤
│ 📊 HRV-Trend (Chart) │
│ - Linie: Täglicher Wert │
│ - Baseline: 30-Tage-Ø │
│ - Warnzone: <-10% │
└─────────────────────────────────────┘
Modal: Vitalwerte erfassen
┌─────────────────────────────────────┐
│ Vitalwerte erfassen │
├─────────────────────────────────────┤
│ Datum: [22.03.2026] │
│ Ruhepuls (bpm): [58] │
│ HRV (ms): [52] (optional) │
│ Notiz: [...] │
├─────────────────────────────────────┤
│ [ Speichern ] [ Abbrechen ] │
└─────────────────────────────────────┘
```
#### `frontend/src/pages/RestDaysPage.jsx`
**Oder Integration in ActivityPage:**
```
┌─────────────────────────────────────┐
│ Aktivitäten & Ruhetage │
├─────────────────────────────────────┤
│ [ Tabs: Training | Ruhetage ] │
├─────────────────────────────────────┤
│ Ruhetag Tab: │
│ [ + Ruhetag erfassen ] │
│ │
│ 22.03.2026 - Vollständige Ruhe │
│ "Muskelkater Beine" │
│ │
│ 20.03.2026 - Aktive Erholung │
│ "Spaziergang" │
└─────────────────────────────────────┘
```
### 5.3 Dashboard-Widgets
**Erweitere:** `frontend/src/pages/Dashboard.jsx`
```javascript
// New widgets
<SleepWidget /> // Letzte Nacht + 7-Tage-Ø
<VitalsWidget /> // Ruhepuls + Trend
<RecoveryWidget /> // 🟢 Gut erholt | Empfehlung
```
**SleepWidget:**
```
┌─────────────────────────────────────┐
│ 🌙 Schlaf │
│ Letzte Nacht: 7h 30min | ★★★★☆ │
│ Ø 7 Tage: 7h 12min | ★★★★☆ │
│ [ Erfassen ] │
└─────────────────────────────────────┘
```
**RecoveryWidget:**
```
┌─────────────────────────────────────┐
│ 🔋 Erholungsstatus │
│ 🟢 Gut erholt │
│ "Intensives Training möglich" │
│ [ Details ] │
└─────────────────────────────────────┘
```
### 5.4 Activity-Liste Erweiterung
**In:** `frontend/src/pages/ActivityPage.jsx`
```javascript
// Add HR zone badge
{e.avg_hr && (
<div style={{
display: 'inline-flex',
alignItems: 'center',
gap: 3,
padding: '2px 6px',
background: getZoneColor(e.hr_zone) + '22',
border: `1px solid ${getZoneColor(e.hr_zone)}`,
borderRadius: 4,
fontSize: 10,
fontWeight: 600,
color: getZoneColor(e.hr_zone)
}}>
<Heart size={10} />
<span>Zone {e.hr_zone}</span>
</div>
)}
function getZoneColor(zone) {
const colors = {
1: '#1D9E75', // Regeneration (grün)
2: '#3498DB', // Grundlage (blau)
3: '#E67E22', // Aerob (orange)
4: '#E74C3C', // Anaerob (rot)
5: '#8E44AD', // Maximal (lila)
}
return colors[zone] || '#95A5A6'
}
```
---
## 6. Implementierungs-Phasen
**Reihenfolge:** 2b → 2c → 2d → 2a → 2e → 2f (Schlaf-Modul zuerst, dann Vitalwerte)
### Phase 2b – Schlaf-Modul Kern (3-4h) ⭐ START HIER
**Ziel:** Schlaf erfassen + Trends sehen
1. ✅ Migration 009 erstellen + deployen (sleep_log + sleep_segments)
2. ✅ Backend Router: `sleep.py` (CRUD + Stats)
3. ✅ Frontend: `SleepPage.jsx` (Schnell + Detail-Eingabe)
4. ✅ Charts: Schlafdauer-Trend, Qualität-Trend
5. ✅ Dashboard: `SleepWidget.jsx`
6. ✅ Navigation: Schlaf-Icon 🌙 hinzufügen
7. ✅ Test: Manuelle Eingabe + Charts funktionieren
**Deliverable:** User kann Schlaf erfassen, sieht 7-Tage-Trends.
### Phase 2c – Apple Health Import (2h)
**Ziel:** Automatischer Schlaf-Import aus CSV
1. ✅ Backend: CSV-Parser für Apple Health Schlaf (inkl. sleep_segments JSONB)
2. ✅ Frontend: Import-Button auf SleepPage
3. ✅ Test: CSV-Import aggregiert Phasen korrekt, speichert Segmente
4. ✅ Validierung: Mehrfach-Import überschreibt nicht manual-Einträge
**Deliverable:** User kann Apple Health Schlaf-CSV importieren, Phasen werden gespeichert.
### Phase 2d – HF-Zonen & Erholung (2-3h)
**Ziel:** HF-Zonen-Badges + Erholungsampel
1. ✅ Migration 008 anwenden (`activity_log` avg_hr/max_hr erweitern)
2. ✅ Backend: HF-Zonen-Berechnung (Endpoint + Badge-Logik)
3. ✅ Backend: Recovery-Status-Berechnung (50% HRV / 30% Ruhepuls / 20% Training)
4. ✅ Frontend: HF-Zonen-Badge in ActivityPage
5. ✅ Frontend: `RecoveryWidget.jsx` im Dashboard
6. ✅ Test: Zonen korrekt berechnet, Erholungsampel funktioniert
**Deliverable:** User sieht HF-Zonen in Aktivitäten, Erholungsstatus im Dashboard.
### Phase 2a – Basis-Vitalwerte (1-2h)
**Ziel:** Ruhetage + Ruhepuls erfassen können
1. ✅ Migration 008 komplett (rest_days, vitals_log, weekly_goals)
2. ✅ Backend Router: `rest_days.py`, `vitals.py`, `weekly_goals.py` (CRUD)
3. ✅ Frontend: `VitalsPage.jsx` (mit 7d + 30d Warnungen)
4. ✅ Frontend: Ruhetage-Tab in ActivityPage integrieren
5. ✅ Dashboard: `VitalsWidget.jsx`
6. ✅ Navigation: Vital-Icon ❤️ hinzufügen
7. ✅ Test: Manuelle Eingabe funktioniert, beide Warnungen angezeigt
**Deliverable:** User kann Ruhepuls + Ruhetage erfassen, sieht Trend-Grafik mit Warnungen.
### Phase 2e – Korrelationen (1-2h)
**Ziel:** Schlaf ↔ Ruhepuls, Training, Gewicht
1. ✅ Backend: Korrelations-Endpoints (`/api/sleep/correlations/*`)
2. ✅ Frontend: Korrelations-Section auf SleepPage
3. ✅ Charts: Streudiagramme + narrative KI-Aussagen
4. ✅ Test: Mindestens 14 Datenpunkte erforderlich
**Deliverable:** User sieht Korrelationen zwischen Schlaf und anderen Metriken.
### Phase 2f – KI-Integration (1-2h, optional)
**Ziel:** Neue KI-Platzhalter + Analyse-Funktionen
1. ✅ Backend: KI-Platzhalter erweitern (21 neue, siehe Abschnitt 7)
2. ✅ Backend: Analyse-Funktionen (Schlafmangel, Übertraining)
3. ✅ Test: KI-Prompts nutzen neue Platzhalter
**Deliverable:** KI-Analyse berücksichtigt Schlaf + Vitalwerte.
---
## 7. KI-Integration (neue Platzhalter)
### 7.1 Training-Platzhalter (aus TRAINING_TYPES.md)
```python
{{trainingstyp_verteilung}} # "60% Kraft, 30% Cardio, 10% Mobility (letzte 4 Wochen)"
{{ruhetage_letzte_woche}} # Anzahl Ruhetage letzte 7 Tage
{{ruhepuls_aktuell}} # heutiger / letzter Ruhepuls
{{ruhepuls_trend}} # "sinkend / stabil / steigend"
{{hrv_aktuell}} # letzter HRV-Wert
{{hrv_baseline}} # persönlicher HRV-Durchschnitt (30 Tage)
{{erholungsstatus}} # "gut / teilweise / schlecht"
{{vo2max}} # aktueller VO2Max-Wert (optional, später)
{{trainingsphase}} # "Aufbau / Erholung / Plateau / unbekannt"
{{hf_zonen_verteilung}} # "Zone 2: 45%, Zone 3: 35%, Zone 4: 20%"
```
### 7.2 Schlaf-Platzhalter (aus SLEEP_MODULE.md)
```python
{{schlaf_avg_dauer}} # "7h 12min (Ø 7 Tage)"
{{schlaf_avg_qualitaet}} # "3,8/5 (Ø 7 Tage)"
{{schlaf_trend_dauer}} # "sinkend / stabil / steigend"
{{schlaf_trend_qualitaet}} # "sinkend / stabil / steigend"
{{schlaf_schulden}} # "+2h 15min (letzte 14 Tage)"
{{schlaf_ziel}} # "7h 30min"
{{schlaf_optimum_fenster}} # "22:00–23:00 Uhr (beste Qualität)"
{{schlaf_tiefschlaf_anteil}} # "18% (Ø letzte 14 Tage)"
{{schlaf_rem_anteil}} # "22% (Ø letzte 14 Tage)"
{{schlaf_aufwachungen}} # "1,2x pro Nacht (Ø 7 Tage)"
{{schlaf_korrelation_puls}} # "Ruhepuls +4 bpm nach <7h Schlaf"
{{schlaf_detail}} # tabellarische Übersicht letzte 7 Nächte
```
### 7.3 KI-Analyse-Funktionen (neu)
**Übertraining erkennen:**
```python
def detect_overtraining(profile_id):
# Check:
# - >5 Krafteinheiten in 7 Tagen ohne Ruhetag
# - Ruhepuls >+7 bpm über Baseline + hohe Trainingsbelastung
# - HRV <-15% unter Baseline
# Return: warning message or None
```
**Schlafmangel warnen:**
```python
def detect_sleep_deprivation(profile_id):
# Check:
# - Schlafdurchschnitt letzte 3 Tage < (Schlafziel - 60min)
# - Schlafschulden >2h (letzte 7 Tage)
# Return: warning + recommendation
```
**Ruhetag empfehlen:**
```python
def recommend_rest_day(profile_id):
# Based on:
# - Erholungsstatus = poor
# - Kein Ruhetag in letzten 5 Tagen
# - Trainingsbelastung hoch
# Return: recommendation or None
```
---
## 8. Testing-Strategie
### 8.1 Unit Tests (Backend)
**Test:** `backend/tests/test_vitals.py`
```python
def test_upsert_vitals_by_date():
# Create entry for 2026-03-22
# Upsert same date with different HR
# Assert: updated, not duplicated
def test_resting_hr_trend():
# Insert 30 days of data
# Call /api/vitals/trend
# Assert: baseline calculated, warning if elevated
```
**Test:** `backend/tests/test_sleep.py`
```python
def test_apple_health_csv_import():
# Import sample CSV (from user)
# Assert: phases aggregated correctly
# Assert: date = wake date, not sleep date
# Assert: German labels mapped
def test_sleep_debt_calculation():
# Insert 7 days with varying durations
# Set sleep_goal_minutes = 450
# Call /api/sleep/debt
# Assert: debt calculated correctly
```
**Test:** `backend/tests/test_recovery_status.py`
```python
def test_recovery_status_calculation():
# Create mock data: low HRV, high resting HR, high training load
# Call /api/stats/recovery-status
# Assert: status = 'poor', recommendation includes "Ruhetag"
```
### 8.2 Integration Tests (Frontend)
**Test:** Manual testing checklist
- [ ] Vitalwerte erfassen → sichtbar in Trend-Chart
- [ ] Ruhetag erfassen → sichtbar in Liste
- [ ] Schlaf erfassen (Schnell) → Dashboard-Widget aktualisiert
- [ ] Schlaf erfassen (Detail) → Phasen-Chart zeigt Daten
- [ ] Apple Health CSV Import → Einträge korrekt aggregiert
- [ ] HF-Zonen-Badge → korrekte Farbe basierend auf avg_hr
- [ ] Erholungsampel → ändert Farbe bei schlechten Werten
- [ ] Korrelationen → Charts zeigen bei >14 Datenpunkten
### 8.3 End-to-End Test
**Scenario:** Kompletter User-Flow
1. User importiert Apple Health Schlaf-CSV (7 Tage)
2. User erfasst Ruhepuls für 7 Tage
3. User trainiert 3x intensiv (avg_hr >80% HFmax)
4. User schläft schlecht (quality 2/5, <6h)
5. Dashboard zeigt Erholungsampel 🔴 "Erholung nötig"
6. KI-Analyse empfiehlt Ruhetag
**Validierung:**
- Schlaf korrekt importiert und aggregiert
- Ruhepuls-Trend zeigt Erhöhung
- HF-Zonen korrekt berechnet
- Erholungsstatus = poor
- KI-Platzhalter befüllt, Empfehlung korrekt
---
## 9. Rollout-Plan
### Development (develop Branch)
1. **Phase 2a:** Vitalwerte-Basis → Test auf dev.mitai.jinkendo.de
2. **Phase 2b:** Schlaf-Modul Kern → Test auf dev
3. **Phase 2c:** Apple Health Import → Test mit echten CSV-Daten
4. **Phase 2d:** HF-Zonen + Erholung → Validierung Berechnungslogik
5. **Phase 2e:** Korrelationen → Test mit Mindestdatenbasis
6. **(Optional) Phase 2f:** KI-Integration
**Validierung:** Jede Phase einzeln testen, Bug-Fixes vor nächster Phase.
### Production (main Branch)
**Merge:** Nach erfolgreicher End-to-End-Validierung auf develop
**Deployment:** Gitea Action deploy-prod.yml → mitai.jinkendo.de
**Post-Deployment:**
- CLAUDE.md aktualisieren: v9d Phase 2 ✅
- Nutzer-Kommunikation: "Neue Features: Schlaf, Vitalwerte, Erholungsstatus"
---
## 10. Offene TODOs (für spätere Versionen)
### Nicht in Phase 2:
- [ ] VO2Max-Schätzung (v9f)
- [ ] Wochenplanung persistieren (v9f, mit Ziele-Modul)
- [ ] HF-Kurven (Zeit × HF) aus Apple Health (v9h)
- [ ] Live-Sync Garmin/Apple Watch (v9h, Connectoren)
- [ ] Push-Notifications "Schlafenszeit" (optional)
- [ ] Schlaf-Coaching / Tipps (optional)
- [ ] SpO2 / Schnarchen (v9e, Vitalwerte erweitert)
### Edge Cases / Nice-to-haves:
- [ ] Schlaf-Import: Mittagsschlaf (Naps) erkennen und separieren
- [ ] Ruhepuls: Automatische Benachrichtigung bei 3+ Tagen Erhöhung
- [ ] HRV: Baseline-Berechnung nach Alter/Geschlecht normiert
- [ ] Schlafphasen: Referenzwerte nach Altersgruppe
---
## 11. Anhang
### 11.1 Apple Health CSV-Struktur (Schlaf)
**Beispiel:** Siehe User-Bereitstellung oben
**Mapping:**
```
Value (Deutsch) → DB-Spalte
Kern → light_minutes
REM → rem_minutes
Tief → deep_minutes
Wach → awake_minutes
Schlafend → (ignorieren)
```
**Aggregations-Logik:**
- Eine Nacht = alle Einträge zwischen erstem "Schlafend"/Start und letztem End derselben Schlafperiode
- Datum = Aufwachdatum (End des letzten Segments)
- Wenn Nacht über 2 Tage geht (z.B. 14.03. 22:00 bis 15.03. 06:00) → Datum = 15.03.
### 11.2 Berechnungsformeln
**Schlafeffizienz:**
```
efficiency = (duration_minutes / (duration_minutes + awake_minutes)) * 100
Nur berechnen wenn awake_minutes vorhanden.
```
**Tiefschlaf-Anteil:**
```
deep_percent = (deep_minutes / duration_minutes) * 100
```
**REM-Anteil:**
```
rem_percent = (rem_minutes / duration_minutes) * 100
```
**Schlafschulden:**
```
sleep_debt = sum(sleep_goal - actual_duration for each day in last 14 days)
Positiv = Defizit, Negativ = Überschuss
Zeitraum: 14 Tage (weniger anfällig für Ausreißer als 7 Tage)
```
**HFmax Standard:**
```
hf_max = 220 - age
Überschreibbar durch profiles.hf_max
```
**HF-Zone:**
```
zone = calculate_hr_zone(avg_hr, hf_max)
Siehe Abschnitt 4.4
```
**VO2Max-Schätzung (optional, später):**
```
vo2max ≈ 15 × (hf_max / resting_hr)
Cooper-Formel, grobe Schätzung
```
---
## 12. Datenbankgröße & Performance
**Schätzung:**
- `rest_days`: ~50 Einträge/Jahr/User → ~200 Bytes/Eintrag → 10 KB/Jahr
- `vitals_log`: ~250 Einträge/Jahr/User → ~200 Bytes/Eintrag → 50 KB/Jahr
- `sleep_log`: ~300 Einträge/Jahr/User → ~400 Bytes/Eintrag → 120 KB/Jahr
**Total Phase 2:** ~180 KB/Jahr/User
**Performance:**
- Indizes auf `(profile_id, date DESC)` → schnelle Bereichsabfragen
- Korrelations-Queries: JOIN über date → <100ms bei <5000 Einträgen
- Trend-Berechnung: AVG() über 7-30 Tage → <50ms
**Empfehlung:** Keine zusätzlichen Optimierungen nötig für <10.000 User.
---
**Ende der Spezifikation**
**Nächste Schritte:**
1. Spec-Review mit User
2. Start Phase 2a Implementierung
3. Iteratives Deployment (develop → test → main)