# Datenbankschema

## Übersicht

**Datenbank:** PostgreSQL 16 Alpine

**Schema-Verwaltung:** Automatische Migrations (db_init.py)

**Tabellen:** 32 Tabellen (Stand v9d Phase 2)

**Primärschlüssel:** UUID (uuid_generate_v4())

**Zeitstempel:** `TIMESTAMP WITH TIME ZONE`, UTC

---

## Migrations-System

### Automatische Schema-Updates

**Location:** `backend/migrations/*.sql`

**Pattern:** `XXX_description.sql` (z.B. `001_initial_schema.sql`)

**Tracking-Tabelle:** `schema_migrations`
```sql
CREATE TABLE schema_migrations (
    id              SERIAL PRIMARY KEY,
    migration_file  VARCHAR(255) UNIQUE NOT NULL,
    applied_at      TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```

**Ablauf:**
1. Container-Start → `db_init.py` läuft
2. Alle Dateien in `backend/migrations/` werden alphabetisch sortiert
3. Noch nicht angewendete Migrationen werden ausgeführt
4. Eintrag in `schema_migrations` nach erfolgreicher Anwendung

**Sicherheit:**
- Transaktionen pro Migration (Rollback bei Fehler)
- Skip bereits angewendeter Migrationen
- Logging aller Migrationen

### Migration-History

| Nr | Datei | Beschreibung | Version |
|----|-------|--------------|---------|
| 001 | `001_initial_schema.sql` | Basis-Tabellen (profiles, sessions, weight, etc.) | v9b |
| 002 | `002_membership_system.sql` | Tiers, Features, Coupons, Access Grants | v9c |
| 003 | `003_email_verification.sql` | E-Mail-Verifizierung für Registrierung | v9c |
| 004 | `004_training_types.sql` | Trainingstypen (23 Basis-Typen) | v9d Phase 1a |
| 005 | `005_extended_training_types.sql` | Extended Types (Gehen, Tanzen, Meditation) | v9d Phase 1a |
| 006 | `006_training_abilities.sql` | abilities JSONB column (Platzhalter) | v9d Phase 1b |
| 007 | `007_activity_type_mappings.sql` | Lernendes Mapping-System | v9d Phase 1b |
| 008-009 | – | Reserved für v9d Phase 2a-c | – |
| 010 | `010_sleep_log.sql` | Schlaf-Modul (JSONB segments) | v9d Phase 2b |
| 011 | `011_rest_days.sql` | Ruhetage (Kraft, Cardio, Entspannung) | v9d Phase 2a |
| 012 | `012_rest_days_unique_constraint.sql` | Unique constraint rest_days | v9d Phase 2a |
| 013 | `013_vitals_log.sql` | Vitalwerte (Ruhepuls, HRV) – **deprecated** | v9d Phase 2c |
| 014 | `014_extended_vitals.sql` | Extended vitals (BP, VO2 Max, SpO2) – **deprecated** | v9d Phase 2c |
| 015 | `015_vitals_refactoring.sql` | **Vitals Refactoring** - Trennung in vitals_baseline + blood_pressure_log | v9d Phase 2d |

**Backup vor Migration 015:**
- `vitals_log` → `vitals_log_backup_pre_015`
- Daten nach Refactoring archiviert (nicht gelöscht)

---

## Tabellen-Übersicht

### Core-Tabellen

| Tabelle | Zweck | Primärschlüssel | Besonderheit |
|---------|-------|-----------------|--------------|
| `profiles` | Nutzerprofile + Auth | UUID | bcrypt-Hashing, Tier-System |
| `sessions` | Auth-Tokens | VARCHAR(64) | Expires-Check via Index |
| `ai_usage` | KI-Call-Tracking | UUID | Daily Count pro Profil |

### Tracking-Tabellen

| Tabelle | Zweck | Primärschlüssel | Unique Constraint |
|---------|-------|-----------------|-------------------|
| `weight_log` | Gewichtsmessungen | UUID | (profile_id, date) |
| `circumference_log` | Umfangsmessungen (8 Punkte) | UUID | – |
| `caliper_log` | Hautfaltenmessungen + BF% | UUID | – |
| `nutrition_log` | Ernährungsdaten (Kalorien + Makros) | UUID | – |
| `activity_log` | Training + Aktivitäten | UUID | – |
| `photos` | Progress-Fotos | UUID | – |
| `sleep_log` | Schlaf + JSONB Phasen | UUID | (profile_id, date) |
| `rest_days` | Multi-dimensionale Ruhetage | UUID | (profile_id, date, rest_type) |
| `vitals_baseline` | Morgenmessungen (RHR, HRV, VO2 Max) | UUID | (profile_id, date) |
| `blood_pressure_log` | Blutdruck mehrfach täglich | UUID | – |

