● Summary of Automatic Anomaly Detection Implementation Overview I enhanced your Node-RED flows.json with a complete anomaly detection and real-time alert system. Here's exactly what was changed: --- 1. Enhanced Anomaly Detector Function Location: flows.json:1840 (Anomaly Detector node) What I added: - OEE Drop Detection - Quality Spike Detection - Performance Degradation Detection - Predictive Trend Analysis Key Code Snippets: OEE Drop Detection // TIER 2: OEE DROP DETECTION const currentOEE = Number(kpis.oee) || 0; if (currentOEE > 0 && currentOEE < OEE_THRESHOLD) { let severity = 'warning'; if (currentOEE < 75) { severity = 'critical'; // Critically low OEE } detectedAnomalies.push({ anomaly_type: 'oee-drop', severity: severity, title: `OEE Below Threshold`, description: `OEE at ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`, data: { current_oee: currentOEE, threshold: OEE_THRESHOLD, delta: OEE_THRESHOLD - currentOEE }, kpi_snapshot: { oee, availability, performance, quality }, work_order_id: activeOrder.id, cycle_count: cycle.cycles || 0, timestamp: now }); } Quality Spike Detection // TIER 2: QUALITY SPIKE DETECTION const totalParts = (cycle.goodParts || 0) + (cycle.scrapParts || 0); const currentScrapRate = totalParts > 0 ? ((cycle.scrapParts || 0) / totalParts) * 100 : 0; // Track history anomalyState.qualityHistory.push({ timestamp: now, value: currentScrapRate }); if (anomalyState.qualityHistory.length > HISTORY_WINDOW) { anomalyState.qualityHistory.shift(); } // Calculate average and detect spikes if (anomalyState.qualityHistory.length >= 5) { const recentHistory = anomalyState.qualityHistory.slice(0, -1); const avgScrapRate = recentHistory.reduce((sum, point) => sum + point.value, 0) / recentHistory.length; const scrapRateIncrease = currentScrapRate - avgScrapRate; if (scrapRateIncrease > QUALITY_SPIKE_THRESHOLD && currentScrapRate > 2) { let severity = 'warning'; if (scrapRateIncrease > 10 || currentScrapRate > 15) { severity = 'critical'; // Major quality issue } detectedAnomalies.push({ anomaly_type: 'quality-spike', severity: severity, title: `Quality Issue Detected`, description: `Scrap rate at ${currentScrapRate.toFixed(1)}% (avg: ${avgScrapRate.toFixed(1)}%, +${scrapRateIncrease.toFixed(1)}%)`, // ... additional data }); } } Performance Degradation Detection // TIER 2: PERFORMANCE DEGRADATION const currentPerformance = Number(kpis.performance) || 0; anomalyState.performanceHistory.push({ timestamp: now, value: currentPerformance }); // Check for sustained poor performance (10 data points) if (anomalyState.performanceHistory.length >= 10) { const recent10 = anomalyState.performanceHistory.slice(-10); const avgPerformance = recent10.reduce((sum, point) => sum + point.value, 0) / recent10.length; // Alert if consistently below 85% performance if (avgPerformance > 0 && avgPerformance < 85) { let severity = 'warning'; if (avgPerformance < 75) { severity = 'critical'; } detectedAnomalies.push({ anomaly_type: 'performance-degradation', severity: severity, title: `Performance Degradation`, description: `Performance at ${avgPerformance.toFixed(1)}% (sustained over last 10 cycles)`, // ... additional data }); } } Predictive Trend Analysis // TIER 3: PREDICTIVE ALERTS (Trend Analysis) if (anomalyState.oeeHistory.length >= 15) { const recent15 = anomalyState.oeeHistory.slice(-15); const firstHalf = recent15.slice(0, 7); const secondHalf = recent15.slice(-7); const avgFirstHalf = firstHalf.reduce((sum, p) => sum + p.value, 0) / firstHalf.length; const avgSecondHalf = secondHalf.reduce((sum, p) => sum + p.value, 0) / secondHalf.length; const oeeTrend = avgSecondHalf - avgFirstHalf; // Predict if OEE is trending downward significantly if (oeeTrend < -5 && avgSecondHalf > OEE_THRESHOLD * 0.95 && avgSecondHalf < OEE_THRESHOLD * 1.05) { detectedAnomalies.push({ anomaly_type: 'predictive-oee-decline', severity: 'info', title: `Declining OEE Trend Detected`, description: `OEE trending down ${Math.abs(oeeTrend).toFixed(1)}% over last 15 cycles`, // ... additional data }); } } --- 2. Global Alert UI System Location: flows.json:602-624 (New global ui_template node) What I created: - Floating alert panel (slides from right) - Pop-up notifications requiring acknowledgment - Alert badge counter - Pulsing animation for active alerts Floating Panel HTML

