- .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
1089 lines
34 KiB
Markdown
1089 lines
34 KiB
Markdown
# 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)
|