### KI-Tabellen

| Tabelle | Zweck | Primärschlüssel | Index |
|---------|-------|-----------------|-------|
| `ai_insights` | KI-Auswertungen | UUID | (profile_id, scope, created DESC) |
| `ai_prompts` | Konfigurierbare Prompts | UUID | slug UNIQUE |

### Membership-System (v9c)

| Tabelle | Zweck | Primärschlüssel | Besonderheit |
|---------|-------|-----------------|--------------|
| `subscriptions` | **deprecated** | UUID | Wurde durch access_grants ersetzt |
| `coupons` | Coupon-Codes | UUID | code UNIQUE |
| `coupon_redemptions` | Einlösungen | UUID | (profile_id, coupon_id) UNIQUE |
| `features` | Feature-Definitionen | VARCHAR(50) | id als String (z.B. 'weight_entries') |
| `tier_limits` | Tier-Feature-Matrix | UUID | (tier_id, feature_id) UNIQUE |
| `user_feature_restrictions` | User-spezifische Limits | UUID | (profile_id, feature_id) UNIQUE |
| `user_feature_usage` | Usage-Tracking | UUID | (profile_id, feature_id) UNIQUE |
| `access_grants` | Zeitlich begrenzte Tier-Zugriffe | UUID | Ersetzt subscriptions |
| `user_activity_log` | Audit-Log | UUID | – |

### Training-System (v9d)

| Tabelle | Zweck | Primärschlüssel | Besonderheit |
|---------|-------|-----------------|--------------|
| `training_types` | 29 Trainingstypen in 7 Kategorien | UUID | abilities JSONB |
| `activity_type_mappings` | Lernendes Mapping-System | UUID | (activity_type, profile_id) UNIQUE |

### Infrastruktur

| Tabelle | Zweck | Primärschlüssel | Verwendung |
|---------|-------|-----------------|------------|
| `schema_migrations` | Migrations-Tracking | SERIAL | Automatisches System |

---

## Detaillierte Tabellen-Beschreibungen

### 1. profiles

**Beschreibung:** Nutzerprofile mit Auth, Permissions, Tier-System

```sql
CREATE TABLE profiles (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name            VARCHAR(255) NOT NULL DEFAULT 'Nutzer',
    avatar_color    VARCHAR(7) DEFAULT '#1D9E75',
    photo_id        UUID,
    sex             VARCHAR(1) DEFAULT 'm' CHECK (sex IN ('m', 'w', 'd')),
    dob             DATE,
    height          NUMERIC(5,2) DEFAULT 178,
    goal_weight     NUMERIC(5,2),
    goal_bf_pct     NUMERIC(4,2),

    -- Auth & Permissions
    role            VARCHAR(20) DEFAULT 'user' CHECK (role IN ('user', 'admin')),
    pin_hash        TEXT,                           -- bcrypt-Hash
    auth_type       VARCHAR(20) DEFAULT 'pin' CHECK (auth_type IN ('pin', 'email')),
    session_days    INTEGER DEFAULT 30,
    ai_enabled      BOOLEAN DEFAULT TRUE,
    ai_limit_day    INTEGER,
    export_enabled  BOOLEAN DEFAULT TRUE,
    email           VARCHAR(255) UNIQUE,

    -- E-Mail-Verifizierung (v9c)
    email_verified         BOOLEAN DEFAULT FALSE,
    verification_token     VARCHAR(64),
    verification_expires   TIMESTAMP WITH TIME ZONE,

    -- Tier-System (v9c)
    tier                VARCHAR(20) DEFAULT 'free' CHECK (tier IN ('free', 'basic', 'premium', 'selfhosted')),
    tier_expires_at     TIMESTAMP WITH TIME ZONE,
    trial_ends_at       TIMESTAMP WITH TIME ZONE,
    invited_by          UUID REFERENCES profiles(id),

    -- Timestamps
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_profiles_email ON profiles(email) WHERE email IS NOT NULL;
CREATE INDEX idx_profiles_tier ON profiles(tier);
```

**Wichtige Felder:**
- `pin_hash`: bcrypt-Hash (Format: `$2b$12$...`) oder Legacy SHA256 (auto-migrated)
- `tier`: Subscription-Tier (überschrieben durch `access_grants`)
- `trial_ends_at`: 14 Tage ab Registrierung
- `email_verified`: Muss `true` sein für Login (außer Legacy-Accounts)

