/** * ALWAS ML-Enhanced Bottleneck Scanner * Drop-in replacement for the existing node-cron bottleneck scanner. * Uses ML models instead of simple 48h threshold. * * Place in: server/cron/mlBottleneckScanner.js * * Setup in server/app.js: * const cron = require('node-cron'); * const mlScanner = require('./cron/mlBottleneckScanner'); * cron.schedule('0 * * * *', () => mlScanner(io)); // hourly */ const ml = require('../utils/alwas-ml-client'); const Block = require('../models/Block'); const Notification = require('../models/Notification'); const User = require('../models/User'); async function mlBottleneckScanner(io) { console.log('[ML Scanner] Starting hourly bottleneck scan...'); try { // Get all in-progress blocks const blocks = await Block.find({ status: { $nin: ['Not Started', 'Completed'] } }).populate('assignedTo'); let highRisk = 0; let mediumRisk = 0; let alerts = []; for (const block of blocks) { try { // Calculate days in current stage const lastTransition = block.transitions?.[block.transitions.length - 1]; const daysSince = lastTransition ? (Date.now() - new Date(lastTransition.timestamp)) / (1000 * 60 * 60 * 24) : 0; const riskData = ml.constructor.formatForBottleneck(block, daysSince); const risk = await ml.predictBottleneck(riskData); if (risk.risk_level === 'High') { highRisk++; // Create notification for assigned engineer if (block.assignedTo) { const notification = await Notification.create({ user: block.assignedTo._id || block.assignedTo, type: 'stuck', message: `⚠️ ML Alert: ${block.name} has HIGH bottleneck risk`, data: { blockId: block._id, risk: risk.risk_level, confidence: risk.confidence, recommendations: risk.recommendations, hours_over_budget: risk.hours_over_budget_ratio, } }); // Real-time socket notification io.emit('newNotification', { userId: block.assignedTo._id || block.assignedTo, notification: notification, }); } // Also notify managers const managers = await User.find({ role: 'manager' }); for (const manager of managers) { await Notification.create({ user: manager._id, type: 'stuck', message: `🔴 ML Bottleneck Alert: ${block.name} (${block.status}) — ${risk.recommendations[0] || 'High risk detected'}`, data: { blockId: block._id, risk: risk.risk_level, confidence: risk.confidence, recommendations: risk.recommendations, engineer: block.assignedTo?.name || 'Unassigned', } }); } alerts.push({ block: block.name, stage: block.status, risk: risk.risk_level, confidence: risk.confidence, reason: risk.recommendations[0] || 'High risk', }); } else if (risk.risk_level === 'Medium') { mediumRisk++; } } catch (blockError) { console.error(`[ML Scanner] Error scanning block ${block._id}:`, blockError.message); } } console.log(`[ML Scanner] Scan complete: ${blocks.length} blocks scanned, ${highRisk} high risk, ${mediumRisk} medium risk`); if (alerts.length > 0) { console.log('[ML Scanner] High-risk blocks:'); alerts.forEach(a => console.log(` - ${a.block} (${a.stage}): ${a.reason}`)); } return { scanned: blocks.length, highRisk, mediumRisk, alerts }; } catch (error) { console.error('[ML Scanner] Fatal error:', error.message); return { error: error.message }; } } module.exports = mlBottleneckScanner;