- .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
34 KiB
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
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:
- Container-Start →
db_init.pyläuft - Alle Dateien in
backend/migrations/werden alphabetisch sortiert - Noch nicht angewendete Migrationen werden ausgeführt
- Eintrag in
schema_migrationsnach 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
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 durchaccess_grants)trial_ends_at: 14 Tage ab Registrierungemail_verified: Musstruesein für Login (außer Legacy-Accounts)
Trigger:
CREATE TRIGGER trigger_profiles_updated
BEFORE UPDATE ON profiles
FOR EACH ROW
EXECUTE FUNCTION update_updated_timestamp();
2. sessions
Beschreibung: Auth-Token-Management
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
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:
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)
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
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)
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
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
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
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
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)
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:
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
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
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
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_entriesphotos,ai_calls,ai_pipeline,data_export,data_import
15. tier_limits
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:
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
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
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:
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
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
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
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):
{
"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
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
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:
[
{"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
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)
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)
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üchternafter_meal– Nach dem Essenexercise– Nach Trainingstress– Unter Stressrest– In Ruhebefore_sleep– Vor dem Schlafenafter_sleep– Nach dem Aufwachenmedication– Nach Medikation
WHO/ISH-Klassifizierung: Backend berechnet Kategorie (Optimal, Normal, Hoch-Normal, Hypertonie 1/2/3)
Beziehungen (Foreign Keys)
Profil-Bezug
Alle Tracking-Tabellen:
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:
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:
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
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
-- 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
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
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)
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)