- .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
46 KiB
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
- Format:
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_segmentsJSONB 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
- Beispiel:
- 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 bekanntduration_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)
- Format:
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
- ✅ Migration 009 erstellen + deployen (sleep_log + sleep_segments)
- ✅ Backend Router:
sleep.py(CRUD + Stats) - ✅ Frontend:
SleepPage.jsx(Schnell + Detail-Eingabe) - ✅ Charts: Schlafdauer-Trend, Qualität-Trend
- ✅ Dashboard:
SleepWidget.jsx - ✅ Navigation: Schlaf-Icon 🌙 hinzufügen
- ✅ 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
- ✅ Backend: CSV-Parser für Apple Health Schlaf (inkl. sleep_segments JSONB)
- ✅ Frontend: Import-Button auf SleepPage
- ✅ Test: CSV-Import aggregiert Phasen korrekt, speichert Segmente
- ✅ 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
- ✅ Migration 008 anwenden (
activity_logavg_hr/max_hr erweitern) - ✅ Backend: HF-Zonen-Berechnung (Endpoint + Badge-Logik)
- ✅ Backend: Recovery-Status-Berechnung (50% HRV / 30% Ruhepuls / 20% Training)
- ✅ Frontend: HF-Zonen-Badge in ActivityPage
- ✅ Frontend:
RecoveryWidget.jsxim Dashboard - ✅ 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
- ✅ Migration 008 komplett (rest_days, vitals_log, weekly_goals)
- ✅ Backend Router:
rest_days.py,vitals.py,weekly_goals.py(CRUD) - ✅ Frontend:
VitalsPage.jsx(mit 7d + 30d Warnungen) - ✅ Frontend: Ruhetage-Tab in ActivityPage integrieren
- ✅ Dashboard:
VitalsWidget.jsx - ✅ Navigation: Vital-Icon ❤️ hinzufügen
- ✅ 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
- ✅ Backend: Korrelations-Endpoints (
/api/sleep/correlations/*) - ✅ Frontend: Korrelations-Section auf SleepPage
- ✅ Charts: Streudiagramme + narrative KI-Aussagen
- ✅ 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
- ✅ Backend: KI-Platzhalter erweitern (21 neue, siehe Abschnitt 7)
- ✅ Backend: Analyse-Funktionen (Schlafmangel, Übertraining)
- ✅ 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: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:
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
- User importiert Apple Health Schlaf-CSV (7 Tage)
- User erfasst Ruhepuls für 7 Tage
- User trainiert 3x intensiv (avg_hr >80% HFmax)
- User schläft schlecht (quality 2/5, <6h)
- Dashboard zeigt Erholungsampel 🔴 "Erholung nötig"
- 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)
- Phase 2a: Vitalwerte-Basis → Test auf dev.mitai.jinkendo.de
- Phase 2b: Schlaf-Modul Kern → Test auf dev
- Phase 2c: Apple Health Import → Test mit echten CSV-Daten
- Phase 2d: HF-Zonen + Erholung → Validierung Berechnungslogik
- Phase 2e: Korrelationen → Test mit Mindestdatenbasis
- (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/Jahrvitals_log: ~250 Einträge/Jahr/User → ~200 Bytes/Eintrag → 50 KB/Jahrsleep_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:
- Spec-Review mit User
- Start Phase 2a Implementierung
- Iteratives Deployment (develop → test → main)