Excellent question! Let me think through this comprehensively - automatic anomaly detection could absolutely be the killer feature here. Let me analyze what we have and design the best approach.

## Current System Strengths We Can Build On:

1. **Real-time cycle tracking** - Every cycle is captured with precise timing
2. **KPI calculation** - OEE, Availability, Performance, Quality computed continuously
3. **Historical data** - KPI history arrays storing trends over time
4. **Database persistence** - Work orders, cycle counts, timestamps all recorded
5. **UI infrastructure** - Home, Alerts, and Graphs tabs ready for enhancement

## Anomaly Types Worth Detecting:

### **Tier 1 - Critical & Immediate Value:**

1. **Production Stoppage**
   - *What*: Machine stopped unexpectedly during active work order
   - *Detection*: No cycle received in >2x theoretical_cycle_time
   - *Impact*: Directly kills Availability & OEE
   - *Value*: Immediate alert to check machine

2. **Slow Cycle Anomaly**
   - *What*: Cycle taking significantly longer than theoretical time
   - *Detection*: Actual cycle duration > 1.5x theoretical_cycle_time
   - *Impact*: Performance degradation
   - *Value*: Early warning of mechanical issues, material flow problems

3. **Scrap Spike**
   - *What*: Multiple scrap parts in succession
   - *Detection*: X scrap parts in last Y cycles (e.g., 2 scrap in last 5 cycles)
   - *Impact*: Quality drops, waste increases
   - *Value*: Catch quality issues before major batch loss

### **Tier 2 - Pattern Recognition:**

4. **Quality Degradation Trend**
   - *What*: Quality slowly declining over time
   - *Detection*: Moving average quality < baseline - threshold
   - *Impact*: Gradual loss of efficiency
   - *Value*: Predictive maintenance signal (tool wear, calibration drift)

5. **Performance Degradation**
   - *What*: Cycles gradually slowing down
   - *Detection*: 10-cycle moving average > baseline + threshold
   - *Impact*: Throughput reduction
   - *Value*: Early warning before major slowdown

6. **OEE Critical Threshold**
   - *What*: Overall OEE dropped below acceptable level
   - *Detection*: OEE < 60% (configurable)
   - *Impact*: Production efficiency
   - *Value*: Executive-level alert

### **Tier 3 - Advanced Insights:**

7. **Recurring Pattern Detection**
   - *What*: Same anomaly happening at regular intervals
   - *Detection*: Pattern matching on event timestamps
   - *Value*: Identify systematic issues (shift changes, material batches)

8. **Correlation Analysis**
   - *What*: Multiple anomalies occurring together
   - *Detection*: Scrap spike + slow cycles = root cause indicator
   - *Value*: Faster troubleshooting

## Proposed Architecture:

```
DATA FLOW:
┌─────────────────────────────────────────────────────────┐
│ Machine Cycles (existing)                               │
│  ↓                                                       │
│ Calculate KPIs (existing)                               │
│  ↓                                                       │
│ ┌─────────────────┐         ┌────────────────────────┐ │
│ │ Record KPI      │         │ ANOMALY DETECTOR       │ │
│ │ History         │         │ (NEW)                  │ │
│ │ (existing)      │    ←────│ • Analyze cycle data   │ │
│ └─────────────────┘         │ • Check thresholds     │ │
│                             │ • Compare to baseline  │ │
│                             │ • Detect patterns      │ │
│                             └────────┬───────────────┘ │
│                                      ↓                  │
│                             ┌────────────────────────┐ │
│                             │ EVENT LOGGER           │ │
│                             │ (NEW)                  │ │
│                             │ • Deduplicate events   │ │
│                             │ • Calculate severity   │ │
│                             │ • Store to DB          │ │
│                             │ • Update global state  │ │
│                             └────────┬───────────────┘ │
│                                      ↓                  │
│                    ┌─────────────────┴─────────────┐   │
│                    ↓                               ↓   │
│           ┌────────────────┐            ┌──────────────┐
│           │ Database       │            │ UI Updates   │
│           │ anomaly_events │            │ • Home badge │
│           │ table          │            │ • Alerts tab │
│           └────────────────┘            └──────────────┘
└─────────────────────────────────────────────────────────┘
```

## Database Schema:

```sql
CREATE TABLE anomaly_events (
    event_id INT AUTO_INCREMENT PRIMARY KEY,
    timestamp BIGINT NOT NULL,
    work_order_id VARCHAR(50),
    anomaly_type VARCHAR(50) NOT NULL,
    severity ENUM('info', 'warning', 'critical') NOT NULL,
    title VARCHAR(200) NOT NULL,
    description TEXT,

    -- Context data (JSON stored as text)
    data_json TEXT,
    kpi_snapshot_json TEXT,

    -- Status tracking
    status ENUM('active', 'acknowledged', 'resolved') DEFAULT 'active',
    acknowledged_at BIGINT,
    resolved_at BIGINT,
    auto_resolved BOOLEAN DEFAULT FALSE,

    -- Metadata
    cycle_count INT,
    notes TEXT,

    INDEX idx_timestamp (timestamp),
    INDEX idx_work_order (work_order_id),
    INDEX idx_status (status),
    INDEX idx_type (anomaly_type)
);
```

## Implementation Plan:

### **Phase 1: Foundation (Start Here)**

1. **Create Database Table** ✓
2. **Create Anomaly Detector Function**
   - Hook into Machine Cycles output
   - Implement 3 core detections:
     - Production stoppage
     - Slow cycle
     - Scrap spike
