/** * Demo Data Seed Script for HealthLog * * Creates a realistic, aspirational demo tenant for the public `demo` * account (demo / demo123demo123 — credentials are intentionally public). * The shape is tuned so a fresh seed reads healthy, engaged, and current * for App-Store / marketing screenshots: * * - 1 admin user (demo/demo123demo123) * - 90 days of measurements (weight, BP, pulse, resting HR, body fat, * sleep, steps) — flat-or-improving trends, most-recent point from today * - A full week of per-stage sleep nights (awake/REM/light(core)/deep), * ~7.5 h each, including last night * - 3 medications with schedules and ~90 days of intake history at high * compliance, with today scheduled on-track (taken or not-yet-due) * - 90 days of mood entries * - Vorsorge (preventive-care) reminders — upcoming dental + annual physical * - One lab panel of biomarkers in healthy ranges * - One resolved illness episode in the past * - An AI-configured Coach with one short sample conversation * - App settings (registration disabled, English locale) * * Every date is relative to "now" so the demo stays fresh on every re-seed. * * Usage: npx tsx scripts/seed-demo.ts * Requires DATABASE_URL env var. The Coach sample conversation additionally * needs ENCRYPTION_KEYS / ENCRYPTION_KEY (the same key the app runs with). */ import { Buffer } from "node:buffer"; import pg from "pg"; import { encryptToBytes } from "../src/lib/ai/coach/bytes-codec"; import { encrypt } from "../src/lib/crypto"; import { VALUE_RANGES } from "../src/lib/validations/measurement"; const DATABASE_URL = process.env.DATABASE_URL; if (!DATABASE_URL) { console.error("DATABASE_URL is required"); process.exit(1); } const pool = new pg.Pool({ connectionString: DATABASE_URL }); // ── Helpers ────────────────────────────────────── function cuid(): string { const chars = "abcdefghijklmnopqrstuvwxyz0123456789"; let id = "c"; for (let i = 0; i < 24; i++) { id += chars[Math.floor(Math.random() * chars.length)]; } return id; } function daysAgo(n: number): Date { const d = new Date(); d.setDate(d.getDate() - n); d.setHours( 7 + Math.floor(Math.random() * 3), Math.floor(Math.random() * 60), 0, 0, ); return d; } // A specific local instant N days ago, at a fixed wall-clock time. Used for // the per-stage sleep segments and the on-track intake slots where the hour // matters (a random morning hour would scatter the sleep stages). function daysAgoAt(n: number, hour: number, minute = 0): Date { const d = new Date(); d.setDate(d.getDate() - n); d.setHours(hour, minute, 0, 0); return d; } function formatDate(d: Date): string { return d.toISOString().split("T")[0]; } // Smooth random walk with mean reversion function randomWalk( start: number, target: number, days: number, volatility: number, // Optional physiological band. A free walk can drift below the floor and // mint an out-of-range row (a systolic-0 BP reading was observed in a // fresh seed — iOS #33); clamping every step keeps the series inside the // same band the input validator enforces. clamp?: { min: number; max: number }, ): number[] { const values: number[] = [start]; for (let i = 1; i < days; i++) { const prev = values[i - 1]; const drift = (target - prev) * 0.03; // mean reversion const noise = (Math.random() - 0.5) * volatility; let next = Math.round((prev + drift + noise) * 10) / 10; if (clamp) next = Math.min(clamp.max, Math.max(clamp.min, next)); values.push(next); } return values; } // ── Main ───────────────────────────────────────── async function seed() { const client = await pool.connect(); try { await client.query("BEGIN"); console.log("Cleaning existing data..."); await client.query("DELETE FROM coach_messages"); await client.query("DELETE FROM coach_conversations"); await client.query("DELETE FROM illness_episodes"); await client.query("DELETE FROM lab_results"); await client.query("DELETE FROM measurement_reminders"); await client.query("DELETE FROM mood_entries"); await client.query("DELETE FROM medication_intake_events"); await client.query("DELETE FROM medication_schedules"); await client.query("DELETE FROM reminder_phase_configs"); await client.query("DELETE FROM medications"); await client.query("DELETE FROM measurements"); await client.query("DELETE FROM user_achievements"); await client.query("DELETE FROM audit_logs"); await client.query("DELETE FROM sessions"); await client.query("DELETE FROM passkeys"); await client.query("DELETE FROM auth_challenges"); await client.query("DELETE FROM notification_channels"); await client.query("DELETE FROM push_subscriptions"); await client.query("DELETE FROM api_tokens"); await client.query("DELETE FROM data_backups"); await client.query("DELETE FROM users"); // ── User ────────────────────────────────── console.log("Creating demo user..."); const userId = cuid(); // argon2id hash of "demo123demo123" — pre-computed // We'll use a bcrypt-compatible approach: register via API later // For now, insert with a placeholder and we'll set it via the app const passwordHash = "$argon2id$v=19$m=65536,t=3,p=4$Kips6OxPAl0vmspO9SoKZQ$oX9gLgwHVnnENCqBloyM13ewuqmhPnw8EpLoemS3MNI"; // AI Coach is configured to the Anthropic provider so the Coach surface // reads as connected. The stored key is a placeholder ciphertext: the // public demo never makes a live provider call (the sample conversation // below supplies the visible content), so no real credential is seeded. const aiProvider = "ANTHROPIC"; const aiModel = "claude-3-5-sonnet"; const aiKeyEncrypted = encrypt("sk-ant-demo-placeholder-not-a-live-key"); await client.query( `INSERT INTO users (id, username, email, password_hash, role, height_cm, date_of_birth, gender, timezone, locale, ai_provider, ai_model, ai_anthropic_key_encrypted, onboarding_completed_at, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW(), NOW(), NOW())`, [ userId, "demo", "demo@healthlog.app", passwordHash, "ADMIN", 182.0, "1990-05-15", "MALE", "Europe/Berlin", "en", aiProvider, aiModel, aiKeyEncrypted, ], ); // ── Measurements (90 days) ──────────────── // The loop runs i = 0..days (inclusive) so the final iteration lands a // point for TODAY (daysAgo(0)) — every headline metric has a current // reading for the dashboard, not one from yesterday. console.log("Creating 90 days of measurements..."); const days = 90; const span = days + 1; // include today // Weight: 86.5 → trending down to a stable ~82 (healthy BMI ~24.7 at 182cm) const weights = randomWalk(86.5, 82.0, span, 0.6); // Systolic BP: 128 → settled in the optimal band ~118 (clamped) const sysBP = randomWalk(128, 118, span, 3, VALUE_RANGES.BLOOD_PRESSURE_SYS); // Diastolic BP: 82 → settled ~76 (clamped) const diaBP = randomWalk(82, 76, span, 2.5, VALUE_RANGES.BLOOD_PRESSURE_DIA); // Spot pulse (daytime heart rate): ~72, gently lower const pulse = randomWalk(74, 70, span, 3, VALUE_RANGES.PULSE); // Resting heart rate: clean ~60, the metric the resting-pulse tile scores const restingHr = randomWalk(64, 58, span, 2, VALUE_RANGES.RESTING_HEART_RATE); // Body fat: 24% → trending to a healthy ~19% const bodyFat = randomWalk(24.0, 19.0, span, 0.4, VALUE_RANGES.BODY_FAT); // Sleep duration in MINUTES (the SLEEP_DURATION unit): ~7h → ~7h45m const sleepMin = randomWalk(420, 465, span, 25, VALUE_RANGES.SLEEP_DURATION); // Steps: 6500 → a solid ~9000 const steps = randomWalk(6500, 9000, span, 1500); for (let i = 0; i < span; i++) { const date = daysAgo(days - i); // Weight (daily) await client.query( `INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at) VALUES ($1, $2, 'WEIGHT', $3, 'kg', 'MANUAL', $4, $4, $4)`, [cuid(), userId, weights[i], date], ); // Blood pressure (daily) await client.query( `INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at) VALUES ($1, $2, 'BLOOD_PRESSURE_SYS', $3, 'mmHg', 'MANUAL', $4, $4, $4)`, [cuid(), userId, Math.round(sysBP[i]), date], ); await client.query( `INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at) VALUES ($1, $2, 'BLOOD_PRESSURE_DIA', $3, 'mmHg', 'MANUAL', $4, $4, $4)`, [cuid(), userId, Math.round(diaBP[i]), date], ); // Spot pulse (daily) await client.query( `INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at) VALUES ($1, $2, 'PULSE', $3, 'bpm', 'MANUAL', $4, $4, $4)`, [cuid(), userId, Math.round(pulse[i]), date], ); // Resting heart rate (daily) — scored by the resting-pulse tile await client.query( `INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at) VALUES ($1, $2, 'RESTING_HEART_RATE', $3, 'bpm', 'MANUAL', $4, $4, $4)`, [cuid(), userId, Math.round(restingHr[i]), date], ); // Body fat (every 2-3 days) if (i % 2 === 0 || Math.random() > 0.5) { await client.query( `INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at) VALUES ($1, $2, 'BODY_FAT', $3, '%', 'MANUAL', $4, $4, $4)`, [cuid(), userId, bodyFat[i], date], ); } // Sleep duration (daily, in minutes). This single aggregate row keeps // the 90-day sleep series continuous for the trend chart. The most // recent seven nights are instead written as per-stage breakdowns below // (one row per stage); skip the aggregate for those so a night is not // double-counted (aggregate + summed stages). const nightsAgo = days - i; if (nightsAgo >= 7) { await client.query( `INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at) VALUES ($1, $2, 'SLEEP_DURATION', $3, 'minutes', 'MANUAL', $4, $4, $4)`, [cuid(), userId, Math.round(sleepMin[i]), date], ); } // Steps (daily) await client.query( `INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at) VALUES ($1, $2, 'ACTIVITY_STEPS', $3, 'steps', 'MANUAL', $4, $4, $4)`, [cuid(), userId, Math.round(steps[i]), date], ); } // ── Per-stage sleep nights (last 7 nights, incl. last night) ── // One SLEEP_DURATION row per stage, all stamped at the same wake instant, // mirroring how a HealthKit night lands (AWAKE / REM / CORE / DEEP, with // the legacy aggregate kept above). The reader sums the stages for the // night's total. Durations target a healthy ~7.5 h with a normal stage // mix (~55% core, ~22% REM, ~18% deep, ~5% awake). Source APPLE_HEALTH // with device_type 'watch' so the night reads as device-tracked rather // than a manual estimate. console.log("Creating per-stage sleep nights (7 nights)..."); const sleepStages: Array<{ stage: string; fraction: number }> = [ { stage: "AWAKE", fraction: 0.05 }, { stage: "REM", fraction: 0.22 }, { stage: "CORE", fraction: 0.55 }, { stage: "DEEP", fraction: 0.18 }, ]; for (let n = 1; n <= 7; n++) { // Wake at ~06:50 with small per-night jitter; total ~7.5 h. const wake = daysAgoAt(n - 1, 6, 40 + Math.floor(Math.random() * 25)); const totalMin = 440 + Math.floor(Math.random() * 40); // 7h20m – 8h for (const { stage, fraction } of sleepStages) { const stageMin = Math.round(totalMin * fraction); await client.query( `INSERT INTO measurements (id, user_id, type, value, unit, source, sleep_stage, device_type, measured_at, created_at, updated_at) VALUES ($1, $2, 'SLEEP_DURATION', $3, 'minutes', 'APPLE_HEALTH', $4, 'watch', $5, $5, $5)`, [cuid(), userId, stageMin, stage, wake], ); } } // ── Medications ─────────────────────────── console.log("Creating medications..."); // Medication 1: Ramipril (blood pressure) const med1Id = cuid(); await client.query( `INSERT INTO medications (id, user_id, name, dose, active, notifications_enabled, created_at, updated_at) VALUES ($1, $2, 'Ramipril', '5mg', true, true, $3, $3)`, [med1Id, userId, daysAgo(120)], ); const sched1Id = cuid(); await client.query( `INSERT INTO medication_schedules (id, medication_id, window_start, window_end, label) VALUES ($1, $2, '08:00', '10:00', 'Morning')`, [sched1Id, med1Id], ); // Medication 2: Vitamin D3 const med2Id = cuid(); await client.query( `INSERT INTO medications (id, user_id, name, dose, active, notifications_enabled, created_at, updated_at) VALUES ($1, $2, 'Vitamin D3', '2000 IU', true, true, $3, $3)`, [med2Id, userId, daysAgo(90)], ); const sched2Id = cuid(); await client.query( `INSERT INTO medication_schedules (id, medication_id, window_start, window_end, label) VALUES ($1, $2, '08:00', '10:00', 'Morning')`, [sched2Id, med2Id], ); // Medication 3: Magnesium (evening) const med3Id = cuid(); await client.query( `INSERT INTO medications (id, user_id, name, dose, active, notifications_enabled, created_at, updated_at) VALUES ($1, $2, 'Magnesium', '400mg', true, true, $3, $3)`, [med3Id, userId, daysAgo(60)], ); const sched3Id = cuid(); await client.query( `INSERT INTO medication_schedules (id, medication_id, window_start, window_end, label) VALUES ($1, $2, '20:00', '22:00', 'Evening')`, [sched3Id, med3Id], ); // ── Medication Intake Events ────────────── // High compliance with no overdue doses. Historical days (i < days) carry // a taken/skip row at ~95%+ adherence. TODAY is handled separately below // so the dashboard always reads on-track (morning doses taken, the // evening dose left as a not-yet-due pending row — never a miss). console.log("Creating intake events (90 days)..."); for (let i = 0; i < days; i++) { const date = daysAgo(days - i); // Ramipril: 98% compliance (miss ~1 every 50 days) const takeMed1 = Math.random() > 0.02; if (takeMed1) { const takenTime = new Date(date); takenTime.setHours(8, Math.floor(Math.random() * 45), 0); await client.query( `INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, false, 'WEB', $5, $5)`, [cuid(), userId, med1Id, date, takenTime], ); } else { await client.query( `INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at, updated_at) VALUES ($1, $2, $3, $4, NULL, true, 'WEB', $4, $4)`, [cuid(), userId, med1Id, date], ); } // Vitamin D3: 95% compliance const takeMed2 = Math.random() > 0.05; if (takeMed2) { const takenTime = new Date(date); takenTime.setHours(8, Math.floor(5 + Math.random() * 50), 0); await client.query( `INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, false, 'WEB', $5, $5)`, [cuid(), userId, med2Id, date, takenTime], ); } else { await client.query( `INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at, updated_at) VALUES ($1, $2, $3, $4, NULL, true, 'WEB', $4, $4)`, [cuid(), userId, med2Id, date], ); } // Magnesium: only last 60 days, 94% compliance if (i >= 30) { const takeMed3 = Math.random() > 0.06; if (takeMed3) { const takenTime = new Date(date); takenTime.setHours(20, Math.floor(Math.random() * 60), 0); await client.query( `INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, false, 'WEB', $5, $5)`, [cuid(), userId, med3Id, date, takenTime], ); } else { await client.query( `INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at, updated_at) VALUES ($1, $2, $3, $4, NULL, true, 'WEB', $4, $4)`, [cuid(), userId, med3Id, date], ); } } } // ── Today's doses (on-track, no overdue) ── // Morning meds: taken inside their 08:00–10:00 window. Evening Magnesium: // a pending row anchored on tonight's 20:00 slot, never auto-missed — it // reads as "not yet due" regardless of the seed wall-clock, and the // compliance engine excludes a plain pending row from the denominator, so // "today" never shows 0% or an overdue dose. console.log("Creating today's on-track doses..."); const todayMorning = daysAgoAt(0, 8, 30); await client.query( `INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at, updated_at) VALUES ($1, $2, $3, $4, $4, false, 'WEB', $4, $4)`, [cuid(), userId, med1Id, todayMorning], ); await client.query( `INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at, updated_at) VALUES ($1, $2, $3, $4, $4, false, 'WEB', $4, $4)`, [cuid(), userId, med2Id, todayMorning], ); const todayEvening = daysAgoAt(0, 20, 0); await client.query( `INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, auto_missed, source, created_at, updated_at) VALUES ($1, $2, $3, $4, NULL, false, false, 'WEB', $4, $4)`, [cuid(), userId, med3Id, todayEvening], ); // ── Mood Entries ───────────────────────── console.log("Creating mood entries (90 days)..."); const moodOptions = ["SUPER_GUT", "GUT", "OKAY", "SCHLECHT", "LAUSIG"]; const tagOptions = [ ["productive", "exercise", "well-rested"], ["focused", "social", "creative"], ["tired", "stressed"], ["anxious", "low-energy"], ["exhausted", "pain"], ]; // Mood trend: starts solid (~3.5) and improves to ~4.4 — flat-or-up, so // the score never dips into an alarming band. const moodTrend = randomWalk(3.5, 4.4, span, 0.7); for (let i = 0; i < span; i++) { // Skip ~5% of days (forgot to log) if (Math.random() < 0.05) continue; const date = daysAgo(days - i); const rawScore = Math.round(Math.min(5, Math.max(1, moodTrend[i]))); const mood = moodOptions[5 - rawScore]; // invert: score 5 = SUPER_GUT const tags = tagOptions[5 - rawScore]; const loggedAt = new Date(date); loggedAt.setHours(21, Math.floor(Math.random() * 59), 0); await client.query( `INSERT INTO mood_entries (id, user_id, date, mood, score, tags, source, mood_logged_at, synced_at, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6, 'WEB', $7, $7, $7, $7)`, [ cuid(), userId, formatDate(date), mood, rawScore, JSON.stringify(tags), loggedAt, ], ); } // ── Vorsorge reminders (preventive care) ── // User-created (origin VORSORGE) checklist reminders with no auto-resolve // measurement target. Both anchored to fire comfortably in the future so // the Vorsorge surface shows sensible upcoming items, not overdue ones. console.log("Creating Vorsorge reminders..."); const dentalNextDue = daysAgo(-45); // ~6 weeks out await client.query( `INSERT INTO measurement_reminders (id, user_id, label, interval_days, anchor_date, origin, notify_hour, location, next_due_at, created_at, updated_at) VALUES ($1, $2, 'Dental check-up', 365, $3, 'VORSORGE', 9, 'Dentist', $4, NOW(), NOW())`, [cuid(), userId, daysAgo(320), dentalNextDue], ); const physicalNextDue = daysAgo(-90); // ~3 months out await client.query( `INSERT INTO measurement_reminders (id, user_id, label, interval_days, anchor_date, origin, notify_hour, location, next_due_at, created_at, updated_at) VALUES ($1, $2, 'Annual physical', 365, $3, 'VORSORGE', 9, 'Family doctor', $4, NOW(), NOW())`, [cuid(), userId, daysAgo(275), physicalNextDue], ); // ── Lab panel (biomarkers in healthy ranges) ── // One grouped panel taken ~10 days ago, every analyte comfortably inside // its reference range, so the labs surface reads clean. console.log("Creating lab panel..."); const panelTakenAt = daysAgo(10); const labResults: Array<{ analyte: string; value: number; unit: string; low: number; high: number; }> = [ { analyte: "Total Cholesterol", value: 178, unit: "mg/dL", low: 0, high: 200 }, { analyte: "LDL", value: 98, unit: "mg/dL", low: 0, high: 130 }, { analyte: "HDL", value: 58, unit: "mg/dL", low: 40, high: 100 }, { analyte: "Triglycerides", value: 92, unit: "mg/dL", low: 0, high: 150 }, { analyte: "HbA1c", value: 5.2, unit: "%", low: 4.0, high: 5.6 }, { analyte: "Fasting Glucose", value: 89, unit: "mg/dL", low: 70, high: 99 }, { analyte: "Vitamin D", value: 42, unit: "ng/mL", low: 30, high: 100 }, { analyte: "TSH", value: 1.8, unit: "mIU/L", low: 0.4, high: 4.0 }, { analyte: "Ferritin", value: 120, unit: "ng/mL", low: 30, high: 400 }, ]; for (const lab of labResults) { await client.query( `INSERT INTO lab_results (id, user_id, panel, analyte, value, unit, reference_low, reference_high, taken_at, source, created_at, updated_at) VALUES ($1, $2, 'Annual blood panel', $3, $4, $5, $6, $7, $8, 'MANUAL', NOW(), NOW())`, [ cuid(), userId, lab.analyte, lab.value, lab.unit, lab.low, lab.high, panelTakenAt, ], ); } // ── Resolved illness episode ────────────── // A short cold ~7 weeks ago, fully recovered, so the illness journal shows // a clean "resolved" entry rather than an active sickness. console.log("Creating resolved illness episode..."); await client.query( `INSERT INTO illness_episodes (id, user_id, label, type, lifecycle, onset_at, resolved_at, created_at, updated_at) VALUES ($1, $2, 'Common cold', 'INFECTION', 'ACUTE', $3, $4, NOW(), NOW())`, [cuid(), userId, daysAgo(49), daysAgo(43)], ); // ── Achievements ───────────────────────── console.log("Creating achievements..."); const achievements = [ { id: "first-measurement", daysAgo: 89 }, { id: "first-medication", daysAgo: 89 }, { id: "first-mood", daysAgo: 89 }, { id: "week-streak-measurements", daysAgo: 82 }, { id: "week-streak-mood", daysAgo: 82 }, { id: "month-streak-measurements", daysAgo: 59 }, { id: "compliance-90", daysAgo: 45 }, { id: "weight-loss-5", daysAgo: 30 }, { id: "bp-normal", daysAgo: 20 }, { id: "steps-10k", daysAgo: 15 }, { id: "month-streak-mood", daysAgo: 10 }, { id: "three-month-streak", daysAgo: 2 }, ]; for (const ach of achievements) { const unlocked = daysAgo(ach.daysAgo); await client.query( `INSERT INTO user_achievements (id, user_id, achievement_id, unlocked_at, created_at) VALUES ($1, $2, $3, $4, $4)`, [cuid(), userId, ach.id, unlocked], ); } // ── Coach sample conversation ───────────── // A short, encrypted two-turn exchange so the Coach surface shows real // content. Messages use the same AES-256-GCM Bytes codec the app writes // with (encryptToBytes → encrypt() under ENCRYPTION_KEYS), so they decrypt // exactly like a live conversation — no faked / plaintext rows. console.log("Creating Coach sample conversation..."); const convoId = cuid(); const convoStart = daysAgo(3); await client.query( `INSERT INTO coach_conversations (id, user_id, title, created_at, updated_at) VALUES ($1, $2, $3, $4, $4)`, [convoId, userId, "Blood pressure trend", convoStart], ); const coachTurns: Array<{ role: string; content: string; metricSourceJson?: string; }> = [ { role: "user", content: "How has my blood pressure been trending lately?", }, { role: "assistant", content: "Your blood pressure has been settling nicely. Over the last few " + "weeks the morning readings have drifted into the optimal band, and " + "your resting heart rate is sitting in a comfortable range too. " + "Keeping up the steady sleep and daily movement looks like it is " + "paying off — worth staying consistent with the morning measurements " + "so the trend stays easy to read.", metricSourceJson: JSON.stringify({ window: "last30days", metrics: ["bloodPressure", "restingHeartRate", "sleep"], }), }, ]; for (let t = 0; t < coachTurns.length; t++) { const turn = coachTurns[t]; const ts = new Date(convoStart.getTime() + t * 60_000); const encrypted = Buffer.from(encryptToBytes(turn.content)); await client.query( `INSERT INTO coach_messages (id, conversation_id, role, encrypted_content, metric_source_json, provider_type, prompt_version, created_at) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`, [ cuid(), convoId, turn.role, encrypted, turn.metricSourceJson ?? null, turn.role === "assistant" ? aiProvider : null, turn.role === "assistant" ? "demo" : null, ts, ], ); } // ── App Settings ───────────────────────── console.log("Creating app settings..."); await client.query( `INSERT INTO app_settings (id, registration_enabled, default_locale) VALUES ('singleton', false, 'en') ON CONFLICT (id) DO UPDATE SET registration_enabled = false, default_locale = 'en'`, ); // ── Audit Log (some login entries) ────── console.log("Creating audit log entries..."); for (let i = 0; i < 15; i++) { const loginDate = daysAgo(Math.floor(Math.random() * 30)); await client.query( `INSERT INTO audit_logs (id, user_id, action, details, ip_address, created_at) VALUES ($1, $2, 'auth.login', '{"method":"password"}', '203.0.113.42', $3)`, [cuid(), userId, loginDate], ); } await client.query("COMMIT"); console.log("\nDemo data seeded successfully!"); console.log(` User: demo / demo123demo123`); console.log(` Measurements: ~${span * 8} entries (8 types, today included)`); console.log(` Sleep: 7 per-stage nights (4 stages each) + daily aggregate`); console.log(` Medications: 3 (high compliance, today on-track)`); console.log(` Mood: ~${Math.round(span * 0.95)} entries`); console.log(` Vorsorge reminders: 2 (dental, annual physical)`); console.log(` Lab panel: ${labResults.length} biomarkers`); console.log(` Illness: 1 resolved episode`); console.log(` Coach: 1 conversation (${coachTurns.length} messages)`); console.log(` Achievements: ${achievements.length}`); } catch (err) { await client.query("ROLLBACK"); console.error("Seed failed:", err); throw err; } finally { client.release(); await pool.end(); } } seed();