mitai-jinkendo/.claude/docs/technical/V9D_PHASE2_VITALS_SLEEP.md
Lars 7940dc7560 docs: Struktur .claude/docs versionieren, working/, Gitea-Index, Regeln
- .gitignore: .claude/docs, rules, commands tracken; settings.local weiter ignorieren
- DOCUMENTATION.md: verbindliche Ablage functional/technical/working/issues
- .claude/README.md: Agent-Einstieg; GITEA_ISSUES_INDEX aus MCP (Stand 2026-04-08)
- Arbeitspapiere von docs/ nach .claude/docs/working/ verschoben
- docs/MEMBERSHIP_SYSTEM.md als Stub; kanonisch technical/MEMBERSHIP_SYSTEM.md
- CLAUDE.md Pflichtlektüre und Links angepasst; docs/README.md vereinfacht

Made-with: Cursor
2026-04-08 13:01:49 +02:00

1258 lines
46 KiB
Markdown
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.

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