  1880→    {
  1881→        "id": "anomaly_detector_node_id",
  1882→        "type": "function",
  1883→        "z": "cac3a4383120cb57",
  1884→        "name": "Anomaly Detector",
  1885→        "func": "// ============================================================\n// ANOMALY DETECTOR - Enhanced with Tier 1 & 2 Detections\n// Detects production anomalies in real-time\n// ============================================================\n\nconst cycle = msg.cycle || {};\nconst kpis = msg.kpis || {};\nconst activeOrder = global.get(\"activeWorkOrder\") || {};\n\n// Must have active work order to detect anomalies\nif (!activeOrder.id) {\n    return null;\n}\n\nconst theoreticalCycleTime = Number(activeOrder.cycleTime) || 0;\nconst now = Date.now();\n\n// Get or initialize anomaly tracking state\nlet anomalyState = global.get(\"anomalyState\") || {\n    lastCycleTime: now,\n    activeStoppageEvent: null,\n    oeeHistory: [],\n    performanceHistory: [],\n    qualityHistory: []\n};\n\n// Configuration\nconst OEE_THRESHOLD = global.get(\"oeeAlertThreshold\") || 90;  // Customizable in settings\nconst HISTORY_WINDOW = 20;  // Keep last 20 data points for trend analysis\nconst QUALITY_SPIKE_THRESHOLD = 5;  // Alert if scrap rate increases by 5%+\n\nconst detectedAnomalies = [];\n\n// ============================================================\n// TIER 1: SLOW CYCLE DETECTION\n// Trigger: Actual cycle time > 1.5x theoretical\n// ============================================================\nif (theoreticalCycleTime > 0) {\n    const timeSinceLastCycle = now - anomalyState.lastCycleTime;\n    const actualCycleTime = timeSinceLastCycle / 1000; // Convert to seconds\n    const threshold = theoreticalCycleTime * 1.5;\n\n    if (actualCycleTime > threshold && anomalyState.lastCycleTime > 0) {\n        const deltaPercent = ((actualCycleTime - theoreticalCycleTime) / theoreticalCycleTime) * 100;\n\n        // Determine severity\n        let severity = 'warning';\n        if (actualCycleTime > theoreticalCycleTime * 2.0) {\n            severity = 'critical';  // 100%+ slower\n        }\n\n        detectedAnomalies.push({\n            anomaly_type: 'slow-cycle',\n            severity: severity,\n            title: `Slow Cycle Detected`,\n            description: `Cycle took ${actualCycleTime.toFixed(1)}s (${deltaPercent.toFixed(0)}% slower than expected ${theoreticalCycleTime}s)`,\n            data: {\n                actual_cycle_time: actualCycleTime,\n                theoretical_cycle_time: theoreticalCycleTime,\n                delta_percent: Math.round(deltaPercent),\n                threshold_multiplier: actualCycleTime / theoreticalCycleTime\n            },\n            kpi_snapshot: {\n                oee: kpis.oee || 0,\n                availability: kpis.availability || 0,\n                performance: kpis.performance || 0,\n                quality: kpis.quality || 0\n            },\n            work_order_id: activeOrder.id,\n            cycle_count: cycle.cycles || 0,\n            timestamp: now\n        });\n\n        node.warn(`[ANOMALY] Slow cycle: ${actualCycleTime.toFixed(1)}s (expected ${theoreticalCycleTime}s)`);\n    }\n}\n\n// ============================================================\n// TIER 1: PRODUCTION STOPPAGE DETECTION\n// Trigger: No cycle in > 3x theoretical cycle time\n// ============================================================\nif (theoreticalCycleTime > 0) {\n    const timeSinceLastCycle = now - anomalyState.lastCycleTime;\n    const stoppageThreshold = theoreticalCycleTime * 3 * 1000; // Convert to ms\n\n    // If we have an active stoppage event and a new cycle arrived, resolve it\n    if (anomalyState.activeStoppageEvent) {\n        // Cycle resumed - mark stoppage as resolved\n        anomalyState.activeStoppageEvent.resolved_at = now;\n        anomalyState.activeStoppageEvent.auto_resolved = true;\n        anomalyState.activeStoppageEvent.status = 'resolved';\n\n        const stoppageDuration = (now - anomalyState.activeStoppageEvent.timestamp) / 1000;\n        node.warn(`[ANOMALY] Production resumed after ${stoppageDuration.toFixed(0)}s stoppage`);\n\n        // Send resolution event\n        detectedAnomalies.push(anomalyState.activeStoppageEvent);\n        anomalyState.activeStoppageEvent = null;\n    }\n\n    // Check if production has stopped (only if no active stoppage event)\n    if (!anomalyState.activeStoppageEvent && timeSinceLastCycle > stoppageThreshold && anomalyState.lastCycleTime > 0) {\n        const stoppageSeconds = timeSinceLastCycle / 1000;\n\n        // Determine severity\n        let severity = 'warning';\n        if (stoppageSeconds > theoreticalCycleTime * 5) {\n            severity = 'critical';  // Stopped for 5x+ theoretical time\n        }\n\n        const stoppageEvent = {\n            anomaly_type: 'production-stopped',\n            severity: severity,\n            title: `Production Stoppage`,\n            description: `No cycles detected for ${stoppageSeconds.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,\n            data: {\n                stoppage_duration_seconds: Math.round(stoppageSeconds),\n                theoretical_cycle_time: theoreticalCycleTime,\n                last_cycle_timestamp: anomalyState.lastCycleTime,\n                threshold_multiplier: stoppageSeconds / theoreticalCycleTime\n            },\n            kpi_snapshot: {\n                oee: kpis.oee || 0,\n                availability: kpis.availability || 0,\n                performance: kpis.performance || 0,\n                quality: kpis.quality || 0\n            },\n            work_order_id: activeOrder.id,\n            cycle_count: cycle.cycles || 0,\n            timestamp: now,\n            status: 'active'\n        };\n\n        detectedAnomalies.push(stoppageEvent);\n        anomalyState.activeStoppageEvent = stoppageEvent;\n\n        node.warn(`[ANOMALY] Production stopped: ${stoppageSeconds.toFixed(0)}s since last cycle`);\n    }\n}\n\n// ============================================================\n// TIER 2: OEE DROP DETECTION\n// Trigger: OEE falls below threshold\n// ============================================================\nconst currentOEE = Number(kpis.oee) || 0;\nif (currentOEE > 0 && currentOEE < OEE_THRESHOLD) {\n    let severity = 'warning';\n    if (currentOEE < 75) {\n        severity = 'critical';  // Critically low OEE\n    }\n\n    detectedAnomalies.push({\n        anomaly_type: 'oee-drop',\n        severity: severity,\n        title: `OEE Below Threshold`,\n        description: `OEE at ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`,\n        data: {\n            current_oee: currentOEE,\n            threshold: OEE_THRESHOLD,\n            delta: OEE_THRESHOLD - currentOEE\n        },\n        kpi_snapshot: {\n            oee: kpis.oee || 0,\n            availability: kpis.availability || 0,\n            performance: kpis.performance || 0,\n            quality: kpis.quality || 0\n        },\n        work_order_id: activeOrder.id,\n        cycle_count: cycle.cycles || 0,\n        timestamp: now\n    });\n\n    node.warn(`[ANOMALY] OEE drop: ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`);\n}\n\n// Update OEE history for trend analysis\nanomalyState.oeeHistory.push({ timestamp: now, value: currentOEE });\nif (anomalyState.oeeHistory.length > HISTORY_WINDOW) {\n    anomalyState.oeeHistory.shift();  // Keep only recent history\n}\n\n// ============================================================\n// TIER 2: QUALITY SPIKE DETECTION\n// Trigger: Sudden increase in scrap/defect rate\n// ============================================================\nconst totalParts = (cycle.goodParts || 0) + (cycle.scrapParts || 0);\nconst currentScrapRate = totalParts > 0 ? ((cycle.scrapParts || 0) / totalParts) * 100 : 0;\n\nanomalyState.qualityHistory.push({ timestamp: now, value: currentScrapRate });\nif (anomalyState.qualityHistory.length > HISTORY_WINDOW) {\n    anomalyState.qualityHistory.shift();\n}\n\n// Calculate average scrap rate from history\nif (anomalyState.qualityHistory.length >= 5) {\n    const recentHistory = anomalyState.qualityHistory.slice(0, -1);  // Exclude current\n    const avgScrapRate = recentHistory.reduce((sum, point) => sum + point.value, 0) / recentHistory.length;\n\n    const scrapRateIncrease = currentScrapRate - avgScrapRate;\n\n    if (scrapRateIncrease > QUALITY_SPIKE_THRESHOLD && currentScrapRate > 2) {\n        let severity = 'warning';\n        if (scrapRateIncrease > 10 || currentScrapRate > 15) {\n            severity = 'critical';  // Major quality issue\n        }\n\n        detectedAnomalies.push({\n            anomaly_type: 'quality-spike',\n            severity: severity,\n            title: `Quality Issue Detected`,\n            description: `Scrap rate at ${currentScrapRate.toFixed(1)}% (avg: ${avgScrapRate.toFixed(1)}%, +${scrapRateIncrease.toFixed(1)}%)`,\n            data: {\n                current_scrap_rate: currentScrapRate,\n                average_scrap_rate: avgScrapRate,\n                increase: scrapRateIncrease,\n                scrap_parts: cycle.scrapParts || 0,\n                total_parts: totalParts\n            },\n            kpi_snapshot: {\n                oee: kpis.oee || 0,\n                availability: kpis.availability || 0,\n                performance: kpis.performance || 0,\n                quality: kpis.quality || 0\n            },\n            work_order_id: activeOrder.id,\n            cycle_count: cycle.cycles || 0,\n            timestamp: now\n        });\n\n        node.warn(`[ANOMALY] Quality spike: Scrap rate ${currentScrapRate.toFixed(1)}% (avg: ${avgScrapRate.toFixed(1)}%)`);\n    }\n}\n\n// ============================================================\n// TIER 2: PERFORMANCE DEGRADATION\n// Trigger: Consistent underperformance over time\n// ============================================================\nconst currentPerformance = Number(kpis.performance) || 0;\nanomalyState.performanceHistory.push({ timestamp: now, value: currentPerformance });\nif (anomalyState.performanceHistory.length > HISTORY_WINDOW) {\n    anomalyState.performanceHistory.shift();\n}\n\n// Check for sustained poor performance (at least 10 data points)\nif (anomalyState.performanceHistory.length >= 10) {\n    const recent10 = anomalyState.performanceHistory.slice(-10);\n    const avgPerformance = recent10.reduce((sum, point) => sum + point.value, 0) / recent10.length;\n\n    // Alert if consistently below 85% performance\n    if (avgPerformance > 0 && avgPerformance < 85) {\n        let severity = 'warning';\n        if (avgPerformance < 75) {\n            severity = 'critical';  // Severely degraded performance\n        }\n\n        detectedAnomalies.push({\n            anomaly_type: 'performance-degradation',\n            severity: severity,\n            title: `Performance Degradation`,\n            description: `Performance at ${avgPerformance.toFixed(1)}% (sustained over last 10 cycles)`,\n            data: {\n                average_performance: avgPerformance,\n                current_performance: currentPerformance,\n                threshold: 85,\n                sample_size: 10\n            },\n            kpi_snapshot: {\n                oee: kpis.oee || 0,\n                availability: kpis.availability || 0,\n                performance: kpis.performance || 0,\n                quality: kpis.quality || 0\n            },\n            work_order_id: activeOrder.id,\n            cycle_count: cycle.cycles || 0,\n            timestamp: now\n        });\n\n        node.warn(`[ANOMALY] Performance degradation: ${avgPerformance.toFixed(1)}% over last 10 cycles`);\n    }\n}\n\n// ============================================================\n// TIER 3: PREDICTIVE ALERTS (Trend Analysis)\n// Predict issues before they become critical\n// ============================================================\nif (anomalyState.oeeHistory.length >= 15) {\n    // Simple linear trend analysis on OEE\n    const recent15 = anomalyState.oeeHistory.slice(-15);\n    const firstHalf = recent15.slice(0, 7);\n    const secondHalf = recent15.slice(-7);\n\n    const avgFirstHalf = firstHalf.reduce((sum, p) => sum + p.value, 0) / firstHalf.length;\n    const avgSecondHalf = secondHalf.reduce((sum, p) => sum + p.value, 0) / secondHalf.length;\n\n    const oeeTrend = avgSecondHalf - avgFirstHalf;\n\n    // Predict if OEE is trending downward significantly\n    if (oeeTrend < -5 && avgSecondHalf > OEE_THRESHOLD * 0.95 && avgSecondHalf < OEE_THRESHOLD * 1.05) {\n        detectedAnomalies.push({\n            anomaly_type: 'predictive-oee-decline',\n            severity: 'info',\n            title: `Declining OEE Trend Detected`,\n            description: `OEE trending down ${Math.abs(oeeTrend).toFixed(1)}% over last 15 cycles. Current: ${avgSecondHalf.toFixed(1)}%`,\n            data: {\n                trend: oeeTrend,\n                first_half_avg: avgFirstHalf,\n                second_half_avg: avgSecondHalf,\n                prediction: 'OEE may drop below threshold soon'\n            },\n            kpi_snapshot: {\n                oee: kpis.oee || 0,\n                availability: kpis.availability || 0,\n                performance: kpis.performance || 0,\n                quality: kpis.quality || 0\n            },\n            work_order_id: activeOrder.id,\n            cycle_count: cycle.cycles || 0,\n            timestamp: now\n        });\n\n        node.warn(`[PREDICTIVE] OEE trending down: ${oeeTrend.toFixed(1)}%`);\n    }\n}\n\n// Update last cycle time for next iteration\nanomalyState.lastCycleTime = now;\nglobal.set(\"anomalyState\", anomalyState);\n\n// ============================================================\n// OUTPUT\n// ============================================================\nif (detectedAnomalies.length > 0) {\n    node.warn(`[ANOMALY DETECTOR] Detected ${detectedAnomalies.length} anomaly/ies`);\n\n    return {\n        topic: \"anomaly-detected\",\n        payload: detectedAnomalies,\n        originalMsg: msg  // Pass through original message for other flows\n    };\n}\n\nreturn null;  // No anomalies detected\n",
  1886→        "outputs": 1,
  1887→        "timeout": 0,
  1888→        "noerr": 0,
  1889→        "initialize": "",
  1890→        "finalize": "",
  1891→        "libs": [],
  1892→        "x": 850,
  1893→        "y": 300,
  1894→        "wires": [
  1895→            [
  1896→                "event_logger_node_id"
  1897→            ]
  1898→        ]
  1899→    },
  1900→    {
  1901→        "id": "event_logger_node_id",
  1902→        "type": "function",
  1903→        "z": "cac3a4383120cb57",
  1904→        "name": "Event Logger",

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

    {
        "id": "anomaly_detector_node_id",
        "type": "function",
        "z": "cac3a4383120cb57",
        "name": "Anomaly Detector",
        "func": "// ============================================================\n// ANOMALY DETECTOR - Enhanced with Tier 1 & 2 Detections\n// Detects production anomalies in real-time\n// ============================================================\n\nconst cycle = msg.cycle || {};\nconst kpis = msg.kpis || {};\nconst activeOrder = global.get(\"activeWorkOrder\") || {};\n\n// Must have active work order to detect anomalies\nif (!activeOrder.id) {\n    return null;\n}\n\nconst theoreticalCycleTime = Number(activeOrder.cycleTime) || 0;\nconst now = Date.now();\n\n// Get or initialize anomaly tracking state\nlet anomalyState = global.get(\"anomalyState\") || {\n    lastCycleTime: now,\n    activeStoppageEvent: null,\n    oeeHistory: [],\n    performanceHistory: [],\n    qualityHistory: []\n};\n\n// Configuration\nconst OEE_THRESHOLD = global.get(\"oeeAlertThreshold\") || 90;  // Customizable in settings\nconst HISTORY_WINDOW = 20;  // Keep last 20 data points for trend analysis\nconst QUALITY_SPIKE_THRESHOLD = 5;  // Alert if scrap rate increases by 5%+\n\nconst detectedAnomalies = [];\n\n// ============================================================\n// TIER 1: SLOW CYCLE DETECTION\n// Trigger: Actual cycle time > 1.5x theoretical\n// ============================================================\nif (theoreticalCycleTime > 0) {\n    const timeSinceLastCycle = now - anomalyState.lastCycleTime;\n    const actualCycleTime = timeSinceLastCycle / 1000; // Convert to seconds\n    const threshold = theoreticalCycleTime * 1.5;\n\n    if (actualCycleTime > threshold && anomalyState.lastCycleTime > 0) {\n        const deltaPercent = ((actualCycleTime - theoreticalCycleTime) / theoreticalCycleTime) * 100;\n\n        // Determine severity\n        let severity = 'warning';\n        if (actualCycleTime > theoreticalCycleTime * 2.0) {\n            severity = 'critical';  // 100%+ slower\n        }\n\n        detectedAnomalies.push({\n            anomaly_type: 'slow-cycle',\n            severity: severity,\n            title: `Slow Cycle Detected`,\n            description: `Cycle took ${actualCycleTime.toFixed(1)}s (${deltaPercent.toFixed(0)}% slower than expected ${theoreticalCycleTime}s)`,\n            data: {\n                actual_cycle_time: actualCycleTime,\n                theoretical_cycle_time: theoreticalCycleTime,\n                delta_percent: Math.round(deltaPercent),\n                threshold_multiplier: actualCycleTime / theoreticalCycleTime\n            },\n            kpi_snapshot: {\n                oee: kpis.oee || 0,\n                availability: kpis.availability || 0,\n                performance: kpis.performance || 0,\n                quality: kpis.quality || 0\n            },\n            work_order_id: activeOrder.id,\n            cycle_count: cycle.cycles || 0,\n            timestamp: now\n        });\n\n        node.warn(`[ANOMALY] Slow cycle: ${actualCycleTime.toFixed(1)}s (expected ${theoreticalCycleTime}s)`);\n    }\n}\n\n// ============================================================\n// TIER 1: PRODUCTION STOPPAGE DETECTION\n// Trigger: No cycle in > 3x theoretical cycle time\n// ============================================================\nif (theoreticalCycleTime > 0) {\n    const timeSinceLastCycle = now - anomalyState.lastCycleTime;\n    const stoppageThreshold = theoreticalCycleTime * 3 * 1000; // Convert to ms\n\n    // If we have an active stoppage event and a new cycle arrived, resolve it\n    if (anomalyState.activeStoppageEvent) {\n        // Cycle resumed - mark stoppage as resolved\n        anomalyState.activeStoppageEvent.resolved_at = now;\n        anomalyState.activeStoppageEvent.auto_resolved = true;\n        anomalyState.activeStoppageEvent.status = 'resolved';\n\n        const stoppageDuration = (now - anomalyState.activeStoppageEvent.timestamp) / 1000;\n        node.warn(`[ANOMALY] Production resumed after ${stoppageDuration.toFixed(0)}s stoppage`);\n\n        // Send resolution event\n        detectedAnomalies.push(anomalyState.activeStoppageEvent);\n        anomalyState.activeStoppageEvent = null;\n    }\n\n    // Check if production has stopped (only if no active stoppage event)\n    if (!anomalyState.activeStoppageEvent && timeSinceLastCycle > stoppageThreshold && anomalyState.lastCycleTime > 0) {\n        const stoppageSeconds = timeSinceLastCycle / 1000;\n\n        // Determine severity\n        let severity = 'warning';\n        if (stoppageSeconds > theoreticalCycleTime * 5) {\n            severity = 'critical';  // Stopped for 5x+ theoretical time\n        }\n\n        const stoppageEvent = {\n            anomaly_type: 'production-stopped',\n            severity: severity,\n            title: `Production Stoppage`,\n            description: `No cycles detected for ${stoppageSeconds.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,\n            data: {\n                stoppage_duration_seconds: Math.round(stoppageSeconds),\n                theoretical_cycle_time: theoreticalCycleTime,\n                last_cycle_timestamp: anomalyState.lastCycleTime,\n                threshold_multiplier: stoppageSeconds / theoreticalCycleTime\n            },\n            kpi_snapshot: {\n                oee: kpis.oee || 0,\n                availability: kpis.availability || 0,\n                performance: kpis.performance || 0,\n                quality: kpis.quality || 0\n            },\n            work_order_id: activeOrder.id,\n            cycle_count: cycle.cycles || 0,\n            timestamp: now,\n            status: 'active'\n        };\n\n        detectedAnomalies.push(stoppageEvent);\n        anomalyState.activeStoppageEvent = stoppageEvent;\n\n        node.warn(`[ANOMALY] Production stopped: ${stoppageSeconds.toFixed(0)}s since last cycle`);\n    }\n}\n\n// ============================================================\n// TIER 2: OEE DROP DETECTION\n// Trigger: OEE falls below threshold\n// ============================================================\nconst currentOEE = Number(kpis.oee) || 0;\nif (currentOEE > 0 && currentOEE < OEE_THRESHOLD) {\n    let severity = 'warning';\n    if (currentOEE < 75) {\n        severity = 'critical';  // Critically low OEE\n    }\n\n    detectedAnomalies.push({\n        anomaly_type: 'oee-drop',\n        severity: severity,\n        title: `OEE Below Threshold`,\n        description: `OEE at ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`,\n        data: {\n            current_oee: currentOEE,\n            threshold: OEE_THRESHOLD,\n            delta: OEE_THRESHOLD - currentOEE\n        },\n        kpi_snapshot: {\n            oee: kpis.oee || 0,\n            availability: kpis.availability || 0,\n            performance: kpis.performance || 0,\n            quality: kpis.quality || 0\n        },\n        work_order_id: activeOrder.id,\n        cycle_count: cycle.cycles || 0,\n        timestamp: now\n    });\n\n    node.warn(`[ANOMALY] OEE drop: ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`);\n}\n\n// Update OEE history for trend analysis\nanomalyState.oeeHistory.push({ timestamp: now, value: currentOEE });\nif (anomalyState.oeeHistory.length > HISTORY_WINDOW) {\n    anomalyState.oeeHistory.shift();  // Keep only recent history\n}\n\n// ============================================================\n// TIER 2: QUALITY SPIKE DETECTION\n// Trigger: Sudden increase in scrap/defect rate\n// ============================================================\nconst totalParts = (cycle.goodParts || 0) + (cycle.scrapParts || 0);\nconst currentScrapRate = totalParts > 0 ? ((cycle.scrapParts || 0) / totalParts) * 100 : 0;\n\nanomalyState.qualityHistory.push({ timestamp: now, value: currentScrapRate });\nif (anomalyState.qualityHistory.length > HISTORY_WINDOW) {\n    anomalyState.qualityHistory.shift();\n}\n\n// Calculate average scrap rate from history\nif (anomalyState.qualityHistory.length >= 5) {\n    const recentHistory = anomalyState.qualityHistory.slice(0, -1);  // Exclude current\n    const avgScrapRate = recentHistory.reduce((sum, point) => sum + point.value, 0) / recentHistory.length;\n\n    const scrapRateIncrease = currentScrapRate - avgScrapRate;\n\n    if (scrapRateIncrease > QUALITY_SPIKE_THRESHOLD && currentScrapRate > 2) {\n        let severity = 'warning';\n        if (scrapRateIncrease > 10 || currentScrapRate > 15) {\n            severity = 'critical';  // Major quality issue\n        }\n\n        detectedAnomalies.push({\n            anomaly_type: 'quality-spike',\n            severity: severity,\n            title: `Quality Issue Detected`,\n            description: `Scrap rate at ${currentScrapRate.toFixed(1)}% (avg: ${avgScrapRate.toFixed(1)}%, +${scrapRateIncrease.toFixed(1)}%)`,\n            data: {\n                current_scrap_rate: currentScrapRate,\n                average_scrap_rate: avgScrapRate,\n                increase: scrapRateIncrease,\n                scrap_parts: cycle.scrapParts || 0,\n                total_parts: totalParts\n            },\n            kpi_snapshot: {\n                oee: kpis.oee || 0,\n                availability: kpis.availability || 0,\n                performance: kpis.performance || 0,\n                quality: kpis.quality || 0\n            },\n            work_order_id: activeOrder.id,\n            cycle_count: cycle.cycles || 0,\n            timestamp: now\n        });\n\n        node.warn(`[ANOMALY] Quality spike: Scrap rate ${currentScrapRate.toFixed(1)}% (avg: ${avgScrapRate.toFixed(1)}%)`);\n    }\n}\n\n// ============================================================\n// TIER 2: PERFORMANCE DEGRADATION\n// Trigger: Consistent underperformance over time\n// ============================================================\nconst currentPerformance = Number(kpis.performance) || 0;\nanomalyState.performanceHistory.push({ timestamp: now, value: currentPerformance });\nif (anomalyState.performanceHistory.length > HISTORY_WINDOW) {\n    anomalyState.performanceHistory.shift();\n}\n\n// Check for sustained poor performance (at least 10 data points)\nif (anomalyState.performanceHistory.length >= 10) {\n    const recent10 = anomalyState.performanceHistory.slice(-10);\n    const avgPerformance = recent10.reduce((sum, point) => sum + point.value, 0) / recent10.length;\n\n    // Alert if consistently below 85% performance\n    if (avgPerformance > 0 && avgPerformance < 85) {\n        let severity = 'warning';\n        if (avgPerformance < 75) {\n            severity = 'critical';  // Severely degraded performance\n        }\n\n        detectedAnomalies.push({\n            anomaly_type: 'performance-degradation',\n            severity: severity,\n            title: `Performance Degradation`,\n            description: `Performance at ${avgPerformance.toFixed(1)}% (sustained over last 10 cycles)`,\n            data: {\n                average_performance: avgPerformance,\n                current_performance: currentPerformance,\n                threshold: 85,\n                sample_size: 10\n            },\n            kpi_snapshot: {\n                oee: kpis.oee || 0,\n                availability: kpis.availability || 0,\n                performance: kpis.performance || 0,\n                quality: kpis.quality || 0\n            },\n            work_order_id: activeOrder.id,\n            cycle_count: cycle.cycles || 0,\n            timestamp: now\n        });\n\n        node.warn(`[ANOMALY] Performance degradation: ${avgPerformance.toFixed(1)}% over last 10 cycles`);\n    }\n}\n\n// ============================================================\n// TIER 3: PREDICTIVE ALERTS (Trend Analysis)\n// Predict issues before they become critical\n// ============================================================\nif (anomalyState.oeeHistory.length >= 15) {\n    // Simple linear trend analysis on OEE\n    const recent15 = anomalyState.oeeHistory.slice(-15);\n    const firstHalf = recent15.slice(0, 7);\n    const secondHalf = recent15.slice(-7);\n\n    const avgFirstHalf = firstHalf.reduce((sum, p) => sum + p.value, 0) / firstHalf.length;\n    const avgSecondHalf = secondHalf.reduce((sum, p) => sum + p.value, 0) / secondHalf.length;\n\n    const oeeTrend = avgSecondHalf - avgFirstHalf;\n\n    // Predict if OEE is trending downward significantly\n    if (oeeTrend < -5 && avgSecondHalf > OEE_THRESHOLD * 0.95 && avgSecondHalf < OEE_THRESHOLD * 1.05) {\n        detectedAnomalies.push({\n            anomaly_type: 'predictive-oee-decline',\n            severity: 'info',\n            title: `Declining OEE Trend Detected`,\n            description: `OEE trending down ${Math.abs(oeeTrend).toFixed(1)}% over last 15 cycles. Current: ${avgSecondHalf.toFixed(1)}%`,\n            data: {\n                trend: oeeTrend,\n                first_half_avg: avgFirstHalf,\n                second_half_avg: avgSecondHalf,\n                prediction: 'OEE may drop below threshold soon'\n            },\n            kpi_snapshot: {\n                oee: kpis.oee || 0,\n                availability: kpis.availability || 0,\n                performance: kpis.performance || 0,\n                quality: kpis.quality || 0\n            },\n            work_order_id: activeOrder.id,\n            cycle_count: cycle.cycles || 0,\n            timestamp: now\n        });\n\n        node.warn(`[PREDICTIVE] OEE trending down: ${oeeTrend.toFixed(1)}%`);\n    }\n}\n\n// Update last cycle time for next iteration\nanomalyState.lastCycleTime = now;\nglobal.set(\"anomalyState\", anomalyState);\n\n// ============================================================\n// OUTPUT\n// ============================================================\nif (detectedAnomalies.length > 0) {\n    node.warn(`[ANOMALY DETECTOR] Detected ${detectedAnomalies.length} anomaly/ies`);\n\n    return {\n        topic: \"anomaly-detected\",\n        payload: detectedAnomalies,\n        originalMsg: msg  // Pass through original message for other flows\n    };\n}\n\nreturn null;  // No anomalies detected\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 850,
        "y": 300,
        "wires": [
            [
                "event_logger_node_id"
            ]
        ]
    },
    {
        "id": "event_logger_node_id",
        "type": "function",
        "z": "cac3a4383120cb57",
        "name": "Event Logger",


Good, I'll insert the new Data Merger node right before the Anomaly Detector. I need to create a JSON node with:1. A unique ID - I'll use "cycle_kpi_data_merger"2. Type: "function"
3. Name: "Merge Cycle + KPI Data"
4. func: The JavaScript function that merges the data5. Position coordinates (x, y)
6. wires: output goes to anomaly_detector_node_id

Let me create this node and insert it before the Anomaly Detector node.


Edit

    },
    {
        "id": "anomaly_detector_node_id",
        "type": "function",
        "z": "cac3a4383120cb57",
        "name": "Anomaly Detector",

    },
    {
        "id": "cycle_kpi_data_merger",
        "type": "function",
        "z": "cac3a4383120cb57",
        "name": "Merge Cycle + KPI Data",
        "func": "// ============================================================\n// DATA MERGER - Combines Cycle + KPI data for Anomaly Detector\n// ============================================================\n\n// Get KPIs from incoming message (from Calculate KPIs node)\nconst kpis = msg.kpis || msg.payload?.kpis || {};\n\n// Get cycle data from global context\nconst activeOrder = global.get(\"activeWorkOrder\") || {};\nconst cycleCount = global.get(\"cycleCount\") || 0;\nconst cavities = Number(global.get(\"moldActive\")) || 1;\n\n// Build cycle object with all necessary data\nconst cycle = {\n    id: activeOrder.id,\n    sku: activeOrder.sku || \"\",\n    cycles: cycleCount,\n    goodParts: Number(activeOrder.good) || 0,\n    scrapParts: Number(activeOrder.scrap) || 0,\n    target: Number(activeOrder.target) || 0,\n    cycleTime: Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0),\n    progressPercent: Number(activeOrder.progressPercent) || 0,\n    cavities: cavities\n};\n\n// Merge both into the message\nmsg.cycle = cycle;\nmsg.kpis = kpis;\n\nnode.warn(`[DATA MERGER] Merged cycle (count: ${cycleCount}) + KPIs (OEE: ${kpis.oee || 0}%) for anomaly detection`);\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 650,
        "y": 300,
        "wires": [
            [
                "anomaly_detector_node_id"
            ]
        ]
    },
    {
        "id": "anomaly_detector_node_id",
        "type": "function",
        "z": "cac3a4383120cb57",
        "name": "Anomaly Detector",