fix: Critical bug fixes for AI chat and children authorization
Some checks failed
CI/CD Pipeline / Lint and Test (push) Has been cancelled
CI/CD Pipeline / E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Application (push) Has been cancelled

## AI Chat Fixes
- **CRITICAL**: Fixed AI chat responding only with sleep-related info
  - Root cause: Current user message was never added to context before sending to AI
  - Added user message to context in ai.service.ts before API call
  - Fixed conversation ID handling for new conversations (undefined check)
  - Fixed children query to properly use FamilyMember join instead of incorrect familyId lookup
  - Added FamilyMember entity to AI module imports

- **Context improvements**:
  - New conversations now use empty history array (not the current message)
  - Properly query user's children across all their families via family membership

## Children Authorization Fix
- **CRITICAL SECURITY**: Fixed authorization bug where all users could see all children
  - Root cause: Controllers used `user.sub` but JWT strategy returns `user.userId`
  - Changed all children controller methods to use `user.userId` instead of `user.sub`
  - Added comprehensive logging to track userId and returned children
  - Backend now correctly filters children by family membership

## WebSocket Authentication
- **Enhanced error handling** in families gateway
  - Better error messages for connection failures
  - Added debug logging for token validation
  - More descriptive error emissions to client
  - Added userId fallback (checks both payload.userId and payload.sub)

## User Experience
- **Auto-clear cache on logout**:
  - Logout now clears localStorage and sessionStorage
  - Prevents stale cached data from persisting across sessions
  - Users get fresh data on every login without manual cache clearing

## Testing
- Backend correctly returns only user's own children (verified in logs)
- AI chat now responds to all types of questions, not just sleep-related
- WebSocket authentication provides clearer error feedback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-10-06 10:55:25 +00:00
parent 5c255298d4
commit 34b8466004
18 changed files with 21557 additions and 51 deletions

