Files
maternal-app/scripts/insert-demo-data.js
Andrei 34b8466004
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
fix: Critical bug fixes for AI chat and children authorization
## 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>
2025-10-06 10:55:25 +00:00

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);