**Trigger:**
```sql
CREATE TRIGGER trigger_profiles_updated
    BEFORE UPDATE ON profiles
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_timestamp();
```

---

### 2. sessions

**Beschreibung:** Auth-Token-Management

```sql
CREATE TABLE sessions (
    token       VARCHAR(64) PRIMARY KEY,                -- secrets.token_urlsafe(32)
    profile_id  UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    expires_at  TIMESTAMP WITH TIME ZONE NOT NULL,
    created     TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_sessions_profile_id ON sessions(profile_id);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
```

**Besonderheiten:**
- Token-Format: 43 Zeichen Base64-URL-safe
- Password-Reset-Tokens: Präfix `reset_` (z.B. `reset_jT9z3xK...`)
- Automatische Bereinigung via `WHERE expires_at > CURRENT_TIMESTAMP`

---

### 3. weight_log

**Beschreibung:** Gewichtsmessungen

```sql
CREATE TABLE weight_log (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id  UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    date        DATE NOT NULL,
    weight      NUMERIC(5,2) NOT NULL,
    note        TEXT,
    source      VARCHAR(20) DEFAULT 'manual',           -- 'manual' | 'apple_health' | 'garmin'
    created     TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_weight_log_profile_date ON weight_log(profile_id, date DESC);
CREATE UNIQUE INDEX idx_weight_log_profile_date_unique ON weight_log(profile_id, date);
```

**Unique Constraint:** Ein Eintrag pro Profil pro Tag

**Upsert-Logik:**
```sql
INSERT INTO weight_log (profile_id, date, weight, note, source)
VALUES (%s, %s, %s, %s, %s)
ON CONFLICT (profile_id, date)
DO UPDATE SET weight=EXCLUDED.weight, note=EXCLUDED.note
WHERE weight_log.source != 'manual';  -- Manuelle Einträge haben Vorrang
```

---

### 4. circumference_log

**Beschreibung:** Umfangsmessungen (8 Punkte)

```sql
CREATE TABLE circumference_log (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id  UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    date        DATE NOT NULL,
    c_neck      NUMERIC(5,2),
    c_chest     NUMERIC(5,2),
    c_waist     NUMERIC(5,2),
    c_belly     NUMERIC(5,2),
    c_hip       NUMERIC(5,2),
    c_thigh     NUMERIC(5,2),
    c_calf      NUMERIC(5,2),
    c_arm       NUMERIC(5,2),
    notes       TEXT,
    photo_id    UUID,
    created     TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_circumference_profile_date ON circumference_log(profile_id, date DESC);
```

**Kein Unique Constraint:** Mehrere Messungen pro Tag möglich

---

### 5. caliper_log

**Beschreibung:** Hautfaltenmessungen + Körperfett-Berechnung

```sql
CREATE TABLE caliper_log (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id      UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    date            DATE NOT NULL,
    sf_method       VARCHAR(20) DEFAULT 'jackson3',     -- 'jackson3' | 'jackson7' | 'durnin' | 'parrillo'
    sf_chest        NUMERIC(5,2),
    sf_axilla       NUMERIC(5,2),
    sf_triceps      NUMERIC(5,2),
    sf_subscap      NUMERIC(5,2),
    sf_suprailiac   NUMERIC(5,2),
    sf_abdomen      NUMERIC(5,2),
    sf_thigh        NUMERIC(5,2),
    sf_calf_med     NUMERIC(5,2),
    sf_lowerback    NUMERIC(5,2),
    sf_biceps       NUMERIC(5,2),
    body_fat_pct    NUMERIC(4,2),                       -- Berechnet
    lean_mass       NUMERIC(5,2),                       -- Berechnet
    fat_mass        NUMERIC(5,2),                       -- Berechnet
    notes           TEXT,
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_caliper_profile_date ON caliper_log(profile_id, date DESC);
```

**Berechnungslogik:** Backend berechnet `body_fat_pct`, `lean_mass`, `fat_mass` bei POST

**Methoden:** Siehe `frontend/src/utils/calc.js` (Jackson-Pollock 3/7, Durnin-Womersley, Parrillo)

---

### 6. nutrition_log

**Beschreibung:** Ernährungsdaten (Kalorien + Makros)

```sql
CREATE TABLE nutrition_log (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id  UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    date        DATE NOT NULL,
    kcal        NUMERIC(7,2),
    protein_g   NUMERIC(6,2),
    fat_g       NUMERIC(6,2),
    carbs_g     NUMERIC(6,2),
    source      VARCHAR(20) DEFAULT 'csv',              -- 'csv' | 'manual'
    created     TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_nutrition_profile_date ON nutrition_log(profile_id, date DESC);
```

