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