mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
feat(demo): reshape demo seed to read current and healthy
Tune the public demo tenant for App-Store screenshots: - Extend the measurement loop to include today so every headline metric has a current reading. - Retarget blood pressure into the optimal band (~118/76), weight to a stable healthy BMI, and add a clean resting-heart-rate series so the resting-pulse tile scores it directly instead of the spot pulse. - Write a full week of per-stage sleep nights (awake/REM/core/deep, ~7.5h, source APPLE_HEALTH) including last night, in minutes, and skip the aggregate row for those nights to avoid double-counting. - Schedule today's doses on-track: morning meds taken, the evening dose pending and never auto-missed, so today never reads overdue or 0%. - Add Vorsorge reminders (dental check-up, annual physical), one healthy lab panel, one resolved illness episode, and an AI-configured Coach with a short encrypted sample conversation. All dates stay relative to now so the demo refreshes on every re-seed.
This commit is contained in:
1 parent
3e04035de8
commit
431259bb06
1 file changed
+295
-41
+295
-41
@@ -1,18 +1,37 @@
|
||||
/**
|
||||
* Demo Data Seed Script for HealthLog
|
||||
*
|
||||
* Creates realistic demo data:
|
||||
* 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, body fat, sleep, steps)
|
||||
* - 3 medications with schedules and intake history
|
||||
* - 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.
|
||||
* 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;
|
||||
@@ -46,6 +65,16 @@ function daysAgo(n: number): Date {
|
||||
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];
|
||||
}
|
||||
@@ -83,6 +112,11 @@ async function seed() {
|
||||
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");
|
||||
@@ -109,9 +143,17 @@ async function seed() {
|
||||
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, onboarding_completed_at, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW(), NOW())`,
|
||||
`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",
|
||||
@@ -123,29 +165,38 @@ async function seed() {
|
||||
"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 ~82.5
|
||||
const weights = randomWalk(86.5, 82.0, days, 0.6);
|
||||
// Systolic BP: 132 → improving to ~124 (clamped to the physiological band)
|
||||
const sysBP = randomWalk(132, 124, days, 4, VALUE_RANGES.BLOOD_PRESSURE_SYS);
|
||||
// Diastolic BP: 85 → improving to ~78 (clamped to the physiological band)
|
||||
const diaBP = randomWalk(85, 78, days, 3, VALUE_RANGES.BLOOD_PRESSURE_DIA);
|
||||
// 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);
|
||||
// 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 < days; i++) {
|
||||
for (let i = 0; i < span; i++) {
|
||||
const date = daysAgo(days - i);
|
||||
|
||||
// Weight (daily)
|
||||
@@ -167,13 +218,20 @@ async function seed() {
|
||||
[cuid(), userId, Math.round(diaBP[i]), date],
|
||||
);
|
||||
|
||||
// Pulse (daily)
|
||||
// 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(
|
||||
@@ -183,12 +241,19 @@ async function seed() {
|
||||
);
|
||||
}
|
||||
|
||||
// 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],
|
||||
);
|
||||
// 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(
|
||||
@@ -198,6 +263,35 @@ async function seed() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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...");
|
||||
|
||||
@@ -219,7 +313,7 @@ async function seed() {
|
||||
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)`,
|
||||
VALUES ($1, $2, 'Vitamin D3', '2000 IU', true, true, $3, $3)`,
|
||||
[med2Id, userId, daysAgo(90)],
|
||||
);
|
||||
const sched2Id = cuid();
|
||||
@@ -244,13 +338,17 @@ async function seed() {
|
||||
);
|
||||
|
||||
// ── 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: 96% compliance (miss ~1 every 25 days)
|
||||
const takeMed1 = Math.random() > 0.04;
|
||||
// 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);
|
||||
@@ -267,8 +365,8 @@ async function seed() {
|
||||
);
|
||||
}
|
||||
|
||||
// Vitamin D3: 92% compliance
|
||||
const takeMed2 = Math.random() > 0.08;
|
||||
// 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);
|
||||
@@ -285,9 +383,9 @@ async function seed() {
|
||||
);
|
||||
}
|
||||
|
||||
// Magnesium: only last 60 days, 88% compliance
|
||||
// Magnesium: only last 60 days, 94% compliance
|
||||
if (i >= 30) {
|
||||
const takeMed3 = Math.random() > 0.12;
|
||||
const takeMed3 = Math.random() > 0.06;
|
||||
if (takeMed3) {
|
||||
const takenTime = new Date(date);
|
||||
takenTime.setHours(20, Math.floor(Math.random() * 60), 0);
|
||||
@@ -306,6 +404,31 @@ async function seed() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 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)...");
|
||||
|
||||
@@ -318,10 +441,11 @@ async function seed() {
|
||||
["exhausted", "pain"],
|
||||
];
|
||||
|
||||
// Mood trend: starts around 3, improves to ~4 over time
|
||||
const moodTrend = randomWalk(3.0, 4.2, days, 0.8);
|
||||
// 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 < days; i++) {
|
||||
for (let i = 0; i < span; i++) {
|
||||
// Skip ~5% of days (forgot to log)
|
||||
if (Math.random() < 0.05) continue;
|
||||
|
||||
@@ -347,6 +471,73 @@ async function seed() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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...");
|
||||
|
||||
@@ -374,6 +565,64 @@ async function seed() {
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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(
|
||||
@@ -396,9 +645,14 @@ async function seed() {
|
||||
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(` 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");
|
||||
|
||||
Reference in new issue
Block a user