**Upsert-Logik:** Wie `weight_log` – ein Eintrag pro Tag

---

### 7. activity_log

**Beschreibung:** Training + Aktivitäten

```sql
CREATE TABLE activity_log (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id      UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    date            DATE NOT NULL,
    start_time      TIME,
    end_time        TIME,
    activity_type   VARCHAR(50) NOT NULL,               -- 'Laufen' | 'Krafttraining' | etc.
    training_type_id UUID REFERENCES training_types(id), -- v9d: Mapping zu Trainingstypen
    duration_min    NUMERIC(6,2),
    kcal_active     NUMERIC(7,2),
    kcal_resting    NUMERIC(7,2),
    hr_avg          NUMERIC(5,2),
    hr_max          NUMERIC(5,2),
    distance_km     NUMERIC(7,2),
    rpe             INTEGER CHECK (rpe >= 1 AND rpe <= 10), -- Rate of Perceived Exertion
    source          VARCHAR(20) DEFAULT 'manual',
    notes           TEXT,
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_activity_profile_date ON activity_log(profile_id, date DESC);
CREATE INDEX idx_activity_training_type ON activity_log(training_type_id);
```

**Duplikat-Erkennung:** Import prüft `(date, start_time)` bei Apple Health CSV

---

### 8. photos

**Beschreibung:** Progress-Fotos

```sql
CREATE TABLE photos (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id  UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    meas_id     UUID,                                   -- Legacy: reference to circumference/caliper
    date        DATE,
    path        TEXT NOT NULL,                          -- Filesystem-Pfad (relativ zu PHOTOS_DIR)
    created     TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_photos_profile_date ON photos(profile_id, date DESC);
```

**Storage:** `/app/photos/` (Docker Volume)

**Format:** JPEG, max 5 MB (Frontend-Validierung)

---

### 9. ai_insights

**Beschreibung:** KI-Auswertungen

```sql
CREATE TABLE ai_insights (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id  UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    scope       VARCHAR(50) NOT NULL,                   -- Prompt-Slug (z.B. 'weight-trend', 'pipeline')
    content     TEXT NOT NULL,                          -- Markdown-formatierter Text
    created     TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_ai_insights_profile_scope ON ai_insights(profile_id, scope, created DESC);
```

**Scopes:** `weight-trend`, `nutrition-analysis`, `training-plan`, `body-composition`, `progress-summary`, `pipeline`

---

### 10. ai_prompts

**Beschreibung:** Konfigurierbare KI-Prompts

```sql
CREATE TABLE ai_prompts (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name        VARCHAR(255) NOT NULL,
    slug        VARCHAR(100) NOT NULL UNIQUE,
    description TEXT,
    template    TEXT NOT NULL,                          -- Prompt-Template mit Platzhaltern
    active      BOOLEAN DEFAULT TRUE,
    sort_order  INTEGER DEFAULT 0,
    created     TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated     TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_ai_prompts_slug ON ai_prompts(slug);
CREATE INDEX idx_ai_prompts_active_sort ON ai_prompts(active, sort_order);
```

**Template-Platzhalter:** `{weight_data}`, `{nutrition_data}`, `{activity_data}`, etc.

**Trigger:** `update_updated_timestamp()` bei UPDATE

---

### 11. ai_usage

**Beschreibung:** KI-Call-Tracking (Daily)

```sql
CREATE TABLE ai_usage (
    id          UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id  UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    date        DATE NOT NULL,
    call_count  INTEGER DEFAULT 0,
    UNIQUE(profile_id, date)
);

CREATE INDEX idx_ai_usage_profile_date ON ai_usage(profile_id, date);
```

**Upsert-Logik:**
```sql
INSERT INTO ai_usage (profile_id, date, call_count)
VALUES (%s, CURRENT_DATE, 1)
ON CONFLICT (profile_id, date)
DO UPDATE SET call_count = ai_usage.call_count + 1;
```

---

## Membership-System (v9c)

### 12. coupons

```sql
CREATE TABLE coupons (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    code            VARCHAR(50) NOT NULL UNIQUE,
    tier_id         VARCHAR(20) NOT NULL,               -- 'basic' | 'premium' | 'selfhosted'
    valid_days      INTEGER NOT NULL,                   -- Gültigkeitsdauer in Tagen
    max_uses        INTEGER,                            -- NULL = unlimited
    uses_count      INTEGER DEFAULT 0,
    expires_at      TIMESTAMP WITH TIME ZONE,
    active          BOOLEAN DEFAULT TRUE,
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_coupons_code ON coupons(code);
```

---

### 13. coupon_redemptions

