Files
HealthLog/scripts/seed-demo.ts
T
Marc-André Bombeck 38309932f3 Initial release v1.0.0
HealthLog — personal health-tracking PWA with weight, blood pressure,
pulse, mood tracking, medication compliance, Withings integration,
doctor report PDF export, and Dracula-themed UI.
2026-03-10 20:14:33 +01:00

376 lines
15 KiB
TypeScript

/**
* Demo Data Seed Script for HealthLog
*
* Creates realistic demo data:
* - 1 admin user (demo/demo123demo123)
* - 90 days of measurements (weight, BP, pulse, body fat, sleep, steps)
* - 3 medications with schedules and intake history
* - 90 days of mood entries
* - App settings (registration disabled, English locale)
*
* Usage: npx tsx scripts/seed-demo.ts
* Requires DATABASE_URL env var.
*/
import pg from "pg";
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;
}
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): 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;
values.push(Math.round((prev + drift + noise) * 10) / 10);
}
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 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";
await client.query(
`INSERT INTO users (id, username, email, password_hash, role, height_cm, date_of_birth, gender, timezone, locale, onboarding_completed_at, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW(), NOW())`,
[userId, "demo", "demo@healthlog.app", passwordHash, "ADMIN", 182.0, "1990-05-15", "MALE", "Europe/Berlin", "en"]
);
// ── Measurements (90 days) ────────────────
console.log("Creating 90 days of measurements...");
const days = 90;
// Weight: 86.5 → trending down to ~82.5
const weights = randomWalk(86.5, 82.0, days, 0.6);
// Systolic BP: 132 → improving to ~124
const sysBP = randomWalk(132, 124, days, 4);
// Diastolic BP: 85 → improving to ~78
const diaBP = randomWalk(85, 78, days, 3);
// Pulse: 72 → slight improvement to ~68
const pulse = randomWalk(72, 68, days, 3);
// Body fat: 24.5% → trending to ~22%
const bodyFat = randomWalk(24.5, 22.0, days, 0.4);
// Sleep: 6.5h → improving to ~7.5h
const sleep = randomWalk(6.5, 7.5, days, 0.8);
// Steps: 5000 → improving to ~8000
const steps = randomWalk(5000, 8500, days, 1500);
for (let i = 0; i < days; 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]
);
// 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]
);
// 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 (daily)
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, 'hours', 'MANUAL', $4, $4, $4)`,
[cuid(), userId, Math.round(sleep[i] * 10) / 10, 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]
);
}
// ── 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 IE', 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 ──────────────
console.log("Creating intake events (90 days)...");
for (let i = 0; i < days; i++) {
const date = daysAgo(days - i);
// Ramipril: 96% compliance (miss ~1 every 25 days)
const takeMed1 = Math.random() > 0.04;
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)
VALUES ($1, $2, $3, $4, $5, false, 'WEB', $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)
VALUES ($1, $2, $3, $4, NULL, true, 'WEB', $4)`,
[cuid(), userId, med1Id, date]
);
}
// Vitamin D3: 92% compliance
const takeMed2 = Math.random() > 0.08;
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)
VALUES ($1, $2, $3, $4, $5, false, 'WEB', $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)
VALUES ($1, $2, $3, $4, NULL, true, 'WEB', $4)`,
[cuid(), userId, med2Id, date]
);
}
// Magnesium: only last 60 days, 88% compliance
if (i >= 30) {
const takeMed3 = Math.random() > 0.12;
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)
VALUES ($1, $2, $3, $4, $5, false, 'WEB', $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)
VALUES ($1, $2, $3, $4, NULL, true, 'WEB', $4)`,
[cuid(), userId, med3Id, date]
);
}
}
}
// ── 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 around 3, improves to ~4 over time
const moodTrend = randomWalk(3.0, 4.2, days, 0.8);
for (let i = 0; i < days; 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]
);
}
// ── 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]
);
}
// ── 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: ${days * 6} entries (7 types, 90 days)`);
console.log(` Medications: 3 (with ${days * 2 + 60} intake events)`);
console.log(` Mood: ~${Math.round(days * 0.95)} entries`);
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();