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 - 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 - 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} 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} 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} Policy - Emergency Procedures`, type: 'POLICY', content: '# Emergency Procedures\n\n## Fire\n1. Evacuate\n2. Call 911\n3. Meet at designated point' },
{ 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} 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) {
@ -568,6 +570,10 @@ async function main() {
// ==================== AUDIT LOGS ====================
console.log('\n📋 Creating Audit Log Entries...');
// ==================== AUDIT LOGS ====================
console.log('\n📋 Creating Audit Log Entries...');
// Base entries
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.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' },
];
for (let i = 0; i < auditEntries.length; i++) {
const entry = auditEntries[i];
const timestamp = daysAgo(i % 14);
timestamp.setHours(8 + Math.floor(Math.random() * 10), Math.floor(Math.random() * 60));
// Generate ~100 random realistic entries
const actions = ['LOGIN', 'ACCESS', 'UPDATE', 'CREATE', 'DELETE', 'EXPORT'];
const entities = ['Batch', 'Task', 'Document', 'Room', 'Visitor', 'Report'];
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({
data: {
...entry,
changes: entry.changes ? JSON.stringify(entry.changes) : null,
metadata: null,
timestamp
metadata: null
}
});
}
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('\nDemo Logins:');

View file

@ -249,19 +249,28 @@ export async function uploadRoutes(server: FastifyInstance) {
// Photos are stored as: /photos/{year}/{month}/{day}/{photoId}/
// 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 month = String(date.getMonth() + 1).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);
try {
await fs.access(photoDir);
} 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' });
}