```sql
CREATE TABLE coupon_redemptions (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id      UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    coupon_id       UUID NOT NULL REFERENCES coupons(id) ON DELETE CASCADE,
    redeemed_at     TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(profile_id, coupon_id)
);

CREATE INDEX idx_coupon_redemptions_profile ON coupon_redemptions(profile_id);
CREATE INDEX idx_coupon_redemptions_coupon ON coupon_redemptions(coupon_id);
```

**Logik:** Ein User kann denselben Coupon nur einmal einlösen

---

### 14. features

```sql
CREATE TABLE features (
    id              VARCHAR(50) PRIMARY KEY,            -- 'weight_entries', 'ai_calls', etc.
    name            VARCHAR(100) NOT NULL,
    description     TEXT,
    limit_type      VARCHAR(20) NOT NULL CHECK (limit_type IN ('count', 'boolean')),
    reset_period    VARCHAR(20) NOT NULL CHECK (reset_period IN ('never', 'daily', 'monthly')),
    default_limit   INTEGER,                            -- NULL = unlimited
    active          BOOLEAN DEFAULT TRUE,
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```

**Standard-Features:**
- `weight_entries`, `circumference_entries`, `caliper_entries`, `activity_entries`, `nutrition_entries`
- `photos`, `ai_calls`, `ai_pipeline`, `data_export`, `data_import`

---

### 15. tier_limits

```sql
CREATE TABLE tier_limits (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tier_id         VARCHAR(20) NOT NULL,               -- 'free', 'basic', 'premium', 'selfhosted'
    feature_id      VARCHAR(50) NOT NULL REFERENCES features(id) ON DELETE CASCADE,
    limit_value     INTEGER,                            -- NULL = unlimited
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(tier_id, feature_id)
);

CREATE INDEX idx_tier_limits_tier ON tier_limits(tier_id);
CREATE INDEX idx_tier_limits_feature ON tier_limits(feature_id);
```

**Beispiel-Werte:**
```sql
INSERT INTO tier_limits (tier_id, feature_id, limit_value) VALUES
('free', 'weight_entries', 100),
('free', 'ai_calls', 10),
('premium', 'weight_entries', NULL),  -- unlimited
('premium', 'ai_calls', NULL);
```

---

### 16. user_feature_restrictions

```sql
CREATE TABLE user_feature_restrictions (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id      UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    feature_id      VARCHAR(50) NOT NULL REFERENCES features(id) ON DELETE CASCADE,
    limit_value     INTEGER,                            -- Überschreibt Tier-Limit
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(profile_id, feature_id)
);

CREATE INDEX idx_user_restrictions_profile ON user_feature_restrictions(profile_id);
```

**Priorität:** User-Restriction > Tier-Limit > Feature-Default

---

### 17. user_feature_usage

```sql
CREATE TABLE user_feature_usage (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id      UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    feature_id      VARCHAR(50) NOT NULL REFERENCES features(id) ON DELETE CASCADE,
    usage_count     INTEGER DEFAULT 0,
    reset_at        TIMESTAMP WITH TIME ZONE,
    updated         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(profile_id, feature_id)
);

CREATE INDEX idx_user_usage_profile ON user_feature_usage(profile_id);
CREATE INDEX idx_user_usage_reset ON user_feature_usage(reset_at);
```

**Upsert-Logik:**
```sql
INSERT INTO user_feature_usage (profile_id, feature_id, usage_count, reset_at)
VALUES (%s, %s, 1, %s)
ON CONFLICT (profile_id, feature_id)
DO UPDATE SET usage_count = user_feature_usage.usage_count + 1, updated = CURRENT_TIMESTAMP;
```

---

### 18. access_grants

```sql
CREATE TABLE access_grants (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id      UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    tier_id         VARCHAR(20) NOT NULL,
    valid_from      TIMESTAMP WITH TIME ZONE NOT NULL,
    valid_until     TIMESTAMP WITH TIME ZONE NOT NULL,
    source          VARCHAR(50),                        -- 'coupon', 'trial', 'manual', 'gift'
    is_active       BOOLEAN DEFAULT TRUE,
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_access_grants_profile ON access_grants(profile_id);
CREATE INDEX idx_access_grants_validity ON access_grants(valid_from, valid_until);
```

**Logik:** Aktiver Grant überschreibt `profiles.tier`

**Priorität:** Grant mit spätestem `valid_until` gewinnt

---

### 19. user_activity_log

```sql
CREATE TABLE user_activity_log (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id      UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    action          VARCHAR(100) NOT NULL,
    details         TEXT,
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_activity_log_profile ON user_activity_log(profile_id);
CREATE INDEX idx_activity_log_created ON user_activity_log(created DESC);
```

