polish: Enhance demo data for Audit Logs and SOPs, fix photo delete bug
Some checks are pending
Deploy to Production / deploy (push) Waiting to run
Test / backend-test (push) Waiting to run
Test / frontend-test (push) Waiting to run

- Seed script now generates 100+ realistic audit logs
- Added Policy, Training, Form, and Checklist document types to seed data
- Fixed bug in photo deletion logic for older photos
This commit is contained in:
fullsizemalt 2025-12-19 15:05:40 -08:00
parent 337f1e6adb
commit bfa3cd21cc
2 changed files with 67 additions and 15 deletions

View file

@ -409,10 +409,12 @@ async function main() {
{ title: `${DEMO_PREFIX} SOP - Nutrient Mixing`, type: 'SOP', content: '# Nutrient Mixing (Front Row Ag)\n\n## Stock Preparation\n1. Part A: 24lbs per 16 gal\n2. Part B: 16lbs + PhosZyme per 16 gal' }, { title: `${DEMO_PREFIX} SOP - Nutrient Mixing`, type: 'SOP', content: '# Nutrient Mixing (Front Row Ag)\n\n## Stock Preparation\n1. Part A: 24lbs per 16 gal\n2. Part B: 16lbs + PhosZyme per 16 gal' },
{ title: `${DEMO_PREFIX} SOP - Clone Processing`, type: 'SOP', content: '# Clone Processing\n\n## Receiving\n1. Inspect for pests\n2. Count and verify\n3. Log in METRC' }, { title: `${DEMO_PREFIX} SOP - Clone Processing`, type: 'SOP', content: '# Clone Processing\n\n## Receiving\n1. Inspect for pests\n2. Count and verify\n3. Log in METRC' },
{ title: `${DEMO_PREFIX} SOP - Harvest Procedure`, type: 'SOP', content: '# Harvest Procedure\n\n## Steps\n1. Cut main stem\n2. Hang dry in Dry Room\n3. Monitor temp/humidity\n4. Trim when stems snap' }, { title: `${DEMO_PREFIX} SOP - Harvest Procedure`, type: 'SOP', content: '# Harvest Procedure\n\n## Steps\n1. Cut main stem\n2. Hang dry in Dry Room\n3. Monitor temp/humidity\n4. Trim when stems snap' },
{ title: `${DEMO_PREFIX} SOP - Visitor Check-In`, type: 'POLICY', content: '# Visitor Check-In\n\n## Requirements\n1. Valid ID\n2. Signed NDA\n3. PPE issued' }, { title: `${DEMO_PREFIX} Policy - Visitor Check-In`, type: 'POLICY', content: '# Visitor Check-In\n\n## Requirements\n1. Valid ID\n2. Signed NDA\n3. PPE issued' },
{ title: `${DEMO_PREFIX} SOP - Emergency Procedures`, type: 'POLICY', content: '# Emergency Procedures\n\n## Fire\n1. Evacuate\n2. Call 911\n3. Meet at designated point' }, { title: `${DEMO_PREFIX} Policy - Emergency Procedures`, type: 'POLICY', content: '# Emergency Procedures\n\n## Fire\n1. Evacuate\n2. Call 911\n3. Meet at designated point' },
{ title: `${DEMO_PREFIX} HVAC Maintenance`, type: 'TRAINING', content: '# HVAC Maintenance\n\n## Quarterly Tasks\n- Replace filters\n- Check refrigerant\n- Clean coils' }, { title: `${DEMO_PREFIX} Training - HVAC Maintenance`, type: 'TRAINING', content: '# HVAC Maintenance\n\n## Quarterly Tasks\n- Replace filters\n- Check refrigerant\n- Clean coils' },
{ title: `${DEMO_PREFIX} METRC Training Guide`, type: 'TRAINING', content: '# METRC Training\n\n## Logging Plants\n1. Scan package tag\n2. Enter plant count\n3. Verify location' }, { title: `${DEMO_PREFIX} Training - METRC Guide`, type: 'TRAINING', content: '# METRC Training\n\n## Logging Plants\n1. Scan package tag\n2. Enter plant count\n3. Verify location' },
{ title: `${DEMO_PREFIX} Form - Incident Report`, type: 'FORM', content: '# Incident Report Form\n\n**Date:** ______\n**Location:** ______\n\n## Description of Incident\n(Please describe what happened, who was involved, and any immediate actions taken)\n\n\n## Witness Statements\n\n' },
{ title: `${DEMO_PREFIX} Checklist - Harvest Cleaning`, type: 'CHECKLIST', content: '# Harvest Cleaning Checklist\n\n- [ ] Remove all plant debris\n- [ ] Wipe down walls with H2O2\n- [ ] Clean and Sanitize trays\n- [ ] Sweep and Mop floors\n- [ ] Empty dehumidifiers' },
]; ];
for (const doc of documents) { for (const doc of documents) {
@ -568,6 +570,10 @@ async function main() {
// ==================== AUDIT LOGS ==================== // ==================== AUDIT LOGS ====================
console.log('\n📋 Creating Audit Log Entries...'); console.log('\n📋 Creating Audit Log Entries...');
// ==================== AUDIT LOGS ====================
console.log('\n📋 Creating Audit Log Entries...');
// Base entries
const auditEntries = [ const auditEntries = [
{ userId: users.admin?.id, userName: 'Admin User', action: 'LOGIN', entity: 'User', entityName: 'Admin login from office', ipAddress: '192.168.1.50' }, { userId: users.admin?.id, userName: 'Admin User', action: 'LOGIN', entity: 'User', entityName: 'Admin login from office', ipAddress: '192.168.1.50' },
{ userId: users.sarah?.id, userName: 'Sarah Chen', action: 'CREATE', entity: 'Batch', entityId: Object.values(batches)[0]?.id, entityName: 'Created batch: Gorilla Glue #4 - B001' }, { userId: users.sarah?.id, userName: 'Sarah Chen', action: 'CREATE', entity: 'Batch', entityId: Object.values(batches)[0]?.id, entityName: 'Created batch: Gorilla Glue #4 - B001' },
@ -586,21 +592,58 @@ async function main() {
{ userId: users.admin?.id, userName: 'Admin User', action: 'UPDATE', entity: 'Role', entityName: 'Updated permissions for Grower role' }, { userId: users.admin?.id, userName: 'Admin User', action: 'UPDATE', entity: 'Role', entityName: 'Updated permissions for Grower role' },
]; ];
for (let i = 0; i < auditEntries.length; i++) { // Generate ~100 random realistic entries
const entry = auditEntries[i]; const actions = ['LOGIN', 'ACCESS', 'UPDATE', 'CREATE', 'DELETE', 'EXPORT'];
const timestamp = daysAgo(i % 14); const entities = ['Batch', 'Task', 'Document', 'Room', 'Visitor', 'Report'];
timestamp.setHours(8 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 60)); const demoStaff = Object.values(users);
for (let i = 0; i < 120; i++) {
const user = demoStaff[Math.floor(Math.random() * demoStaff.length)];
const action = actions[Math.floor(Math.random() * actions.length)];
let entity = entities[Math.floor(Math.random() * entities.length)];
let entityName = `${entity} #${1000 + i}`;
let changes = null;
if (action === 'LOGIN') {
entity = 'User';
entityName = 'User Login';
} else if (action === 'ACCESS') {
entityName = `Accessed ${entity} details`;
} else if (action === 'UPDATE') {
changes = { status: { from: 'PENDING', to: 'COMPLETED' } };
entityName = `Updated ${entity} status`;
}
const daysBack = Math.floor(Math.random() * 30);
const timestamp = daysAgo(daysBack);
timestamp.setHours(8 + Math.floor(Math.random() * 12), Math.floor(Math.random() * 60)); // Business hours
auditEntries.push({
userId: user.id,
userName: user.name.replace(DEMO_PREFIX + ' ', ''),
action,
entity,
entityName,
changes,
timestamp,
ipAddress: `192.168.1.${50 + Math.floor(Math.random() * 100)}`
});
}
// Sort by timestamp desc
auditEntries.sort((a, b) => b.timestamp - a.timestamp);
for (const entry of auditEntries) {
await prisma.auditLog.create({ await prisma.auditLog.create({
data: { data: {
...entry, ...entry,
changes: entry.changes ? JSON.stringify(entry.changes) : null, changes: entry.changes ? JSON.stringify(entry.changes) : null,
metadata: null, metadata: null
timestamp
} }
}); });
} }
console.log(`${auditEntries.length} audit log entries`); console.log(`${auditEntries.length} audit log entries (15 fixed + 120 generated)`);
console.log('\n✨ Demo seeding complete!'); console.log('\n✨ Demo seeding complete!');
console.log('\nDemo Logins:'); console.log('\nDemo Logins:');

View file

@ -249,19 +249,28 @@ export async function uploadRoutes(server: FastifyInstance) {
// Photos are stored as: /photos/{year}/{month}/{day}/{photoId}/ // Photos are stored as: /photos/{year}/{month}/{day}/{photoId}/
// We need to search for it // We need to search for it
const date = new Date(); // Parse date from photoId (format: photo_TIMESTAMP_HASH)
const parts = photoId.split('_');
if (parts.length < 2) {
return reply.status(400).send({ error: 'Invalid photo ID format' });
}
const timestamp = parseInt(parts[1]);
const date = new Date(timestamp);
if (isNaN(date.getTime())) {
return reply.status(400).send({ error: 'Invalid photo timestamp' });
}
const year = date.getFullYear(); const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0');
// Try today's date first (most common case)
let photoDir = path.join(STORAGE_PATH, `${year}/${month}/${day}`, photoId); let photoDir = path.join(STORAGE_PATH, `${year}/${month}/${day}`, photoId);
try { try {
await fs.access(photoDir); await fs.access(photoDir);
} catch { } catch {
// Photo not found in today's folder - would need to search
// For now, return not found
return reply.status(404).send({ error: 'Photo not found' }); return reply.status(404).send({ error: 'Photo not found' });
} }