## 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>
486 lines
16 KiB
JavaScript
486 lines
16 KiB
JavaScript
/**
|
|
* 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);
|