**Verwendung:** Audit-Log (Login, Coupon-Einlösung, Feature-Zugriff-Denial)

---

## Training-System (v9d)

### 20. training_types

```sql
CREATE TABLE training_types (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name            VARCHAR(100) NOT NULL,
    category        VARCHAR(50) NOT NULL,               -- 'Kraft', 'Cardio', 'Flexibilität', etc.
    color           VARCHAR(7) DEFAULT '#1D9E75',
    icon            VARCHAR(50),                        -- 'dumbbell', 'running', 'yoga', etc.
    abilities       JSONB,                              -- v9f: Fähigkeiten-Matrix
    sort_order      INTEGER DEFAULT 0,
    active          BOOLEAN DEFAULT TRUE,
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_training_types_category ON training_types(category);
CREATE INDEX idx_training_types_active ON training_types(active);
```

**abilities Format (v9f):**
```json
{
  "Kraft": {"Oberkörper": 8, "Unterkörper": 2, "Core": 5},
  "Ausdauer": {"Aerob": 9, "Anaerob": 3},
  "Beweglichkeit": {"Dynamisch": 6, "Statisch": 4},
  "Koordination": 7,
  "Schnelligkeit": 5,
  "Gleichgewicht": 3
}
```

**Kategorien:**
- Kraft (8 Typen)
- Cardio (6 Typen)
- Flexibilität (4 Typen)
- Spiel & Sport (4 Typen)
- Alltag & Bewegung (3 Typen)
- Outdoor & Natur (2 Typen)
- Geist & Meditation (2 Typen)

**Gesamt:** 29 Trainingstypen

---

### 21. activity_type_mappings

```sql
CREATE TABLE activity_type_mappings (
    id                  UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    activity_type       VARCHAR(100) NOT NULL,          -- 'Laufen', 'Running', etc.
    training_type_id    UUID NOT NULL REFERENCES training_types(id) ON DELETE CASCADE,
    profile_id          UUID REFERENCES profiles(id) ON DELETE CASCADE, -- NULL = global
    is_global           BOOLEAN DEFAULT FALSE,
    created             TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(activity_type, profile_id)
);

CREATE INDEX idx_activity_mappings_activity ON activity_type_mappings(activity_type);
CREATE INDEX idx_activity_mappings_training ON activity_type_mappings(training_type_id);
CREATE INDEX idx_activity_mappings_profile ON activity_type_mappings(profile_id);
```

**Lernendes System:**
- Bulk-Kategorisierung in UI speichert neue Mappings
- User-spezifische Mappings überschreiben globale Mappings
- Admin kann globale Mappings erstellen

**Standard-Mappings:** 40+ vordefiniert (Deutsch + Englisch)

---

## Sleep & Vitals (v9d Phase 2)

### 22. sleep_log

```sql
CREATE TABLE sleep_log (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id      UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    date            DATE NOT NULL,
    bedtime         TIME,
    wakeup          TIME,
    duration_min    INTEGER NOT NULL,
    quality         INTEGER CHECK (quality >= 1 AND quality <= 10),
    sleep_segments  JSONB,                              -- Schlafphasen (Deep, REM, Light, Awake)
    notes           TEXT,
    source          VARCHAR(20) DEFAULT 'manual',
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(profile_id, date)
);

CREATE INDEX idx_sleep_profile_date ON sleep_log(profile_id, date DESC);
```

**sleep_segments Format:**
```json
[
  {"phase": "deep", "start": "23:30", "end": "01:15"},
  {"phase": "rem", "start": "01:15", "end": "02:45"},
  {"phase": "light", "start": "02:45", "end": "06:00"},
  {"phase": "awake", "start": "06:00", "end": "06:15"}
]
```

---

### 23. rest_days

```sql
CREATE TABLE rest_days (
    id              UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id      UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    date            DATE NOT NULL,
    rest_type       VARCHAR(20) NOT NULL CHECK (rest_type IN ('kraft', 'cardio', 'entspannung')),
    reason          TEXT,
    notes           TEXT,
    created         TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(profile_id, date, rest_type)
);

CREATE INDEX idx_rest_days_profile_date ON rest_days(profile_id, date DESC);
```

**Multi-Dimensional Rest:** Mehrere rest_types pro Tag möglich (z.B. kraft + cardio)

---

### 24. vitals_baseline

**Beschreibung:** Morgenmessungen (1x täglich)

