-- ================================================================ -- 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), 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), 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, meas_id UUID, -- Legacy: reference to measurement (circumference/caliper) date DATE, path TEXT NOT NULL, 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; DROP TRIGGER IF EXISTS trigger_profiles_updated ON profiles; CREATE TRIGGER trigger_profiles_updated BEFORE UPDATE ON profiles FOR EACH ROW EXECUTE FUNCTION update_updated_timestamp(); DROP TRIGGER IF EXISTS trigger_ai_prompts_updated ON ai_prompts; CREATE TRIGGER trigger_ai_prompts_updated 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)';