  1700→        "x": 950,
  1701→        "y": 700,
  1702→        "wires": [
  1703→            [
  1704→                "8e589c36104b3f0d"
  1705→            ]
  1706→        ]
  1707→    },
  1708→    {
  1709→        "id": "db_guard_db_guard_graphs",
  1710→        "type": "switch",
  1711→        "z": "cac3a4383120cb57",
  1712→        "name": "DB Guard (Graphs)",
  1713→        "property": "topic",
  1714→        "propertyType": "msg",
  1715→        "rules": [
  1716→            {
  1717→                "t": "istype",
  1718→                "v": "string",
  1719→                "vt": "string"
  1720→            }
  1721→        ],
  1722→        "checkall": "true",
  1723→        "repair": false,
  1724→        "outputs": 1,
  1725→        "x": 630,
  1726→        "y": 560,
  1727→        "wires": [
  1728→            [
  1729→                "graph_mariadb_node"
  1730→            ]
  1731→        ]
  1732→    },
  1733→    {
  1734→        "id": "db_guard_db_guard_cycles",
  1735→        "type": "switch",
  1736→        "z": "cac3a4383120cb57",
  1737→        "name": "DB Guard (Cycles)",
  1738→        "property": "topic",
  1739→        "propertyType": "msg",
  1740→        "rules": [
  1741→            {
  1742→                "t": "istype",
  1743→                "v": "string",
  1744→                "vt": "string"
  1745→            }
  1746→        ],
  1747→        "checkall": "true",
  1748→        "repair": false,
  1749→        "outputs": 1,
  1750→        "x": 690,
  1751→        "y": 600,
  1752→        "wires": [
  1753→            [
  1754→                "f6ad294bc02618c9"
  1755→            ]
  1756→        ]
  1757→    },
  1758→    {
  1759→        "id": "8e589c36104b3f0d",
  1760→        "type": "link out",
  1761→        "z": "cac3a4383120cb57",
  1762→        "name": "link out 10",
  1763→        "mode": "link",
  1764→        "links": [
  1765→            "394cfca6b72f6444"
  1766→        ],
  1767→        "x": 945,
  1768→        "y": 760,
  1769→        "wires": []
  1770→    },
  1771→    {
  1772→        "id": "4fabedfe0668e215",
  1773→        "type": "debug",
  1774→        "z": "cac3a4383120cb57",
  1775→        "name": "machine cycles output 4",
  1776→        "active": true,
  1777→        "tosidebar": true,
  1778→        "console": false,
  1779→        "tostatus": false,
  1780→        "complete": "true",
  1781→        "targetType": "full",
  1782→        "statusVal": "",
  1783→        "statusType": "auto",
  1784→        "x": 1100,
  1785→        "y": 1020,
  1786→        "wires": []
  1787→    },
  1788→    {
  1789→        "id": "68747d170d530562",
  1790→        "type": "debug",
  1791→        "z": "cac3a4383120cb57",
  1792→        "name": "Machine cycles output 1",
  1793→        "active": true,
  1794→        "tosidebar": true,
  1795→        "console": false,
  1796→        "tostatus": false,
  1797→        "complete": "true",
  1798→        "targetType": "full",
  1799→        "statusVal": "",
  1800→        "statusType": "auto",
  1801→        "x": 1090,
  1802→        "y": 900,
  1803→        "wires": []
  1804→    },
  1805→    {
  1806→        "id": "4b0281e42c6cd4d1",
  1807→        "type": "debug",
  1808→        "z": "cac3a4383120cb57",
  1809→        "name": "machine cycles output 2",
  1810→        "active": true,
  1811→        "tosidebar": true,
  1812→        "console": false,
  1813→        "tostatus": false,
  1814→        "complete": "true",
  1815→        "targetType": "full",
  1816→        "statusVal": "",
  1817→        "statusType": "auto",
  1818→        "x": 1090,
  1819→        "y": 940,
  1820→        "wires": []
  1821→    },
  1822→    {
  1823→        "id": "02fdc53901e0b70e",
  1824→        "type": "debug",
  1825→        "z": "cac3a4383120cb57",
  1826→        "name": "calculate kpis debug",
  1827→        "active": true,
  1828→        "tosidebar": true,
  1829→        "console": false,
  1830→        "tostatus": false,
  1831→        "complete": "true",
  1832→        "targetType": "full",
  1833→        "statusVal": "",
  1834→        "statusType": "auto",
  1835→        "x": 1500,
  1836→        "y": 980,
  1837→        "wires": []
  1838→    },
  1839→    {
  1840→        "id": "b03da2270d8def93",
  1841→        "type": "debug",
  1842→        "z": "cac3a4383120cb57",
  1843→        "name": "Machine cycles output 3",
  1844→        "active": true,
  1845→        "tosidebar": true,
  1846→        "console": false,
  1847→        "tostatus": false,
  1848→        "complete": "true",
  1849→        "targetType": "full",
  1850→        "statusVal": "",
  1851→        "statusType": "auto",
  1852→        "x": 1090,
  1853→        "y": 980,
  1854→        "wires": []
  1855→    },
  1856→    {
  1857→        "id": "progress_check_handler_node",
  1858→        "type": "function",
  1859→        "z": "cac3a4383120cb57",
  1860→        "g": "b7ab5e0cc02b9508",
  1861→        "name": "Progress Check Handler",
  1862→        "func": "// Handle DB result from start-work-order progress check\nif (msg._mode === \"start-check-progress\") {\n    const order = flow.get(\"pendingWorkOrder\");\n\n    if (!order || !order.id) {\n        node.error(\"No pending work order found\", msg);\n        return [null, null];\n    }\n\n    // Get progress from DB query result\n    const dbRow = (Array.isArray(msg.payload) && msg.payload.length > 0) ? msg.payload[0] : null;\n    const cycleCount = dbRow ? (Number(dbRow.cycle_count) || 0) : 0;\n    const goodParts = dbRow ? (Number(dbRow.good_parts) || 0) : 0;\n    const scrapParts = dbRow ? (Number(dbRow.scrap_parts) || 0) : 0;\n    const targetQty = dbRow ? (Number(dbRow.target_qty) || 0) : (Number(order.target) || 0);\n\n    node.warn(`[PROGRESS-CHECK] WO ${order.id}: cycles=${cycleCount}, good=${goodParts}, target=${targetQty}`);\n\n    // Check if work order has existing progress\n    if (cycleCount > 0 || goodParts > 0) {\n        // Work order has progress - send prompt to UI\n        node.warn(`[PROGRESS-CHECK] Work order has existing progress - sending prompt to UI`);\n\n        const promptMsg = {\n            _mode: \"resume-prompt\",\n            topic: \"resumePrompt\",\n            payload: {\n                id: order.id,\n                sku: order.sku || \"\",\n                cycleCount: cycleCount,\n                goodParts: goodParts,\n                targetQty: targetQty,\n                progressPercent: targetQty > 0 ? Math.round((goodParts / targetQty) * 100) : 0,\n                // Include full order object for resume/restart actions\n                order: {...order, cycle_count: cycleCount, good_parts: goodParts, scrap: scrapParts}\n            }\n        };\n\n        return [null, promptMsg];\n    } else {\n        // No existing progress - proceed with normal start\n        // But still use DB values (even if 0) to ensure DB is source of truth\n        node.warn(`[PROGRESS-CHECK] No existing progress - proceeding with normal start`);\n\n        // Update order object with DB values (makes DB the source of truth)\n        order.cycle_count = cycleCount;  // Will be 0 from DB\n        order.good_parts = goodParts;    // Will be 0 from DB\n        order.scrap = scrapParts;        // Will be 0 from DB\n        order.good = goodParts;          // For consistency\n        order.target = targetQty;        // From DB\n\n        const startMsg = {\n            _mode: \"start\",\n            startOrder: order,\n            topic: \"UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, updated_at = CASE WHEN work_order_id = ? THEN NOW() ELSE updated_at END WHERE status <> 'DONE'\",\n            payload: [order.id, order.id]\n        };\n\n        // Initialize global state with DB values (even if 0)\n        global.set(\"activeWorkOrder\", order);\n        global.set(\"cycleCount\", cycleCount);  // Use DB value instead of hardcoded 0\n        flow.set(\"lastMachineState\", 0);\n        global.set(\"scrapPromptIssuedFor\", null);\n\n        node.warn(`[PROGRESS-CHECK] Initialized from DB: cycles=${cycleCount}, good=${goodParts}, scrap=${scrapParts}`);\n\n        return [startMsg, null];\n    }\n}\n\n// Pass through all other messages\nreturn [msg, null];",
  1863→        "outputs": 2,
  1864→        "timeout": 0,
  1865→        "noerr": 0,
  1866→        "initialize": "",
  1867→        "finalize": "",
  1868→        "libs": [],
  1869→        "x": 1090,
  1870→        "y": 340,
  1871→        "wires": [
  1872→            [
  1873→                "578c92e75bf0f266"
  1874→            ],
  1875→            [
  1876→                "f2bab26e27e2023d"
  1877→            ]
  1878→        ]
  1879→    },
  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",
  1905→        "func": "// ============================================================\n// EVENT LOGGER\n// Deduplicates and logs anomaly events to database\n// ============================================================\n\nconst anomalies = msg.payload || [];\n\nif (!Array.isArray(anomalies) || anomalies.length === 0) {\n    return null;\n}\n\n// Get or initialize active anomalies list\nlet activeAnomalies = global.get(\"activeAnomalies\") || [];\nconst now = Date.now();\nconst DEDUP_WINDOW = 5 * 60 * 1000; // 5 minutes\n\nconst dbInserts = [];\nconst uiUpdates = [];\n\nanomalies.forEach(anomaly => {\n    // ============================================================\n    // DEDUPLICATION LOGIC\n    // Don't create new event if same type exists in last 5 minutes\n    // ============================================================\n    const existingIndex = activeAnomalies.findIndex(existing =>\n        existing.anomaly_type === anomaly.anomaly_type &&\n        existing.work_order_id === anomaly.work_order_id &&\n        existing.status === 'active' &&\n        (now - existing.timestamp) < DEDUP_WINDOW\n    );\n\n    if (existingIndex !== -1) {\n        // Update existing event\n        const existing = activeAnomalies[existingIndex];\n        existing.occurrence_count = (existing.occurrence_count || 1) + 1;\n        existing.last_occurrence = now;\n\n        // Update in database\n        const updateQuery = `UPDATE anomaly_events\n            SET occurrence_count = ?, last_occurrence = ?\n            WHERE event_id = ?`;\n\n        dbInserts.push({\n            topic: updateQuery,\n            payload: [existing.occurrence_count, existing.last_occurrence, existing.event_id]\n        });\n\n        node.warn(`[EVENT LOGGER] Updated existing ${anomaly.anomaly_type} event (occurrence #${existing.occurrence_count})`);\n\n    } else if (anomaly.status === 'resolved') {\n        // ============================================================\n        // RESOLVE EVENT\n        // ============================================================\n        const resolveIndex = activeAnomalies.findIndex(existing =>\n            existing.anomaly_type === anomaly.anomaly_type &&\n            existing.work_order_id === anomaly.work_order_id &&\n            existing.status === 'active'\n        );\n\n        if (resolveIndex !== -1) {\n            const existing = activeAnomalies[resolveIndex];\n            existing.status = 'resolved';\n            existing.resolved_at = anomaly.resolved_at || now;\n            existing.auto_resolved = anomaly.auto_resolved || false;\n\n            // Update in database\n            const resolveQuery = `UPDATE anomaly_events\n                SET status = 'resolved', resolved_at = ?, auto_resolved = ?\n                WHERE event_id = ?`;\n\n            dbInserts.push({\n                topic: resolveQuery,\n                payload: [existing.resolved_at, existing.auto_resolved, existing.event_id]\n            });\n\n            // Remove from active list\n            activeAnomalies.splice(resolveIndex, 1);\n\n            node.warn(`[EVENT LOGGER] Resolved ${anomaly.anomaly_type} event (auto: ${existing.auto_resolved})`);\n\n            uiUpdates.push({\n                event_id: existing.event_id,\n                status: 'resolved'\n            });\n        }\n\n    } else {\n        // ============================================================\n        // NEW EVENT\n        // ============================================================\n        const insertQuery = `INSERT INTO anomaly_events\n            (timestamp, work_order_id, anomaly_type, severity, title, description,\n             data_json, kpi_snapshot_json, status, cycle_count, occurrence_count, last_occurrence)\n            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\n\n        const dataJson = JSON.stringify(anomaly.data || {});\n        const kpiJson = JSON.stringify(anomaly.kpi_snapshot || {});\n\n        dbInserts.push({\n            topic: insertQuery,\n            payload: [\n                anomaly.timestamp,\n                anomaly.work_order_id,\n                anomaly.anomaly_type,\n                anomaly.severity,\n                anomaly.title,\n                anomaly.description,\n                dataJson,\n                kpiJson,\n                'active',\n                anomaly.cycle_count,\n                1,  // occurrence_count\n                anomaly.timestamp  // last_occurrence\n            ],\n            _storeEventId: true,  // Flag to get generated event_id\n            _anomaly: anomaly  // Keep reference for later\n        });\n\n        node.warn(`[EVENT LOGGER] New ${anomaly.anomaly_type} event: ${anomaly.title}`);\n    }\n});\n\n// Save active anomalies to global context\nglobal.set(\"activeAnomalies\", activeAnomalies);\n\n// ============================================================\n// OUTPUT\n// ============================================================\n// Output 1: Database inserts (to mysql node)\n// Output 2: UI updates (to Home/Alerts tabs)\n\nif (dbInserts.length > 0) {\n    // Send each insert as a separate message\n    return [dbInserts, {\n        topic: \"anomaly-ui-update\",\n        payload: {\n            activeCount: activeAnomalies.length,\n            activeAnomalies: activeAnomalies,\n            updates: uiUpdates\n        }\n    }];\n} else {\n    return [null, {\n        topic: \"anomaly-ui-update\",\n        payload: {\n            activeCount: activeAnomalies.length,\n            activeAnomalies: activeAnomalies,\n            updates: uiUpdates\n        }\n    }];\n}\n",
  1906→        "outputs": 2,
  1907→        "timeout": 0,
  1908→        "noerr": 0,
  1909→        "initialize": "",
  1910→        "finalize": "",
  1911→        "libs": [],
  1912→        "x": 1050,
  1913→        "y": 300,
  1914→        "wires": [
  1915→            [
  1916→                "anomaly_split_node_id"
  1917→            ],
  1918→            [
  1919→                "anomaly_alert_ui_global"
  1920→            ]
  1921→        ]
  1922→    },
  1923→    {
  1924→        "id": "anomaly_split_node_id",
  1925→        "type": "split",
  1926→        "z": "cac3a4383120cb57",
  1927→        "name": "Split DB Inserts",
  1928→        "splt": "\\n",
  1929→        "spltType": "str",
  1930→        "arraySplt": 1,
  1931→        "arraySpltType": "len",
  1932→        "stream": false,
  1933→        "addname": "",
  1934→        "x": 1270,
  1935→        "y": 240,
  1936→        "wires": [
  1937→            [
  1938→                "anomaly_mysql_node_id"
  1939→            ]
  1940→        ]
  1941→    },
  1942→    {
  1943→        "id": "anomaly_mysql_node_id",
  1944→        "type": "mysql",
  1945→        "z": "cac3a4383120cb57",
  1946→        "mydb": "00d8ad2b0277f906",
  1947→        "name": "Anomaly Events DB",
  1948→        "x": 1270,
  1949→        "y": 280,
  1950→        "wires": [
  1951→            [
  1952→                "anomaly_db_debug_node_id"
  1953→            ]
  1954→        ]
  1955→    },
  1956→    {
  1957→        "id": "anomaly_db_debug_node_id",
  1958→        "type": "debug",
  1959→        "z": "cac3a4383120cb57",
  1960→        "name": "Anomaly DB Result",
  1961→        "active": true,
  1962→        "tosidebar": true,
  1963→        "console": false,
  1964→        "tostatus": false,
  1965→        "complete": "true",
  1966→        "targetType": "full",
  1967→        "statusVal": "",
  1968→        "statusType": "auto",
  1969→        "x": 1490,
  1970→        "y": 280,
  1971→        "wires": []
  1972→    },
  1973→    {
  1974→        "id": "init_oee_threshold",
  1975→        "type": "inject",
  1976→        "z": "cac3a4383120cb57",
  1977→        "name": "Initialize OEE Threshold (90%)",
  1978→        "props": [
  1979→            {
  1980→                "p": "payload"
  1981→            }
  1982→        ],
  1983→        "repeat": "",
  1984→        "crontab": "",
  1985→        "once": true,
  1986→        "onceDelay": 0.1,
  1987→        "topic": "",
  1988→        "payload": "90",
  1989→        "payloadType": "num",
  1990→        "x": 290,
  1991→        "y": 940,
  1992→        "wires": [
  1993→            [
  1994→                "set_oee_threshold_global"
  1995→            ]
  1996→        ]
  1997→    },
  1998→    {
  1999→        "id": "set_oee_threshold_global",
  2000→        "type": "function",
  2001→        "z": "cac3a4383120cb57",
  2002→        "name": "Set OEE Threshold Global",
  2003→        "func": "// Initialize OEE alert threshold\nconst threshold = Number(msg.payload) || 90;\nglobal.set(\"oeeAlertThreshold\", threshold);\n\nnode.warn(`[CONFIG] OEE Alert Threshold set to ${threshold}%`);\n\nreturn msg;",
  2004→        "outputs": 1,
  2005→        "timeout": 0,
  2006→        "noerr": 0,
  2007→        "initialize": "",
  2008→        "finalize": "",
  2009→        "libs": [],
  2010→        "x": 550,
  2011→        "y": 940,
  2012→        "wires": [
  2013→            []
  2014→        ]
  2015→    },
  2016→    {
  2017→        "id": "verify_create_table_inject",
  2018→        "type": "inject",
  2019→        "z": "cac3a4383120cb57",
  2020→        "name": "VERIFY & CREATE anomaly_events (click me)",
  2021→        "props": [
  2022→            {
  2023→                "p": "payload"
  2024→            }
  2025→        ],
  2026→        "repeat": "",
  2027→        "crontab": "",
  2028→        "once": false,
  2029→        "onceDelay": 0.1,
  2030→        "topic": "",
  2031→        "payload": "",
  2032→        "payloadType": "date",
  2033→        "x": 220,
  2034→        "y": 1000,
  2035→        "wires": [
  2036→            [
  2037→                "verify_create_table_check"
  2038→            ]
  2039→        ]
  2040→    },
  2041→    {
  2042→        "id": "verify_create_table_check",
  2043→        "type": "function",
  2044→        "z": "cac3a4383120cb57",
  2045→        "name": "Check Table Exists",
  2046→        "func": "// First check if table exists\nmsg.topic = \"SHOW TABLES LIKE 'anomaly_events'\";\nmsg.payload = [];\nreturn msg;",
  2047→        "outputs": 1,
  2048→        "timeout": 0,
  2049→        "noerr": 0,
  2050→        "initialize": "",
  2051→        "finalize": "",
  2052→        "libs": [],
  2053→        "x": 480,
  2054→        "y": 1000,
  2055→        "wires": [
  2056→            [
  2057→                "verify_create_table_mysql_check"
  2058→            ]
  2059→        ]
  2060→    },
  2061→    {
  2062→        "id": "verify_create_table_mysql_check",
  2063→        "type": "mysql",
  2064→        "z": "cac3a4383120cb57",
  2065→        "mydb": "00d8ad2b0277f906",
  2066→        "name": "Check Table",
  2067→        "x": 680,
  2068→        "y": 1000,
  2069→        "wires": [
  2070→            [
  2071→                "verify_create_table_switch"
  2072→            ]
  2073→        ]
  2074→    },
  2075→    {
  2076→        "id": "verify_create_table_switch",
  2077→        "type": "switch",
  2078→        "z": "cac3a4383120cb57",
  2079→        "name": "Table Exists?",
  2080→        "property": "payload.length",
  2081→        "propertyType": "msg",
  2082→        "rules": [
  2083→            {
  2084→                "t": "eq",
  2085→                "v": "0",
  2086→                "vt": "num"
  2087→            },
  2088→            {
  2089→                "t": "gt",
  2090→                "v": "0",
  2091→                "vt": "num"
  2092→            }
  2093→        ],
  2094→        "checkall": "true",
  2095→        "repair": false,
  2096→        "outputs": 2,
  2097→        "x": 850,
  2098→        "y": 1000,
  2099→        "wires": [

<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>

        "x": 950,
        "y": 700,
        "wires": [
            [
                "8e589c36104b3f0d"
            ]
        ]
    },
    {
        "id": "db_guard_db_guard_graphs",
        "type": "switch",
        "z": "cac3a4383120cb57",
        "name": "DB Guard (Graphs)",
        "property": "topic",
        "propertyType": "msg",
        "rules": [
            {
                "t": "istype",
                "v": "string",
                "vt": "string"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 1,
        "x": 630,
        "y": 560,
        "wires": [
            [
                "graph_mariadb_node"
            ]
        ]
    },
    {
        "id": "db_guard_db_guard_cycles",
        "type": "switch",
        "z": "cac3a4383120cb57",
        "name": "DB Guard (Cycles)",
        "property": "topic",
        "propertyType": "msg",
        "rules": [
            {
                "t": "istype",
                "v": "string",
                "vt": "string"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 1,
        "x": 690,
        "y": 600,
        "wires": [
            [
                "f6ad294bc02618c9"
            ]
        ]
    },
    {
        "id": "8e589c36104b3f0d",
        "type": "link out",
        "z": "cac3a4383120cb57",
        "name": "link out 10",
        "mode": "link",
        "links": [
            "394cfca6b72f6444"
        ],
        "x": 945,
        "y": 760,
        "wires": []
    },
    {
        "id": "4fabedfe0668e215",
        "type": "debug",
        "z": "cac3a4383120cb57",
        "name": "machine cycles output 4",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1100,
        "y": 1020,
        "wires": []
    },
    {
        "id": "68747d170d530562",
        "type": "debug",
        "z": "cac3a4383120cb57",
        "name": "Machine cycles output 1",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1090,
        "y": 900,
        "wires": []
    },
    {
        "id": "4b0281e42c6cd4d1",
        "type": "debug",
        "z": "cac3a4383120cb57",
        "name": "machine cycles output 2",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1090,
        "y": 940,
        "wires": []
    },
    {
        "id": "02fdc53901e0b70e",
        "type": "debug",
        "z": "cac3a4383120cb57",
        "name": "calculate kpis debug",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1500,
        "y": 980,
        "wires": []
    },
    {
        "id": "b03da2270d8def93",
        "type": "debug",
        "z": "cac3a4383120cb57",
        "name": "Machine cycles output 3",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1090,
        "y": 980,
        "wires": []
    },
    {
        "id": "progress_check_handler_node",
        "type": "function",
        "z": "cac3a4383120cb57",
        "g": "b7ab5e0cc02b9508",
        "name": "Progress Check Handler",
        "func": "// Handle DB result from start-work-order progress check\nif (msg._mode === \"start-check-progress\") {\n    const order = flow.get(\"pendingWorkOrder\");\n\n    if (!order || !order.id) {\n        node.error(\"No pending work order found\", msg);\n        return [null, null];\n    }\n\n    // Get progress from DB query result\n    const dbRow = (Array.isArray(msg.payload) && msg.payload.length > 0) ? msg.payload[0] : null;\n    const cycleCount = dbRow ? (Number(dbRow.cycle_count) || 0) : 0;\n    const goodParts = dbRow ? (Number(dbRow.good_parts) || 0) : 0;\n    const scrapParts = dbRow ? (Number(dbRow.scrap_parts) || 0) : 0;\n    const targetQty = dbRow ? (Number(dbRow.target_qty) || 0) : (Number(order.target) || 0);\n\n    node.warn(`[PROGRESS-CHECK] WO ${order.id}: cycles=${cycleCount}, good=${goodParts}, target=${targetQty}`);\n\n    // Check if work order has existing progress\n    if (cycleCount > 0 || goodParts > 0) {\n        // Work order has progress - send prompt to UI\n        node.warn(`[PROGRESS-CHECK] Work order has existing progress - sending prompt to UI`);\n\n        const promptMsg = {\n            _mode: \"resume-prompt\",\n            topic: \"resumePrompt\",\n            payload: {\n                id: order.id,\n                sku: order.sku || \"\",\n                cycleCount: cycleCount,\n                goodParts: goodParts,\n                targetQty: targetQty,\n                progressPercent: targetQty > 0 ? Math.round((goodParts / targetQty) * 100) : 0,\n                // Include full order object for resume/restart actions\n                order: {...order, cycle_count: cycleCount, good_parts: goodParts, scrap: scrapParts}\n            }\n        };\n\n        return [null, promptMsg];\n    } else {\n        // No existing progress - proceed with normal start\n        // But still use DB values (even if 0) to ensure DB is source of truth\n        node.warn(`[PROGRESS-CHECK] No existing progress - proceeding with normal start`);\n\n        // Update order object with DB values (makes DB the source of truth)\n        order.cycle_count = cycleCount;  // Will be 0 from DB\n        order.good_parts = goodParts;    // Will be 0 from DB\n        order.scrap = scrapParts;        // Will be 0 from DB\n        order.good = goodParts;          // For consistency\n        order.target = targetQty;        // From DB\n\n        const startMsg = {\n            _mode: \"start\",\n            startOrder: order,\n            topic: \"UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, updated_at = CASE WHEN work_order_id = ? THEN NOW() ELSE updated_at END WHERE status <> 'DONE'\",\n            payload: [order.id, order.id]\n        };\n\n        // Initialize global state with DB values (even if 0)\n        global.set(\"activeWorkOrder\", order);\n        global.set(\"cycleCount\", cycleCount);  // Use DB value instead of hardcoded 0\n        flow.set(\"lastMachineState\", 0);\n        global.set(\"scrapPromptIssuedFor\", null);\n\n        node.warn(`[PROGRESS-CHECK] Initialized from DB: cycles=${cycleCount}, good=${goodParts}, scrap=${scrapParts}`);\n\n        return [startMsg, null];\n    }\n}\n\n// Pass through all other messages\nreturn [msg, null];",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1090,
        "y": 340,
        "wires": [
            [
                "578c92e75bf0f266"
            ],
            [
                "f2bab26e27e2023d"
            ]
        ]
    },
    {
        "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",
        "func": "// ============================================================\n// EVENT LOGGER\n// Deduplicates and logs anomaly events to database\n// ============================================================\n\nconst anomalies = msg.payload || [];\n\nif (!Array.isArray(anomalies) || anomalies.length === 0) {\n    return null;\n}\n\n// Get or initialize active anomalies list\nlet activeAnomalies = global.get(\"activeAnomalies\") || [];\nconst now = Date.now();\nconst DEDUP_WINDOW = 5 * 60 * 1000; // 5 minutes\n\nconst dbInserts = [];\nconst uiUpdates = [];\n\nanomalies.forEach(anomaly => {\n    // ============================================================\n    // DEDUPLICATION LOGIC\n    // Don't create new event if same type exists in last 5 minutes\n    // ============================================================\n    const existingIndex = activeAnomalies.findIndex(existing =>\n        existing.anomaly_type === anomaly.anomaly_type &&\n        existing.work_order_id === anomaly.work_order_id &&\n        existing.status === 'active' &&\n        (now - existing.timestamp) < DEDUP_WINDOW\n    );\n\n    if (existingIndex !== -1) {\n        // Update existing event\n        const existing = activeAnomalies[existingIndex];\n        existing.occurrence_count = (existing.occurrence_count || 1) + 1;\n        existing.last_occurrence = now;\n\n        // Update in database\n        const updateQuery = `UPDATE anomaly_events\n            SET occurrence_count = ?, last_occurrence = ?\n            WHERE event_id = ?`;\n\n        dbInserts.push({\n            topic: updateQuery,\n            payload: [existing.occurrence_count, existing.last_occurrence, existing.event_id]\n        });\n\n        node.warn(`[EVENT LOGGER] Updated existing ${anomaly.anomaly_type} event (occurrence #${existing.occurrence_count})`);\n\n    } else if (anomaly.status === 'resolved') {\n        // ============================================================\n        // RESOLVE EVENT\n        // ============================================================\n        const resolveIndex = activeAnomalies.findIndex(existing =>\n            existing.anomaly_type === anomaly.anomaly_type &&\n            existing.work_order_id === anomaly.work_order_id &&\n            existing.status === 'active'\n        );\n\n        if (resolveIndex !== -1) {\n            const existing = activeAnomalies[resolveIndex];\n            existing.status = 'resolved';\n            existing.resolved_at = anomaly.resolved_at || now;\n            existing.auto_resolved = anomaly.auto_resolved || false;\n\n            // Update in database\n            const resolveQuery = `UPDATE anomaly_events\n                SET status = 'resolved', resolved_at = ?, auto_resolved = ?\n                WHERE event_id = ?`;\n\n            dbInserts.push({\n                topic: resolveQuery,\n                payload: [existing.resolved_at, existing.auto_resolved, existing.event_id]\n            });\n\n            // Remove from active list\n            activeAnomalies.splice(resolveIndex, 1);\n\n            node.warn(`[EVENT LOGGER] Resolved ${anomaly.anomaly_type} event (auto: ${existing.auto_resolved})`);\n\n            uiUpdates.push({\n                event_id: existing.event_id,\n                status: 'resolved'\n            });\n        }\n\n    } else {\n        // ============================================================\n        // NEW EVENT\n        // ============================================================\n        const insertQuery = `INSERT INTO anomaly_events\n            (timestamp, work_order_id, anomaly_type, severity, title, description,\n             data_json, kpi_snapshot_json, status, cycle_count, occurrence_count, last_occurrence)\n            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;\n\n        const dataJson = JSON.stringify(anomaly.data || {});\n        const kpiJson = JSON.stringify(anomaly.kpi_snapshot || {});\n\n        dbInserts.push({\n            topic: insertQuery,\n            payload: [\n                anomaly.timestamp,\n                anomaly.work_order_id,\n                anomaly.anomaly_type,\n                anomaly.severity,\n                anomaly.title,\n                anomaly.description,\n                dataJson,\n                kpiJson,\n                'active',\n                anomaly.cycle_count,\n                1,  // occurrence_count\n                anomaly.timestamp  // last_occurrence\n            ],\n            _storeEventId: true,  // Flag to get generated event_id\n            _anomaly: anomaly  // Keep reference for later\n        });\n\n        node.warn(`[EVENT LOGGER] New ${anomaly.anomaly_type} event: ${anomaly.title}`);\n    }\n});\n\n// Save active anomalies to global context\nglobal.set(\"activeAnomalies\", activeAnomalies);\n\n// ============================================================\n// OUTPUT\n// ============================================================\n// Output 1: Database inserts (to mysql node)\n// Output 2: UI updates (to Home/Alerts tabs)\n\nif (dbInserts.length > 0) {\n    // Send each insert as a separate message\n    return [dbInserts, {\n        topic: \"anomaly-ui-update\",\n        payload: {\n            activeCount: activeAnomalies.length,\n            activeAnomalies: activeAnomalies,\n            updates: uiUpdates\n        }\n    }];\n} else {\n    return [null, {\n        topic: \"anomaly-ui-update\",\n        payload: {\n            activeCount: activeAnomalies.length,\n            activeAnomalies: activeAnomalies,\n            updates: uiUpdates\n        }\n    }];\n}\n",
        "outputs": 2,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 1050,
        "y": 300,
        "wires": [
            [
                "anomaly_split_node_id"
            ],
            [
                "anomaly_alert_ui_global"
            ]
        ]
    },
    {
        "id": "anomaly_split_node_id",
        "type": "split",
        "z": "cac3a4383120cb57",
        "name": "Split DB Inserts",
        "splt": "\\n",
        "spltType": "str",
        "arraySplt": 1,
        "arraySpltType": "len",
        "stream": false,
        "addname": "",
        "x": 1270,
        "y": 240,
        "wires": [
            [
                "anomaly_mysql_node_id"
            ]
        ]
    },
    {
        "id": "anomaly_mysql_node_id",
        "type": "mysql",
        "z": "cac3a4383120cb57",
        "mydb": "00d8ad2b0277f906",
        "name": "Anomaly Events DB",
        "x": 1270,
        "y": 280,
        "wires": [
            [
                "anomaly_db_debug_node_id"
            ]
        ]
    },
    {
        "id": "anomaly_db_debug_node_id",
        "type": "debug",
        "z": "cac3a4383120cb57",
        "name": "Anomaly DB Result",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1490,
        "y": 280,
        "wires": []
    },
    {
        "id": "init_oee_threshold",
        "type": "inject",
        "z": "cac3a4383120cb57",
        "name": "Initialize OEE Threshold (90%)",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "90",
        "payloadType": "num",
        "x": 290,
        "y": 940,
        "wires": [
            [
                "set_oee_threshold_global"
            ]
        ]
    },
    {
        "id": "set_oee_threshold_global",
        "type": "function",
        "z": "cac3a4383120cb57",
        "name": "Set OEE Threshold Global",
        "func": "// Initialize OEE alert threshold\nconst threshold = Number(msg.payload) || 90;\nglobal.set(\"oeeAlertThreshold\", threshold);\n\nnode.warn(`[CONFIG] OEE Alert Threshold set to ${threshold}%`);\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 550,
        "y": 940,
        "wires": [
            []
        ]
    },
    {
        "id": "verify_create_table_inject",
        "type": "inject",
        "z": "cac3a4383120cb57",
        "name": "VERIFY & CREATE anomaly_events (click me)",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 220,
        "y": 1000,
        "wires": [
            [
                "verify_create_table_check"
            ]
        ]
    },
    {
        "id": "verify_create_table_check",
        "type": "function",
        "z": "cac3a4383120cb57",
        "name": "Check Table Exists",
        "func": "// First check if table exists\nmsg.topic = \"SHOW TABLES LIKE 'anomaly_events'\";\nmsg.payload = [];\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 480,
        "y": 1000,
        "wires": [
            [
                "verify_create_table_mysql_check"
            ]
        ]
    },
    {
        "id": "verify_create_table_mysql_check",
        "type": "mysql",
        "z": "cac3a4383120cb57",
        "mydb": "00d8ad2b0277f906",
        "name": "Check Table",
        "x": 680,
        "y": 1000,
        "wires": [
            [
                "verify_create_table_switch"
            ]
        ]
    },
    {
        "id": "verify_create_table_switch",
        "type": "switch",
        "z": "cac3a4383120cb57",
        "name": "Table Exists?",
        "property": "payload.length",
        "propertyType": "msg",
        "rules": [
            {
                "t": "eq",
                "v": "0",
                "vt": "num"
            },
            {
                "t": "gt",
                "v": "0",
                "vt": "num"
            }
        ],
        "checkall": "true",
        "repair": false,
        "outputs": 2,
        "x": 850,
        "y": 1000,
        "wires": [


580→        "id": "06f9769e8b0d5355",
   581→        "type": "ui_template",
   582→        "z": "cac3a4383120cb57",
   583→        "g": "ec32d0a62eacfb22",
   584→        "group": "",
   585→        "name": "General Style",
   586→        "order": 0,
   587→        "width": 0,
   588→        "height": 0,
   589→        "format": "",
   590→        "storeOutMessages": true,
   591→        "fwdInMessages": true,
   592→        "resendOnRefresh": true,
   593→        "templateScope": "global",
   594→        "className": "",
   595→        "x": 560,
   596→        "y": 140,
   597→        "wires": [
   598→            []
   599→        ]
   600→    },
   601→    {
   602→        "id": "anomaly_alert_ui_global",
   603→        "type": "ui_template",
   604→        "z": "cac3a4383120cb57",
   605→        "g": "ec32d0a62eacfb22",
   606→        "group": "",
   607→        "name": "Anomaly Alert System (Global)",
   608→        "order": 0,
   609→        "width": 0,
   610→        "height": 0,
   611→        "format": "\n\n\n<button id=\"anomaly-toggle-btn\" ng-click=\"toggleAnomalyPanel()\">\n   0\">{{activeAnomalyCount}}\n  ⚠\n  ALERTS\n</button>\n\n\n
\n
\n
Active Alerts
\n    <button class=\"anomaly-close-btn\" ng-click=\"toggleAnomalyPanel()\">×</button>\n
\n  \n
\n
\n
✅
\n
No active alerts
\n
All systems operating normally
\n
\n    \n
\n
\n
{{anomaly.title}}
\n        {{anomaly.severity}}\n
\n
{{anomaly.description}}
\n
{{formatTimestamp(anomaly.timestamp)}}
\n
\n        <button class=\"anomaly-ack-btn\" ng-click=\"acknowledgeAnomaly(anomaly)\">Acknowledge</button>\n
\n
\n
\n
\n\n\n
\n
\n
\n
{{popup.title}}
\n      <button class=\"anomaly-popup-close\" ng-click=\"closePopup(popup)\">×</button>\n
\n
{{popup.description}}
\n    <button class=\"anomaly-popup-ack\" ng-click=\"acknowledgePopup(popup)\">Acknowledge Alert</button>\n
\n
\n\n",
   612→        "storeOutMessages": true,
   613→        "fwdInMessages": true,
   614→        "resendOnRefresh": true,
   615→        "templateScope": "global",
   616→        "className": "",
   617→        "x": 670,
   618→        "y": 20,
   619→        "wires": [
   620→            [
   621→                "anomaly_acknowledge_handler"
   622→            ]
   623→        ]
   624→    },
   625→    {
   626→        "id": "anomaly_acknowledge_handler",
   627→        "type": "function",
   628→        "z": "cac3a4383120cb57",
   629→        "g": "ec32d0a62eacfb22",
   630→        "name": "Handle Anomaly Acknowledgment",
   631→        "func": "// Handle acknowledgment from UI\nif (msg.topic === 'acknowledge-anomaly') {\n    const ackData = msg.payload || {};\n    const eventId = ackData.event_id;\n    const ackTimestamp = ackData.timestamp || Date.now();\n    \n    if (!eventId) {\n        return null;\n    }\n    \n    // Update database\n    const updateQuery = `UPDATE anomaly_events \n        SET status = 'acknowledged', acknowledged_at = ? \n        WHERE event_id = ?`;\n    \n    msg.topic = updateQuery;\n    msg.payload = [ackTimestamp, eventId];\n    \n    node.warn(`[ANOMALY ACK] Event ${eventId} acknowledged`);\n    \n    return msg;\n}\n\nreturn null;",
   632→        "outputs": 1,
   633→        "timeout": 0,
   634→        "noerr": 0,
   635→        "initialize": "",
   636→        "finalize": "",
   637→        "libs": [],
   638→        "x": 820,
   639→        "y": 160,
   640→        "wires": [
   641→            [
   642→                "anomaly_mysql_node_id"
   643→            ]
   644→        ]
   645→    },
   646→    {
   647→        "id": "6ad64dedab2042b9",
   648→        "type": "inject",
   649→        "z": "cac3a4383120cb57",
   650→        "g": "16bb591480852f51",
   651→        "name": "Simula Inyectora",
   652→        "props": [
   653→            {
   654→                "p": "payload"
   655→            }
   656→        ],
   657→        "repeat": "1",
   658→        "crontab": "",
   659→        "once": true,
   660→        "onceDelay": 0.1,
   661→        "topic": "",
   662→        "payload": "1",
   663→        "payloadType": "num",
   664→        "x": 170,
   665→        "y": 380,
   666→        "wires": [
   667→            [
   668→                "0f5ee343ed17976c"
   669→            ]
   670→        ]
   671→    },
   672→    {
   673→        "id": "0f5ee343ed17976c",
   674→        "type": "function",
   675→        "z": "cac3a4383120cb57",
   676→        "g": "16bb591480852f51",
   677→        "name": "1,0",
   678→        "func": "// Get current global value (default to 0 if not set)\nlet estado = global.get('Estado_maquina') || 0;\nlet stop = flow.get('stop') || false;\n\nif (stop) {\n    // Manual stop active → force 0, don't reschedule\n    global.set('Estado_maquina', 0);\n    msg.payload = 0;\n    node.send(msg);\n    return;\n}\n\n// Toggle between 1 and 0\nestado = estado === 1 ? 0 : 1;\n\n// Update the global variable\nglobal.set('Estado_maquina', estado);\n\n// Send it out\nmsg.payload = estado;\nreturn msg;\n",
   679→        "outputs": 1,
   680→        "timeout": 0,
   681→        "noerr": 0,
   682→        "initialize": "",
   683→        "finalize": "",
   684→        "libs": [],
   685→        "x": 490,
   686→        "y": 400,
   687→        "wires": [
   688→            [
   689→                "0d023d87a13bf56f"
   690→            ]
   691→        ]
   692→    },
   693→    {
   694→        "id": "4e025693949ec4bd",
   695→        "type": "inject",
   696→        "z": "cac3a4383120cb57",
   697→        "g": "16bb591480852f51",
   698→        "name": "Manual Stop",
   699→        "props": [
   700→            {
   701→                "p": "payload"
   702→            },
   703→            {
   704→                "p": "topic",
   705→                "vt": "str"
   706→            }
   707→        ],
   708→        "repeat": "",
   709→        "crontab": "",
   710→        "once": false,
   711→        "onceDelay": 0.1,
   712→        "topic": "",
   713→        "payload": "",
   714→        "payloadType": "date",
   715→        "x": 150,
   716→        "y": 420,
   717→        "wires": [
   718→            [
   719→                "b55c91c096a366db"
   720→            ]
   721→        ]
   722→    },
   723→    {
   724→        "id": "b55c91c096a366db",
   725→        "type": "change",
   726→        "z": "cac3a4383120cb57",
   727→        "g": "16bb591480852f51",
   728→        "name": "",
   729→        "rules": [
   730→            {
   731→                "t": "set",
   732→                "p": "stop",
   733→                "pt": "flow",
   734→                "to": "true",
   735→                "tot": "bool"
   736→            }
   737→        ],
   738→        "action": "",
   739→        "property": "",
   740→        "from": "",
   741→        "to": "",
   742→        "reg": false,
   743→        "x": 330,
   744→        "y": 420,
   745→        "wires": [
   746→            [
   747→                "0f5ee343ed17976c"
   748→            ]
   749→        ]
   750→    },
   751→    {
   752→        "id": "33d1f41119e0e262",
   753→        "type": "inject",
   754→        "z": "cac3a4383120cb57",
   755→        "g": "16bb591480852f51",
   756→        "name": "Manual Start",
   757→        "props": [
   758→            {
   759→                "p": "payload"
   760→            },
   761→            {
   762→                "p": "topic",
   763→                "vt": "str"
   764→            }
   765→        ],
   766→        "repeat": "",
   767→        "crontab": "",
   768→        "once": false,
   769→        "onceDelay": 0.1,
   770→        "topic": "",
   771→        "payload": "",
   772→        "payloadType": "date",
   773→        "x": 150,
   774→        "y": 460,
   775→        "wires": [
   776→            [
   777→                "f98ae23b2430c206"
   778→            ]
   779→        ]

<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": "06f9769e8b0d5355",
        "type": "ui_template",
        "z": "cac3a4383120cb57",
        "g": "ec32d0a62eacfb22",
        "group": "",
        "name": "General Style",
        "order": 0,
        "width": 0,
        "height": 0,
        "format": "",
        "storeOutMessages": true,
        "fwdInMessages": true,
        "resendOnRefresh": true,
        "templateScope": "global",
        "className": "",
        "x": 560,
        "y": 140,
        "wires": [
            []
        ]
    },
    {
        "id": "anomaly_alert_ui_global",
        "type": "ui_template",
        "z": "cac3a4383120cb57",
        "g": "ec32d0a62eacfb22",
        "group": "",
        "name": "Anomaly Alert System (Global)",
        "order": 0,
        "width": 0,
        "height": 0,
        "format": "\n\n\n<button id=\"anomaly-toggle-btn\" ng-click=\"toggleAnomalyPanel()\">\n   0\">{{activeAnomalyCount}}\n  ⚠\n  ALERTS\n</button>\n\n\n
\n
\n
Active Alerts
\n    <button class=\"anomaly-close-btn\" ng-click=\"toggleAnomalyPanel()\">×</button>\n
\n  \n
\n
\n
✅
\n
No active alerts
\n
All systems operating normally
\n
\n    \n
\n
\n
{{anomaly.title}}
\n        {{anomaly.severity}}\n
\n
{{anomaly.description}}
\n
{{formatTimestamp(anomaly.timestamp)}}
\n
\n        <button class=\"anomaly-ack-btn\" ng-click=\"acknowledgeAnomaly(anomaly)\">Acknowledge</button>\n
\n
\n
\n
\n\n\n
\n
\n
\n
{{popup.title}}
\n      <button class=\"anomaly-popup-close\" ng-click=\"closePopup(popup)\">×</button>\n
\n
{{popup.description}}
\n    <button class=\"anomaly-popup-ack\" ng-click=\"acknowledgePopup(popup)\">Acknowledge Alert</button>\n
\n
\n\n",
        "storeOutMessages": true,
        "fwdInMessages": true,
        "resendOnRefresh": true,
        "templateScope": "global",
        "className": "",
        "x": 670,
        "y": 20,
        "wires": [
            [
                "anomaly_acknowledge_handler"
            ]
        ]
    },
    {
        "id": "anomaly_acknowledge_handler",
        "type": "function",
        "z": "cac3a4383120cb57",
        "g": "ec32d0a62eacfb22",
        "name": "Handle Anomaly Acknowledgment",
        "func": "// Handle acknowledgment from UI\nif (msg.topic === 'acknowledge-anomaly') {\n    const ackData = msg.payload || {};\n    const eventId = ackData.event_id;\n    const ackTimestamp = ackData.timestamp || Date.now();\n    \n    if (!eventId) {\n        return null;\n    }\n    \n    // Update database\n    const updateQuery = `UPDATE anomaly_events \n        SET status = 'acknowledged', acknowledged_at = ? \n        WHERE event_id = ?`;\n    \n    msg.topic = updateQuery;\n    msg.payload = [ackTimestamp, eventId];\n    \n    node.warn(`[ANOMALY ACK] Event ${eventId} acknowledged`);\n    \n    return msg;\n}\n\nreturn null;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 820,
        "y": 160,
        "wires": [
            [
                "anomaly_mysql_node_id"
            ]
        ]
    },
    {
        "id": "6ad64dedab2042b9",
        "type": "inject",
        "z": "cac3a4383120cb57",
        "g": "16bb591480852f51",
        "name": "Simula Inyectora",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "1",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "1",
        "payloadType": "num",
        "x": 170,
        "y": 380,
        "wires": [
            [
                "0f5ee343ed17976c"
            ]
        ]
    },
    {
        "id": "0f5ee343ed17976c",
        "type": "function",
        "z": "cac3a4383120cb57",
        "g": "16bb591480852f51",
        "name": "1,0",
        "func": "// Get current global value (default to 0 if not set)\nlet estado = global.get('Estado_maquina') || 0;\nlet stop = flow.get('stop') || false;\n\nif (stop) {\n    // Manual stop active → force 0, don't reschedule\n    global.set('Estado_maquina', 0);\n    msg.payload = 0;\n    node.send(msg);\n    return;\n}\n\n// Toggle between 1 and 0\nestado = estado === 1 ? 0 : 1;\n\n// Update the global variable\nglobal.set('Estado_maquina', estado);\n\n// Send it out\nmsg.payload = estado;\nreturn msg;\n",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 490,
        "y": 400,
        "wires": [
            [
                "0d023d87a13bf56f"
            ]
        ]
    },
    {
        "id": "4e025693949ec4bd",
        "type": "inject",
        "z": "cac3a4383120cb57",
        "g": "16bb591480852f51",
        "name": "Manual Stop",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 150,
        "y": 420,
        "wires": [
            [
                "b55c91c096a366db"
            ]
        ]
    },
    {
        "id": "b55c91c096a366db",
        "type": "change",
        "z": "cac3a4383120cb57",
        "g": "16bb591480852f51",
        "name": "",
        "rules": [
            {
                "t": "set",
                "p": "stop",
                "pt": "flow",
                "to": "true",
                "tot": "bool"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 330,
        "y": 420,
        "wires": [
            [
                "0f5ee343ed17976c"
            ]
        ]
    },
    {
        "id": "33d1f41119e0e262",
        "type": "inject",
        "z": "cac3a4383120cb57",
        "g": "16bb591480852f51",
        "name": "Manual Start",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 150,
        "y": 460,
        "wires": [
            [
                "f98ae23b2430c206"
            ]
        ]


  1950→        "wires": [
  1951→            [
  1952→                "anomaly_db_debug_node_id"
  1953→            ]
  1954→        ]
  1955→    },
  1956→    {
  1957→        "id": "anomaly_db_debug_node_id",
  1958→        "type": "debug",
  1959→        "z": "cac3a4383120cb57",
  1960→        "name": "Anomaly DB Result",
  1961→        "active": true,
  1962→        "tosidebar": true,
  1963→        "console": false,
  1964→        "tostatus": false,
  1965→        "complete": "true",
  1966→        "targetType": "full",
  1967→        "statusVal": "",
  1968→        "statusType": "auto",
  1969→        "x": 1490,
  1970→        "y": 280,
  1971→        "wires": []
  1972→    },
  1973→    {
  1974→        "id": "init_oee_threshold",
  1975→        "type": "inject",
  1976→        "z": "cac3a4383120cb57",
  1977→        "name": "Initialize OEE Threshold (90%)",
  1978→        "props": [
  1979→            {
  1980→                "p": "payload"
  1981→            }
  1982→        ],
  1983→        "repeat": "",
  1984→        "crontab": "",
  1985→        "once": true,
  1986→        "onceDelay": 0.1,
  1987→        "topic": "",
  1988→        "payload": "90",
  1989→        "payloadType": "num",
  1990→        "x": 290,
  1991→        "y": 940,
  1992→        "wires": [
  1993→            [
  1994→                "set_oee_threshold_global"
  1995→            ]
  1996→        ]
  1997→    },
  1998→    {
  1999→        "id": "set_oee_threshold_global",
  2000→        "type": "function",
  2001→        "z": "cac3a4383120cb57",
  2002→        "name": "Set OEE Threshold Global",
  2003→        "func": "// Initialize OEE alert threshold\nconst threshold = Number(msg.payload) || 90;\nglobal.set(\"oeeAlertThreshold\", threshold);\n\nnode.warn(`[CONFIG] OEE Alert Threshold set to ${threshold}%`);\n\nreturn msg;",
  2004→        "outputs": 1,
  2005→        "timeout": 0,
  2006→        "noerr": 0,
  2007→        "initialize": "",
  2008→        "finalize": "",
  2009→        "libs": [],
  2010→        "x": 550,
  2011→        "y": 940,
  2012→        "wires": [
  2013→            []
  2014→        ]
  2015→    },
  2016→    {
  2017→        "id": "verify_create_table_inject",
  2018→        "type": "inject",
  2019→        "z": "cac3a4383120cb57",
  2020→        "name": "VERIFY & CREATE anomaly_events (click me)",
  2021→        "props": [
  2022→            {
  2023→                "p": "payload"
  2024→            }
  2025→        ],
  2026→        "repeat": "",
  2027→        "crontab": "",
  2028→        "once": false,
  2029→        "onceDelay": 0.1,
  2030→        "topic": "",
  2031→        "payload": "",
  2032→        "payloadType": "date",
  2033→        "x": 220,
  2034→        "y": 1000,
  2035→        "wires": [
  2036→            [
  2037→                "verify_create_table_check"
  2038→            ]
  2039→        ]
  2040→    },
  2041→    {
  2042→        "id": "verify_create_table_check",
  2043→        "type": "function",
  2044→        "z": "cac3a4383120cb57",
  2045→        "name": "Check Table Exists",
  2046→        "func": "// First check if table exists\nmsg.topic = \"SHOW TABLES LIKE 'anomaly_events'\";\nmsg.payload = [];\nreturn msg;",
  2047→        "outputs": 1,
  2048→        "timeout": 0,
  2049→        "noerr": 0,

<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>

        "wires": [
            [
                "anomaly_db_debug_node_id"
            ]
        ]
    },
    {
        "id": "anomaly_db_debug_node_id",
        "type": "debug",
        "z": "cac3a4383120cb57",
        "name": "Anomaly DB Result",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 1490,
        "y": 280,
        "wires": []
    },
    {
        "id": "init_oee_threshold",
        "type": "inject",
        "z": "cac3a4383120cb57",
        "name": "Initialize OEE Threshold (90%)",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "90",
        "payloadType": "num",
        "x": 290,
        "y": 940,
        "wires": [
            [
                "set_oee_threshold_global"
            ]
        ]
    },
    {
        "id": "set_oee_threshold_global",
        "type": "function",
        "z": "cac3a4383120cb57",
        "name": "Set OEE Threshold Global",
        "func": "// Initialize OEE alert threshold\nconst threshold = Number(msg.payload) || 90;\nglobal.set(\"oeeAlertThreshold\", threshold);\n\nnode.warn(`[CONFIG] OEE Alert Threshold set to ${threshold}%`);\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 550,
        "y": 940,
        "wires": [
            []
        ]
    },
    {
        "id": "verify_create_table_inject",
        "type": "inject",
        "z": "cac3a4383120cb57",
        "name": "VERIFY & CREATE anomaly_events (click me)",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 220,
        "y": 1000,
        "wires": [
            [
                "verify_create_table_check"
            ]
        ]
    },
    {
        "id": "verify_create_table_check",
        "type": "function",
        "z": "cac3a4383120cb57",
        "name": "Check Table Exists",
        "func": "// First check if table exists\nmsg.topic = \"SHOW TABLES LIKE 'anomaly_events'\";\nmsg.payload = [];\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,


Perfect! I've now read the key sections of the flows.json file and the summary document. Let me understand what was implemented:

1. **Anomaly Detector** (lines 1881-1899): A comprehensive function node that detects multiple types of anomalies:
   - Slow cycle detection
   - Production stoppage detection
   - OEE drop detection
   - Quality spike detection
   - Performance degradation detection
   - Predictive trend analysis

2. **Event Logger** (lines 1901-1922): Handles deduplication and logging of anomaly events to the database, with two outputs:
   - Output 1: DB inserts
   - Output 2: UI updates (wired to the global alert UI)

3. **Global Alert UI** (lines 602-624): A ui_template node that creates:
   - Floating alert panel (slides from right)
   - Pop-up notifications
   - Alert badge counter
   - Pulsing animations

4. **Acknowledgment Handler** (lines 626-644): Handles when users acknowledge alerts from the UI

5. **OEE Threshold Initialization** (lines 1974-2015): Sets up the OEE threshold (default 90%) on startup

The system is essentially complete based on the summary. Now I need to let the user know I'm up to speed and ready to continue. I should ask what they'd like to work on next or if there are any issues they're experiencing.


Perfect! I'm now up to speed on the automatic anomaly detection system. Let me summarize what I've found:

## Current Implementation

The system has been successfully implemented with:

### 1. **Anomaly Detector** (flows.json:1881)
Detects 6 types of anomalies:
- **Tier 1**: Slow cycles, production stoppages
- **Tier 2**: OEE drops, quality spikes, performance degradation
- **Tier 3**: Predictive OEE decline trends

### 2. **Event Logger** (flows.json:1901)
Handles deduplication and database logging with:
- 5-minute deduplication window
- Automatic event resolution
- Two outputs: DB inserts and UI updates

### 3. **Global Alert UI** (flows.json:602)
Floating panel system with:
- Slide-in alert panel from the right
- Pop-up notifications for critical/warning alerts
- Badge counter with pulsing animation
- Acknowledge button functionality

### 4. **Acknowledgment Handler** (flows.json:626)
Updates database when alerts are acknowledged

### 5. **Configuration** (flows.json:1974)
OEE threshold initializer (default: 90%)

## System Flow
Data → Anomaly Detector → Event Logger → [DB + UI] → User Acknowledgment → DB Update

---

I'm ready to continue! What would you like to work on next? Some possibilities:
- Testing the system
- Adjusting thresholds or detection logic
- Adding new anomaly types
- UI improvements
- Database table creation/verification
- Something else?


Question, I still need to create the table in my db right? I can use create if not exist, but looking at my db, I cant see it .Please provide prompt to create as needed in this situation. Thanks


The user is asking about creating the database table for the anomaly detection system. They mentioned they can't see the table in their database and need help creating it.

Looking at the flows.json file I read, there's a node called "verify_create_table_inject" at line 2017 which appears to be a system to verify and create the table. But I should provide them with the actual SQL CREATE TABLE statement they need.

Let me look at what fields are being inserted in the Event Logger to understand the table structure:

From the Event Logger code (line 1905):
```javascript
const insertQuery = `INSERT INTO anomaly_events
    (timestamp, work_order_id, anomaly_type, severity, title, description,
     data_json, kpi_snapshot_json, status, cycle_count, occurrence_count, last_occurrence)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
```

And from the update queries, I can see these additional fields:
- `event_id` (primary key, auto-increment)
- `acknowledged_at` (timestamp)
- `resolved_at` (timestamp)
- `auto_resolved` (boolean)

So the complete table structure should include all these fields. Let me create a proper CREATE TABLE IF NOT EXISTS statement for MySQL/MariaDB.


You're absolutely right! You need to create the `anomaly_events` table in your database. Here's the SQL statement you need:

```sql
CREATE TABLE IF NOT EXISTS anomaly_events (
    event_id INT AUTO_INCREMENT PRIMARY KEY,
    timestamp BIGINT NOT NULL,
    work_order_id VARCHAR(255) NOT NULL,
    anomaly_type VARCHAR(100) NOT NULL,
    severity ENUM('critical', 'warning', 'info') NOT NULL,
    title VARCHAR(255) NOT NULL,
    description TEXT,
    data_json TEXT,
    kpi_snapshot_json TEXT,
    status ENUM('active', 'acknowledged', 'resolved') DEFAULT 'active',
    cycle_count INT DEFAULT 0,
    occurrence_count INT DEFAULT 1,
    last_occurrence BIGINT,
    acknowledged_at BIGINT,
    resolved_at BIGINT,
    auto_resolved BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_work_order (work_order_id),
    INDEX idx_status (status),
    INDEX idx_timestamp (timestamp),
    INDEX idx_anomaly_type (anomaly_type)
);
```

## How to Run It

You have **two options**:

### Option 1: Use the Built-in Node-RED Flow
Looking at your flows.json, there's already a node called **"VERIFY & CREATE anomaly_events (click me)"** at line 2017. You should be able to:
1. Open your Node-RED editor
2. Find this inject node (it should be visible in your flow)
3. Click it to verify and create the table

However, I noticed the flow might be incomplete. Let me check if it has the CREATE TABLE logic...

### Option 2: Run SQL Directly (Recommended)
Execute the SQL statement above directly in your MySQL/MariaDB client:
- **phpMyAdmin**: Go to SQL tab and paste the statement
- **MySQL Workbench**: Open a query window and execute
- **Command line**: `mysql -u username -p database_name` then paste the SQL

**Which method would you prefer?** If you'd like me to complete the Node-RED verification flow to make it fully automated, I can do that too!


Great claude, I got the table created. I cant see what was implemented here in the summary. I cant see the floating widget, cant see anything in the table created. Can you help me see what wasnt implemented, why Im not being able to see the implementations. Be careful, dont edit anything, I just want you to identify, figure out what happened, if there is a wiring issue, coding issue. Take a look and figure out what happened please. If you have a question, dont assume, just let me know please. Thanks


The user says they can't see:
1. The floating alert widget/panel in the UI
2. Any data in the anomaly_events table

They want me to investigate why the implementation isn't visible/working, but NOT to edit anything yet - just identify the issues.

Let me think about what could be wrong:

1. **Floating UI Widget not visible**:
   - The global UI template exists (anomaly_alert_ui_global at line 602)
   - But I need to check if it's properly configured to display on a dashboard
   - Need to check the "group" field - I see it says `"group": ""` which might mean it's not assigned to a dashboard group
   - The templateScope is set to "global" which is correct
   - But for it to actually show up, the user needs to be viewing a Node-RED dashboard

2. **No data in the table**:
   - This could mean anomalies aren't being detected
   - Or the flow isn't running
   - Or there's no production data flowing through
   - I need to check if the Anomaly Detector is actually receiving data
   - Need to check the wiring - where does data come FROM to get TO the anomaly detector?

Let me search for what feeds into the anomaly detector. I need to see what's wired TO the anomaly_detector_node_id. Looking at the code I read, the anomaly detector expects:
```javascript
const cycle = msg.cycle || {};
const kpis = msg.kpis || {};
const activeOrder = global.get("activeWorkOrder") || {};
```

So it needs a message with `cycle` and `kpis` data.

I should:
1. Search for what nodes are wired to the anomaly detector input
2. Check if the UI template is properly configured
3. Ask the user questions about what they're seeing

Let me read more of the flows.json to understand the wiring and find what feeds the anomaly detector.


Good thinking! Let me investigate without making any changes. I need to understand what's happening (or not happening).


1246:                "anomaly_detector_node_id"
1881:        "id": "anomaly_detector_node_id",


No matches found


Interesting! The grep for wires containing anomaly_detector_node_id found nothing, which means nothing is wired TO the anomaly detector as an input. But the first grep shows it's referenced at line 1246 and defined at line 1881. Let me check line 1246 to see what that reference is.


  1230→        "outputs": 4,
  1231→        "timeout": 0,
  1232→        "noerr": 0,
  1233→        "initialize": "",
  1234→        "finalize": "",
  1235→        "libs": [],
  1236→        "x": 640,
  1237→        "y": 400,
  1238→        "wires": [
  1239→            [
  1240→                "dbc7a5ee041845ed",
  1241→                "68747d170d530562"
  1242→            ],
  1243→            [
  1244→                "00b6132848964bd9",
  1245→                "4b0281e42c6cd4d1",
  1246→                "anomaly_detector_node_id"
  1247→            ],
  1248→            [
  1249→                "00b6132848964bd9",
  1250→                "b03da2270d8def93"
  1251→            ],
  1252→            [
  1253→                "db_guard_db_guard_cycles",
  1254→                "4fabedfe0668e215"
  1255→            ]
  1256→        ]
  1257→    },
  1258→    {
  1259→        "id": "dbc7a5ee041845ed",

<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>

        "outputs": 4,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 640,
        "y": 400,
        "wires": [
            [
                "dbc7a5ee041845ed",
                "68747d170d530562"
            ],
            [
                "00b6132848964bd9",
                "4b0281e42c6cd4d1",
                "anomaly_detector_node_id"
            ],
            [
                "00b6132848964bd9",
                "b03da2270d8def93"
            ],
            [
                "db_guard_db_guard_cycles",
                "4fabedfe0668e215"
            ]
        ]
    },
    {
        "id": "dbc7a5ee041845ed",


Good! So I found that the anomaly_detector_node_id IS wired as an output from some node. It's on output 2 (the second array in the wires). Let me find what node this is by reading backward from line 1230.