```sql
CREATE TABLE vitals_baseline (
    id                  UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id          UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    date                DATE NOT NULL,
    resting_hr          INTEGER,                        -- Ruhepuls (bpm)
    hrv                 INTEGER,                        -- Herzfrequenzvariabilität (ms)
    vo2_max             NUMERIC(5,2),                   -- VO2 Max (ml/kg/min)
    spo2                INTEGER,                        -- Sauerstoffsättigung (%)
    respiratory_rate    INTEGER,                        -- Atemfrequenz (pro Minute)
    notes               TEXT,
    source              VARCHAR(20) DEFAULT 'manual',
    created             TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(profile_id, date)
);

CREATE INDEX idx_vitals_baseline_profile_date ON vitals_baseline(profile_id, date DESC);
```

**Messung:** Morgens nüchtern, direkt nach dem Aufwachen

---

### 25. blood_pressure_log

**Beschreibung:** Blutdruck (mehrfach täglich)

```sql
CREATE TABLE blood_pressure_log (
    id                      UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    profile_id              UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
    date                    DATE NOT NULL,
    time                    TIME NOT NULL,
    systolic                INTEGER NOT NULL,           -- Systolisch (mmHg)
    diastolic               INTEGER NOT NULL,           -- Diastolisch (mmHg)
    pulse                   INTEGER,                    -- Puls (bpm)
    context                 VARCHAR(50),                -- 'fasting', 'after_meal', 'exercise', etc.
    irregular_heartbeat     BOOLEAN DEFAULT FALSE,
    afib_warning            BOOLEAN DEFAULT FALSE,
    notes                   TEXT,
    source                  VARCHAR(20) DEFAULT 'manual',
    created                 TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_blood_pressure_profile_date ON blood_pressure_log(profile_id, date DESC, time DESC);
```

**Contexts:**
- `fasting` – Nüchtern
- `after_meal` – Nach dem Essen
- `exercise` – Nach Training
- `stress` – Unter Stress
- `rest` – In Ruhe
- `before_sleep` – Vor dem Schlafen
- `after_sleep` – Nach dem Aufwachen
- `medication` – Nach Medikation

**WHO/ISH-Klassifizierung:** Backend berechnet Kategorie (Optimal, Normal, Hoch-Normal, Hypertonie 1/2/3)

---

## Beziehungen (Foreign Keys)

### Profil-Bezug

**Alle Tracking-Tabellen:**
```sql
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE
```

**Kaskadierendes Löschen:** Beim Löschen eines Profils werden alle zugehörigen Daten gelöscht

### Membership-Beziehungen

```
profiles
  ↓ (1:n)
user_feature_usage
user_feature_restrictions
access_grants
coupon_redemptions

features
  ↓ (1:n)
tier_limits
user_feature_restrictions
user_feature_usage
```

### Training-Beziehungen

```
training_types
  ↓ (1:n)
activity_log (training_type_id)
activity_type_mappings (training_type_id)

profiles
  ↓ (1:n)
activity_type_mappings (profile_id, NULL = global)
```

---

## Design-Entscheidungen

### 1. Warum UUID statt Integer?

**Entscheidung:** UUID als Primärschlüssel

**Gründe:**
- Keine Kollisionen bei verteilten Systemen
- Sicherheit (kein Raten von IDs)
- Vorbereitung für Multi-Server-Setup

**Nachteil:** Größerer Index (16 Bytes vs. 4 Bytes)

**Akzeptiert weil:** Performance-Impact minimal bei <100k Einträgen pro Tabelle

---

### 2. Warum JSONB für sleep_segments?

**Entscheidung:** JSONB statt normalisierte Tabellen

**Gründe:**
- Flexibilität (variable Anzahl Phasen pro Nacht)
- Atomarität (gesamter Schlaf = 1 Zeile)
- Performance (kein JOIN nötig)
- Native PostgreSQL-Support für JSON-Queries

**Beispiel-Query:**
```sql
SELECT date, sleep_segments->>0 AS first_phase
FROM sleep_log
WHERE profile_id = '...';
```

---

### 3. Warum Unique Constraint auf (profile_id, date)?

**Entscheidung:** Mehrere Tabellen haben `UNIQUE(profile_id, date)`

**Tabellen:** `weight_log`, `sleep_log`, `vitals_baseline`

**Gründe:**
- Upsert-Logik (nur ein Eintrag pro Tag)
- Vereinfacht Frontend (kein Duplikat-Handling)
- Performance (Index für schnelle Lookups)

**Ausnahmen:** `blood_pressure_log`, `activity_log` (mehrfach täglich erlaubt)

---

### 4. Warum source-Feld?

**Entscheidung:** Alle Import-fähigen Tabellen haben `source VARCHAR(20)`

**Werte:** `manual`, `apple_health`, `garmin`, `withings`, etc.

