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

46 KiB
Raw Permalink Blame 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:

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

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

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:

// 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

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

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

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

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

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)

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

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

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:

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:

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)

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:

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)

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:

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

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

# 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

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

// 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

// 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

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

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

{{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:

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:

def detect_sleep_deprivation(profile_id):
    # Check:
    # - Schlafdurchschnitt letzte 3 Tage < (Schlafziel - 60min)
    # - Schlafschulden >2h (letzte 7 Tage)
    # Return: warning + recommendation

Ruhetag empfehlen:

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

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

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

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)