2026-03-18 08:27:33 +01:00
|
|
|
|
-- ================================================================
|
|
|
|
|
|
-- MITAI JINKENDO v9b – PostgreSQL Schema
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
-- Migration from SQLite to PostgreSQL
|
|
|
|
|
|
-- Includes v9b Tier System features
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
-- Enable UUID Extension
|
|
|
|
|
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
|
|
|
|
|
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
-- CORE TABLES
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
-- ── Profiles Table ──────────────────────────────────────────────
|
|
|
|
|
|
-- User/Profile management with auth and permissions
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS 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,
|
|
|
|
|
|
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,
|
|
|
|
|
|
|
|
|
|
|
|
-- v9b: Tier System
|
|
|
|
|
|
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 IF NOT EXISTS idx_profiles_email ON profiles(email) WHERE email IS NOT NULL;
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_profiles_tier ON profiles(tier);
|
|
|
|
|
|
|
|
|
|
|
|
-- ── Sessions Table ──────────────────────────────────────────────
|
|
|
|
|
|
-- Auth token management
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
|
|
|
|
token VARCHAR(64) PRIMARY KEY,
|
|
|
|
|
|
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 IF NOT EXISTS idx_sessions_profile_id ON sessions(profile_id);
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON sessions(expires_at);
|
|
|
|
|
|
|
|
|
|
|
|
-- ── AI Usage Tracking ───────────────────────────────────────────
|
|
|
|
|
|
-- Daily AI call limits per profile
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_ai_usage_profile_date ON ai_usage(profile_id, date);
|
|
|
|
|
|
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
-- TRACKING TABLES
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
-- ── Weight Log ──────────────────────────────────────────────────
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS 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',
|
|
|
|
|
|
created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_weight_log_profile_date ON weight_log(profile_id, date DESC);
|
|
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_weight_log_profile_date_unique ON weight_log(profile_id, date);
|
|
|
|
|
|
|
|
|
|
|
|
-- ── Circumference Log ───────────────────────────────────────────
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS 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 IF NOT EXISTS idx_circumference_profile_date ON circumference_log(profile_id, date DESC);
|
|
|
|
|
|
|
|
|
|
|
|
-- ── Caliper Log ─────────────────────────────────────────────────
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS 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',
|
|
|
|
|
|
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),
|
|
|
|
|
|
lean_mass NUMERIC(5,2),
|
|
|
|
|
|
fat_mass NUMERIC(5,2),
|
|
|
|
|
|
notes TEXT,
|
|
|
|
|
|
created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_caliper_profile_date ON caliper_log(profile_id, date DESC);
|
|
|
|
|
|
|
|
|
|
|
|
-- ── Nutrition Log ───────────────────────────────────────────────
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS 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',
|
|
|
|
|
|
created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_nutrition_profile_date ON nutrition_log(profile_id, date DESC);
|
|
|
|
|
|
|
|
|
|
|
|
-- ── Activity Log ────────────────────────────────────────────────
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS 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,
|
|
|
|
|
|
duration_min NUMERIC(6,2),
|
2026-04-11 06:41:23 +02:00
|
|
|
|
kcal_active NUMERIC(12,2),
|
|
|
|
|
|
kcal_resting NUMERIC(12,2),
|
|
|
|
|
|
hr_avg NUMERIC(6,2),
|
|
|
|
|
|
hr_max NUMERIC(6,2),
|
2026-03-18 08:27:33 +01:00
|
|
|
|
distance_km NUMERIC(7,2),
|
|
|
|
|
|
rpe INTEGER CHECK (rpe >= 1 AND rpe <= 10),
|
|
|
|
|
|
source VARCHAR(20) DEFAULT 'manual',
|
|
|
|
|
|
notes TEXT,
|
|
|
|
|
|
created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_activity_profile_date ON activity_log(profile_id, date DESC);
|
|
|
|
|
|
|
|
|
|
|
|
-- ── Photos ──────────────────────────────────────────────────────
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS photos (
|
|
|
|
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
|
|
|
|
profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE,
|
2026-03-18 12:23:13 +01:00
|
|
|
|
meas_id UUID, -- Legacy: reference to measurement (circumference/caliper)
|
2026-03-18 08:27:33 +01:00
|
|
|
|
date DATE,
|
|
|
|
|
|
path TEXT NOT NULL,
|
2026-04-19 10:13:22 +02:00
|
|
|
|
taken_at TIMESTAMPTZ, -- Aufnahmezeit aus EXIF (optional)
|
2026-03-18 08:27:33 +01:00
|
|
|
|
created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_photos_profile_date ON photos(profile_id, date DESC);
|
|
|
|
|
|
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
-- AI TABLES
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
-- ── AI Insights ─────────────────────────────────────────────────
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS 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,
|
|
|
|
|
|
content TEXT NOT NULL,
|
|
|
|
|
|
created TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ai_insights_profile_scope ON ai_insights(profile_id, scope, created DESC);
|
|
|
|
|
|
|
|
|
|
|
|
-- ── AI Prompts ──────────────────────────────────────────────────
|
|
|
|
|
|
CREATE TABLE IF NOT EXISTS 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,
|
|
|
|
|
|
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 IF NOT EXISTS idx_ai_prompts_slug ON ai_prompts(slug);
|
|
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_ai_prompts_active_sort ON ai_prompts(active, sort_order);
|
|
|
|
|
|
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
-- TRIGGERS
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
-- Auto-update timestamp trigger for profiles
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION update_updated_timestamp()
|
|
|
|
|
|
RETURNS TRIGGER AS $$
|
|
|
|
|
|
BEGIN
|
|
|
|
|
|
NEW.updated = CURRENT_TIMESTAMP;
|
|
|
|
|
|
RETURN NEW;
|
|
|
|
|
|
END;
|
|
|
|
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
|
|
|
2026-03-18 11:59:09 +01:00
|
|
|
|
DROP TRIGGER IF EXISTS trigger_profiles_updated ON profiles;
|
|
|
|
|
|
CREATE TRIGGER trigger_profiles_updated
|
2026-03-18 08:27:33 +01:00
|
|
|
|
BEFORE UPDATE ON profiles
|
|
|
|
|
|
FOR EACH ROW
|
|
|
|
|
|
EXECUTE FUNCTION update_updated_timestamp();
|
|
|
|
|
|
|
2026-03-18 11:59:09 +01:00
|
|
|
|
DROP TRIGGER IF EXISTS trigger_ai_prompts_updated ON ai_prompts;
|
|
|
|
|
|
CREATE TRIGGER trigger_ai_prompts_updated
|
2026-03-18 08:27:33 +01:00
|
|
|
|
BEFORE UPDATE ON ai_prompts
|
|
|
|
|
|
FOR EACH ROW
|
|
|
|
|
|
EXECUTE FUNCTION update_updated_timestamp();
|
|
|
|
|
|
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
-- COMMENTS (Documentation)
|
|
|
|
|
|
-- ================================================================
|
|
|
|
|
|
|
|
|
|
|
|
COMMENT ON TABLE profiles IS 'User profiles with auth, permissions, and tier system';
|
|
|
|
|
|
COMMENT ON TABLE sessions IS 'Active auth tokens';
|
|
|
|
|
|
COMMENT ON TABLE ai_usage IS 'Daily AI call tracking per profile';
|
|
|
|
|
|
COMMENT ON TABLE weight_log IS 'Weight measurements';
|
|
|
|
|
|
COMMENT ON TABLE circumference_log IS 'Body circumference measurements (8 points)';
|
|
|
|
|
|
COMMENT ON TABLE caliper_log IS 'Skinfold measurements with body fat calculations';
|
|
|
|
|
|
COMMENT ON TABLE nutrition_log IS 'Daily nutrition intake (calories + macros)';
|
|
|
|
|
|
COMMENT ON TABLE activity_log IS 'Training sessions and activities';
|
|
|
|
|
|
COMMENT ON TABLE photos IS 'Progress photos';
|
|
|
|
|
|
COMMENT ON TABLE ai_insights IS 'AI-generated analysis results';
|
|
|
|
|
|
COMMENT ON TABLE ai_prompts IS 'Configurable AI prompt templates';
|
|
|
|
|
|
|
|
|
|
|
|
COMMENT ON COLUMN profiles.tier IS 'Subscription tier: free, basic, premium, selfhosted';
|
|
|
|
|
|
COMMENT ON COLUMN profiles.trial_ends_at IS 'Trial expiration timestamp (14 days from registration)';
|
|
|
|
|
|
COMMENT ON COLUMN profiles.tier_expires_at IS 'Paid tier expiration timestamp';
|
|
|
|
|
|
COMMENT ON COLUMN profiles.invited_by IS 'Profile ID of inviter (for beta invitations)';
|