20385
scripts/demo-data.sql Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,486 @@
/**
* Demo Data Generator for ParentFlow
* Generates realistic activity data for demo user's children from birth to present
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
// Child data from database
const ALICE = {
id: 'chd_xr7ymrde3vf0',
name: 'Alice',
birthDate: new Date('2020-07-09'),
gender: 'female',
ageMonths: 64, // ~5 years 4 months
};
const ROBERT = {
id: 'chd_8b58nlkopebg',
name: 'Robert',
birthDate: new Date('2025-02-04'),
gender: 'male',
ageMonths: 8,
};
const FAMILY_ID = 'fam_vpusjt4fhsxu';
// Helper: Random between min and max
const random = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;
// Helper: Random choice from array
const choice = <T>(arr: T[]): T => arr[random(0, arr.length - 1)];
// Helper: Add days to date
const addDays = (date: Date, days: number) => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
// Helper: Add hours to date
const addHours = (date: Date, hours: number) => {
const result = new Date(date);
result.setHours(result.getHours() + hours);
return result;
};
// Helper: Add minutes to date
const addMinutes = (date: Date, minutes: number) => {
const result = new Date(date);
result.setMinutes(result.getMinutes() + minutes);
return result;
};
// Generate feeding activities
async function generateFeedingActivities(childId: string, birthDate: Date, ageMonths: number) {
console.log(`Generating feeding activities for ${childId}...`);
const activities = [];
const today = new Date();
let currentDate = new Date(birthDate);
while (currentDate < today) {
const currentAgeMonths = Math.floor((currentDate.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30));
// Feeding frequency changes with age
let feedingsPerDay = 8; // Newborn
if (currentAgeMonths >= 3) feedingsPerDay = 6;
if (currentAgeMonths >= 6) feedingsPerDay = 5;
if (currentAgeMonths >= 12) feedingsPerDay = 4; // 3 meals + 1 snack
if (currentAgeMonths >= 24) feedingsPerDay = 4; // 3 meals + 1 snack
for (let i = 0; i < feedingsPerDay; i++) {
const hour = currentAgeMonths < 6
? random(0, 23) // Newborns eat around the clock
: [7, 10, 13, 17, 19][i] || random(8, 18); // Older kids have schedule
const timestamp = new Date(currentDate);
timestamp.setHours(hour, random(0, 59), 0, 0);
let type = 'breast';
let amount = null;
let duration = null;
let notes = '';
if (currentAgeMonths < 6) {
// 0-6 months: mainly breast/bottle
type = choice(['breast', 'bottle']);
if (type === 'breast') {
duration = random(15, 40); // minutes
} else {
amount = random(60, 180); // ml
}
} else if (currentAgeMonths < 12) {
// 6-12 months: introducing solids
type = choice(['breast', 'bottle', 'solid', 'solid']);
if (type === 'breast') {
duration = random(10, 25);
} else if (type === 'bottle') {
amount = random(120, 240);
} else {
notes = choice(['Puréed vegetables', 'Banana mash', 'Rice cereal', 'Oatmeal', 'Puréed chicken', 'Sweet potato']);
}
} else {
// 12+ months: mostly solids
type = 'solid';
const meals = [
'Scrambled eggs and toast',
'Oatmeal with berries',
'Yogurt and fruit',
'Mac and cheese',
'Chicken nuggets and veggies',
'Pasta with tomato sauce',
'Grilled cheese sandwich',
'Rice and beans',
'Fish sticks and peas',
'Pancakes',
'Quesadilla',
'Soup and crackers',
];
notes = choice(meals);
}
activities.push({
id: `act_feed_${childId}_${timestamp.getTime()}`,
child_id: childId,
family_id: FAMILY_ID,
type: 'feeding',
timestamp,
data: {
feedingType: type,
amount,
duration,
side: type === 'breast' ? choice(['left', 'right', 'both']) : null,
notes,
},
created_at: timestamp,
updated_at: timestamp,
});
}
currentDate = addDays(currentDate, 1);
}
return activities;
}
// Generate sleep activities
async function generateSleepActivities(childId: string, birthDate: Date, ageMonths: number) {
console.log(`Generating sleep activities for ${childId}...`);
const activities = [];
const today = new Date();
let currentDate = new Date(birthDate);
while (currentDate < today) {
const currentAgeMonths = Math.floor((currentDate.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30));
// Sleep patterns change with age
let napsPerDay = 4; // Newborn
let nightSleepHours = 8;
if (currentAgeMonths >= 3) { napsPerDay = 3; nightSleepHours = 10; }
if (currentAgeMonths >= 6) { napsPerDay = 2; nightSleepHours = 11; }
if (currentAgeMonths >= 12) { napsPerDay = 1; nightSleepHours = 11; }
if (currentAgeMonths >= 18) { napsPerDay = 1; nightSleepHours = 11; }
if (currentAgeMonths >= 36) { napsPerDay = 0; nightSleepHours = 10; }
// Night sleep
const bedtime = new Date(currentDate);
bedtime.setHours(currentAgeMonths < 12 ? random(19, 21) : random(20, 22), random(0, 59), 0, 0);
const wakeTime = addHours(bedtime, nightSleepHours + random(-1, 1));
activities.push({
id: `act_sleep_night_${childId}_${bedtime.getTime()}`,
child_id: childId,
family_id: FAMILY_ID,
type: 'sleep',
timestamp: bedtime,
data: {
startedAt: bedtime.toISOString(),
endedAt: wakeTime.toISOString(),
duration: Math.floor((wakeTime.getTime() - bedtime.getTime()) / (1000 * 60)),
sleepType: 'night',
quality: choice(['excellent', 'good', 'good', 'fair']),
notes: '',
},
created_at: bedtime,
updated_at: wakeTime,
});
// Naps
for (let i = 0; i < napsPerDay; i++) {
const napHour = currentAgeMonths < 6
? random(9, 16)
: [9, 13, 16][i] || 13;
const napStart = new Date(currentDate);
napStart.setHours(napHour, random(0, 59), 0, 0);
const napDuration = currentAgeMonths < 6
? random(30, 120) // Newborns: 30min-2h
: currentAgeMonths < 18
? random(60, 120) // Babies: 1-2h
: random(60, 90); // Toddlers: 1-1.5h
const napEnd = addMinutes(napStart, napDuration);
activities.push({
id: `act_sleep_nap_${childId}_${napStart.getTime()}`,
child_id: childId,
family_id: FAMILY_ID,
type: 'sleep',
timestamp: napStart,
data: {
startedAt: napStart.toISOString(),
endedAt: napEnd.toISOString(),
duration: napDuration,
sleepType: 'nap',
quality: choice(['excellent', 'good', 'good', 'fair']),
notes: '',
},
created_at: napStart,
updated_at: napEnd,
});
}
currentDate = addDays(currentDate, 1);
}
return activities;
}
// Generate diaper activities
async function generateDiaperActivities(childId: string, birthDate: Date, ageMonths: number) {
console.log(`Generating diaper activities for ${childId}...`);
const activities = [];
const today = new Date();
let currentDate = new Date(birthDate);
while (currentDate < today) {
const currentAgeMonths = Math.floor((currentDate.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30));
// Stop generating diapers after 30 months (potty trained)
if (currentAgeMonths >= 30) {
currentDate = addDays(currentDate, 1);
continue;
}
// Diaper frequency changes with age
let changesPerDay = 8; // Newborn
if (currentAgeMonths >= 3) changesPerDay = 6;
if (currentAgeMonths >= 6) changesPerDay = 5;
if (currentAgeMonths >= 12) changesPerDay = 4;
if (currentAgeMonths >= 24) changesPerDay = 3;
for (let i = 0; i < changesPerDay; i++) {
const hour = random(6, 20);
const timestamp = new Date(currentDate);
timestamp.setHours(hour, random(0, 59), 0, 0);
const wetOnly = random(1, 10) <= 6; // 60% wet only
const poopOnly = random(1, 10) <= 1; // 10% poop only
const both = !wetOnly && !poopOnly; // 30% both
activities.push({
id: `act_diaper_${childId}_${timestamp.getTime()}`,
child_id: childId,
family_id: FAMILY_ID,
type: 'diaper',
timestamp,
data: {
isWet: wetOnly || both,
isPoopy: poopOnly || both,
rash: random(1, 100) <= 5, // 5% chance of rash
notes: '',
},
created_at: timestamp,
updated_at: timestamp,
});
}
currentDate = addDays(currentDate, 1);
}
return activities;
}
// Generate growth measurements
async function generateGrowthMeasurements(childId: string, birthDate: Date, ageMonths: number, gender: string) {
console.log(`Generating growth measurements for ${childId}...`);
const measurements = [];
// WHO growth standards (approximate)
const getExpectedWeight = (months: number, gender: string) => {
// Birth weight ~3.5kg, doubles by 5 months, triples by 12 months
if (months === 0) return 3.5 + random(-5, 5) / 10;
if (months <= 6) return 3.5 + (months * 0.7) + random(-3, 3) / 10;
if (months <= 12) return 7 + (months - 6) * 0.3 + random(-3, 3) / 10;
if (months <= 24) return 10 + (months - 12) * 0.2 + random(-3, 3) / 10;
return 12.5 + (months - 24) * 0.15 + random(-5, 5) / 10;
};
const getExpectedHeight = (months: number) => {
// Birth ~50cm, ~75cm at 12 months, ~86cm at 24 months
if (months === 0) return 50 + random(-2, 2);
if (months <= 12) return 50 + (months * 2.1) + random(-2, 2);
if (months <= 24) return 75 + (months - 12) * 0.9 + random(-2, 2);
return 86 + (months - 24) * 0.4 + random(-2, 2);
};
const getExpectedHeadCircumference = (months: number) => {
// Birth ~35cm, increases rapidly first year
if (months === 0) return 35 + random(-1, 1);
if (months <= 12) return 35 + (months * 1.2) + random(-1, 1);
if (months <= 24) return 47 + (months - 12) * 0.3 + random(-1, 1);
return 50 + (months - 24) * 0.1 + random(-1, 1);
};
// Measurements at birth, 2 weeks, 1 month, then monthly for first year, then every 3 months
const measurementSchedule = [0, 0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63];
for (const month of measurementSchedule) {
if (month > ageMonths) break;
const measurementDate = addDays(birthDate, month * 30);
measurements.push({
id: `act_growth_${childId}_${measurementDate.getTime()}`,
child_id: childId,
family_id: FAMILY_ID,
type: 'growth',
timestamp: measurementDate,
data: {
weight: getExpectedWeight(month, gender),
height: getExpectedHeight(month),
headCircumference: month <= 24 ? getExpectedHeadCircumference(month) : null,
notes: month === 0 ? 'Birth measurements' : `${month} month checkup`,
},
created_at: measurementDate,
updated_at: measurementDate,
});
}
return measurements;
}
// Generate medicine/vaccination records
async function generateMedicineActivities(childId: string, birthDate: Date, ageMonths: number) {
console.log(`Generating medicine/vaccination activities for ${childId}...`);
const activities = [];
// CDC vaccination schedule
const vaccinations = [
{ ageMonths: 0, name: 'Hepatitis B (1st dose)', type: 'vaccine' },
{ ageMonths: 1, name: 'Hepatitis B (2nd dose)', type: 'vaccine' },
{ ageMonths: 2, name: 'DTaP, IPV, Hib, PCV13, Rotavirus (1st doses)', type: 'vaccine' },
{ ageMonths: 4, name: 'DTaP, IPV, Hib, PCV13, Rotavirus (2nd doses)', type: 'vaccine' },
{ ageMonths: 6, name: 'DTaP, IPV, Hib, PCV13, Rotavirus (3rd doses)', type: 'vaccine' },
{ ageMonths: 6, name: 'Influenza (annual)', type: 'vaccine' },
{ ageMonths: 12, name: 'MMR, Varicella, Hepatitis A (1st doses)', type: 'vaccine' },
{ ageMonths: 15, name: 'DTaP (4th dose)', type: 'vaccine' },
{ ageMonths: 18, name: 'Hepatitis A (2nd dose)', type: 'vaccine' },
{ ageMonths: 18, name: 'Influenza (annual)', type: 'vaccine' },
{ ageMonths: 30, name: 'Influenza (annual)', type: 'vaccine' },
{ ageMonths: 42, name: 'Influenza (annual)', type: 'vaccine' },
{ ageMonths: 54, name: 'Influenza (annual)', type: 'vaccine' },
];
for (const vacc of vaccinations) {
if (vacc.ageMonths > ageMonths) continue;
const vaccDate = addDays(birthDate, vacc.ageMonths * 30);
activities.push({
id: `act_medicine_${childId}_${vaccDate.getTime()}`,
child_id: childId,
family_id: FAMILY_ID,
type: 'medicine',
timestamp: vaccDate,
data: {
medicationType: vacc.type,
name: vacc.name,
dosage: '',
notes: 'Well-child visit',
},
created_at: vaccDate,
updated_at: vaccDate,
});
}
// Occasional fever medication (1-2 times per year after 6 months)
const feverMedicineMonths = [];
for (let month = 6; month <= ageMonths; month += random(4, 8)) {
feverMedicineMonths.push(month);
}
for (const month of feverMedicineMonths) {
const medicineDate = addDays(birthDate, month * 30 + random(0, 29));
activities.push({
id: `act_medicine_fever_${childId}_${medicineDate.getTime()}`,
child_id: childId,
family_id: FAMILY_ID,
type: 'medicine',
timestamp: medicineDate,
data: {
medicationType: 'medication',
name: choice(['Infant Tylenol', 'Infant Ibuprofen']),
dosage: '2.5ml',
notes: 'Fever reducer',
},
created_at: medicineDate,
updated_at: medicineDate,
});
}
return activities;
}
// Main execution
async function main() {
try {
console.log('Starting demo data generation for ParentFlow...\n');
// Generate data for Alice (5 years old)
console.log('=== Generating data for Alice (5 years old) ===');
const aliceFeeding = await generateFeedingActivities(ALICE.id, ALICE.birthDate, ALICE.ageMonths);
const aliceSleep = await generateSleepActivities(ALICE.id, ALICE.birthDate, ALICE.ageMonths);
const aliceDiaper = await generateDiaperActivities(ALICE.id, ALICE.birthDate, ALICE.ageMonths);
const aliceGrowth = await generateGrowthMeasurements(ALICE.id, ALICE.birthDate, ALICE.ageMonths, ALICE.gender);
const aliceMedicine = await generateMedicineActivities(ALICE.id, ALICE.birthDate, ALICE.ageMonths);
console.log(`\nAlice activities generated:
- Feeding: ${aliceFeeding.length}
- Sleep: ${aliceSleep.length}
- Diaper: ${aliceDiaper.length}
- Growth: ${aliceGrowth.length}
- Medicine: ${aliceMedicine.length}
- Total: ${aliceFeeding.length + aliceSleep.length + aliceDiaper.length + aliceGrowth.length + aliceMedicine.length}
`);
// Generate data for Robert (8 months old)
console.log('\n=== Generating data for Robert (8 months old) ===');
const robertFeeding = await generateFeedingActivities(ROBERT.id, ROBERT.birthDate, ROBERT.ageMonths);
const robertSleep = await generateSleepActivities(ROBERT.id, ROBERT.birthDate, ROBERT.ageMonths);
const robertDiaper = await generateDiaperActivities(ROBERT.id, ROBERT.birthDate, ROBERT.ageMonths);
const robertGrowth = await generateGrowthMeasurements(ROBERT.id, ROBERT.birthDate, ROBERT.ageMonths, ROBERT.gender);
const robertMedicine = await generateMedicineActivities(ROBERT.id, ROBERT.birthDate, ROBERT.ageMonths);
console.log(`\nRobert activities generated:
- Feeding: ${robertFeeding.length}
- Sleep: ${robertSleep.length}
- Diaper: ${robertDiaper.length}
- Growth: ${robertGrowth.length}
- Medicine: ${robertMedicine.length}
- Total: ${robertFeeding.length + robertSleep.length + robertDiaper.length + robertGrowth.length + robertMedicine.length}
`);
// Combine all activities
const allActivities = [
...aliceFeeding, ...aliceSleep, ...aliceDiaper, ...aliceGrowth, ...aliceMedicine,
...robertFeeding, ...robertSleep, ...robertDiaper, ...robertGrowth, ...robertMedicine,
];
console.log(`\n=== Total activities to insert: ${allActivities.length} ===\n`);
// Save to JSON file for SQL import
const fs = require('fs');
fs.writeFileSync(
'/root/maternal-app/scripts/demo-data.json',
JSON.stringify(allActivities, null, 2)
);
console.log('Demo data saved to /root/maternal-app/scripts/demo-data.json');
console.log('\nRun the SQL import script to insert this data into the database.');
} catch (error) {
console.error('Error generating demo data:', error);
process.exit(1);
} finally {
await prisma.$disconnect();
}
}
main();

485
scripts/insert-demo-data.js Normal file
View File

@@ -0,0 +1,485 @@
/**
* Demo Data Insertion Script for ParentFlow
* Generates and inserts realistic activity data for demo user's children
*/
// Simple nano ID generator (alphanumeric, 16 chars)
function generateId(prefix = 'act') {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
let id = prefix + '_';
for (let i = 0; i < 12; i++) {
id += chars[Math.floor(Math.random() * chars.length)];
}
return id;
}
// Child data from database
const ALICE = {
id: 'chd_xr7ymrde3vf0',
name: 'Alice',
birthDate: new Date('2020-07-09'),
gender: 'female',
ageMonths: Math.floor((new Date() - new Date('2020-07-09')) / (1000 * 60 * 60 * 24 * 30)),
};
const ROBERT = {
id: 'chd_8b58nlkopebg',
name: 'Robert',
birthDate: new Date('2025-02-04'),
gender: 'male',
ageMonths: Math.floor((new Date() - new Date('2025-02-04')) / (1000 * 60 * 60 * 24 * 30)),
};
const FAMILY_ID = 'fam_vpusjt4fhsxu';
const USER_ID = 'usr_p40br2mryafh'; // Demo user ID
// Helper functions
const random = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const choice = (arr) => arr[random(0, arr.length - 1)];
const addDays = (date, days) => {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
};
const addHours = (date, hours) => {
const result = new Date(date);
result.setHours(result.getHours() + hours);
return result;
};
const addMinutes = (date, minutes) => {
const result = new Date(date);
result.setMinutes(result.getMinutes() + minutes);
return result;
};
// Generate feeding activities
function generateFeedingActivities(childId, birthDate, ageMonths) {
console.log(`Generating feeding activities for ${childId}...`);
const activities = [];
const today = new Date();
let currentDate = new Date(birthDate);
while (currentDate < today) {
const currentAgeMonths = Math.floor((currentDate.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30));
let feedingsPerDay = 8; // Newborn
if (currentAgeMonths >= 3) feedingsPerDay = 6;
if (currentAgeMonths >= 6) feedingsPerDay = 5;
if (currentAgeMonths >= 12) feedingsPerDay = 4;
if (currentAgeMonths >= 24) feedingsPerDay = 4;
for (let i = 0; i < feedingsPerDay; i++) {
const hour = currentAgeMonths < 6
? random(0, 23)
: [7, 10, 13, 17, 19][i] || random(8, 18);
const timestamp = new Date(currentDate);
timestamp.setHours(hour, random(0, 59), 0, 0);
let type = 'breast';
let amount = null;
let duration = null;
let notes = '';
if (currentAgeMonths < 6) {
type = choice(['breast', 'bottle']);
if (type === 'breast') {
duration = random(15, 40);
} else {
amount = random(60, 180);
}
} else if (currentAgeMonths < 12) {
type = choice(['breast', 'bottle', 'solid', 'solid']);
if (type === 'breast') {
duration = random(10, 25);
} else if (type === 'bottle') {
amount = random(120, 240);
} else {
notes = choice(['Puréed vegetables', 'Banana mash', 'Rice cereal', 'Oatmeal', 'Puréed chicken', 'Sweet potato']);
}
} else {
type = 'solid';
const meals = [
'Scrambled eggs and toast', 'Oatmeal with berries', 'Yogurt and fruit',
'Mac and cheese', 'Chicken nuggets and veggies', 'Pasta with tomato sauce',
'Grilled cheese sandwich', 'Rice and beans', 'Fish sticks and peas',
'Pancakes', 'Quesadilla', 'Soup and crackers',
];
notes = choice(meals);
}
activities.push({
id: generateId('act'),
child_id: childId,
type: 'feeding',
started_at: timestamp,
ended_at: type === 'breast' && duration ? addMinutes(timestamp, duration) : timestamp,
logged_by: USER_ID,
notes: notes,
metadata: {
feedingType: type,
amount,
duration,
side: type === 'breast' ? choice(['left', 'right', 'both']) : null,
},
created_at: timestamp,
updated_at: timestamp,
});
}
currentDate = addDays(currentDate, 1);
}
return activities;
}
// Generate sleep activities
function generateSleepActivities(childId, birthDate, ageMonths) {
console.log(`Generating sleep activities for ${childId}...`);
const activities = [];
const today = new Date();
let currentDate = new Date(birthDate);
while (currentDate < today) {
const currentAgeMonths = Math.floor((currentDate.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30));
let napsPerDay = 4; // Newborn
let nightSleepHours = 8;
if (currentAgeMonths >= 3) { napsPerDay = 3; nightSleepHours = 10; }
if (currentAgeMonths >= 6) { napsPerDay = 2; nightSleepHours = 11; }
if (currentAgeMonths >= 12) { napsPerDay = 1; nightSleepHours = 11; }
if (currentAgeMonths >= 18) { napsPerDay = 1; nightSleepHours = 11; }
if (currentAgeMonths >= 36) { napsPerDay = 0; nightSleepHours = 10; }
// Night sleep
const bedtime = new Date(currentDate);
bedtime.setHours(currentAgeMonths < 12 ? random(19, 21) : random(20, 22), random(0, 59), 0, 0);
const wakeTime = addHours(bedtime, nightSleepHours + random(-1, 1));
activities.push({
id: generateId('act'),
child_id: childId,
type: 'sleep',
started_at: bedtime,
ended_at: wakeTime,
logged_by: USER_ID,
notes: '',
metadata: {
duration: Math.floor((wakeTime.getTime() - bedtime.getTime()) / (1000 * 60)),
sleepType: 'night',
quality: choice(['excellent', 'good', 'good', 'fair']),
},
created_at: bedtime,
updated_at: wakeTime,
});
// Naps
for (let i = 0; i < napsPerDay; i++) {
const napHour = currentAgeMonths < 6 ? random(9, 16) : [9, 13, 16][i] || 13;
const napStart = new Date(currentDate);
napStart.setHours(napHour, random(0, 59), 0, 0);
const napDuration = currentAgeMonths < 6
? random(30, 120)
: currentAgeMonths < 18
? random(60, 120)
: random(60, 90);
const napEnd = addMinutes(napStart, napDuration);
activities.push({
id: generateId('act'),
child_id: childId,
type: 'sleep',
started_at: napStart,
ended_at: napEnd,
logged_by: USER_ID,
notes: '',
metadata: {
duration: napDuration,
sleepType: 'nap',
quality: choice(['excellent', 'good', 'good', 'fair']),
},
created_at: napStart,
updated_at: napEnd,
});
}
currentDate = addDays(currentDate, 1);
}
return activities;
}
// Generate diaper activities
function generateDiaperActivities(childId, birthDate, ageMonths) {
console.log(`Generating diaper activities for ${childId}...`);
const activities = [];
const today = new Date();
let currentDate = new Date(birthDate);
while (currentDate < today) {
const currentAgeMonths = Math.floor((currentDate.getTime() - birthDate.getTime()) / (1000 * 60 * 60 * 24 * 30));
// Stop at 30 months (potty trained)
if (currentAgeMonths >= 30) {
currentDate = addDays(currentDate, 1);
continue;
}
let changesPerDay = 8;
if (currentAgeMonths >= 3) changesPerDay = 6;
if (currentAgeMonths >= 6) changesPerDay = 5;
if (currentAgeMonths >= 12) changesPerDay = 4;
if (currentAgeMonths >= 24) changesPerDay = 3;
for (let i = 0; i < changesPerDay; i++) {
const hour = random(6, 20);
const timestamp = new Date(currentDate);
timestamp.setHours(hour, random(0, 59), 0, 0);
const wetOnly = random(1, 10) <= 6;
const poopOnly = random(1, 10) <= 1;
const both = !wetOnly && !poopOnly;
activities.push({
id: generateId('act'),
child_id: childId,
type: 'diaper',
started_at: timestamp,
ended_at: timestamp,
logged_by: USER_ID,
notes: '',
metadata: {
isWet: wetOnly || both,
isPoopy: poopOnly || both,
rash: random(1, 100) <= 5,
},
created_at: timestamp,
updated_at: timestamp,
});
}
currentDate = addDays(currentDate, 1);
}
return activities;
}
// Generate growth measurements
function generateGrowthMeasurements(childId, birthDate, ageMonths, gender) {
console.log(`Generating growth measurements for ${childId}...`);
const measurements = [];
const getExpectedWeight = (months) => {
if (months === 0) return 3.5 + random(-5, 5) / 10;
if (months <= 6) return 3.5 + (months * 0.7) + random(-3, 3) / 10;
if (months <= 12) return 7 + (months - 6) * 0.3 + random(-3, 3) / 10;
if (months <= 24) return 10 + (months - 12) * 0.2 + random(-3, 3) / 10;
return 12.5 + (months - 24) * 0.15 + random(-5, 5) / 10;
};
const getExpectedHeight = (months) => {
if (months === 0) return 50 + random(-2, 2);
if (months <= 12) return 50 + (months * 2.1) + random(-2, 2);
if (months <= 24) return 75 + (months - 12) * 0.9 + random(-2, 2);
return 86 + (months - 24) * 0.4 + random(-2, 2);
};
const getExpectedHeadCircumference = (months) => {
if (months === 0) return 35 + random(-1, 1);
if (months <= 12) return 35 + (months * 1.2) + random(-1, 1);
if (months <= 24) return 47 + (months - 12) * 0.3 + random(-1, 1);
return 50 + (months - 24) * 0.1 + random(-1, 1);
};
const schedule = [0, 0.5, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 15, 18, 21, 24, 27, 30, 33, 36, 39, 42, 45, 48, 51, 54, 57, 60, 63];
for (const month of schedule) {
if (month > ageMonths) break;
const measurementDate = addDays(birthDate, month * 30);
measurements.push({
id: generateId('act'),
child_id: childId,
type: 'growth',
started_at: measurementDate,
ended_at: measurementDate,
logged_by: USER_ID,
notes: month === 0 ? 'Birth measurements' : `${month} month checkup`,
metadata: {
weight: getExpectedWeight(month),
height: getExpectedHeight(month),
headCircumference: month <= 24 ? getExpectedHeadCircumference(month) : null,
},
created_at: measurementDate,
updated_at: measurementDate,
});
}
return measurements;
}
// Generate medicine/vaccination records
function generateMedicineActivities(childId, birthDate, ageMonths) {
console.log(`Generating medicine/vaccination activities for ${childId}...`);
const activities = [];
const vaccinations = [
{ ageMonths: 0, name: 'Hepatitis B (1st dose)', type: 'vaccine' },
{ ageMonths: 1, name: 'Hepatitis B (2nd dose)', type: 'vaccine' },
{ ageMonths: 2, name: 'DTaP, IPV, Hib, PCV13, Rotavirus (1st doses)', type: 'vaccine' },
{ ageMonths: 4, name: 'DTaP, IPV, Hib, PCV13, Rotavirus (2nd doses)', type: 'vaccine' },
{ ageMonths: 6, name: 'DTaP, IPV, Hib, PCV13, Rotavirus (3rd doses)', type: 'vaccine' },
{ ageMonths: 6, name: 'Influenza (annual)', type: 'vaccine' },
{ ageMonths: 12, name: 'MMR, Varicella, Hepatitis A (1st doses)', type: 'vaccine' },
{ ageMonths: 15, name: 'DTaP (4th dose)', type: 'vaccine' },
{ ageMonths: 18, name: 'Hepatitis A (2nd dose)', type: 'vaccine' },
{ ageMonths: 18, name: 'Influenza (annual)', type: 'vaccine' },
{ ageMonths: 30, name: 'Influenza (annual)', type: 'vaccine' },
{ ageMonths: 42, name: 'Influenza (annual)', type: 'vaccine' },
{ ageMonths: 54, name: 'Influenza (annual)', type: 'vaccine' },
];
for (const vacc of vaccinations) {
if (vacc.ageMonths > ageMonths) continue;
const vaccDate = addDays(birthDate, vacc.ageMonths * 30);
activities.push({
id: generateId('act'),
child_id: childId,
type: 'medicine',
started_at: vaccDate,
ended_at: vaccDate,
logged_by: USER_ID,
notes: 'Well-child visit',
metadata: {
medicationType: vacc.type,
name: vacc.name,
dosage: '',
},
created_at: vaccDate,
updated_at: vaccDate,
});
}
// Occasional fever medication
const feverMedicineMonths = [];
for (let month = 6; month <= ageMonths; month += random(4, 8)) {
feverMedicineMonths.push(month);
}
for (const month of feverMedicineMonths) {
const medicineDate = addDays(birthDate, month * 30 + random(0, 29));
activities.push({
id: generateId('act'),
child_id: childId,
type: 'medicine',
started_at: medicineDate,
ended_at: medicineDate,
logged_by: USER_ID,
notes: 'Fever reducer',
metadata: {
medicationType: 'medication',
name: choice(['Infant Tylenol', 'Infant Ibuprofen']),
dosage: '2.5ml',
},
created_at: medicineDate,
updated_at: medicineDate,
});
}
return activities;
}
// Generate SQL INSERT statements
function generateSQLInserts(activities) {
const sqlStatements = [];
for (const activity of activities) {
const startedAt = activity.started_at.toISOString();
const endedAt = activity.ended_at ? activity.ended_at.toISOString() : startedAt;
const createdAt = activity.created_at.toISOString();
const updatedAt = activity.updated_at.toISOString();
const metadataJson = JSON.stringify(activity.metadata).replace(/'/g, "''");
const notes = (activity.notes || '').replace(/'/g, "''");
const sql = `INSERT INTO activities (id, child_id, type, started_at, ended_at, logged_by, notes, metadata, created_at, updated_at) VALUES ('${activity.id}', '${activity.child_id}', '${activity.type}', '${startedAt}', '${endedAt}', '${activity.logged_by}', '${notes}', '${metadataJson}', '${createdAt}', '${updatedAt}');`;
sqlStatements.push(sql);
}
return sqlStatements;
}
// Main execution
async function main() {
console.log('Starting demo data generation for ParentFlow...\n');
// Generate data for Alice (5 years old)
console.log('=== Generating data for Alice (5 years old) ===');
const aliceFeeding = generateFeedingActivities(ALICE.id, ALICE.birthDate, ALICE.ageMonths);
const aliceSleep = generateSleepActivities(ALICE.id, ALICE.birthDate, ALICE.ageMonths);
const aliceDiaper = generateDiaperActivities(ALICE.id, ALICE.birthDate, ALICE.ageMonths);
const aliceGrowth = generateGrowthMeasurements(ALICE.id, ALICE.birthDate, ALICE.ageMonths, ALICE.gender);
const aliceMedicine = generateMedicineActivities(ALICE.id, ALICE.birthDate, ALICE.ageMonths);
console.log(`\nAlice activities generated:
- Feeding: ${aliceFeeding.length}
- Sleep: ${aliceSleep.length}
- Diaper: ${aliceDiaper.length}
- Growth: ${aliceGrowth.length}
- Medicine: ${aliceMedicine.length}
- Total: ${aliceFeeding.length + aliceSleep.length + aliceDiaper.length + aliceGrowth.length + aliceMedicine.length}
`);
// Generate data for Robert (8 months old)
console.log('\n=== Generating data for Robert (8 months old) ===');
const robertFeeding = generateFeedingActivities(ROBERT.id, ROBERT.birthDate, ROBERT.ageMonths);
const robertSleep = generateSleepActivities(ROBERT.id, ROBERT.birthDate, ROBERT.ageMonths);
const robertDiaper = generateDiaperActivities(ROBERT.id, ROBERT.birthDate, ROBERT.ageMonths);
const robertGrowth = generateGrowthMeasurements(ROBERT.id, ROBERT.birthDate, ROBERT.ageMonths, ROBERT.gender);
const robertMedicine = generateMedicineActivities(ROBERT.id, ROBERT.birthDate, ROBERT.ageMonths);
console.log(`\nRobert activities generated:
- Feeding: ${robertFeeding.length}
- Sleep: ${robertSleep.length}
- Diaper: ${robertDiaper.length}
- Growth: ${robertGrowth.length}
- Medicine: ${robertMedicine.length}
- Total: ${robertFeeding.length + robertSleep.length + robertDiaper.length + robertGrowth.length + robertMedicine.length}
`);
// Combine all activities
const allActivities = [
...aliceFeeding, ...aliceSleep, ...aliceDiaper, ...aliceGrowth, ...aliceMedicine,
...robertFeeding, ...robertSleep, ...robertDiaper, ...robertGrowth, ...robertMedicine,
];
console.log(`\n=== Total activities to insert: ${allActivities.length} ===\n`);
// Generate SQL
const sqlStatements = generateSQLInserts(allActivities);
// Save to file
const fs = require('fs');
const sqlFile = '/root/maternal-app/scripts/demo-data.sql';
fs.writeFileSync(sqlFile, '-- Demo data for ParentFlow\n');
fs.writeFileSync(sqlFile, '-- Generated: ' + new Date().toISOString() + '\n\n', { flag: 'a' });
fs.writeFileSync(sqlFile, 'BEGIN;\n\n', { flag: 'a' });
for (const sql of sqlStatements) {
fs.writeFileSync(sqlFile, sql + '\n', { flag: 'a' });
}
fs.writeFileSync(sqlFile, '\nCOMMIT;\n', { flag: 'a' });
console.log(`SQL file saved to ${sqlFile}`);
console.log(`\nTo import: docker exec maternal-postgres psql -U maternal_user -d maternal_app -f /demo-data.sql`);
}
main().catch(console.error);