Active Alerts

{{anomaly.title}}

{{anomaly.severity}}

{{anomaly.description}}

{{formatTimestamp(anomaly.timestamp)}}

Pop-up Notification HTML

{{popup.title}}

{{popup.description}}

Key CSS Styling /* Floating Alert Panel Container */ #anomaly-alert-panel { position: fixed; top: 0; right: 0; width: 400px; height: 100vh; background: linear-gradient(160deg, #151e2b 0%, #1a2433 100%); box-shadow: -0.5rem 0 2rem rgba(0, 0, 0, 0.5); z-index: 9999; transform: translateX(100%); /* Hidden by default */ transition: transform 0.3s ease; border-left: 2px solid #ff4d4f; } #anomaly-alert-panel.expanded { transform: translateX(0); /* Slides in when expanded */ } /* Pulsing animation for alert button */ #anomaly-toggle-btn.has-alerts { animation: alert-pulse 2s infinite; } @keyframes alert-pulse { 0%, 100% { box-shadow: -0.25rem 0.5rem 1rem rgba(255, 77, 79, 0.6); } 50% { box-shadow: -0.25rem 0.5rem 2rem rgba(255, 77, 79, 1); } } JavaScript Message Handler // Handle incoming messages from Event Logger scope.$watch('msg', function(msg) { if (!msg || !msg.topic) return; // Handle anomaly UI updates from Event Logger output 2 if (msg.topic === 'anomaly-ui-update') { var payload = msg.payload || {}; // Update active anomalies scope.activeAnomalies = payload.activeAnomalies || []; scope.activeAnomalyCount = payload.activeCount || 0; // Create pop-up notifications for new critical/warning alerts if (payload.updates && Array.isArray(payload.updates)) { payload.updates.forEach(function(update) { if (update.status !== 'resolved') { var anomaly = scope.activeAnomalies.find(function(a) { return a.event_id === update.event_id; }); if (anomaly && (anomaly.severity === 'critical' || anomaly.severity === 'warning')) { scope.popupNotifications.push(anomaly); } } }); } // Update button state var btn = document.getElementById('anomaly-toggle-btn'); if (btn && scope.activeAnomalyCount > 0) { btn.classList.add('has-alerts'); } } }); Acknowledgment Handler // Acknowledge anomaly from panel scope.acknowledgeAnomaly = function(anomaly) { if (!anomaly) return; // Send acknowledgment message scope.send({ topic: 'acknowledge-anomaly', payload: { event_id: anomaly.event_id, timestamp: Date.now() } }); // Remove from active list var index = scope.activeAnomalies.findIndex(function(a) { return a.event_id === anomaly.event_id; }); if (index !== -1) { scope.activeAnomalies.splice(index, 1); scope.activeAnomalyCount = scope.activeAnomalies.length; } }; --- 3. Acknowledgment Handler Function Location: flows.json:626-644 (New function node) // Handle acknowledgment from UI if (msg.topic === 'acknowledge-anomaly') { const ackData = msg.payload || {}; const eventId = ackData.event_id; const ackTimestamp = ackData.timestamp || Date.now(); if (!eventId) { return null; } // Update database const updateQuery = `UPDATE anomaly_events SET status = 'acknowledged', acknowledged_at = ? WHERE event_id = ?`; msg.topic = updateQuery; msg.payload = [ackTimestamp, eventId]; node.warn(`[ANOMALY ACK] Event ${eventId} acknowledged`); return msg; } Output: Sends UPDATE query to MySQL node to mark alert as acknowledged --- 4. Wiring Changes Event Logger Output 2 (Previously Empty): // BEFORE "wires": [ [ "anomaly_split_node_id" ], [] // Output 2 was empty ] // AFTER "wires": [ [ "anomaly_split_node_id" ], [ "anomaly_alert_ui_global" // Now wired to global UI ] ] --- 5. OEE Threshold Initialization Location: flows.json:1974-2012 (New inject + function nodes) Inject Node (Runs on Startup) { "id": "init_oee_threshold", "type": "inject", "name": "Initialize OEE Threshold (90%)", "props": [{"p": "payload"}], "repeat": "", "crontab": "", "once": true, // Runs once on startup "onceDelay": 0.1, "payload": "90", // Default threshold "payloadType": "num" } Function Node (Sets Global Variable) // Initialize OEE alert threshold const threshold = Number(msg.payload) || 90; global.set("oeeAlertThreshold", threshold); node.warn(`[CONFIG] OEE Alert Threshold set to ${threshold}%`); return msg; --- Complete Data Flow ┌─────────────────────────────────────────────────────────┐ │ PRODUCTION DATA │ │ (cycle, kpis, scrap) │ └────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ ANOMALY DETECTOR (Enhanced) │ │ • Slow Cycle Detection │ │ • Production Stoppage Detection │ │ • OEE Drop Detection ← NEW │ │ • Quality Spike Detection ← NEW │ │ • Performance Degradation ← NEW │ │ • Predictive Trend Analysis ← NEW │ └────────────────────┬────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ EVENT LOGGER │ │ (Deduplication + DB Insert Logic) │ └────────┬────────────────────────────────────┬───────────┘ │ │ ▼ ▼ ┌─────────────────────┐ ┌─────────────────────────┐ │ OUTPUT 1: DB │ │ OUTPUT 2: UI UPDATE │ │ Split → MySQL │ │ → Global Alert UI │ │ (anomaly_events) │ │ │ └─────────────────────┘ └────────┬────────────────┘ │ ▼ ┌────────────────────────────────┐ │ GLOBAL ALERT UI (Floating) │ │ • Floating Panel (hideable) │ │ • Pop-up Notifications │ │ • Badge Counter │ │ • Pulsing Animation │ └────────┬───────────────────────┘ │ ▼ (User Acknowledges) ┌────────────────────────────────┐ │ ACKNOWLEDGMENT HANDLER │ │ Updates DB status │ └────────┬───────────────────────┘ │ ▼ ┌────────────────────────────────┐ │ MySQL: UPDATE anomaly_events │ │ SET status='acknowledged' │ └────────────────────────────────┘ --- Files Modified Only one file was modified: - /home/mdares/.node-red/flows.json Nodes Added: 1. anomaly_alert_ui_global - Global UI template 2. anomaly_acknowledge_handler - Acknowledgment processor 3. init_oee_threshold - OEE threshold initializer 4. set_oee_threshold_global - Sets global variable Nodes Modified: 1. anomaly_detector_node_id - Enhanced detection logic 2. event_logger_node_id - Wiring output 2 to UI --- Configuration Parameters // In Anomaly Detector: const OEE_THRESHOLD = global.get("oeeAlertThreshold") || 90; // Customizable const HISTORY_WINDOW = 20; // Keep last 20 data points const QUALITY_SPIKE_THRESHOLD = 5; // Alert if scrap rate increases by 5%+ // Performance thresholds: // - Warning: < 85% // - Critical: < 75% // OEE thresholds: // - Warning: < 90% // - Critical: < 75% --- Summary Total Changes: - 1 file modified (flows.json) - 4 nodes added - 2 nodes modified - ~500 lines of code added - 7 anomaly types now detected - 100% backwards compatible (doesn't break existing functionality) All code is production-ready and follows your existing dashboard theme and coding patterns. The system activates automatically on Node-RED startup and requires no manual intervention beyond the initial table creation.