3. **Create Event Logger Function**
   - Deduplication logic (don't log same anomaly repeatedly)
   - Severity calculation
   - Database insert
4. **Global State Management**
   - `activeAnomalies` array in global context
   - Quick access for UI

### **Phase 2: UI - Immediate Visibility**

1. **Home Tab Enhancements**
   - Alert badge showing count of active anomalies
   - Color-coded severity indicator
   - Click to expand details panel
   - Most recent/critical anomaly shown inline

2. **Alerts Tab Redesign**
   - **Active Anomalies Section**
     - Real-time list of current issues
     - Severity badges (🔴 Critical, 🟡 Warning, 🔵 Info)
     - Time since occurrence
     - Acknowledge button
     - Resolve button
   - **Recent Events Section**
     - Last 24 hours of events
     - Resolved events shown grayed out
   - **Event Details Modal**
     - Full description
     - KPI snapshot at time of event
     - Related work order info
     - Add notes capability

### **Phase 3: Historical Analysis**

1. **Events History View** (new tab or section)
   - Timeline visualization
   - Filter by: type, severity, date range, work order
   - Export to CSV
   - Statistics: most common anomalies, MTBF, etc.

2. **Graph Annotations**
   - Mark anomaly events on KPI graphs
   - Visual indicators (⚠) at timestamp
   - Hover to see event details
   - Filter graphs by event type

### **Phase 4: Smart Detection**

1. **Baseline Calculation**
   - Calculate rolling baseline for each KPI
   - Adaptive thresholds based on historical performance
   - Account for normal variance

2. **Trend Detection**
   - Moving averages
   - Linear regression for degradation detection
   - Statistical outlier detection

3. **Pattern Recognition**
   - Time-of-day patterns
   - Work order patterns (specific SKUs have issues)
   - Operator patterns (shift-related issues)

### **Phase 5: Actions & Notifications**

1. **Automated Actions**
   - Configurable responses to anomalies
   - Example: Auto-pause production on critical quality drop

2. **Notification System**
   - Browser notifications
   - Email/SMS integration (future)
   - Escalation rules (if unacknowledged for X time)

## Smart Implementation Details:

### **Deduplication Logic:**
```javascript
// Don't create new event if same type exists in last 5 minutes
const existingEvent = activeAnomalies.find(e =>
  e.anomaly_type === newAnomaly.type &&
  (now - e.timestamp) < 300000 &&
  e.status === 'active'
);

if (existingEvent) {
  // Update existing event instead
  existingEvent.occurrence_count++;
  existingEvent.last_occurrence = now;
} else {
  // Create new event
  activeAnomalies.push(newAnomaly);
}
```

### **Auto-Resolution:**
```javascript
// Mark as resolved when conditions normalize
if (anomaly.type === 'production-stopped' && newCycleReceived) {
  anomaly.status = 'resolved';
  anomaly.resolved_at = Date.now();
  anomaly.auto_resolved = true;
}
```

### **Severity Calculation:**
```javascript
// Dynamic severity based on threshold exceeded
function calculateSeverity(actual, expected, type) {
  const ratio = actual / expected;

  if (type === 'cycle-time') {
    if (ratio > 2.0) return 'critical';  // 100% slower
    if (ratio > 1.5) return 'warning';   // 50% slower
    return 'info';
  }

  if (type === 'quality') {
    if (actual < 90) return 'critical';
    if (actual < 95) return 'warning';
    return 'info';
  }
}
```

## Why This Approach Works:

1. **Non-Invasive**: Taps into existing data flows without breaking anything
2. **Immediate Value**: Users see alerts from day 1
3. **Scalable**: Start simple, add sophistication over time
4. **Actionable**: Not just detection, but clear indication of what's wrong
5. **Data-Driven**: Builds historical data for ML/analytics later
6. **Visual**: Makes the invisible visible - users can SEE the value
7. **Configurable**: Thresholds can be tuned per machine/product

## Competitive Advantage:

Most manufacturing systems are **reactive** - you notice problems after production runs complete. This makes your system **proactive** - catching issues in real-time, potentially saving hours of bad production.

The key differentiator: **"Know about problems before your operators do"**

---

**My Recommendation**: Start with Phase 1 (3 core anomaly types) and Phase 2 (UI visibility). Get that working and demonstrable, then iterate. The foundation we build will support increasingly sophisticated detection over time.

What do you think? Should we start implementing this, and if so, which anomaly types are most critical for your use case?


Sounds good, lets start with the implementation. Most critical anomaly types are when a machine has a cycle that is slower than normal or slower than its theoretical or just way off of the normal cycle time. when there is a lot of scrap reported at the end of a work order, lets say if more than 10% of target_qty, then it can create an event of x amount of waste, Why is there so much waste? Instead of just saying, oh well theres scrap. production stoppage is of course critical as well lets start there, then we can work into implementing the rest


Great! The user wants to start implementing the anomaly detection system. They've identified the most critical anomaly types:

1. **Slow cycle** - cycle time slower than theoretical or normal
2. **High scrap/waste** - when scrap > 10% of target_qty at end of work order
3. **Production stoppage** - critical

Let me create a plan for implementing these three anomaly types:

## Phase 1: Foundation

### Step 1: Create database table for anomaly_events
- Define schema as planned
- Create the table

### Step 2: Create Anomaly Detector function
- Hook into Machine Cycles flow
- Detect:
  1. Slow cycle (cycle_time > theoretical_cycle_time * threshold)
  2. Production stoppage (no cycle in > 2x theoretical_cycle_time)
  3. High scrap (at work order completion, scrap > 10% of target)

### Step 3: Create Event Logger function
- Receives anomaly events
- Deduplicates
- Stores to database
- Updates global context

### Step 4: Update UI
- Add alerts badge to Home tab
- Show active anomalies

Let me use TodoWrite to track this implementation, and then start with Step 1.


Perfect! Let's implement these three critical anomaly types. I'll track our progress and build this systematically.


TodoWrite

Create anomaly_events database table

Create Anomaly Detector function (slow cycle, stoppage, high scrap)

Create Event Logger function with deduplication

Wire anomaly detection into Machine Cycles flow

Add high scrap detection to complete-work-order handler

Enhance Home tab with anomaly alerts badge

Enhance Alerts tab to show active anomalies

Test anomaly detection with real cycles


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create anomaly_events database table

Create Anomaly Detector function (slow cycle, stoppage, high scrap)

Create Event Logger function with deduplication

Wire anomaly detection into Machine Cycles flow

Add high scrap detection to complete-work-order handler

Enhance Home tab with anomaly alerts badge

Enhance Alerts tab to show active anomalies

Test anomaly detection with real cycles


Good! Now let me start with creating the database table. I'll create a SQL script to define the anomaly_events table.


Let me start by creating the database table for storing anomaly events:


Write

-- Anomaly Events Table for Manufacturing Anomaly Detection
-- Stores all detected anomalies with full context for analysis

CREATE TABLE IF NOT EXISTS anomaly_events (
    event_id INT AUTO_INCREMENT PRIMARY KEY,
    timestamp BIGINT NOT NULL COMMENT 'Unix timestamp in milliseconds',
    work_order_id VARCHAR(50) COMMENT 'Associated work order if applicable',

    -- Anomaly Classification
    anomaly_type VARCHAR(50) NOT NULL COMMENT 'Type: slow-cycle, production-stopped, high-scrap, etc.',
    severity ENUM('info', 'warning', 'critical') NOT NULL DEFAULT 'warning',

    -- Event Details
    title VARCHAR(200) NOT NULL COMMENT 'Short human-readable title',
    description TEXT COMMENT 'Detailed description of the anomaly',

    -- Context Data (JSON stored as TEXT)
    data_json TEXT COMMENT 'Anomaly-specific data: actual_value, expected_value, delta, etc.',
    kpi_snapshot_json TEXT COMMENT 'KPI values at time of event: OEE, Availability, Performance, Quality',

    -- Status Tracking
    status ENUM('active', 'acknowledged', 'resolved') DEFAULT 'active',
    acknowledged_at BIGINT COMMENT 'When user acknowledged',
    resolved_at BIGINT COMMENT 'When anomaly was resolved',
    auto_resolved BOOLEAN DEFAULT FALSE COMMENT 'True if system auto-resolved',

    -- Additional Metadata
    cycle_count INT COMMENT 'Cycle count at time of event',
    occurrence_count INT DEFAULT 1 COMMENT 'How many times this occurred (for deduplication)',
    last_occurrence BIGINT COMMENT 'Last time this anomaly re-occurred',
    notes TEXT COMMENT 'User notes',

    -- Indexes for fast queries
    INDEX idx_timestamp (timestamp),
    INDEX idx_work_order (work_order_id),
    INDEX idx_status (status),
    INDEX idx_type (anomaly_type),
    INDEX idx_severity (severity),
    INDEX idx_type_status (anomaly_type, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Stores detected anomalies and events';


File created successfully at: /tmp/create_anomaly_events_table.sql

-- Anomaly Events Table for Manufacturing Anomaly Detection
-- Stores all detected anomalies with full context for analysis

CREATE TABLE IF NOT EXISTS anomaly_events (
    event_id INT AUTO_INCREMENT PRIMARY KEY,
    timestamp BIGINT NOT NULL COMMENT 'Unix timestamp in milliseconds',
    work_order_id VARCHAR(50) COMMENT 'Associated work order if applicable',

    -- Anomaly Classification
    anomaly_type VARCHAR(50) NOT NULL COMMENT 'Type: slow-cycle, production-stopped, high-scrap, etc.',
    severity ENUM('info', 'warning', 'critical') NOT NULL DEFAULT 'warning',

    -- Event Details
    title VARCHAR(200) NOT NULL COMMENT 'Short human-readable title',
    description TEXT COMMENT 'Detailed description of the anomaly',

    -- Context Data (JSON stored as TEXT)
    data_json TEXT COMMENT 'Anomaly-specific data: actual_value, expected_value, delta, etc.',
    kpi_snapshot_json TEXT COMMENT 'KPI values at time of event: OEE, Availability, Performance, Quality',

    -- Status Tracking
    status ENUM('active', 'acknowledged', 'resolved') DEFAULT 'active',
    acknowledged_at BIGINT COMMENT 'When user acknowledged',
    resolved_at BIGINT COMMENT 'When anomaly was resolved',
    auto_resolved BOOLEAN DEFAULT FALSE COMMENT 'True if system auto-resolved',

    -- Additional Metadata
    cycle_count INT COMMENT 'Cycle count at time of event',
    occurrence_count INT DEFAULT 1 COMMENT 'How many times this occurred (for deduplication)',
    last_occurrence BIGINT COMMENT 'Last time this anomaly re-occurred',
    notes TEXT COMMENT 'User notes',

    -- Indexes for fast queries
    INDEX idx_timestamp (timestamp),
    INDEX idx_work_order (work_order_id),
    INDEX idx_status (status),
    INDEX idx_type (anomaly_type),
    INDEX idx_severity (severity),
    INDEX idx_type_status (anomaly_type, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Stores detected anomalies and events';


Good! Now let me execute this SQL to create the table in the database. I'll need to find the database credentials. Let me check the flows.json for the database configuration.


Bash

jq -r '.[] | select(.type == "mysql") | {id: .id, name: .name, mydb: .mydb}' /home/mdares/.node-red/flows.json | head -20

Find MySQL node configuration


{
  "id": "c9d8e7f6a5b4c3d2",
  "name": "Mold Presets DB",
  "mydb": "00d8ad2b0277f906"
}
{
  "id": "f6ad294bc02618c9",
  "name": "mariaDB",
  "mydb": "00d8ad2b0277f906"
}
{
  "id": "graph_mariadb_node",
  "name": "mariaDB (Graph Data)",
  "mydb": "00d8ad2b0277f906"
}
{
  "id": "alert_insert_mysql",
  "name": "Log Alert to DB",
  "mydb": "00d8ad2b0277f906"
}


Good, they all use the same database config ID. Let me find the database configuration details.


jq -r '.[] | select(.id == "00d8ad2b0277f906")' /home/mdares/.node-red/flows.json

Get database configuration


Perfect! The database is on 10.147.20.244:3306, database name is "machine_data". I'll need to execute the SQL to create the table. Let me use mysql command line. I might need credentials, but let me try first.


mysql -h 10.147.20.244 -u root -p'newrootpassword' machine_data < /tmp/create_anomaly_events_table.sql && echo "✓ Table created successfully" || echo "✗ Failed to create table"

Create anomaly_events table


✗ Failed to create table
/bin/bash: line 1: mysql: command not found


MySQL client isn't installed. Let me try mariadb command instead, or I could create a Node-RED function to execute this SQL. Actually, the easiest way is to create a simple inject node that executes this SQL. But for now, let me try mariadb command.


which mariadb mysql 2>/dev/null || echo "No MySQL/MariaDB client found"

Check for database clients


No MySQL/MariaDB client found


No MySQL client is installed. I'll need to create the table through Node-RED. Let me create a Python script that adds an inject node and function node to execute this SQL through the existing Node-RED database connection.


No MySQL client installed. Let me create the table through Node-RED by adding a one-time setup node:


Write

#!/usr/bin/env python3
import json

with open('/home/mdares/.node-red/flows.json', 'r') as f:
    flows = json.load(f)

# SQL to create the table
create_table_sql = """CREATE TABLE IF NOT EXISTS anomaly_events (
    event_id INT AUTO_INCREMENT PRIMARY KEY,
    timestamp BIGINT NOT NULL,
    work_order_id VARCHAR(50),
    anomaly_type VARCHAR(50) NOT NULL,
    severity ENUM('info', 'warning', 'critical') NOT NULL DEFAULT 'warning',
    title VARCHAR(200) NOT NULL,
    description TEXT,
    data_json TEXT,
    kpi_snapshot_json TEXT,
    status ENUM('active', 'acknowledged', 'resolved') DEFAULT 'active',
    acknowledged_at BIGINT,
    resolved_at BIGINT,
    auto_resolved BOOLEAN DEFAULT FALSE,
    cycle_count INT,
    occurrence_count INT DEFAULT 1,
    last_occurrence BIGINT,
    notes TEXT,
    INDEX idx_timestamp (timestamp),
    INDEX idx_work_order (work_order_id),
    INDEX idx_status (status),
    INDEX idx_type (anomaly_type),
    INDEX idx_severity (severity)
)"""

# Find the tab where we'll add the setup node (use the same tab as other nodes)
tab_id = None
for node in flows:
    if node.get('type') == 'tab':
        tab_id = node['id']
        break

if not tab_id:
    print("✗ Could not find a tab to add the node to")
    exit(1)

# Create an inject node to trigger the table creation
inject_node = {
    "id": "create_table_inject_temp",
    "type": "inject",
    "z": tab_id,
    "name": "CREATE anomaly_events table (run once)",
    "props": [{"p": "payload"}],
    "repeat": "",
    "crontab": "",
    "once": False,
    "onceDelay": 0.1,
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 250,
    "y": 900,
    "wires": [["create_table_function_temp"]]
}

# Create a function node with the SQL
function_node = {
    "id": "create_table_function_temp",
    "type": "function",
    "z": tab_id,
    "name": "Create Table SQL",
    "func": f"""// Create anomaly_events table
msg.topic = `{create_table_sql}`;
msg.payload = [];
return msg;""",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 500,
    "y": 900,
    "wires": [["create_table_mysql_temp"]]
}

# Create a MySQL node to execute the SQL
mysql_node = {
    "id": "create_table_mysql_temp",
    "type": "mysql",
    "z": tab_id,
    "mydb": "00d8ad2b0277f906",
    "name": "Execute Create Table",
    "x": 730,
    "y": 900,
    "wires": [["create_table_debug_temp"]]
}

# Create a debug node to show result
debug_node = {
    "id": "create_table_debug_temp",
    "type": "debug",
    "z": tab_id,
    "name": "Table Created",
    "active": True,
    "tosidebar": True,
    "console": False,
    "tostatus": False,
    "complete": "true",
    "targetType": "full",
    "statusVal": "",
    "statusType": "auto",
    "x": 960,
    "y": 900,
    "wires": []
}

# Add all nodes to flows
flows.extend([inject_node, function_node, mysql_node, debug_node])

with open('/home/mdares/.node-red/flows.json', 'w') as f:
    json.dump(flows, f, indent=4)

print("✓ Added CREATE TABLE nodes to flows.json")
print("  - Inject node: 'CREATE anomaly_events table (run once)'")
print("  - After Node-RED restarts, click this inject button ONCE to create the table")
print("  - Check debug panel to confirm table creation")
print("  - These temporary nodes can be deleted after use")


File created successfully at: /tmp/add_create_table_node.py

#!/usr/bin/env python3
import json

with open('/home/mdares/.node-red/flows.json', 'r') as f:
    flows = json.load(f)

# SQL to create the table
create_table_sql = """CREATE TABLE IF NOT EXISTS anomaly_events (
    event_id INT AUTO_INCREMENT PRIMARY KEY,
    timestamp BIGINT NOT NULL,
    work_order_id VARCHAR(50),
    anomaly_type VARCHAR(50) NOT NULL,
    severity ENUM('info', 'warning', 'critical') NOT NULL DEFAULT 'warning',
    title VARCHAR(200) NOT NULL,
    description TEXT,
    data_json TEXT,
    kpi_snapshot_json TEXT,
    status ENUM('active', 'acknowledged', 'resolved') DEFAULT 'active',
    acknowledged_at BIGINT,
    resolved_at BIGINT,
    auto_resolved BOOLEAN DEFAULT FALSE,
    cycle_count INT,
    occurrence_count INT DEFAULT 1,
    last_occurrence BIGINT,
    notes TEXT,
    INDEX idx_timestamp (timestamp),
    INDEX idx_work_order (work_order_id),
    INDEX idx_status (status),
    INDEX idx_type (anomaly_type),
    INDEX idx_severity (severity)
)"""

# Find the tab where we'll add the setup node (use the same tab as other nodes)
tab_id = None
for node in flows:
    if node.get('type') == 'tab':
        tab_id = node['id']
        break

if not tab_id:
    print("✗ Could not find a tab to add the node to")
    exit(1)

# Create an inject node to trigger the table creation
inject_node = {
    "id": "create_table_inject_temp",
    "type": "inject",
    "z": tab_id,
    "name": "CREATE anomaly_events table (run once)",
    "props": [{"p": "payload"}],
    "repeat": "",
    "crontab": "",
    "once": False,
    "onceDelay": 0.1,
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 250,
    "y": 900,
    "wires": [["create_table_function_temp"]]
}

# Create a function node with the SQL
function_node = {
    "id": "create_table_function_temp",
    "type": "function",
    "z": tab_id,
    "name": "Create Table SQL",
    "func": f"""// Create anomaly_events table
msg.topic = `{create_table_sql}`;
msg.payload = [];
return msg;""",
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 500,
    "y": 900,
    "wires": [["create_table_mysql_temp"]]
}

# Create a MySQL node to execute the SQL
mysql_node = {
    "id": "create_table_mysql_temp",
    "type": "mysql",
    "z": tab_id,
    "mydb": "00d8ad2b0277f906",
    "name": "Execute Create Table",
    "x": 730,
    "y": 900,
    "wires": [["create_table_debug_temp"]]
}

# Create a debug node to show result
debug_node = {
    "id": "create_table_debug_temp",
    "type": "debug",
    "z": tab_id,
    "name": "Table Created",
    "active": True,
    "tosidebar": True,
    "console": False,
    "tostatus": False,
    "complete": "true",
    "targetType": "full",
    "statusVal": "",
    "statusType": "auto",
    "x": 960,
    "y": 900,
    "wires": []
}

# Add all nodes to flows
flows.extend([inject_node, function_node, mysql_node, debug_node])

with open('/home/mdares/.node-red/flows.json', 'w') as f:
    json.dump(flows, f, indent=4)

print("✓ Added CREATE TABLE nodes to flows.json")
print("  - Inject node: 'CREATE anomaly_events table (run once)'")
print("  - After Node-RED restarts, click this inject button ONCE to create the table")
print("  - Check debug panel to confirm table creation")
print("  - These temporary nodes can be deleted after use")


Good, let me run this script to add the nodes, then restart Node-RED.


Bash

Add CREATE TABLE nodes to flows


✓ Added CREATE TABLE nodes to flows.json
  - Inject node: 'CREATE anomaly_events table (run once)'
  - After Node-RED restarts, click this inject button ONCE to create the table
  - Check debug panel to confirm table creation
  - These temporary nodes can be deleted after use


Good! Now let me restart Node-RED so the new nodes appear, then I can use the Node-RED UI to trigger the table creation. After that, I'll move on to creating the Anomaly Detector function.


systemctl restart nodered && sleep 5 && systemctl status nodered --no-pager | head -12

Restart Node-RED


● nodered.service - Node-RED graphical event wiring tool
     Loaded: loaded (/usr/lib/systemd/system/nodered.service; disabled; preset: enabled)
     Active: active (running) since Sat 2025-11-29 16:58:28 UTC; 5s ago
       Docs: http://nodered.org/docs/hardware/raspberrypi.html
   Main PID: 50618 (node-red)
      Tasks: 11 (limit: 7014)
     Memory: 93.5M (peak: 93.9M)
        CPU: 1.155s
     CGroup: /system.slice/nodered.service
             └─50618 node-red

Nov 29 16:58:29 mdares Node-RED[50618]: 29 Nov 16:58:29 - [info] Started flows


Good! Node-RED is running. Now I need to tell the user to click the inject button in the Node-RED UI to create the table. But actually, I could automate this by making it an "inject once on startup" node. Let me modify the script to make it auto-execute once on deploy.

Actually, looking at my code, I set "once": False. Let me update it to True so it runs automatically on startup.


Write

#!/usr/bin/env python3
import json

with open('/home/mdares/.node-red/flows.json', 'r') as f:
    flows = json.load(f)

# Find the inject node and set it to run once on startup
for node in flows:
    if node.get('id') == 'create_table_inject_temp':
        node['once'] = True
        node['onceDelay'] = 1
        print("✓ Set inject node to run once on startup")
        break

with open('/home/mdares/.node-red/flows.json', 'w') as f:
    json.dump(flows, f, indent=4)

print("✓ Updated flows.json")


File created successfully at: /tmp/set_inject_once.py

#!/usr/bin/env python3
import json

with open('/home/mdares/.node-red/flows.json', 'r') as f:
    flows = json.load(f)

# Find the inject node and set it to run once on startup
for node in flows:
    if node.get('id') == 'create_table_inject_temp':
        node['once'] = True
        node['onceDelay'] = 1
        print("✓ Set inject node to run once on startup")
        break

with open('/home/mdares/.node-red/flows.json', 'w') as f:
    json.dump(flows, f, indent=4)

print("✓ Updated flows.json")


Let me run this and restart Node-RED so the table gets created automatically.


Bash

python3 /tmp/set_inject_once.py && systemctl restart nodered && sleep 6

Auto-create table on startup


✓ Set inject node to run once on startup
✓ Updated flows.json


Good! Let me check the logs to see if the table was created successfully.


journalctl -u nodered --since "10 seconds ago" --no-pager | grep -i "table\|error\|created" | tail -10

Check if table was created


Hint: You are currently not seeing messages from other users and the system.
      Users in groups 'adm', 'systemd-journal' can see all messages.
      Pass -q to turn off this notice.


No output. Let me check the Node-RED debug log in a different way, or verify the table exists by querying it through Node-RED. Actually, let me move forward with creating the Anomaly Detector function, and we'll verify the table was created when we test the full flow. If it wasn't created, we'll see an error when we try to insert.

Let me update the todo and create the Anomaly Detector function now.


TodoWrite

Create anomaly_events database table

Create Anomaly Detector function (slow cycle, stoppage, high scrap)

Create Event Logger function with deduplication

Wire anomaly detection into Machine Cycles flow

Add high scrap detection to complete-work-order handler

Enhance Home tab with anomaly alerts badge

Enhance Alerts tab to show active anomalies

Test anomaly detection with real cycles


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create anomaly_events database table

Create Anomaly Detector function (slow cycle, stoppage, high scrap)

Create Event Logger function with deduplication

Wire anomaly detection into Machine Cycles flow

Add high scrap detection to complete-work-order handler

Enhance Home tab with anomaly alerts badge

Enhance Alerts tab to show active anomalies

Test anomaly detection with real cycles

Create anomaly_events database table

Test anomaly detection with real cycles


Good! Now let me create the Anomaly Detector function. This function will:

1. Receive cycle data from Machine Cycles function
2. Check for:   - Slow cycle (cycle_time > theoretical_cycle_time * threshold)
   - Production stoppage (time since last cycle > 2x theoretical_cycle_time)
3. Generate anomaly event objects
4. Send to Event Logger

The high scrap detection will be added to the complete-work-order handler separately since it happens at work order completion, not per cycle.

Let me create the Anomaly Detector function.


Now let me create the core Anomaly Detector function. This will analyze each cycle for anomalies:


Write

// ============================================================
// ANOMALY DETECTOR
// Detects production anomalies in real-time
// ============================================================

const cycle = msg.cycle || {};
const kpis = msg.kpis || {};
const activeOrder = global.get("activeWorkOrder") || {};

// Must have active work order to detect anomalies
if (!activeOrder.id) {
    return null;
}

const theoreticalCycleTime = Number(activeOrder.cycleTime) || 0;
const now = Date.now();

// Get or initialize anomaly tracking state
let anomalyState = global.get("anomalyState") || {
    lastCycleTime: now,
    activeStoppageEvent: null
};

const detectedAnomalies = [];

// ============================================================
// 1. SLOW CYCLE DETECTION
// Trigger: Actual cycle time > 1.5x theoretical
// ============================================================
if (theoreticalCycleTime > 0) {
    const timeSinceLastCycle = now - anomalyState.lastCycleTime;
    const actualCycleTime = timeSinceLastCycle / 1000; // Convert to seconds
    const threshold = theoreticalCycleTime * 1.5;

    if (actualCycleTime > threshold && anomalyState.lastCycleTime > 0) {
        const deltaPercent = ((actualCycleTime - theoreticalCycleTime) / theoreticalCycleTime) * 100;

        // Determine severity
        let severity = 'warning';
        if (actualCycleTime > theoreticalCycleTime * 2.0) {
            severity = 'critical';  // 100%+ slower
        }

        detectedAnomalies.push({
            anomaly_type: 'slow-cycle',
            severity: severity,
            title: `Slow Cycle Detected`,
            description: `Cycle took ${actualCycleTime.toFixed(1)}s (${deltaPercent.toFixed(0)}% slower than expected ${theoreticalCycleTime}s)`,
            data: {
                actual_cycle_time: actualCycleTime,
                theoretical_cycle_time: theoreticalCycleTime,
                delta_percent: Math.round(deltaPercent),
                threshold_multiplier: actualCycleTime / theoreticalCycleTime
            },
            kpi_snapshot: {
                oee: kpis.oee || 0,
                availability: kpis.availability || 0,
                performance: kpis.performance || 0,
                quality: kpis.quality || 0
            },
            work_order_id: activeOrder.id,
            cycle_count: cycle.cycles || 0,
            timestamp: now
        });

        node.warn(`[ANOMALY] Slow cycle: ${actualCycleTime.toFixed(1)}s (expected ${theoreticalCycleTime}s)`);
    }
}

// ============================================================
// 2. PRODUCTION STOPPAGE DETECTION
// Trigger: No cycle in > 3x theoretical cycle time
// ============================================================
if (theoreticalCycleTime > 0) {
    const timeSinceLastCycle = now - anomalyState.lastCycleTime;
    const stoppageThreshold = theoreticalCycleTime * 3 * 1000; // Convert to ms

    // If we have an active stoppage event and a new cycle arrived, resolve it
    if (anomalyState.activeStoppageEvent) {
        // Cycle resumed - mark stoppage as resolved
        anomalyState.activeStoppageEvent.resolved_at = now;
        anomalyState.activeStoppageEvent.auto_resolved = true;
        anomalyState.activeStoppageEvent.status = 'resolved';

        const stoppageDuration = (now - anomalyState.activeStoppageEvent.timestamp) / 1000;
        node.warn(`[ANOMALY] Production resumed after ${stoppageDuration.toFixed(0)}s stoppage`);

        // Send resolution event
        detectedAnomalies.push(anomalyState.activeStoppageEvent);
        anomalyState.activeStoppageEvent = null;
    }

    // Check if production has stopped (only if no active stoppage event)
    if (!anomalyState.activeStoppageEvent && timeSinceLastCycle > stoppageThreshold && anomalyState.lastCycleTime > 0) {
        const stoppageSeconds = timeSinceLastCycle / 1000;

        // Determine severity
        let severity = 'warning';
        if (stoppageSeconds > theoreticalCycleTime * 5) {
            severity = 'critical';  // Stopped for 5x+ theoretical time
        }

        const stoppageEvent = {
            anomaly_type: 'production-stopped',
            severity: severity,
            title: `Production Stoppage`,
            description: `No cycles detected for ${stoppageSeconds.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,
            data: {
                stoppage_duration_seconds: Math.round(stoppageSeconds),
                theoretical_cycle_time: theoreticalCycleTime,
                last_cycle_timestamp: anomalyState.lastCycleTime,
                threshold_multiplier: stoppageSeconds / theoreticalCycleTime
            },
            kpi_snapshot: {
                oee: kpis.oee || 0,
                availability: kpis.availability || 0,
                performance: kpis.performance || 0,
                quality: kpis.quality || 0
            },
            work_order_id: activeOrder.id,
            cycle_count: cycle.cycles || 0,
            timestamp: now,
            status: 'active'
        };

        detectedAnomalies.push(stoppageEvent);
        anomalyState.activeStoppageEvent = stoppageEvent;

        node.warn(`[ANOMALY] Production stopped: ${stoppageSeconds.toFixed(0)}s since last cycle`);
    }
}

// Update last cycle time for next iteration
anomalyState.lastCycleTime = now;
global.set("anomalyState", anomalyState);

// ============================================================
// OUTPUT
// ============================================================
if (detectedAnomalies.length > 0) {
    node.warn(`[ANOMALY DETECTOR] Detected ${detectedAnomalies.length} anomaly/ies`);

    return {
        topic: "anomaly-detected",
        payload: detectedAnomalies,
        originalMsg: msg  // Pass through original message for other flows
    };
}

return null;  // No anomalies detected


File created successfully at: /tmp/anomaly_detector_function.js

// ============================================================
// ANOMALY DETECTOR
// Detects production anomalies in real-time
// ============================================================

const cycle = msg.cycle || {};
const kpis = msg.kpis || {};
const activeOrder = global.get("activeWorkOrder") || {};

// Must have active work order to detect anomalies
if (!activeOrder.id) {
    return null;
}

const theoreticalCycleTime = Number(activeOrder.cycleTime) || 0;
const now = Date.now();

// Get or initialize anomaly tracking state
let anomalyState = global.get("anomalyState") || {
    lastCycleTime: now,
    activeStoppageEvent: null
};

const detectedAnomalies = [];

// ============================================================
// 1. SLOW CYCLE DETECTION
// Trigger: Actual cycle time > 1.5x theoretical
// ============================================================
if (theoreticalCycleTime > 0) {
    const timeSinceLastCycle = now - anomalyState.lastCycleTime;
    const actualCycleTime = timeSinceLastCycle / 1000; // Convert to seconds
    const threshold = theoreticalCycleTime * 1.5;

    if (actualCycleTime > threshold && anomalyState.lastCycleTime > 0) {
        const deltaPercent = ((actualCycleTime - theoreticalCycleTime) / theoreticalCycleTime) * 100;

        // Determine severity
        let severity = 'warning';
        if (actualCycleTime > theoreticalCycleTime * 2.0) {
            severity = 'critical';  // 100%+ slower
        }

        detectedAnomalies.push({
            anomaly_type: 'slow-cycle',
            severity: severity,
            title: `Slow Cycle Detected`,
            description: `Cycle took ${actualCycleTime.toFixed(1)}s (${deltaPercent.toFixed(0)}% slower than expected ${theoreticalCycleTime}s)`,
            data: {
                actual_cycle_time: actualCycleTime,
                theoretical_cycle_time: theoreticalCycleTime,
                delta_percent: Math.round(deltaPercent),
                threshold_multiplier: actualCycleTime / theoreticalCycleTime
            },
            kpi_snapshot: {
                oee: kpis.oee || 0,
                availability: kpis.availability || 0,
                performance: kpis.performance || 0,
                quality: kpis.quality || 0
            },
            work_order_id: activeOrder.id,
            cycle_count: cycle.cycles || 0,
            timestamp: now
        });

        node.warn(`[ANOMALY] Slow cycle: ${actualCycleTime.toFixed(1)}s (expected ${theoreticalCycleTime}s)`);
    }
}

// ============================================================
// 2. PRODUCTION STOPPAGE DETECTION
// Trigger: No cycle in > 3x theoretical cycle time
// ============================================================
if (theoreticalCycleTime > 0) {
    const timeSinceLastCycle = now - anomalyState.lastCycleTime;
    const stoppageThreshold = theoreticalCycleTime * 3 * 1000; // Convert to ms

    // If we have an active stoppage event and a new cycle arrived, resolve it
    if (anomalyState.activeStoppageEvent) {
        // Cycle resumed - mark stoppage as resolved
        anomalyState.activeStoppageEvent.resolved_at = now;
        anomalyState.activeStoppageEvent.auto_resolved = true;
        anomalyState.activeStoppageEvent.status = 'resolved';

        const stoppageDuration = (now - anomalyState.activeStoppageEvent.timestamp) / 1000;
        node.warn(`[ANOMALY] Production resumed after ${stoppageDuration.toFixed(0)}s stoppage`);

        // Send resolution event
        detectedAnomalies.push(anomalyState.activeStoppageEvent);
        anomalyState.activeStoppageEvent = null;
    }

    // Check if production has stopped (only if no active stoppage event)
    if (!anomalyState.activeStoppageEvent && timeSinceLastCycle > stoppageThreshold && anomalyState.lastCycleTime > 0) {
        const stoppageSeconds = timeSinceLastCycle / 1000;

        // Determine severity
        let severity = 'warning';
        if (stoppageSeconds > theoreticalCycleTime * 5) {
            severity = 'critical';  // Stopped for 5x+ theoretical time
        }

        const stoppageEvent = {
            anomaly_type: 'production-stopped',
            severity: severity,
            title: `Production Stoppage`,
            description: `No cycles detected for ${stoppageSeconds.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,
            data: {
                stoppage_duration_seconds: Math.round(stoppageSeconds),
                theoretical_cycle_time: theoreticalCycleTime,
                last_cycle_timestamp: anomalyState.lastCycleTime,
                threshold_multiplier: stoppageSeconds / theoreticalCycleTime
            },
            kpi_snapshot: {
                oee: kpis.oee || 0,
                availability: kpis.availability || 0,
                performance: kpis.performance || 0,
                quality: kpis.quality || 0
            },
            work_order_id: activeOrder.id,
            cycle_count: cycle.cycles || 0,
            timestamp: now,
            status: 'active'
        };

        detectedAnomalies.push(stoppageEvent);
        anomalyState.activeStoppageEvent = stoppageEvent;

        node.warn(`[ANOMALY] Production stopped: ${stoppageSeconds.toFixed(0)}s since last cycle`);
    }
}

// Update last cycle time for next iteration
anomalyState.lastCycleTime = now;
global.set("anomalyState", anomalyState);

// ============================================================
// OUTPUT
// ============================================================
if (detectedAnomalies.length > 0) {
    node.warn(`[ANOMALY DETECTOR] Detected ${detectedAnomalies.length} anomaly/ies`);

    return {
        topic: "anomaly-detected",
        payload: detectedAnomalies,
        originalMsg: msg  // Pass through original message for other flows
    };
}

return null;  // No anomalies detected


Great! Now let me create the Event Logger function that will receive anomaly events, deduplicate them, and store them to the database.


Now let me create the Event Logger function that handles deduplication and database storage:


// ============================================================
// EVENT LOGGER
// Deduplicates and logs anomaly events to database
// ============================================================

const anomalies = msg.payload || [];

if (!Array.isArray(anomalies) || anomalies.length === 0) {
    return null;
}

// Get or initialize active anomalies list
let activeAnomalies = global.get("activeAnomalies") || [];
const now = Date.now();
const DEDUP_WINDOW = 5 * 60 * 1000; // 5 minutes

const dbInserts = [];
const uiUpdates = [];

anomalies.forEach(anomaly => {
    // ============================================================
    // DEDUPLICATION LOGIC
    // Don't create new event if same type exists in last 5 minutes
    // ============================================================
    const existingIndex = activeAnomalies.findIndex(existing =>
        existing.anomaly_type === anomaly.anomaly_type &&
        existing.work_order_id === anomaly.work_order_id &&
        existing.status === 'active' &&
        (now - existing.timestamp) < DEDUP_WINDOW
    );

    if (existingIndex !== -1) {
        // Update existing event
        const existing = activeAnomalies[existingIndex];
        existing.occurrence_count = (existing.occurrence_count || 1) + 1;
        existing.last_occurrence = now;

        // Update in database
        const updateQuery = `UPDATE anomaly_events
            SET occurrence_count = ?, last_occurrence = ?
            WHERE event_id = ?`;

        dbInserts.push({
            topic: updateQuery,
            payload: [existing.occurrence_count, existing.last_occurrence, existing.event_id]
        });

        node.warn(`[EVENT LOGGER] Updated existing ${anomaly.anomaly_type} event (occurrence #${existing.occurrence_count})`);

    } else if (anomaly.status === 'resolved') {
        // ============================================================
        // RESOLVE EVENT
        // ============================================================
        const resolveIndex = activeAnomalies.findIndex(existing =>
            existing.anomaly_type === anomaly.anomaly_type &&
            existing.work_order_id === anomaly.work_order_id &&
            existing.status === 'active'
        );

        if (resolveIndex !== -1) {
            const existing = activeAnomalies[resolveIndex];
            existing.status = 'resolved';
            existing.resolved_at = anomaly.resolved_at || now;
            existing.auto_resolved = anomaly.auto_resolved || false;

            // Update in database
            const resolveQuery = `UPDATE anomaly_events
                SET status = 'resolved', resolved_at = ?, auto_resolved = ?
                WHERE event_id = ?`;

            dbInserts.push({
                topic: resolveQuery,
                payload: [existing.resolved_at, existing.auto_resolved, existing.event_id]
            });

            // Remove from active list
            activeAnomalies.splice(resolveIndex, 1);

            node.warn(`[EVENT LOGGER] Resolved ${anomaly.anomaly_type} event (auto: ${existing.auto_resolved})`);

            uiUpdates.push({
                event_id: existing.event_id,
                status: 'resolved'
            });
        }

    } else {
        // ============================================================
        // NEW EVENT
        // ============================================================
        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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;

        const dataJson = JSON.stringify(anomaly.data || {});
        const kpiJson = JSON.stringify(anomaly.kpi_snapshot || {});

        dbInserts.push({
            topic: insertQuery,
            payload: [
                anomaly.timestamp,
                anomaly.work_order_id,
                anomaly.anomaly_type,
                anomaly.severity,
                anomaly.title,
                anomaly.description,
                dataJson,
                kpiJson,
                'active',
                anomaly.cycle_count,
                1,  // occurrence_count
                anomaly.timestamp  // last_occurrence
            ],
            _storeEventId: true,  // Flag to get generated event_id
            _anomaly: anomaly  // Keep reference for later
        });

        node.warn(`[EVENT LOGGER] New ${anomaly.anomaly_type} event: ${anomaly.title}`);
    }
});

// Save active anomalies to global context
global.set("activeAnomalies", activeAnomalies);

// ============================================================
// OUTPUT
// ============================================================
// Output 1: Database inserts (to mysql node)
// Output 2: UI updates (to Home/Alerts tabs)

if (dbInserts.length > 0) {
    // Send each insert as a separate message
    return [dbInserts, {
        topic: "anomaly-ui-update",
        payload: {
            activeCount: activeAnomalies.length,
            activeAnomalies: activeAnomalies,
            updates: uiUpdates
        }
    }];
} else {
    return [null, {
        topic: "anomaly-ui-update",
        payload: {
            activeCount: activeAnomalies.length,
            activeAnomalies: activeAnomalies,
            updates: uiUpdates
        }
    }];
}


File created successfully at: /tmp/event_logger_function.js

// ============================================================
// EVENT LOGGER
// Deduplicates and logs anomaly events to database
// ============================================================

const anomalies = msg.payload || [];

if (!Array.isArray(anomalies) || anomalies.length === 0) {
    return null;
}

// Get or initialize active anomalies list
let activeAnomalies = global.get("activeAnomalies") || [];
const now = Date.now();
const DEDUP_WINDOW = 5 * 60 * 1000; // 5 minutes

const dbInserts = [];
const uiUpdates = [];

anomalies.forEach(anomaly => {
    // ============================================================
    // DEDUPLICATION LOGIC
    // Don't create new event if same type exists in last 5 minutes
    // ============================================================
    const existingIndex = activeAnomalies.findIndex(existing =>
        existing.anomaly_type === anomaly.anomaly_type &&
        existing.work_order_id === anomaly.work_order_id &&
        existing.status === 'active' &&
        (now - existing.timestamp) < DEDUP_WINDOW
    );

    if (existingIndex !== -1) {
        // Update existing event
        const existing = activeAnomalies[existingIndex];
        existing.occurrence_count = (existing.occurrence_count || 1) + 1;
        existing.last_occurrence = now;

        // Update in database
        const updateQuery = `UPDATE anomaly_events
            SET occurrence_count = ?, last_occurrence = ?
            WHERE event_id = ?`;

        dbInserts.push({
            topic: updateQuery,
            payload: [existing.occurrence_count, existing.last_occurrence, existing.event_id]
        });

        node.warn(`[EVENT LOGGER] Updated existing ${anomaly.anomaly_type} event (occurrence #${existing.occurrence_count})`);

    } else if (anomaly.status === 'resolved') {
        // ============================================================
        // RESOLVE EVENT
        // ============================================================
        const resolveIndex = activeAnomalies.findIndex(existing =>
            existing.anomaly_type === anomaly.anomaly_type &&
            existing.work_order_id === anomaly.work_order_id &&
            existing.status === 'active'
        );

        if (resolveIndex !== -1) {
            const existing = activeAnomalies[resolveIndex];
            existing.status = 'resolved';
            existing.resolved_at = anomaly.resolved_at || now;
            existing.auto_resolved = anomaly.auto_resolved || false;

            // Update in database
            const resolveQuery = `UPDATE anomaly_events
                SET status = 'resolved', resolved_at = ?, auto_resolved = ?
                WHERE event_id = ?`;

            dbInserts.push({
                topic: resolveQuery,
                payload: [existing.resolved_at, existing.auto_resolved, existing.event_id]
            });

            // Remove from active list
            activeAnomalies.splice(resolveIndex, 1);

            node.warn(`[EVENT LOGGER] Resolved ${anomaly.anomaly_type} event (auto: ${existing.auto_resolved})`);

            uiUpdates.push({
                event_id: existing.event_id,
                status: 'resolved'
            });
        }

    } else {
        // ============================================================
        // NEW EVENT
        // ============================================================
        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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;

        const dataJson = JSON.stringify(anomaly.data || {});
        const kpiJson = JSON.stringify(anomaly.kpi_snapshot || {});

        dbInserts.push({
            topic: insertQuery,
            payload: [
                anomaly.timestamp,
                anomaly.work_order_id,
                anomaly.anomaly_type,
                anomaly.severity,
                anomaly.title,
                anomaly.description,
                dataJson,
                kpiJson,
                'active',
                anomaly.cycle_count,
                1,  // occurrence_count
                anomaly.timestamp  // last_occurrence
            ],
            _storeEventId: true,  // Flag to get generated event_id
            _anomaly: anomaly  // Keep reference for later
        });

        node.warn(`[EVENT LOGGER] New ${anomaly.anomaly_type} event: ${anomaly.title}`);
    }
});

// Save active anomalies to global context
global.set("activeAnomalies", activeAnomalies);

// ============================================================
// OUTPUT
// ============================================================
// Output 1: Database inserts (to mysql node)
// Output 2: UI updates (to Home/Alerts tabs)

if (dbInserts.length > 0) {
    // Send each insert as a separate message
    return [dbInserts, {
        topic: "anomaly-ui-update",
        payload: {
            activeCount: activeAnomalies.length,
            activeAnomalies: activeAnomalies,
            updates: uiUpdates
        }
    }];
} else {
    return [null, {
        topic: "anomaly-ui-update",
        payload: {
            activeCount: activeAnomalies.length,
            activeAnomalies: activeAnomalies,
            updates: uiUpdates
        }
    }];
}


Good! Now I need to create a script that will add these two functions to the flows.json and wire them properly. The flow should be:

Machine Cycles → Anomaly Detector → Event Logger → (1) MySQL, (2) UI Update

Let me also create the high scrap detection that goes in the complete-work-order handler.


Now let me create the high scrap detection for work order completion:


// High Scrap Detection - Add to complete-work-order handler
// This code should be added to the "Work Order buttons" function
// in the "complete-work-order" case

// After calculating final counts, check for high scrap
const targetQty = Number(order.target) || 0;
const scrapCount = Number(order.scrap) || 0;
const scrapPercent = targetQty > 0 ? (scrapCount / targetQty) * 100 : 0;

// Trigger: Scrap > 10% of target quantity
if (scrapPercent > 10 && targetQty > 0) {
    const severity = scrapPercent > 25 ? 'critical' : 'warning';

    const highScrapAnomaly = {
        anomaly_type: 'high-scrap',
        severity: severity,
        title: `High Waste Detected`,
        description: `Work order completed with ${scrapCount} scrap parts (${scrapPercent.toFixed(1)}% of target ${targetQty}). Why is there so much waste?`,
        data: {
            scrap_count: scrapCount,
            target_quantity: targetQty,
            scrap_percent: Math.round(scrapPercent * 10) / 10,
            good_parts: Number(order.good) || 0,
            total_cycles: global.get("cycleCount") || 0
        },
        kpi_snapshot: {
            oee: (msg.kpis && msg.kpis.oee) || 0,
            availability: (msg.kpis && msg.kpis.availability) || 0,
            performance: (msg.kpis && msg.kpis.performance) || 0,
            quality: (msg.kpis && msg.kpis.quality) || 0
        },
        work_order_id: order.id,
        cycle_count: global.get("cycleCount") || 0,
        timestamp: Date.now(),
        status: 'active'
    };

    // Send to Event Logger
    // This would be a separate output from the complete-work-order handler
    return {
        topic: "anomaly-detected",
        payload: [highScrapAnomaly]
    };
}

return null;


File created successfully at: /tmp/high_scrap_detection.js

// High Scrap Detection - Add to complete-work-order handler
// This code should be added to the "Work Order buttons" function
// in the "complete-work-order" case

// After calculating final counts, check for high scrap
const targetQty = Number(order.target) || 0;
const scrapCount = Number(order.scrap) || 0;
const scrapPercent = targetQty > 0 ? (scrapCount / targetQty) * 100 : 0;

// Trigger: Scrap > 10% of target quantity
if (scrapPercent > 10 && targetQty > 0) {
    const severity = scrapPercent > 25 ? 'critical' : 'warning';

    const highScrapAnomaly = {
        anomaly_type: 'high-scrap',
        severity: severity,
        title: `High Waste Detected`,
        description: `Work order completed with ${scrapCount} scrap parts (${scrapPercent.toFixed(1)}% of target ${targetQty}). Why is there so much waste?`,
        data: {
            scrap_count: scrapCount,
            target_quantity: targetQty,
            scrap_percent: Math.round(scrapPercent * 10) / 10,
            good_parts: Number(order.good) || 0,
            total_cycles: global.get("cycleCount") || 0
        },
        kpi_snapshot: {
            oee: (msg.kpis && msg.kpis.oee) || 0,
            availability: (msg.kpis && msg.kpis.availability) || 0,
            performance: (msg.kpis && msg.kpis.performance) || 0,
            quality: (msg.kpis && msg.kpis.quality) || 0
        },
        work_order_id: order.id,
        cycle_count: global.get("cycleCount") || 0,
        timestamp: Date.now(),
        status: 'active'
    };

    // Send to Event Logger
    // This would be a separate output from the complete-work-order handler
    return {
        topic: "anomaly-detected",
        payload: [highScrapAnomaly]
    };
}

return null;


Good! Now let me create a comprehensive script that:1. Adds the Anomaly Detector function node
2. Adds the Event Logger function node3. Wires Anomaly Detector to the Machine Cycles output
4. Wires Event Logger outputs to MySQL and UI
5. Adds high scrap detection to the complete-work-order handlerLet me create this script.


TodoWrite

Create anomaly_events database table

Create Anomaly Detector function (slow cycle, stoppage, high scrap)

Create Event Logger function with deduplication

Wire anomaly detection into Machine Cycles flow

Add high scrap detection to complete-work-order handler

Enhance Home tab with anomaly alerts badge

Enhance Alerts tab to show active anomalies

Test anomaly detection with real cycles


Create anomaly_events database table

Test anomaly detection with real cycles

Create anomaly_events database table

Test anomaly detection with real cycles


Now let me create the script to add the nodes and wire them up.


Now let me create a script to add these nodes to the flow and wire everything together:


     1→// ============================================================
     2→// ANOMALY DETECTOR
     3→// Detects production anomalies in real-time
     4→// ============================================================
     5→
     6→const cycle = msg.cycle || {};
     7→const kpis = msg.kpis || {};
     8→const activeOrder = global.get("activeWorkOrder") || {};
     9→
    10→// Must have active work order to detect anomalies
    11→if (!activeOrder.id) {
    12→    return null;
    13→}
    14→
    15→const theoreticalCycleTime = Number(activeOrder.cycleTime) || 0;
    16→const now = Date.now();
    17→
    18→// Get or initialize anomaly tracking state
    19→let anomalyState = global.get("anomalyState") || {
    20→    lastCycleTime: now,
    21→    activeStoppageEvent: null
    22→};
    23→
    24→const detectedAnomalies = [];
    25→
    26→// ============================================================
    27→// 1. SLOW CYCLE DETECTION
    28→// Trigger: Actual cycle time > 1.5x theoretical
    29→// ============================================================
    30→if (theoreticalCycleTime > 0) {
    31→    const timeSinceLastCycle = now - anomalyState.lastCycleTime;
    32→    const actualCycleTime = timeSinceLastCycle / 1000; // Convert to seconds
    33→    const threshold = theoreticalCycleTime * 1.5;
    34→
    35→    if (actualCycleTime > threshold && anomalyState.lastCycleTime > 0) {
    36→        const deltaPercent = ((actualCycleTime - theoreticalCycleTime) / theoreticalCycleTime) * 100;
    37→
    38→        // Determine severity
    39→        let severity = 'warning';
    40→        if (actualCycleTime > theoreticalCycleTime * 2.0) {
    41→            severity = 'critical';  // 100%+ slower
    42→        }
    43→
    44→        detectedAnomalies.push({
    45→            anomaly_type: 'slow-cycle',
    46→            severity: severity,
    47→            title: `Slow Cycle Detected`,
    48→            description: `Cycle took ${actualCycleTime.toFixed(1)}s (${deltaPercent.toFixed(0)}% slower than expected ${theoreticalCycleTime}s)`,
    49→            data: {
    50→                actual_cycle_time: actualCycleTime,
    51→                theoretical_cycle_time: theoreticalCycleTime,
    52→                delta_percent: Math.round(deltaPercent),
    53→                threshold_multiplier: actualCycleTime / theoreticalCycleTime
    54→            },
    55→            kpi_snapshot: {
    56→                oee: kpis.oee || 0,
    57→                availability: kpis.availability || 0,
    58→                performance: kpis.performance || 0,
    59→                quality: kpis.quality || 0
    60→            },
    61→            work_order_id: activeOrder.id,
    62→            cycle_count: cycle.cycles || 0,
    63→            timestamp: now
    64→        });
    65→
    66→        node.warn(`[ANOMALY] Slow cycle: ${actualCycleTime.toFixed(1)}s (expected ${theoreticalCycleTime}s)`);
    67→    }
    68→}
    69→
    70→// ============================================================
    71→// 2. PRODUCTION STOPPAGE DETECTION
    72→// Trigger: No cycle in > 3x theoretical cycle time
    73→// ============================================================
    74→if (theoreticalCycleTime > 0) {
    75→    const timeSinceLastCycle = now - anomalyState.lastCycleTime;
    76→    const stoppageThreshold = theoreticalCycleTime * 3 * 1000; // Convert to ms
    77→
    78→    // If we have an active stoppage event and a new cycle arrived, resolve it
    79→    if (anomalyState.activeStoppageEvent) {
    80→        // Cycle resumed - mark stoppage as resolved
    81→        anomalyState.activeStoppageEvent.resolved_at = now;
    82→        anomalyState.activeStoppageEvent.auto_resolved = true;
    83→        anomalyState.activeStoppageEvent.status = 'resolved';
    84→
    85→        const stoppageDuration = (now - anomalyState.activeStoppageEvent.timestamp) / 1000;
    86→        node.warn(`[ANOMALY] Production resumed after ${stoppageDuration.toFixed(0)}s stoppage`);
    87→
    88→        // Send resolution event
    89→        detectedAnomalies.push(anomalyState.activeStoppageEvent);
    90→        anomalyState.activeStoppageEvent = null;
    91→    }
    92→
    93→    // Check if production has stopped (only if no active stoppage event)
    94→    if (!anomalyState.activeStoppageEvent && timeSinceLastCycle > stoppageThreshold && anomalyState.lastCycleTime > 0) {
    95→        const stoppageSeconds = timeSinceLastCycle / 1000;
    96→
    97→        // Determine severity
    98→        let severity = 'warning';
    99→        if (stoppageSeconds > theoreticalCycleTime * 5) {
   100→            severity = 'critical';  // Stopped for 5x+ theoretical time
   101→        }
   102→
   103→        const stoppageEvent = {
   104→            anomaly_type: 'production-stopped',
   105→            severity: severity,
   106→            title: `Production Stoppage`,
   107→            description: `No cycles detected for ${stoppageSeconds.toFixed(0)}s (expected cycle every ${theoreticalCycleTime}s)`,
   108→            data: {
   109→                stoppage_duration_seconds: Math.round(stoppageSeconds),
   110→                theoretical_cycle_time: theoreticalCycleTime,
   111→                last_cycle_timestamp: anomalyState.lastCycleTime,
   112→                threshold_multiplier: stoppageSeconds / theoreticalCycleTime
   113→            },
   114→            kpi_snapshot: {
   115→                oee: kpis.oee || 0,
   116→                availability: kpis.availability || 0,
   117→                performance: kpis.performance || 0,
   118→                quality: kpis.quality || 0
   119→            },
   120→            work_order_id: activeOrder.id,
   121→            cycle_count: cycle.cycles || 0,
   122→            timestamp: now,
   123→            status: 'active'
   124→        };
   125→
   126→        detectedAnomalies.push(stoppageEvent);
   127→        anomalyState.activeStoppageEvent = stoppageEvent;
   128→
   129→        node.warn(`[ANOMALY] Production stopped: ${stoppageSeconds.toFixed(0)}s since last cycle`);
   130→    }
   131→}
   132→
   133→// Update last cycle time for next iteration
   134→anomalyState.lastCycleTime = now;
   135→global.set("anomalyState", anomalyState);
   136→
   137→// ============================================================
   138→// OUTPUT
   139→// ============================================================
   140→if (detectedAnomalies.length > 0) {
   141→    node.warn(`[ANOMALY DETECTOR] Detected ${detectedAnomalies.length} anomaly/ies`);
   142→
   143→    return {
   144→        topic: "anomaly-detected",
   145→        payload: detectedAnomalies,
   146→        originalMsg: msg  // Pass through original message for other flows
   147→    };
   148→}
   149→
   150→return null;  // No anomalies detected
   151→

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


Good, now let me read the event logger function too, then create a script to add both nodes to flows.json.


     1→// ============================================================
     2→// EVENT LOGGER
     3→// Deduplicates and logs anomaly events to database
     4→// ============================================================
     5→
     6→const anomalies = msg.payload || [];
     7→
     8→if (!Array.isArray(anomalies) || anomalies.length === 0) {
     9→    return null;
    10→}
    11→
    12→// Get or initialize active anomalies list
    13→let activeAnomalies = global.get("activeAnomalies") || [];
    14→const now = Date.now();
    15→const DEDUP_WINDOW = 5 * 60 * 1000; // 5 minutes
    16→
    17→const dbInserts = [];
    18→const uiUpdates = [];
    19→
    20→anomalies.forEach(anomaly => {
    21→    // ============================================================
    22→    // DEDUPLICATION LOGIC
    23→    // Don't create new event if same type exists in last 5 minutes
    24→    // ============================================================
    25→    const existingIndex = activeAnomalies.findIndex(existing =>
    26→        existing.anomaly_type === anomaly.anomaly_type &&
    27→        existing.work_order_id === anomaly.work_order_id &&
    28→        existing.status === 'active' &&
    29→        (now - existing.timestamp) < DEDUP_WINDOW
    30→    );
    31→
    32→    if (existingIndex !== -1) {
    33→        // Update existing event
    34→        const existing = activeAnomalies[existingIndex];
    35→        existing.occurrence_count = (existing.occurrence_count || 1) + 1;
    36→        existing.last_occurrence = now;
    37→
    38→        // Update in database
    39→        const updateQuery = `UPDATE anomaly_events
    40→            SET occurrence_count = ?, last_occurrence = ?
    41→            WHERE event_id = ?`;
    42→
    43→        dbInserts.push({
    44→            topic: updateQuery,
    45→            payload: [existing.occurrence_count, existing.last_occurrence, existing.event_id]
    46→        });
    47→
    48→        node.warn(`[EVENT LOGGER] Updated existing ${anomaly.anomaly_type} event (occurrence #${existing.occurrence_count})`);
    49→
    50→    } else if (anomaly.status === 'resolved') {
    51→        // ============================================================
    52→        // RESOLVE EVENT
    53→        // ============================================================
    54→        const resolveIndex = activeAnomalies.findIndex(existing =>
    55→            existing.anomaly_type === anomaly.anomaly_type &&
    56→            existing.work_order_id === anomaly.work_order_id &&
    57→            existing.status === 'active'
    58→        );
    59→
    60→        if (resolveIndex !== -1) {
    61→            const existing = activeAnomalies[resolveIndex];
    62→            existing.status = 'resolved';
    63→            existing.resolved_at = anomaly.resolved_at || now;
    64→            existing.auto_resolved = anomaly.auto_resolved || false;
    65→
    66→            // Update in database
    67→            const resolveQuery = `UPDATE anomaly_events
    68→                SET status = 'resolved', resolved_at = ?, auto_resolved = ?
    69→                WHERE event_id = ?`;
    70→
    71→            dbInserts.push({
    72→                topic: resolveQuery,
    73→                payload: [existing.resolved_at, existing.auto_resolved, existing.event_id]
    74→            });
    75→
    76→            // Remove from active list
    77→            activeAnomalies.splice(resolveIndex, 1);
    78→
    79→            node.warn(`[EVENT LOGGER] Resolved ${anomaly.anomaly_type} event (auto: ${existing.auto_resolved})`);
    80→
    81→            uiUpdates.push({
    82→                event_id: existing.event_id,
    83→                status: 'resolved'
    84→            });
    85→        }
    86→
    87→    } else {
    88→        // ============================================================
    89→        // NEW EVENT
    90→        // ============================================================
    91→        const insertQuery = `INSERT INTO anomaly_events
    92→            (timestamp, work_order_id, anomaly_type, severity, title, description,
    93→             data_json, kpi_snapshot_json, status, cycle_count, occurrence_count, last_occurrence)
    94→            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
    95→
    96→        const dataJson = JSON.stringify(anomaly.data || {});
    97→        const kpiJson = JSON.stringify(anomaly.kpi_snapshot || {});
    98→
    99→        dbInserts.push({
   100→            topic: insertQuery,
   101→            payload: [
   102→                anomaly.timestamp,
   103→                anomaly.work_order_id,
   104→                anomaly.anomaly_type,
   105→                anomaly.severity,
   106→                anomaly.title,
   107→                anomaly.description,
   108→                dataJson,
   109→                kpiJson,
   110→                'active',
   111→                anomaly.cycle_count,
   112→                1,  // occurrence_count
   113→                anomaly.timestamp  // last_occurrence
   114→            ],
   115→            _storeEventId: true,  // Flag to get generated event_id
   116→            _anomaly: anomaly  // Keep reference for later
   117→        });
   118→
   119→        node.warn(`[EVENT LOGGER] New ${anomaly.anomaly_type} event: ${anomaly.title}`);
   120→    }
   121→});
   122→
   123→// Save active anomalies to global context
   124→global.set("activeAnomalies", activeAnomalies);
   125→
   126→// ============================================================
   127→// OUTPUT
   128→// ============================================================
   129→// Output 1: Database inserts (to mysql node)
   130→// Output 2: UI updates (to Home/Alerts tabs)
   131→
   132→if (dbInserts.length > 0) {
   133→    // Send each insert as a separate message
   134→    return [dbInserts, {
   135→        topic: "anomaly-ui-update",
   136→        payload: {
   137→            activeCount: activeAnomalies.length,
   138→            activeAnomalies: activeAnomalies,
   139→            updates: uiUpdates
   140→        }
   141→    }];
   142→} else {
   143→    return [null, {
   144→        topic: "anomaly-ui-update",
   145→        payload: {
   146→            activeCount: activeAnomalies.length,
   147→            activeAnomalies: activeAnomalies,
   148→            updates: uiUpdates
   149→        }
   150→    }];
   151→}
   152→

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


Good! Now I need to create a comprehensive Python script that adds these two nodes to the flows and wires them correctly. Let me also need to find the Machine Cycles node ID so I can wire the Anomaly Detector to it.


Bash

jq -r '.[] | select(.name == "machine cycles") | .id' /home/mdares/.node-red/flows.json

Find Machine Cycles node ID


No match. Let me search with a different pattern.


jq -r '.[] | select(.name and (.name | ascii_downcase | contains("machine"))) | {id: .id, name: .name}' /home/mdares/.node-red/flows.json | head -10

Find Machine-related nodes


{
  "id": "00d8ad2b0277f906",
  "name": "machine_data"
}
{
  "id": "0d023d87a13bf56f",
  "name": "Machine cycles"
}
{
  "id": "4fabedfe0668e215",


Perfect! The Machine Cycles node ID is 0d023d87a13bf56f. Now let me check its current wiring to see where it outputs.


jq '.[] | select(.id == "0d023d87a13bf56f") | {outputs: .outputs, wires: .wires}' /home/mdares/.node-red/flows.json

Check Machine Cycles outputs


Perfect! Machine Cycles has 4 outputs. I'll add the Anomaly Detector to tap into one of the outputs (probably output 2 which goes to Calculate KPIs). Actually, looking at the wiring, output 2 goes to "00b6132848964bd9" which is probably Calculate KPIs. I can add the Anomaly Detector as another target of that output.

Actually, a better approach is to add the Anomaly Detector as a fifth output from Machine Cycles, or I can tap into an existing output. Let me create a Python script that adds the two new nodes and wires them properly.


Now let me create a comprehensive script to add the anomaly detection nodes and wire everything:


Write

#!/usr/bin/env python3
import json

# Read the function code files
with open('/tmp/anomaly_detector_function.js', 'r') as f:
    anomaly_detector_code = f.read()

with open('/tmp/event_logger_function.js', 'r') as f:
    event_logger_code = f.read()

# Load flows.json
with open('/home/mdares/.node-red/flows.json', 'r') as f:
    flows = json.load(f)

# Find the main tab and Machine Cycles node
tab_id = None
machine_cycles_node = None

for node in flows:
    if node.get('type') == 'tab' and not tab_id:
        tab_id = node['id']
    if node.get('id') == '0d023d87a13bf56f':
        machine_cycles_node = node

if not tab_id or not machine_cycles_node:
    print("✗ Could not find required nodes")
    exit(1)

# ============================================================
# 1. CREATE ANOMALY DETECTOR FUNCTION NODE
# ============================================================
anomaly_detector_node = {
    "id": "anomaly_detector_node_id",
    "type": "function",
    "z": tab_id,
    "name": "Anomaly Detector",
    "func": anomaly_detector_code,
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 850,
    "y": 300,
    "wires": [["event_logger_node_id"]]
}

# ============================================================
# 2. CREATE EVENT LOGGER FUNCTION NODE
# ============================================================
event_logger_node = {
    "id": "event_logger_node_id",
    "type": "function",
    "z": tab_id,
    "name": "Event Logger",
    "func": event_logger_code,
    "outputs": 2,  # Output 1: DB inserts, Output 2: UI updates
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 1050,
    "y": 300,
    "wires": [
        ["anomaly_mysql_node_id"],  # Output 1: to MySQL
        []  # Output 2: UI updates (will wire to Home tab later)
    ]
}

# ============================================================
# 3. CREATE MYSQL NODE FOR ANOMALY EVENTS
# ============================================================
anomaly_mysql_node = {
    "id": "anomaly_mysql_node_id",
    "type": "mysql",
    "z": tab_id,
    "mydb": "00d8ad2b0277f906",
    "name": "Anomaly Events DB",
    "x": 1270,
    "y": 280,
    "wires": [["anomaly_db_debug_node_id"]]
}

# ============================================================
# 4. CREATE DEBUG NODE
# ============================================================
anomaly_debug_node = {
    "id": "anomaly_db_debug_node_id",
    "type": "debug",
    "z": tab_id,
    "name": "Anomaly DB Result",
    "active": True,
    "tosidebar": True,
    "console": False,
    "tostatus": False,
    "complete": "true",
    "targetType": "full",
    "statusVal": "",
    "statusType": "auto",
    "x": 1490,
    "y": 280,
    "wires": []
}

# ============================================================
# 5. CREATE SPLIT NODE (to handle array of DB inserts)
# ============================================================
split_node = {
    "id": "anomaly_split_node_id",
    "type": "split",
    "z": tab_id,
    "name": "Split DB Inserts",
    "splt": "\\n",
    "spltType": "str",
    "arraySplt": 1,
    "arraySpltType": "len",
    "stream": False,
    "addname": "",
    "x": 1270,
    "y": 240,
    "wires": [["anomaly_mysql_node_id"]]
}

# Update Event Logger to send to split node instead
event_logger_node["wires"][0] = ["anomaly_split_node_id"]

# ============================================================
# 6. WIRE ANOMALY DETECTOR TO MACHINE CYCLES OUTPUT 2
# ============================================================
# Machine Cycles output 2 already goes to Calculate KPIs
# Add Anomaly Detector as an additional target
if len(machine_cycles_node["wires"]) > 1:
    machine_cycles_node["wires"][1].append("anomaly_detector_node_id")
    print("✓ Wired Anomaly Detector to Machine Cycles output 2")
else:
    print("✗ Could not wire to Machine Cycles")

# ============================================================
# 7. ADD ALL NEW NODES TO FLOWS
# ============================================================
flows.extend([
    anomaly_detector_node,
    event_logger_node,
    split_node,
    anomaly_mysql_node,
    anomaly_debug_node
])

# Save flows.json
with open('/home/mdares/.node-red/flows.json', 'w') as f:
    json.dump(flows, f, indent=4)

print("✓ Added Anomaly Detection nodes to flows.json")
print("  - Anomaly Detector function")
print("  - Event Logger function")
print("  - Split node (for DB inserts)")
print("  - MySQL node (Anomaly Events DB)")
print("  - Debug node")
print("")
print("✓ Wired into Machine Cycles flow")
print("  Machine Cycles → Anomaly Detector → Event Logger → MySQL")


File created successfully at: /tmp/add_anomaly_detection_nodes.py

#!/usr/bin/env python3
import json

# Read the function code files
with open('/tmp/anomaly_detector_function.js', 'r') as f:
    anomaly_detector_code = f.read()

with open('/tmp/event_logger_function.js', 'r') as f:
    event_logger_code = f.read()

# Load flows.json
with open('/home/mdares/.node-red/flows.json', 'r') as f:
    flows = json.load(f)

# Find the main tab and Machine Cycles node
tab_id = None
machine_cycles_node = None

for node in flows:
    if node.get('type') == 'tab' and not tab_id:
        tab_id = node['id']
    if node.get('id') == '0d023d87a13bf56f':
        machine_cycles_node = node

if not tab_id or not machine_cycles_node:
    print("✗ Could not find required nodes")
    exit(1)

# ============================================================
# 1. CREATE ANOMALY DETECTOR FUNCTION NODE
# ============================================================
anomaly_detector_node = {
    "id": "anomaly_detector_node_id",
    "type": "function",
    "z": tab_id,
    "name": "Anomaly Detector",
    "func": anomaly_detector_code,
    "outputs": 1,
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 850,
    "y": 300,
    "wires": [["event_logger_node_id"]]
}

# ============================================================
# 2. CREATE EVENT LOGGER FUNCTION NODE
# ============================================================
event_logger_node = {
    "id": "event_logger_node_id",
    "type": "function",
    "z": tab_id,
    "name": "Event Logger",
    "func": event_logger_code,
    "outputs": 2,  # Output 1: DB inserts, Output 2: UI updates
    "timeout": 0,
    "noerr": 0,
    "initialize": "",
    "finalize": "",
    "libs": [],
    "x": 1050,
    "y": 300,
    "wires": [
        ["anomaly_mysql_node_id"],  # Output 1: to MySQL
        []  # Output 2: UI updates (will wire to Home tab later)
    ]
}

# ============================================================
# 3. CREATE MYSQL NODE FOR ANOMALY EVENTS
# ============================================================
anomaly_mysql_node = {
    "id": "anomaly_mysql_node_id",
    "type": "mysql",
    "z": tab_id,
    "mydb": "00d8ad2b0277f906",
    "name": "Anomaly Events DB",
    "x": 1270,
    "y": 280,
    "wires": [["anomaly_db_debug_node_id"]]
}

# ============================================================
# 4. CREATE DEBUG NODE
# ============================================================
anomaly_debug_node = {
    "id": "anomaly_db_debug_node_id",
    "type": "debug",
    "z": tab_id,
    "name": "Anomaly DB Result",
    "active": True,
    "tosidebar": True,
    "console": False,
    "tostatus": False,
    "complete": "true",
    "targetType": "full",
    "statusVal": "",
    "statusType": "auto",
    "x": 1490,
    "y": 280,
    "wires": []
}

# ============================================================
# 5. CREATE SPLIT NODE (to handle array of DB inserts)
# ============================================================
split_node = {
    "id": "anomaly_split_node_id",
    "type": "split",
    "z": tab_id,
    "name": "Split DB Inserts",
    "splt": "\\n",
    "spltType": "str",
    "arraySplt": 1,
    "arraySpltType": "len",
    "stream": False,
    "addname": "",
    "x": 1270,
    "y": 240,
    "wires": [["anomaly_mysql_node_id"]]
}

# Update Event Logger to send to split node instead
event_logger_node["wires"][0] = ["anomaly_split_node_id"]

# ============================================================
# 6. WIRE ANOMALY DETECTOR TO MACHINE CYCLES OUTPUT 2
# ============================================================
# Machine Cycles output 2 already goes to Calculate KPIs
# Add Anomaly Detector as an additional target
if len(machine_cycles_node["wires"]) > 1:
    machine_cycles_node["wires"][1].append("anomaly_detector_node_id")
    print("✓ Wired Anomaly Detector to Machine Cycles output 2")
else:
    print("✗ Could not wire to Machine Cycles")

# ============================================================
# 7. ADD ALL NEW NODES TO FLOWS
# ============================================================
flows.extend([
    anomaly_detector_node,
    event_logger_node,
    split_node,
    anomaly_mysql_node,
    anomaly_debug_node
])

# Save flows.json
with open('/home/mdares/.node-red/flows.json', 'w') as f:
    json.dump(flows, f, indent=4)

print("✓ Added Anomaly Detection nodes to flows.json")
print("  - Anomaly Detector function")
print("  - Event Logger function")
print("  - Split node (for DB inserts)")
print("  - MySQL node (Anomaly Events DB)")
print("  - Debug node")
print("")
print("✓ Wired into Machine Cycles flow")
print("  Machine Cycles → Anomaly Detector → Event Logger → MySQL")


Good! Now let me run this script to add the nodes and wire them up.


Bash

Add anomaly detection nodes


✓ Wired Anomaly Detector to Machine Cycles output 2
✓ Added Anomaly Detection nodes to flows.json
  - Anomaly Detector function
  - Event Logger function
  - Split node (for DB inserts)
  - MySQL node (Anomaly Events DB)
  - Debug node

✓ Wired into Machine Cycles flow
  Machine Cycles → Anomaly Detector → Event Logger → MySQL


Excellent! Now let me update the todo and add the high scrap detection to the complete-work-order handler. Let me find the Work Order buttons node.


TodoWrite

Create anomaly_events database table

Create Anomaly Detector function (slow cycle, stoppage, high scrap)

Create Event Logger function with deduplication

Wire anomaly detection into Machine Cycles flow

Add high scrap detection to complete-work-order handler

Enhance Home tab with anomaly alerts badge

Enhance Alerts tab to show active anomalies

Test anomaly detection with real cycles


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Create anomaly_events database table

Create Anomaly Detector function (slow cycle, stoppage, high scrap)

Create Event Logger function with deduplication

Wire anomaly detection into Machine Cycles flow

Add high scrap detection to complete-work-order handler

Enhance Home tab with anomaly alerts badge

Enhance Alerts tab to show active anomalies

Test anomaly detection with real cycles

Create anomaly_events database table

Test anomaly detection with real cycles


Now I need to add the high scrap detection to the complete-work-order handler. The Work Order buttons function node ID is 9bbd4fade968036d. Let me read that node and add the high scrap detection code.


Bash

jq '.[] | select(.id == "9bbd4fade968036d") | .outputs' /home/mdares/.node-red/flows.json

Check Work Order buttons outputs


The Work Order buttons node has 4 outputs. I need to add a 5th output for anomaly detection, or I can reuse one of the existing outputs. Let me check the current wiring to see what the outputs are used for.


jq '.[] | select(.id == "9bbd4fade968036d") | .wires' /home/mdares/.node-red/flows.json

Check Work Order buttons wiring


Good, the node has 4 outputs. I'll add a 5th output for anomaly detection from the complete-work-order handler. Let me create a script that:1. Adds the outputs count to 5
2. Adds the high scrap detection code to the complete-work-order case
3. Wires the 5th output to the Event Logger

Let me create this script.


Write

#!/usr/bin/env python3
import json

with open('/home/mdares/.node-red/flows.json', 'r') as f:
    flows = json.load(f)

# Find Work Order buttons node
work_order_buttons_node = None
for node in flows:
    if node.get('id') == '9bbd4fade968036d':
        work_order_buttons_node = node
        break

if not work_order_buttons_node:
    print("✗ Could not find Work Order buttons node")
    exit(1)

# Get the current function code
func_code = work_order_buttons_node.get('func', '')

# Find the complete-work-order case and add high scrap detection
# Look for the end of the complete-work-order case (before the "return" statement)

# Add the high scrap detection code before the final database insert
high_scrap_code = '''
    // ============================================================
    // HIGH SCRAP DETECTION
    // ============================================================
    const targetQty = Number(order.target) || 0;
    const scrapCount = Number(order.scrap) || 0;
    const scrapPercent = targetQty > 0 ? (scrapCount / targetQty) * 100 : 0;

    // Trigger: Scrap > 10% of target quantity
    if (scrapPercent > 10 && targetQty > 0) {
        const severity = scrapPercent > 25 ? 'critical' : 'warning';

        const highScrapAnomaly = {
            anomaly_type: 'high-scrap',
            severity: severity,
            title: `High Waste Detected`,
            description: `Work order completed with ${scrapCount} scrap parts (${scrapPercent.toFixed(1)}% of target ${targetQty}). Why is there so much waste?`,
            data: {
                scrap_count: scrapCount,
                target_quantity: targetQty,
                scrap_percent: Math.round(scrapPercent * 10) / 10,
                good_parts: Number(order.good) || 0,
                total_cycles: global.get("cycleCount") || 0
            },
            kpi_snapshot: {
                oee: (msg.kpis && msg.kpis.oee) || 0,
                availability: (msg.kpis && msg.kpis.availability) || 0,
                performance: (msg.kpis && msg.kpis.performance) || 0,
                quality: (msg.kpis && msg.kpis.quality) || 0
            },
            work_order_id: order.id,
            cycle_count: global.get("cycleCount") || 0,
            timestamp: Date.now(),
            status: 'active'
        };

        node.warn(`[HIGH SCRAP] Detected ${scrapPercent.toFixed(1)}% scrap on work order ${order.id}`);

        // Send to Event Logger (output 5)
        const anomalyMsg = {
            topic: "anomaly-detected",
            payload: [highScrapAnomaly]
        };

        // Return with anomaly message on output 5
        return [dbMsg, null, null, null, anomalyMsg];
    }
'''

# Find the complete-work-order case and inject the code
# Look for the section where dbMsg is created and before the return statement
search_pattern = 'case "complete-work-order":'
if search_pattern in func_code:
    # Find the return statement in this case
    # We want to add the code before "return [dbMsg, null, null, null];"

    # Split by the case
    parts = func_code.split(search_pattern)
    before_case = parts[0]
    after_case = parts[1]

    # Find the return statement within this case
    # Look for "return [dbMsg,"
    return_pattern = 'return [dbMsg, null, null, null];'

    if return_pattern in after_case:
        # Split at the return statement
        case_parts = after_case.split(return_pattern, 1)
        case_code = case_parts[0]
        after_return = case_parts[1]

        # Inject the high scrap detection code before the return
        new_case_code = case_code + high_scrap_code + '\\n    ' + return_pattern

        # Reconstruct the function code
        func_code = before_case + search_pattern + new_case_code + after_return

        work_order_buttons_node['func'] = func_code
        print("✓ Added high scrap detection to complete-work-order handler")
    else:
        print("⚠ Could not find return statement pattern")
else:
    print("✗ Could not find complete-work-order case")
    exit(1)

# Increase outputs count to 5
work_order_buttons_node['outputs'] = 5

# Add the 5th output wire to Event Logger
current_wires = work_order_buttons_node['wires']
current_wires.append(['event_logger_node_id'])  # Add 5th output to Event Logger
work_order_buttons_node['wires'] = current_wires

print("✓ Added 5th output to Work Order buttons node")
print("✓ Wired to Event Logger")

# Save flows.json
with open('/home/mdares/.node-red/flows.json', 'w') as f:
    json.dump(flows, f, indent=4)

print("✓ flows.json updated")


File created successfully at: /tmp/add_high_scrap_detection.py

#!/usr/bin/env python3
import json

with open('/home/mdares/.node-red/flows.json', 'r') as f:
    flows = json.load(f)

# Find Work Order buttons node
work_order_buttons_node = None
for node in flows:
    if node.get('id') == '9bbd4fade968036d':
        work_order_buttons_node = node
        break

if not work_order_buttons_node:
    print("✗ Could not find Work Order buttons node")
    exit(1)

# Get the current function code
func_code = work_order_buttons_node.get('func', '')

# Find the complete-work-order case and add high scrap detection
# Look for the end of the complete-work-order case (before the "return" statement)

# Add the high scrap detection code before the final database insert
high_scrap_code = '''
    // ============================================================
    // HIGH SCRAP DETECTION
    // ============================================================
    const targetQty = Number(order.target) || 0;
    const scrapCount = Number(order.scrap) || 0;
    const scrapPercent = targetQty > 0 ? (scrapCount / targetQty) * 100 : 0;

    // Trigger: Scrap > 10% of target quantity
    if (scrapPercent > 10 && targetQty > 0) {
        const severity = scrapPercent > 25 ? 'critical' : 'warning';

        const highScrapAnomaly = {
            anomaly_type: 'high-scrap',
            severity: severity,
            title: `High Waste Detected`,
            description: `Work order completed with ${scrapCount} scrap parts (${scrapPercent.toFixed(1)}% of target ${targetQty}). Why is there so much waste?`,
            data: {
                scrap_count: scrapCount,
                target_quantity: targetQty,
                scrap_percent: Math.round(scrapPercent * 10) / 10,
                good_parts: Number(order.good) || 0,
                total_cycles: global.get("cycleCount") || 0
            },
            kpi_snapshot: {
                oee: (msg.kpis && msg.kpis.oee) || 0,
                availability: (msg.kpis && msg.kpis.availability) || 0,
                performance: (msg.kpis && msg.kpis.performance) || 0,
                quality: (msg.kpis && msg.kpis.quality) || 0
            },
            work_order_id: order.id,
            cycle_count: global.get("cycleCount") || 0,
            timestamp: Date.now(),
            status: 'active'
        };

        node.warn(`[HIGH SCRAP] Detected ${scrapPercent.toFixed(1)}% scrap on work order ${order.id}`);

        // Send to Event Logger (output 5)
        const anomalyMsg = {
            topic: "anomaly-detected",
            payload: [highScrapAnomaly]
        };

        // Return with anomaly message on output 5
        return [dbMsg, null, null, null, anomalyMsg];
    }
'''

# Find the complete-work-order case and inject the code
# Look for the section where dbMsg is created and before the return statement
search_pattern = 'case "complete-work-order":'
if search_pattern in func_code:
    # Find the return statement in this case
    # We want to add the code before "return [dbMsg, null, null, null];"

    # Split by the case
    parts = func_code.split(search_pattern)
    before_case = parts[0]
    after_case = parts[1]

    # Find the return statement within this case
    # Look for "return [dbMsg,"
    return_pattern = 'return [dbMsg, null, null, null];'

    if return_pattern in after_case:
        # Split at the return statement
        case_parts = after_case.split(return_pattern, 1)
        case_code = case_parts[0]
        after_return = case_parts[1]

        # Inject the high scrap detection code before the return
        new_case_code = case_code + high_scrap_code + '\\n    ' + return_pattern

        # Reconstruct the function code
        func_code = before_case + search_pattern + new_case_code + after_return

        work_order_buttons_node['func'] = func_code
        print("✓ Added high scrap detection to complete-work-order handler")
    else:
        print("⚠ Could not find return statement pattern")
else:
    print("✗ Could not find complete-work-order case")
    exit(1)

# Increase outputs count to 5
work_order_buttons_node['outputs'] = 5

# Add the 5th output wire to Event Logger
current_wires = work_order_buttons_node['wires']
current_wires.append(['event_logger_node_id'])  # Add 5th output to Event Logger
work_order_buttons_node['wires'] = current_wires

print("✓ Added 5th output to Work Order buttons node")
print("✓ Wired to Event Logger")

# Save flows.json
with open('/home/mdares/.node-red/flows.json', 'w') as f:
    json.dump(flows, f, indent=4)

print("✓ flows.json updated")


Good! Let me run this script to add the high scrap detection.


Bash

Add high scrap detection


⚠ Could not find return statement pattern
✓ Added 5th output to Work Order buttons node
✓ Wired to Event Logger
✓ flows.json updated


The return statement pattern wasn't found. Let me check the actual code in the complete-work-order case to see what the return statement looks like. Let me extract just that case.


jq -r '.[] | select(.id == "9bbd4fade968036d") | .func' /home/mdares/.node-red/flows.json | grep -A 50 'case "complete-work-order"' | head -60

Check complete-work-order case code


    case "complete-work-order": {
        msg._mode = "complete";
        const order = msg.payload || {};
        if (!order.id) {
            node.error("No work order id supplied for complete", msg);
            return [null, null, null, null];
        }

        // Get final values from global state before clearing
        const activeOrder = global.get("activeWorkOrder") || {};
        const finalCycleCount = Number(global.get("cycleCount") || 0);
        const finalGoodParts = Number(activeOrder.good) || 0;
        const finalScrapParts = Number(activeOrder.scrap) || 0;

        node.warn(`[COMPLETE] Persisting final values: cycles=${finalCycleCount}, good=${finalGoodParts}, scrap=${finalScrapParts}`);

        msg.completeOrder = order;

        // SQL: Persist final counts AND set status to DONE
        msg.topic = "UPDATE work_orders SET status = 'DONE', cycle_count = ?, good_parts = ?, scrap_parts = ?, progress_percent = 100, updated_at = NOW() WHERE work_order_id = ?";
        msg.payload = [finalCycleCount, finalGoodParts, finalScrapParts, order.id];

        // Clear ALL state on completion
        global.set("activeWorkOrder", null);
        global.set("trackingEnabled", false);
        global.set("productionStarted", false);
        global.set("kpiStartupMode", false);
        global.set("operatingTime", 0);
        global.set("lastCycleTime", null);
        global.set("cycleCount", 0);
        flow.set("lastMachineState", 0);
        global.set("scrapPromptIssuedFor", null);

        node.warn('[COMPLETE] Cleared all state flags');
        return [null, null, null, msg];
    }
    case "get-current-state": {
        // Return current state for UI sync on tab switch
        const activeOrder = global.get("activeWorkOrder") || null;
        const trackingEnabled = global.get("trackingEnabled") || false;
        const productionStarted = global.get("productionStarted") || false;
        const kpis = global.get("currentKPIs") || { oee: 0, availability: 0, performance: 0, quality: 0 };

        msg._mode = "current-state";
        msg.payload = {
            activeWorkOrder: activeOrder,
            trackingEnabled: trackingEnabled,
            productionStarted: productionStarted,
            kpis: kpis
        };


I see! The return statement is `return [null, null, null, msg];` not `return [dbMsg, null, null, null];`. The function writes the update to msg.topic and msg.payload, then returns msg on output 4. Let me update my script to inject the high scrap detection code correctly.