**Gründe:**
- Provenance-Tracking (woher kommen Daten)
- Konflikt-Auflösung (manuelle Einträge haben Vorrang)
- Debugging (Import-Fehler tracken)

**Upsert-Logik:**
```sql
ON CONFLICT (profile_id, date)
DO UPDATE SET ... WHERE table.source != 'manual';
```

---

### 5. Warum String-IDs für Features?

**Entscheidung:** `features.id` ist `VARCHAR(50)` statt UUID

**Beispiel:** `'weight_entries'`, `'ai_calls'`

**Gründe:**
- Lesbarkeit in Logs/Code
- Einfachere Referenzierung in Frontend
- Keine Notwendigkeit für Auto-Generated IDs

---

### 6. Warum abilities als JSONB?

**Entscheidung:** `training_types.abilities` als JSONB (v9f)

**Gründe:**
- Flexible Schema-Evolution (neue Fähigkeiten ohne Migration)
- Nested Structure (Hierarchie: Kraft → Oberkörper → Brust)
- Native PostgreSQL-Aggregation (AVG, SUM über JSONB)

**Alternative:** Normalisierte Tabellen (abgelehnt wegen Overhead)

---

## Wichtige Queries

### 1. Latest Weight mit 7-Tage-Durchschnitt

```sql
SELECT
    w1.date,
    w1.weight,
    (
        SELECT AVG(w2.weight)
        FROM weight_log w2
        WHERE w2.profile_id = w1.profile_id
        AND w2.date BETWEEN w1.date - INTERVAL '7 days' AND w1.date
    ) AS avg_7d
FROM weight_log w1
WHERE w1.profile_id = %s
ORDER BY w1.date DESC
LIMIT 1;
```

### 2. Feature-Limit-Check mit Hierarchie

```sql
-- 1. User-Restriction (höchste Priorität)
SELECT limit_value FROM user_feature_restrictions
WHERE profile_id = %s AND feature_id = %s;

-- 2. Tier-Limit (falls keine Restriction)
SELECT limit_value FROM tier_limits
WHERE tier_id = (
    SELECT tier FROM profiles WHERE id = %s
) AND feature_id = %s;

-- 3. Feature-Default (falls kein Tier-Limit)
SELECT default_limit FROM features WHERE id = %s;
```

### 3. Aktiver Access Grant

```sql
SELECT tier_id FROM access_grants
WHERE profile_id = %s
  AND is_active = true
  AND valid_from <= CURRENT_TIMESTAMP
  AND valid_until > CURRENT_TIMESTAMP
ORDER BY valid_until DESC
LIMIT 1;
```

### 4. Training-Type-Distribution

```sql
SELECT
    tt.name,
    tt.color,
    COUNT(*) AS count,
    SUM(al.duration_min) AS total_duration_min
FROM activity_log al
JOIN training_types tt ON al.training_type_id = tt.id
WHERE al.profile_id = %s
  AND al.date >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY tt.id, tt.name, tt.color
ORDER BY total_duration_min DESC;
```

### 5. Sleep-Schuld (Sleep Debt)

```sql
SELECT
    SUM(
        CASE
            WHEN duration_min < 480 THEN 480 - duration_min  -- 8h = 480min Target
            ELSE 0
        END
    ) AS sleep_debt_min
FROM sleep_log
WHERE profile_id = %s
  AND date >= CURRENT_DATE - INTERVAL '14 days';
```

---

## Zusammenfassung

**Tabellen:** 32 (Stand v9d Phase 2)

**Primärschlüssel:** UUID (uuid_generate_v4())

**Indizes:** 60+ für Performance-Optimierung

**Unique Constraints:** 15+ für Datenintegrität

**JSONB-Spalten:** 2 (`sleep_segments`, `abilities`)

**Migrations:** Automatisch via `db_init.py`

**Design-Highlights:**
- ✅ UUID-basierte IDs (Sicherheit + Skalierbarkeit)
- ✅ JSONB für flexible Strukturen (Schlafphasen, Fähigkeiten)
- ✅ Unique Constraints für Upsert-Logik
- ✅ Source-Tracking für Import-Daten
- ✅ Kaskadierendes Löschen (ON DELETE CASCADE)
- ✅ Automatische Timestamps (created, updated)
- ✅ Check Constraints für Datenvalidierung
- ✅ Index-Optimierung für häufige Queries

**Bekannte Limitationen:**
- Keine Soft-Deletes (physisches Löschen)
- Keine Audit-Trail für Änderungen (außer user_activity_log)
- Keine Partitionierung (akzeptabel bis >1M Einträge)
