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>
This commit is contained in:
486
scripts/generate-demo-data.ts
Normal file
486
scripts/generate-demo-data.ts
Normal 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();
|
||||
Reference in New Issue
Block a user