diff --git a/backend/prisma/seed-demo.js b/backend/prisma/seed-demo.js index 5ecf9c2..2f89ed3 100644 --- a/backend/prisma/seed-demo.js +++ b/backend/prisma/seed-demo.js @@ -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:'); diff --git a/backend/src/routes/upload.routes.ts b/backend/src/routes/upload.routes.ts index ed056ad..94315bc 100644 --- a/backend/src/routes/upload.routes.ts +++ b/backend/src/routes/upload.routes.ts @@ -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' }); }