The file /home/mdares/.node-red/flows.json has been updated. Here's the result of running `cat -n` on a snippet of the edited file:
  1969→        "x": 1490,
  1970→        "y": 280,
  1971→        "wires": []
  1972→    },
  1973→    {
  1974→        "id": "init_oee_threshold",
  1975→        "type": "inject",
  1976→        "z": "cac3a4383120cb57",
  1977→        "name": "Initialize OEE Threshold (90%)",
  1978→        "props": [
  1979→            {
  1980→                "p": "payload"
  1981→            }
  1982→        ],
  1983→        "repeat": "",
  1984→        "crontab": "",
  1985→        "once": true,
  1986→        "onceDelay": 0.1,
  1987→        "topic": "",
  1988→        "payload": "90",
  1989→        "payloadType": "num",
  1990→        "x": 290,
  1991→        "y": 940,
  1992→        "wires": [
  1993→            [
  1994→                "set_oee_threshold_global"
  1995→            ]
  1996→        ]
  1997→    },
  1998→    {
  1999→        "id": "set_oee_threshold_global",
  2000→        "type": "function",
  2001→        "z": "cac3a4383120cb57",
  2002→        "name": "Set OEE Threshold Global",
  2003→        "func": "// Initialize OEE alert threshold\nconst threshold = Number(msg.payload) || 90;\nglobal.set(\"oeeAlertThreshold\", threshold);\n\nnode.warn(`[CONFIG] OEE Alert Threshold set to ${threshold}%`);\n\nreturn msg;",
  2004→        "outputs": 1,
  2005→        "timeout": 0,
  2006→        "noerr": 0,
  2007→        "initialize": "",
  2008→        "finalize": "",
  2009→        "libs": [],
  2010→        "x": 550,
  2011→        "y": 940,
  2012→        "wires": [[]]
  2013→    },
  2014→    {
  2015→        "id": "verify_create_table_inject",
  2016→        "type": "inject",
  2017→        "z": "cac3a4383120cb57",
  2018→        "name": "VERIFY & CREATE anomaly_events (click me)",
  2019→        "props": [
  2020→            {
  2021→                "p": "payload"
  2022→            }

    {
        "id": "init_oee_threshold",
        "type": "inject",
        "z": "cac3a4383120cb57",
        "name": "Initialize OEE Threshold (90%)",
        "props": [
            {
                "p": "payload"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": true,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "90",
        "payloadType": "num",
        "x": 290,
        "y": 940,
        "wires": [
            [
                "set_oee_threshold_global"
            ]
        ]
    },
    {
        "id": "set_oee_threshold_global",
        "type": "function",
        "z": "cac3a4383120cb57",
        "name": "Set OEE Threshold Global",
        "func": "// Initialize OEE alert threshold\nconst threshold = Number(msg.payload) || 90;\nglobal.set(\"oeeAlertThreshold\", threshold);\n\nnode.warn(`[CONFIG] OEE Alert Threshold set to ${threshold}%`);\n\nreturn msg;",
        "outputs": 1,
        "timeout": 0,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 550,
        "y": 940,
        "wires": [[]]
    },
    {
        "id": "verify_create_table_inject",
        "type": "inject",
        "z": "cac3a4383120cb57",
        "name": "VERIFY & CREATE anomaly_events (click me)",

theme-custom

Context: grey

Home Template

      <button class="sb-btn active" data-label="Home" ng-click="gotoTab('Home')">🏠</button>
      <button class="sb-btn" data-label="Work Orders" ng-click="gotoTab('Work Orders')">📋</button>
      <button class="sb-btn" data-label="Alerts" ng-click="gotoTab('Alerts')">⚠</button>
      <button class="sb-btn" data-label="Graphs" ng-click="gotoTab('Graphs')">📊</button>
      <button class="sb-btn" data-label="Help" ng-click="gotoTab('Help')">❓</button>
      <button class="sb-btn" data-label="Settings" ng-click="gotoTab('Settings')">⚙</button>

OEE V1.0

OEE

0%

Availability

0%

Performance

0%

Quality

0%

Current Work Order

Work Order ID

SKU

Cycle Time

0

0%

Good Parts

0

out of 0

MachineOFFLINE

ProductionSTOPPED

          <button id="start-button" type="button" class="start" ng-click="toggleStartStop()" ng-class="{'stop-btn': isProductionRunning}" ng-disabled="!hasActiveOrder && !isProductionRunning">{{ isProductionRunning ? 'STOP' : 'START' }}</button>

Work Order Already In Progress

{{ resumePrompt.id }}

      {{ resumePrompt.goodParts }} of {{ resumePrompt.targetQty }} parts completed
      ({{ resumePrompt.progressPercent }}%)

Cycle Count: {{ resumePrompt.cycleCount }}

      <button class="prompt-continue" ng-click="resumeWorkOrder()">
        Resume from {{ resumePrompt.goodParts }} parts
      </button>
      <button class="prompt-yes" ng-click="confirmRestart()">
        Restart from 0 (Warning: Progress will be lost!)
      </button>

Work Order Complete

{{ scrapPrompt.orderId }}

Produced {{ scrapPrompt.produced }} of {{ scrapPrompt.target }} pieces

Were there any scrap parts?

{{ scrapPrompt.scrapCount || 0 }}

{{ scrapPrompt.error }}

        <button class="numpad-btn" ng-click="addDigit(7)">7</button>
        <button class="numpad-btn" ng-click="addDigit(8)">8</button>
        <button class="numpad-btn" ng-click="addDigit(9)">9</button>

        <button class="numpad-btn" ng-click="addDigit(4)">4</button>
        <button class="numpad-btn" ng-click="addDigit(5)">5</button>
        <button class="numpad-btn" ng-click="addDigit(6)">6</button>

        <button class="numpad-btn" ng-click="addDigit(1)">1</button>
        <button class="numpad-btn" ng-click="addDigit(2)">2</button>
        <button class="numpad-btn" ng-click="addDigit(3)">3</button>

        <button class="numpad-btn numpad-clear" ng-click="clearScrap()">C</button>
        <button class="numpad-btn" ng-click="addDigit(0)">0</button>
        <button class="numpad-btn numpad-back" ng-click="backspace()">⌫</button>

        <button class="scrap-submit-btn" ng-click="submitScrapValidated(scrapPrompt.scrapCount)">
          Submit Scrap
        </button>

        <input type="checkbox" id="remind-checkbox" ng-model="scrapPrompt.remindAgain" style="width: 1.2rem; height: 1.2rem; cursor: pointer;">

          Remind me again if we keep overproducing

      <button class="prompt-continue" ng-click="skipScrap()">
        No, Continue Production
      </button>
      <button class="prompt-yes" ng-click="openScrapEntry()">
        Yes, Enter Scrap
      </button>

Alerts Template

      <button class="sb-btn" data-label="Home" ng-click="gotoTab('Home')">🏠</button>
      <button class="sb-btn" data-label="Work Orders" ng-click="gotoTab('Work Orders')">📋</button>
      <button class="sb-btn active" data-label="Alerts" ng-click="gotoTab('Alerts')">⚠</button>
      <button class="sb-btn" data-label="Graphs" ng-click="gotoTab('Graphs')">📊</button>
      <button class="sb-btn" data-label="Help" ng-click="gotoTab('Help')">❓</button>
      <button class="sb-btn" data-label="Settings" ng-click="gotoTab('Settings')">⚙</button>

OEE V1.0

Incidents

        <button class="quick-alert-btn" data-alert="Material Out">Material Out</button>
        <button class="quick-alert-btn" data-alert="Machine Stopped">Machine Stopped</button>
        <button class="quick-alert-btn" data-alert="Emergency Stop">Emergency Stop</button>

          Alert type
          <select id="alert-type">
            <option>Quality Defect Detected</option>
            <option>Tooling/Mold Issue</option>
            <option>Temperature Out of Range</option>
            <option>Pressure Issue</option>
            <option>Cycle Time Deviation</option>
            <option>Safety Interlock Triggered</option>
            <option>Hydraulic/Pneumatic Failure</option>
            <option>Electrical Fault</option>
            <option>Sensor Malfunction</option>
            <option>Other</option>
          </select>

          Description
          <textarea id="alert-description" rows="5" placeholder="Add context or instructions (optional)"></textarea>

        <button id="alert-send" class="alert-send" type="button">Send Alert</button>

Graphs Template

    <button class="sb-btn" ng-click="gotoTab('Home')">🏠</button>
    <button class="sb-btn" ng-click="gotoTab('Work Orders')">📋</button>
    <button class="sb-btn" ng-click="gotoTab('Alerts')">⚠</button>
    <button class="sb-btn active" ng-click="gotoTab('Graphs')">📊</button>
    <button class="sb-btn" ng-click="gotoTab('Help')">❓</button>
    <button class="sb-btn" ng-click="gotoTab('Settings')">⚙</button>

OEE V1.0

Graphs

          <button class="filter-btn active" ng-click="selectRange('24h')">24 Hours</button>
          <button class="filter-btn" ng-click="selectRange('7d')">7 Days</button>
          <button class="filter-btn" ng-click="selectRange('30d')">30 Days</button>
          <button class="filter-btn" ng-click="selectRange('90d')">90 Days</button>

OEE

<canvas id="chart-oee"></canvas>

Availability

<canvas id="chart-availability"></canvas>

Performance

<canvas id="chart-performance"></canvas>

Quality

<canvas id="chart-quality"></canvas>

Help Template

      <button class="sb-btn" data-label="Home" ng-click="gotoTab('Home')">🏠</button>
      <button class="sb-btn" data-label="Work Orders" ng-click="gotoTab('Work Orders')">📋</button>
      <button class="sb-btn" data-label="Alerts" ng-click="gotoTab('Alerts')">⚠</button>
      <button class="sb-btn" data-label="Graphs" ng-click="gotoTab('Graphs')">📊</button>
      <button class="sb-btn active" data-label="Help" ng-click="gotoTab('Help')">❓</button>
      <button class="sb-btn" data-label="Settings" ng-click="gotoTab('Settings')">⚙</button>

OEE V1.0

Help

About this Dashboard

This interface tracks Overall Equipment Effectiveness metrics, production progress, and incident logging for injection molding operations. Navigate between tabs using the sidebar to access work orders, real-time monitoring, performance graphs, incident reporting, and machine configuration.

Getting Started with a Work Order

Go to the Work Orders tab and upload an Excel file with your work orders, or select from existing orders in the table. Click Load to activate a work order, then navigate to the Home tab where you'll see the current work order details. Before starting production, configure your mold settings in the Settings tab by selecting a mold preset or entering cavity counts manually.

Running Production

On the Home tab, press the START button to begin production. The system tracks cycle counts, good parts, scrap, and progress toward your target quantity. Press STOP to pause production. Monitor real-time KPIs including OEE, Availability, Performance, and Quality metrics displayed on the dashboard.

Logging Incidents

Use the Alerts tab to record production incidents. Quick-log common issues with preset buttons like Material Out, Machine Stopped, or Emergency Stop. For detailed logging, select an alert type from the dropdown, add notes, and submit. All incidents are timestamped and contribute to availability and OEE calculations.

Configuring Molds

In the Settings tab, use the Mold Presets section to search for your mold by manufacturer and name. Select a preset to automatically load cavity counts, or manually adjust the Mold Configuration fields below. If your mold isn't available, use the Add Mold button in the Integrations section to create a new preset with manufacturer, name, and cavity details.

Viewing Performance Data

The Graphs tab displays historical OEE trends broken down by Availability, Performance, and Quality. Use these charts to identify patterns, track improvements, and diagnose recurring issues affecting your production efficiency.

Settings Template

      <button class="sb-btn" data-label="Home" ng-click="gotoTab('Home')">🏠</button>
      <button class="sb-btn" data-label="Work Orders" ng-click="gotoTab('Work Orders')">📋</button>
      <button class="sb-btn" data-label="Alerts" ng-click="gotoTab('Alerts')">⚠</button>
      <button class="sb-btn" data-label="Graphs" ng-click="gotoTab('Graphs')">📊</button>
      <button class="sb-btn" data-label="Help" ng-click="gotoTab('Help')">❓</button>
      <button class="sb-btn active" data-label="Settings" ng-click="gotoTab('Settings')">⚙</button>

OEE V1.0

Settings

Mold Presets

            Manufacturer
            <select id="preset-manufacturer"
                    ng-model="selectedManufacturer"
                    ng-change="onManufacturerChange()">
              <option value="">Select manufacturer...</option>
              <option ng-repeat="mfg in manufacturers" value="{{mfg}}">{{mfg}}</option>
            </select>

            Mold
            <select id="preset-mold"
                    ng-model="selectedMold"
                    ng-options="preset as preset.mold_name for preset in moldsForManufacturer | orderBy:'mold_name'"
                    ng-change="onMoldChange()">
              <option value="">Select mold...</option>
            </select>

Select a manufacturer and mold from the dropdowns above.

                Manufacturer
                <input id="new-mold-manufacturer"
                       type="text"
                       ng-model="newMold.manufacturer"
                       placeholder="Type manufacturer..." />

                Mold Name
                <input id="new-mold-name"
                       type="text"
                       ng-model="newMold.mold_name"
                       placeholder="Type mold name..." />

                Mold Cavities
                <input id="new-mold-cavities"
                        type="text"
                        ng-model="newMold.cavities"
                        placeholder="Type Theoretical Cavities..." />

                Active Cavities
                <input id="new-mold-active"
                        type="text"
                        ng-model="newMold.active"
                        placeholder="Type Active Cavities..." />

                <button class="preset-select-btn"
                        type="button"
                        ng-click="saveNewMold()"
                        ng-disabled="isSavingMold">
                  {{ isSavingMold ? 'Saving...' : 'Save' }}
                </button>
                <button class="preset-select-btn"
                        type="button"
                        ng-click="cancelAddMold()"
                        ng-disabled="isSavingMold">
                  Cancel
                </button>

Mold Configuration

            Mold Cavities
              <input id="mold-total" type="number" ng-model="moldTotal" ng-change="updateMoldSettings()" />

            Active Cavities
              <input id="mold-active" type="number" ng-model="moldActive" ng-change="updateMoldSettings()" />

Integrations

            <button class="integrations-button" type="button" disabled>Connect to ERP</button>

Can't find the mold you're looking for?

            <button class="preset-select-btn" type="button" ng-click="openAddMold()">Add Mold</button>

WO Template

      <button class="sb-btn" data-label="Home" ng-click="gotoTab('Home')">🏠</button>
      <button class="sb-btn active" data-label="Work Orders" ng-click="gotoTab('Work Orders')">📋</button>
      <button class="sb-btn" data-label="Alerts" ng-click="gotoTab('Alerts')">⚠</button>
      <button class="sb-btn" data-label="Graphs" ng-click="gotoTab('Graphs')">📊</button>
      <button class="sb-btn" data-label="Help" ng-click="gotoTab('Help')">❓</button>
      <button class="sb-btn" data-label="Settings" ng-click="gotoTab('Settings')">⚙</button>

OEE V1.0

Work Orders

            0 selected

          <button class="action-btn" data-action="upload-excel">Upload Excel</button>
          <button class="action-btn" data-action="start-work-order" id="load-btn">Load</button>
          <button class="action-btn" data-action="complete-work-order" id="done-btn">Done</button>
          <button class="action-btn" data-action="refresh-work-orders">Refresh</button>
          <button class="action-btn exit-multi-select" id="exit-multi-select-btn" style="display:none;">Exit Multi-Select</button>
          <input type="file" id="wo-file" accept=".xlsx" style="display:none" />

                ID
                SKU
                TARGET
                GOOD
                SCRAP
                PROGRESS
                STATUS
                LAST UPDATE

          0 items
          Tip: Long-press or right-click to multi-select

Tab navigation

if (msg.ui_control && msg.ui_control.tab) {
    msg.payload = { tab: msg.ui_control.tab };
    delete msg.ui_control;
    return msg;
}
return null;

General Style

<style>
    ::selection {
    background-color: blue; /* Or a specific blue hex code like #0000FF */
    color: white; /* Text color when selected */
    }
    /* Removes shadows and background tints from UI groups/cards */
    .nr-dashboard-theme ui-card-panel,
    body.nr-dashboard-theme md-content md-card {
        box-shadow: none !important;
        background-color: transparent !important;
        /* or set your desired background color */
        border-radius: 0px !important;
        /* Removes rounded corners if present */
        text-shadow: none !important;
    }

    /* Removes tints/shadows from specific widgets like buttons */
    .nr-dashboard-theme .nr-dashboard-button .md-button {
        box-shadow: none !important;
        background-color: transparent !important;
    }

    /* Adjust the main background of the page if needed */
    .masonry-container {
        background: none !important;
        /* Removes any linear gradients or colors */
    }

    /* ... your existing CSS rules ... */

    /* Change the background color when text is highlighted by the user */
    ::selection {
    background-color: transparent !important; /* Makes the background clear */
    color: inherit !important; /* Keeps the original text color */
    }

    /* For older browsers (Firefox specific) */
    ::-moz-selection {
    background-color: transparent !important;
    color: inherit !important;
    }

    /* ... your previous CSS rules ... */

    /* ... your previous CSS rules ... */

    /* Override the background color for the 'How to Start / Stop Production' title */
    .help-title {
    background-color: transparent !important;
    }
    .card help-card {
    background-color: transparent !important;
    }
    .panel-title {
    background-color: transparent !important;
    }

/* Override Node-RED Dashboard's gray backgrounds on text elements */
.nr-dashboard-template p,
.nr-dashboard-template h1,
.nr-dashboard-template h2,
.nr-dashboard-template h3,
.nr-dashboard-template h4,
.nr-dashboard-template label {
  background-color: transparent !important;
}
</style>

Anomaly Alert System (Global)

<button id="anomaly-toggle-btn" ng-click="toggleAnomalyPanel()">
   0">{{activeAnomalyCount}}
  ⚠
  ALERTS
</button>

Active Alerts

    <button class="anomaly-close-btn" ng-click="toggleAnomalyPanel()">×</button>

✅

No active alerts

All systems operating normally

{{anomaly.title}}

        {{anomaly.severity}}

{{anomaly.description}}

{{formatTimestamp(anomaly.timestamp)}}

        <button class="anomaly-ack-btn" ng-click="acknowledgeAnomaly(anomaly)">Acknowledge</button>

{{popup.title}}

      <button class="anomaly-popup-close" ng-click="closePopup(popup)">×</button>

{{popup.description}}

    <button class="anomaly-popup-ack" ng-click="acknowledgePopup(popup)">Acknowledge Alert</button>

Handle Anomaly Acknowledgment

// Handle acknowledgment from UI
if (msg.topic === 'acknowledge-anomaly') {
    const ackData = msg.payload || {};
    const eventId = ackData.event_id;
    const ackTimestamp = ackData.timestamp || Date.now();

    if (!eventId) {
        return null;
    }

    // Update database
    const updateQuery = `UPDATE anomaly_events
        SET status = 'acknowledged', acknowledged_at = ?
        WHERE event_id = ?`;

    msg.topic = updateQuery;
    msg.payload = [ackTimestamp, eventId];

    node.warn(`[ANOMALY ACK] Event ${eventId} acknowledged`);

    return msg;
}

return null;

// Get current global value (default to 0 if not set)
let estado = global.get('Estado_maquina') || 0;
let stop = flow.get('stop') || false;

if (stop) {
    // Manual stop active → force 0, don't reschedule
    global.set('Estado_maquina', 0);
    msg.payload = 0;
    node.send(msg);
    return;
}

// Toggle between 1 and 0
estado = estado === 1 ? 0 : 1;

// Update the global variable
global.set('Estado_maquina', estado);

// Send it out
msg.payload = estado;
return msg;

Cavities Settings

if (msg.topic === "moldSettings" && msg.payload) {
    const total = Number(msg.payload.total || 0);
    const active = Number(msg.payload.active || 0);

    // Store globally
    global.set("moldTotal", total);
    global.set("moldActive", active);

    node.status({ fill: "green", shape: "dot", text: `Saved: ${active}/${total}` });

    msg.payload = { saved: true, total, active };
    return msg;
}

// Handle preset selection
if (msg.topic === "selectMoldPreset" && msg.payload) {
    const preset = msg.payload;
    const total = Number(preset.theoretical_cavities || 0);
    const active = Number(preset.functional_cavities || 0);

    // Store globally
    global.set("moldTotal", total);
    global.set("moldActive", active);

    node.status({ fill: "blue", shape: "dot", text: `Preset: ${preset.mold_name}` });

    // Send to UI to update fields
    msg.topic = "moldPresetSelected";
    msg.payload = { total, active, presetName: preset.mold_name };
    return msg;
}

node.status({ fill: "red", shape: "ring", text: "Invalid payload" });
return null;

Mold Presets Handler

const topic = msg.topic || '';
const payload = msg.payload || {};

// ===== IGNORE NON-MOLD TOPICS SILENTLY =====
// These are KPI/dashboard messages not meant for this handler
const ignoredTopics = [
    'machineStatus',
    'kpis',
    'chartsData',
    'activeWorkOrder',
    'workOrderCycle',
    'workOrdersList',
    'scrapPrompt',
    'uploadStatus'
];

if (ignoredTopics.includes(topic) || topic === '') {
    return null;  // Silent ignore
}

// Log only mold-related requests
node.warn(`Received: ${topic}`);

// CRITICAL: Use a processing lock to prevent simultaneous requests
let dedupeKey = topic;
if (topic === 'addMoldPreset') {
    dedupeKey = `add_${payload.manufacturer}_${payload.mold_name}`;
} else if (topic === 'getMoldsByManufacturer') {
    dedupeKey = `getmolds_${payload.manufacturer}`;
}

const lockKey = `lock_${dedupeKey}`;
const lastRequestKey = `last_request_${dedupeKey}`;

// Check if currently processing this request
if (flow.get(lockKey) === true) {
    node.warn(`${topic} already processing - duplicate blocked`);
    return null;
}

// Check timing
const now = Date.now();
const lastRequestTime = flow.get(lastRequestKey) || 0;
if (now - lastRequestTime < 2000) {
    node.warn(`Duplicate ${topic} request ignored (within 2s)`);
    return null;
}

// Set lock IMMEDIATELY before any async operations
flow.set(lockKey, true);
flow.set(lastRequestKey, now);

// Release lock after 3 seconds (safety timeout)
setTimeout(() => {
    flow.set(lockKey, false);
}, 3000);

// Load all presets (legacy)
if (topic === 'loadMoldPresets') {
    msg._originalTopic = 'loadMoldPresets';
    msg.topic = 'SELECT * FROM mold_presets ORDER BY manufacturer, mold_name;';
    node.warn('Querying all presets');
    return msg;
}

// Search/filter presets (legacy)
if (topic === 'searchMoldPresets') {
    const filters = msg.payload || {};
    const searchTerm = (filters.searchTerm || '').trim().replace(/['\"\\\\]/g, '');
    const manufacturer = (filters.manufacturer || '').replace(/['\"\\\\]/g, '');
    const theoreticalCavities = filters.theoreticalCavities || '';

    let query = 'SELECT * FROM mold_presets WHERE 1=1';

    if (searchTerm) {
        const searchPattern = `%${searchTerm}%`;
        query += ` AND (mold_name LIKE '${searchPattern.replace(/'/g, "''")}' OR manufacturer LIKE '${searchPattern.replace(/'/g, "''")}')`;
    }

    if (manufacturer && manufacturer !== 'All') {
        query += ` AND manufacturer = '${manufacturer.replace(/'/g, "''")}'`;
    }

    if (theoreticalCavities && theoreticalCavities !== '') {
        const cavities = Number(theoreticalCavities);
        if (!isNaN(cavities)) {
            query += ` AND theoretical_cavities = ${cavities}`;
        }
    }

    query += ' ORDER BY manufacturer, mold_name;';

    msg._originalTopic = 'searchMoldPresets';
    msg.topic = query;
    return msg;
}

// Get unique manufacturers for dropdown
if (topic === 'getManufacturers') {
    msg._originalTopic = 'getManufacturers';
    msg.topic = 'SELECT DISTINCT manufacturer FROM mold_presets ORDER BY manufacturer;';
    node.warn('Querying manufacturers');
    return msg;
}

// Get molds for a specific manufacturer
if (topic === 'getMoldsByManufacturer') {
    const data = msg.payload || {};
    const manufacturerRaw = (data.manufacturer || '').trim();
    if (!manufacturerRaw) {
        node.warn('No manufacturer provided');
        return null;
    }

    const manufacturerSafe = manufacturerRaw.replace(/['\"\\\\]/g, '').replace(/'/g, "''");

    msg._originalTopic = 'getMoldsByManufacturer';
    msg.topic = `SELECT * FROM mold_presets WHERE manufacturer = '${manufacturerSafe}' ORDER BY mold_name;`;
    node.warn(`Querying molds for: ${manufacturerSafe}`);
    return msg;
}

// Add a new mold preset - CRITICAL: Strong deduplication
if (topic === 'addMoldPreset') {
    const data = msg.payload || {};
    const manufacturerRaw = (data.manufacturer || '').trim();
    const moldNameRaw = (data.mold_name || '').trim();
    const theoreticalRaw = (data.theoretical || '').trim();
    const activeRaw = (data.active || '').trim();

    if (!manufacturerRaw || !moldNameRaw || !theoreticalRaw || !activeRaw) {
        node.status({ fill: 'red', shape: 'ring', text: 'Missing value' });
        node.warn('Missing required fields');
        return null;
    }

    // Additional safety check for already-processed flag
    if (msg._addMoldProcessed) {
        node.warn('addMoldPreset already processed flag detected, ignoring');
        return null;
    }
    msg._addMoldProcessed = true;

    const manufacturerSafe = manufacturerRaw.replace(/['\"\\\\]/g, '').replace(/'/g, "''");
    const moldNameSafe = moldNameRaw.replace(/['\"\\\\]/g, '').replace(/'/g, "''");
    const theoreticalSafe = theoreticalRaw.replace(/['\"\\\\]/g, '').replace(/'/g, "''");
    const activeSafe = activeRaw.replace(/['\"\\\\]/g, '').replace(/'/g, "''");

    msg._originalTopic = 'addMoldPreset';
    msg.topic =
        "INSERT INTO mold_presets (manufacturer, mold_name, theoretical_cavities, functional_cavities) " +
        "VALUES ('" + manufacturerSafe + "', '" + moldNameSafe + "', " + theoreticalSafe + ", " + activeSafe + ");";

    node.status({ fill: 'blue', shape: 'dot', text: 'Inserting mold...' });
    node.warn(`Inserting: ${manufacturerSafe} - ${moldNameSafe}`);
    return msg;
}

node.warn(`Unknown topic: ${topic}`);
return null;

Process DB Results

// Replace function in "Process DB Results" node

const originalTopic = msg._originalTopic || '';
const dbResults = Array.isArray(msg.payload) ? msg.payload : [];

if (!originalTopic) {
    return null;
}

// IMPORTANT: Clear socketid to prevent loops back to sender
delete msg._socketid;
delete msg.socketid;

// Manufacturers query → list for first dropdown
if (originalTopic === 'getManufacturers') {
    const manufacturers = dbResults
        .map(row => row.manufacturer)
        .filter((mfg, index, arr) => mfg && arr.indexOf(mfg) === index)
        .sort();

    msg.topic = 'manufacturersList';
    msg.payload = manufacturers;

    node.status({ fill: 'green', shape: 'dot', text: `${manufacturers.length} manufacturers` });
    return msg;
}

// Preset lists (legacy load/search)
if (originalTopic === 'loadMoldPresets' || originalTopic === 'searchMoldPresets') {
    const presets = dbResults.map(row => ({
        mold_name: row.mold_name || '',
        manufacturer: row.manufacturer || '',
        theoretical_cavities: Number(row.theoretical_cavities) || 0,
        functional_cavities: Number(row.functional_cavities) || 0
    }));

    msg.topic = 'moldPresetsList';
    msg.payload = presets;

    node.status({ fill: 'green', shape: 'dot', text: `${presets.length} presets found` });
    return msg;
}

// Molds for selected manufacturer
if (originalTopic === 'getMoldsByManufacturer') {
    const presets = dbResults.map(row => ({
        mold_name: row.mold_name || '',
        manufacturer: row.manufacturer || '',
        theoretical_cavities: Number(row.theoretical_cavities) || 0,
        functional_cavities: Number(row.functional_cavities) || 0
    }));

    msg.topic = 'moldPresetsList';
    msg.payload = presets;

    node.status({ fill: 'blue', shape: 'dot', text: `${presets.length} molds for manufacturer` });
    return msg;
}

// Result of inserting a new mold
if (originalTopic === 'addMoldPreset') {
    msg.topic = 'addMoldResult';
    msg.payload = {
        success: true,
        result: msg.payload
    };

    node.status({ fill: 'green', shape: 'dot', text: 'Mold added' });
    return msg;
}

node.status({ fill: 'yellow', shape: 'ring', text: 'Unknown topic: ' + originalTopic });
return null;

Work Order buttons

switch (msg.action) {
    case "upload-excel":
        msg._mode = "upload";
        return [msg, null, null, null];
    case "refresh-work-orders":
        msg._mode = "select";
        msg.topic = "SELECT * FROM work_orders ORDER BY created_at DESC;";
        return [null, msg, null, null];
    case "start-work-order": {
        msg._mode = "start-check-progress";
        const order = msg.payload || {};
        if (!order.id) {
            node.error("No work order id supplied for start", msg);
            return [null, null, null, null];
        }

        // Store order data temporarily for after DB query
        flow.set("pendingWorkOrder", order);

        // Query database to check for existing progress
        msg.topic = "SELECT cycle_count, good_parts, scrap_parts, progress_percent, target_qty FROM work_orders WHERE work_order_id = ? LIMIT 1";
        msg.payload = [order.id];

        node.warn(`[START-WO] Checking progress for WO ${order.id}`);
        return [null, msg, null, null];
    }
    case "resume-work-order": {
        msg._mode = "resume";
        const order = msg.payload || {};
        if (!order.id) {
            node.error("No work order id supplied for resume", msg);
            return [null, null, null, null];
        }

        node.warn(`[RESUME-WO] Resuming WO ${order.id} with existing progress`);

        // Set status to RUNNING without resetting progress
        msg.topic = "UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, updated_at = CASE WHEN work_order_id = ? THEN NOW() ELSE updated_at END WHERE status <> 'DONE'";
        msg.payload = [order.id, order.id];
        msg.startOrder = order;

        // Load existing values into global state
        // IMPORTANT: Also set scrap so good_parts calculation is correct
        order.scrap = Number(order.scrap) || 0;
        order.good = Number(order.good_parts) || 0;

        global.set("activeWorkOrder", order);
        global.set("cycleCount", Number(order.cycle_count) || 0);
        flow.set("lastMachineState", 0);
        global.set("scrapPromptIssuedFor", null);

        node.warn(`[RESUME-WO] Set cycleCount=${order.cycle_count}, scrap=${order.scrap}, good=${order.good}`);
        return [null, null, msg, null];
    }
    case "restart-work-order": {
        msg._mode = "restart";
        const order = msg.payload || {};
        if (!order.id) {
            node.error("No work order id supplied for restart", msg);
            return [null, null, null, null];
        }

        node.warn(`[RESTART-WO] Restarting WO ${order.id} - resetting progress to 0`);

        // Reset progress in database AND set status to RUNNING
        msg.topic = "UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, cycle_count = 0, good_parts = 0, scrap_parts = 0, progress_percent = 0, updated_at = NOW() WHERE work_order_id = ? OR status = 'RUNNING'";
        msg.payload = [order.id, order.id];
        msg.startOrder = order;

        // Initialize global state to 0
        order.scrap = 0;
        order.good = 0;

        global.set("activeWorkOrder", order);
        global.set("cycleCount", 0);
        flow.set("lastMachineState", 0);
        global.set("scrapPromptIssuedFor", null);

        node.warn(`[RESTART-WO] Reset cycleCount=0, scrap=0, good=0`);
        return [null, null, msg, null];
    }
    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);

        // ============================================================
        // HIGH SCRAP DETECTION
        // ============================================================
        const targetQty = Number(activeOrder.target) || 0;
        const scrapCount = finalScrapParts;
        const scrapPercent = targetQty > 0 ? (scrapCount / targetQty) * 100 : 0;

        // Trigger: Scrap > 10% of target quantity
        let anomalyMsg = null;
        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: finalGoodParts,
                    total_cycles: finalCycleCount
                },
                kpi_snapshot: {
                    oee: (msg.kpis && msg.kpis.oee) || global.get("currentKPIs")?.oee || 0,
                    availability: (msg.kpis && msg.kpis.availability) || global.get("currentKPIs")?.availability || 0,
                    performance: (msg.kpis && msg.kpis.performance) || global.get("currentKPIs")?.performance || 0,
                    quality: (msg.kpis && msg.kpis.quality) || global.get("currentKPIs")?.quality || 0
                },
                work_order_id: order.id,
                cycle_count: finalCycleCount,
                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)
            anomalyMsg = {
                topic: "anomaly-detected",
                payload: [highScrapAnomaly]
            };
        }

        node.warn('[COMPLETE] Cleared all state flags');
        return [null, null, null, msg, anomalyMsg];
    }
    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
        };

        return [null, msg, null, null];
    }
    case "restore-session": {
        // Query DB for any RUNNING work order on startup
        msg._mode = "restore-query";
        msg.topic = "SELECT * FROM work_orders WHERE status = 'RUNNING' LIMIT 1";
        msg.payload = [];
        node.warn('[RESTORE] Checking for running work order on startup');
        return [null, msg, null, null];
    }
    case "scrap-entry": {
        const { id, scrap } = msg.payload || {};
        const scrapNum = Number(scrap) || 0;

        if (!id) {
            node.error("No work order id supplied for scrap entry", msg);
            return [null, null, null, null];
        }

        const activeOrder = global.get("activeWorkOrder");
        if (activeOrder && activeOrder.id === id) {
            activeOrder.scrap = (Number(activeOrder.scrap) || 0) + scrapNum;
            global.set("activeWorkOrder", activeOrder);
        }

        global.set("scrapPromptIssuedFor", null);

        msg._mode = "scrap-update";
        msg.scrapEntry = { id, scrap: scrapNum };
        // SQL with bound parameters for safety
        msg.topic = "UPDATE work_orders SET scrap_parts = scrap_parts + ?, updated_at = NOW() WHERE work_order_id = ?";
        msg.payload = [scrapNum, id];

        return [null, null, msg, null];
    }
    case "scrap-skip": {
        const { id, remindAgain } = msg.payload || {};

        if (!id) {
            node.error("No work order id supplied for scrap skip", msg);
            return [null, null, null, null];
        }

        if (remindAgain) {
            global.set("scrapPromptIssuedFor", null);
        }

        msg._mode = "scrap-skipped";
        return [null, null, null, null];
    }
    case "start": {
        // START with KPI timestamp init - FIXED
        const now = Date.now();

        global.set("trackingEnabled", true);
        global.set("productionStarted", true);
        global.set("kpiStartupMode", true);
        global.set("kpiBuffer", []);
        global.set("lastKPIRecordTime", now - 60000);
        global.set("productionStartTime", now);
        global.set("lastMachineCycleTime", now);
        global.set("lastCycleTime", now);
        global.set("operatingTime", 0);

        node.warn('[START] Initialized: trackingEnabled=true, productionStarted=true, kpiStartupMode=true, operatingTime=0');

        const activeOrder = global.get("activeWorkOrder") || {};
        msg._mode = "production-state";

        msg.payload = msg.payload || {};

        msg.trackingEnabled = true;
        msg.productionStarted = true;
        msg.machineOnline = true;

        msg.payload.trackingEnabled = true;
        msg.payload.productionStarted = true;
        msg.payload.machineOnline = true;

        return [null, msg, null, null];
    }
    case "stop": {
        global.set("trackingEnabled", false);
        global.set("productionStarted", false);
        node.warn('[STOP] Set trackingEnabled=false, productionStarted=false');

        // Send UI update so button state reflects change
        msg._mode = "production-state";
        msg.payload = msg.payload || {};
        msg.trackingEnabled = false;
        msg.productionStarted = false;
        msg.machineOnline = true;
        msg.payload.trackingEnabled = false;
        msg.payload.productionStarted = false;
        msg.payload.machineOnline = true;

        return [null, msg, null, null];
    }
    case "start-tracking": {
        const activeOrder = global.get('activeOrder') || {};

        if (!activeOrder.id) {
            node.warn('[START] Cannot start tracking: No active order loaded.');
            return [null, { topic: "alert", payload: "Error: No active work order loaded." }, null, null];
        }

        const now = Date.now();
        global.set("trackingEnabled", true);
        global.set("kpiBuffer", []);
        global.set("lastKPIRecordTime", now - 60000);
        global.set("lastMachineCycleTime", now);
        global.set("lastCycleTime", now);
        global.set("operatingTime", 0.001);
        node.warn('[START] Cleared kpiBuffer for fresh production run');

        // FIX: Use work_order_id consistently
        const dbMsg = {
            topic: `UPDATE work_orders SET production_start_time = ${now}, is_tracking = 1 WHERE work_order_id = '${activeOrder.id}'`,
            payload: []
        };

        const stateMsg = {
            topic: "machineStatus",
            payload: msg.payload || {}
        };

        stateMsg.payload.trackingEnabled = true;
        stateMsg.payload.productionStarted = true;
        stateMsg.payload.machineOnline = true;

        return [dbMsg, stateMsg, null, null];
    }
}

Build Insert SQL

const rows = Array.isArray(msg.payload) ? msg.payload : [];
const vals = rows.map(r => `(
'${r["Work Order ID"]}',
'${r["SKU"]}',
${Number(r["Target Quantity"]) || 0},
${Number(r["Theoretical Cycle Time (Seconds)"]) || 0},
'PENDING')`).join(',\n');

msg.topic = `
INSERT INTO work_orders (work_order_id, sku, target_qty, cycle_time, status)
VALUES
${vals}
ON DUPLICATE KEY UPDATE
  sku=VALUES(sku),
  target_qty=VALUES(target_qty),
  cycle_time=VALUES(cycle_time);
`;
return msg;

Back to UI

const mode = msg._mode || '';
const started = msg.startOrder || null;
const completed = msg.completeOrder || null;

delete msg._mode;
delete msg.startOrder;
delete msg.completeOrder;
delete msg.action;
delete msg.filename;

// ========================================================
// MODE: UPLOAD
// ========================================================
if (mode === "upload") {
    msg.topic = "uploadStatus";
    msg.payload = { message: "✅ Work orders uploaded successfully." };
    return [msg, null, null, null];
}

// ========================================================
// MODE: SELECT (Load Work Orders)
// ========================================================
if (mode === "select") {
    const rawRows = Array.isArray(msg.payload) ? msg.payload : [];
    msg.topic = "workOrdersList";
    msg.payload = rawRows.map(row => ({
        id: row.work_order_id ?? row.id ?? "",
        sku: row.sku ?? "",
        target: Number(row.target_qty ?? row.target ?? 0),
        good: Number(row.good_parts ?? row.good ?? 0),
        scrap: Number(row.scrap_count ?? row.scrap ?? 0),
        progressPercent: Number(row.progress_percent ?? row.progress ?? 0),
        status: (row.status ?? "PENDING").toUpperCase(),
        lastUpdateIso: row.updated_at ?? row.last_update ?? null,
        cycleTime: Number(row.cycle_time ?? row.theoretical_cycle_time ?? 0)
    }));
    return [msg, null, null, null];
}

// ========================================================
// MODE: START WORK ORDER
// ========================================================
if (mode === "start") {
    const order = started || {};
    const kpis = msg.kpis || global.get("currentKPIs") || {
        oee: 0, availability: 0, performance: 0, quality: 0
    };

    const homeMsg = {
        topic: "activeWorkOrder",
        payload: {
            id: order.id || "",
            sku: order.sku || "",
            target: Number(order.target) || 0,
            good: Number(order.good) || 0,
            scrap: Number(order.scrap) || 0,
            cycleTime: Number(order.cycleTime || order.theoreticalCycleTime || 0),
            progressPercent: Number(order.progressPercent) || 0,
            lastUpdateIso: order.lastUpdateIso || null,
            kpis: kpis
        }
    };

    return [null, homeMsg, null, null];
}

// ========================================================
// MODE: COMPLETE WORK ORDER
// ========================================================
if (mode === "complete") {
    const homeMsg = { topic: "activeWorkOrder", payload: null };
    return [null, homeMsg, null, null];
}

// ========================================================
// MODE: CYCLE UPDATE DURING PRODUCTION
// ========================================================
if (mode === "cycle") {
    const cycle = msg.cycle || {};

    const workOrderMsg = {
        topic: "workOrderCycle",
        payload: {
            id: cycle.id || "",
            sku: cycle.sku || "",
            target: Number(cycle.target) || 0,
            good: Number(cycle.good) || 0,
            scrap: Number(cycle.scrap) || 0,
            progressPercent: Number(cycle.progressPercent) || 0,
            lastUpdateIso: cycle.lastUpdateIso || new Date().toISOString(),
            status: cycle.progressPercent >= 100 ? "DONE" : "RUNNING"
        }
    };

    const kpis = msg.kpis || global.get("currentKPIs") || {
        oee: 0, availability: 0, performance: 0, quality: 0
    };

    const homeMsg = {
        topic: "activeWorkOrder",
        payload: {
            id: cycle.id || "",
            sku: cycle.sku || "",
            target: Number(cycle.target) || 0,
            good: Number(cycle.good) || 0,
            scrap: Number(cycle.scrap) || 0,
            cycleTime: Number(cycle.cycleTime) || 0,
            progressPercent: Number(cycle.progressPercent) || 0,
            lastUpdateIso: cycle.lastUpdateIso || new Date().toISOString(),
            kpis: kpis
        }
    };

    return [workOrderMsg, homeMsg, null, null];
}

// ========================================================
// MODE: MACHINE PRODUCTION STATE
// ========================================================
if (mode === "production-state") {
    const homeMsg = {
        topic: "machineStatus",
        payload: {
            machineOnline: msg.machineOnline ?? true,
            productionStarted: !!msg.productionStarted,
            trackingEnabled: msg.payload?.trackingEnabled ?? msg.trackingEnabled ?? false
        }
    };
    return [null, homeMsg, null, null];
}

// ========================================================
// MODE: CURRENT STATE (for tab switch sync)
// ========================================================
if (mode === "current-state") {
    const state = msg.payload || {};
    const homeMsg = {
        topic: "currentState",
        payload: {
            activeWorkOrder: state.activeWorkOrder,
            trackingEnabled: state.trackingEnabled,
            productionStarted: state.productionStarted,
            kpis: state.kpis
        }
    };
    return [null, homeMsg, null, null];
}

// ========================================================
// MODE: RESTORE QUERY (startup state recovery)
// ========================================================
if (mode === "restore-query") {
    const rows = Array.isArray(msg.payload) ? msg.payload : [];

    if (rows.length > 0) {
        const row = rows[0];
        const restoredOrder = {
            id: row.work_order_id || row.id || "",
            sku: row.sku || "",
            target: Number(row.target_qty || row.target || 0),
            good: Number(row.good_parts || row.good || 0),
            scrap: Number(row.scrap_parts || row.scrap || 0),
            progressPercent: Number(row.progress_percent || 0),
            cycleTime: Number(row.cycle_time || 0),
            lastUpdateIso: row.updated_at || null
        };

        // Restore global state
        global.set("activeWorkOrder", restoredOrder);
        global.set("cycleCount", Number(row.cycle_count) || 0);
        // Don't auto-start tracking - user must click START
        global.set("trackingEnabled", false);
        global.set("productionStarted", false);

        node.warn('[RESTORE] Restored work order: ' + restoredOrder.id + ' with ' + global.get("cycleCount") + ' cycles');

        const homeMsg = {
            topic: "activeWorkOrder",
            payload: restoredOrder
        };

        // Set status back to RUNNING in database (if not already DONE)
        // This prevents user from having to "Load" the work order again
        const dbMsg = {
            topic: "UPDATE work_orders SET status = 'RUNNING', updated_at = NOW() WHERE work_order_id = ? AND status != 'DONE'",
            payload: [restoredOrder.id]
        };

        return [dbMsg, homeMsg, null, null];
    } else {
        node.warn('[RESTORE] No running work order found');
    }
    return [null, null, null, null];
}

// ========================================================
// MODE: SCRAP PROMPT
// ========================================================
if (mode === "scrap-prompt") {
    const prompt = msg.scrapPrompt || {};

    const homeMsg = { topic: "scrapPrompt", payload: prompt };
    const tabMsg = { ui_control: { tab: "Home" } };

    // output1: nothing
    // output2: home template
    // output3: tab navigation
    // output4: graphs template (unused here)
    return [null, homeMsg, tabMsg, null];
}

// ========================================================
// MODE: SCRAP UPDATE
// ========================================================
if (mode === "scrap-update") {
    const activeOrder = global.get("activeWorkOrder") || {};
    const kpis = msg.kpis || global.get("currentKPIs") || {
        oee: 0, availability: 0, performance: 0, quality: 0
    };

    const homeMsg = {
        topic: "activeWorkOrder",
        payload: {
            id: activeOrder.id || "",
            sku: activeOrder.sku || "",
            target: Number(activeOrder.target) || 0,
            good: Number(activeOrder.good) || 0,
            scrap: Number(activeOrder.scrap) || 0,
            cycleTime: Number(activeOrder.cycleTime) || 0,
            progressPercent: Number(activeOrder.progressPercent) || 0,
            lastUpdateIso: activeOrder.lastUpdateIso || new Date().toISOString(),
            kpis: kpis
        }
    };

    return [null, homeMsg, null, null];
}

// ========================================================
// MODE: SCRAP COMPLETE
// ========================================================
if (mode === "scrap-complete") {
    const homeMsg = { topic: "activeWorkOrder", payload: null };
    return [null, homeMsg, null, null];
}

// ========================================================
// MODE: CHARTS → SEND REAL DATA TO GRAPH TEMPLATE
// ========================================================
//if (mode === "charts") {

//    const realOEE = msg.realOEE || global.get("realOEE") || [];
//    const realAvailability = msg.realAvailability || global.get("realAvailability") || [];
//    const realPerformance = msg.realPerformance || global.get("realPerformance") || [];
//    const realQuality = msg.realQuality || global.get("realQuality") || [];

//    const chartsMsg = {
//        topic: "chartsData",
//        payload: {
//            oee: realOEE,
//            availability: realAvailability,
//            performance: realPerformance,
//            quality: realQuality
//        }
//    };

    // Send ONLY to output #4
//    return [null, null, null, chartsMsg];
//}

// ========================================================
// MODE: RESUME-PROMPT
// ========================================================
if (mode === "resume-prompt") {
    // Forward the resume prompt to Home UI
    // Also set activeWorkOrder so Start button becomes enabled
    const order = msg.payload.order || null;

    if (order) {
        // Set activeWorkOrder in global so Start button is enabled
        global.set("activeWorkOrder", order);
        node.warn(`[RESUME-PROMPT] Set activeWorkOrder to ${order.id} - Start button should now be enabled`);
    }

    // Send prompt message to Home template
    const homeMsg = {
        topic: msg.topic || "resumePrompt",
        payload: msg.payload
    };

    return [null, homeMsg, null, null];
}

// ========================================================
// DEFAULT
// ========================================================
return [null, null, null, null];

const filename =
    msg.filename ||
    (msg.meta && msg.meta.filename) ||
    (msg.payload && msg.payload.filename) ||
    msg.name ||
    'upload.xlsx';

const candidates = [];
if (typeof msg.payload === 'string') candidates.push(msg.payload);
if (msg.payload && typeof msg.payload.payload === 'string') candidates.push(msg.payload.payload);
if (msg.payload && typeof msg.payload.file === 'string') candidates.push(msg.payload.file);
if (msg.payload && typeof msg.payload.base64 === 'string') candidates.push(msg.payload.base64);
if (typeof msg.file === 'string') candidates.push(msg.file);
if (typeof msg.data === 'string') candidates.push(msg.data);

function stripDataUrl(s) {
    return (s && s.startsWith('data:')) ? s.split(',')[1] : s;
}

let b64 = candidates.map(stripDataUrl).find(s => typeof s === 'string' && s.length > 0);
if (!b64 && Buffer.isBuffer(msg.payload)) { msg.filename = filename; return msg; }
if (!b64) { node.error('No base64 data found on msg', msg); return null; }

msg.payload = Buffer.from(b64, 'base64');
msg.filename = filename;
return msg;

Refresh Trigger

if (msg._mode === "start" || msg._mode === "complete" || msg._mode === "resume" || msg._mode === "restart") {
    // Preserve original message for Back to UI (output 2)
    const originalMsg = {...msg};
    // Create select message for refreshing WO table (output 1)
    msg._mode = "select";
    msg.topic = "SELECT * FROM work_orders ORDER BY updated_at DESC;";
    return [msg, originalMsg];
}
if (msg._mode === "cycle" || msg._mode === "production-state") {
    return [null, msg];
}
if (msg._mode === "scrap-prompt") {
    return [null, msg];
}
if (msg._mode === "restore-query") {
    // Pass restore query results to Back to UI
    return [null, msg];
}
if (msg._mode === "current-state") {
    // Pass current state to Back to UI
    return [null, msg];
}
if (msg._mode === "scrap-complete") {
    // Preserve original message for Back to UI (output 2)
    const originalMsg = {...msg};
    // Create select message for refreshing WO table (output 1)
    msg._mode = "select";
    msg.topic = "SELECT * FROM work_orders ORDER BY updated_at DESC;";
    return [msg, originalMsg];
}
return [null, msg];

Machine cycles

const current = Number(msg.payload) || 0;

let zeroStreak = flow.get("zeroStreak") || 0;
zeroStreak = current === 0 ? zeroStreak + 1 : 0;
flow.set("zeroStreak", zeroStreak);

const prev = flow.get("lastMachineState") ?? 0;
flow.set("lastMachineState", current);

global.set("machineOnline", true);

let productionRunning = !!global.get("productionStarted");
let stateChanged = false;

if (current === 1 && !productionRunning) {
    productionRunning = true;
    stateChanged = true;
} else if (current === 0 && zeroStreak >= 2 && productionRunning) {
    productionRunning = false;
    stateChanged = true;
}

global.set("productionStarted", productionRunning);

const stateMsg = stateChanged
    ? {
        _mode: "production-state",
        machineOnline: true,
        productionStarted: productionRunning
    }
    : null;

const activeOrder = global.get("activeWorkOrder");
// FIX: moldActive is object, extract cavities property
const cavities = Number(global.get("moldActive")) || 1;
if (!activeOrder || !activeOrder.id || cavities <= 0) {
    return [null, stateMsg, { _triggerKPI: true }, null];
}

var trackingEnabled = !!global.get("trackingEnabled");
if (!trackingEnabled) {
    return [null, stateMsg, { _triggerKPI: true }, null];
}

if (prev === 1 || current !== 1) {
    return [null, stateMsg, { _triggerKPI: true }, null];
}

let cycles = Number(global.get("cycleCount") || 0) + 1;
global.set("cycleCount", cycles);

// Clear startup mode after first real cycle
if (global.get("kpiStartupMode")) {
    global.set("kpiStartupMode", false);
    node.warn('[MACHINE CYCLE] First cycle - cleared kpiStartupMode');
}

const now = Date.now();
const lastCycleTime = global.get("lastCycleTime") || now;
const timeSinceLastCycle = now - lastCycleTime;

let operatingTime = global.get("operatingTime") || 0;
operatingTime += (timeSinceLastCycle / 1000);

global.set("operatingTime", operatingTime);
node.warn(`[MACHINE CYCLE] operatingTime updated to ${operatingTime}`);
global.set("lastCycleTime", now);

const scrapTotal = Number(activeOrder.scrap) || 0;
const totalProduced = cycles * cavities;
// FIX: Guard negative produced
const produced = Math.max(0, totalProduced - scrapTotal);
const target = Number(activeOrder.target) || 0;
const progress = target > 0 ? Math.min(100, Math.round((produced / target) * 100)) : 0;

activeOrder.good = produced;
activeOrder.progressPercent = progress;
activeOrder.lastUpdateIso = new Date().toISOString();
global.set("activeWorkOrder", activeOrder);

const promptIssued = global.get("scrapPromptIssuedFor") || null;
if (!promptIssued && target > 0 && produced >= target) {
    global.set("scrapPromptIssuedFor", activeOrder.id);
    msg._mode = "scrap-prompt";
    msg.scrapPrompt = {
        id: activeOrder.id,
        sku: activeOrder.sku || "",
        target,
        produced
    };
    return [null, msg, null, null];
}

const dbMsg = {
    _mode: "cycle",
    cycle: {
        id: activeOrder.id,
        sku: activeOrder.sku || "",
        target,
        good: produced,
        scrap: Number(activeOrder.scrap) || 0,
        cycleTime: Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0),
        progressPercent: progress,
        lastUpdateIso: activeOrder.lastUpdateIso,
        machineOnline: true,
        productionStarted: productionRunning
    },
    // SQL with bound parameters for safety
    topic: "UPDATE work_orders SET good_parts = ?, progress_percent = ?, updated_at = NOW() WHERE work_order_id = ?",
    payload: [produced, progress, activeOrder.id]
};

const kpiTrigger = { _triggerKPI: true };

if (trackingEnabled && dbMsg) {
    global.set("lastMachineCycleTime", Date.now());
}

// 4th output: Persist to work_orders table on EVERY cycle
// This ensures database is always up-to-date with latest production data
const persistWorkOrder = {
    topic: "UPDATE work_orders SET cycle_count = ?, good_parts = ?, scrap_parts = ?, progress_percent = ?, updated_at = NOW() WHERE work_order_id = ?",
    payload: [cycles, produced, scrapTotal, progress, activeOrder.id]
};
node.warn(`[WORK ORDER SYNC] Persisting to DB: cycles=${cycles}, good=${produced}, scrap=${scrapTotal}, progress=${progress}%`);

return [dbMsg, stateMsg, kpiTrigger, persistWorkOrder];

Calculate KPIs

// ========================================
// OEE KPI ENGINE – FIXED: No duplicates, proper startup mode
// ========================================

// Pull system state - SINGLE declarations only
const trackingEnabled = global.get("trackingEnabled") || false;
const activeOrder = global.get("activeWorkOrder") || {};
const cycleCount = global.get("cycleCount") || 0;
// FIX: moldActive is a number, not an object
const cavities = Number(global.get("moldActive")) || 1;
const kpiStartupMode = global.get("kpiStartupMode") || false;

// Guard against missing critical data
if (!trackingEnabled || !activeOrder.id) {
  return null;
}

const prev = global.get("currentKPIs") || {
    availability: 100,
    performance: 100,
    quality: 100,
    oee: 100
};

msg.kpis = {
    quality: prev.quality,
    performance: prev.performance,
    availability: prev.availability,
    oee: prev.oee
};

// ========================================
// STARTUP MODE: Show optimistic 100% until first cycle
// ========================================
if (kpiStartupMode) {
    msg.kpis = { oee: 100, availability: 100, performance: 100, quality: 100 };
    global.set("currentKPIs", msg.kpis);
    node.warn('[KPI] Startup mode - showing 100% until first cycle');
    return msg;
}

// ========================================
// 1) QUALITY
// ----------------------------------------
const good = Number(activeOrder.good) || 0;
const scrap = Number(activeOrder.scrap) || 0;
const total = good + scrap;

if (total > 0) {
    msg.kpis.quality = (good / total) * 100;
} else {
    msg.kpis.quality = prev.quality ?? 100;
}
msg.kpis.quality = Math.min(100, msg.kpis.quality);

// ========================================
// 2) PERFORMANCE
// ----------------------------------------
const idealCycle = Number(activeOrder.cycleTime) || 0;
const operatingTime = global.get("operatingTime") || 0;

if (cycleCount > 0 && idealCycle > 0 && operatingTime > 0) {
    const targetCount = operatingTime / idealCycle;
    msg.kpis.performance = (cycleCount / targetCount) * 100;
    msg.kpis.performance = Math.min(100, msg.kpis.performance);
} else {
    msg.kpis.performance = prev.performance ?? 100;
}

// ========================================
// 3) AVAILABILITY
// ----------------------------------------
const now = Date.now();
const lastCycleTime = global.get("lastMachineCycleTime") || now;
const timeSinceLastCycle = now - lastCycleTime;
const BRIEF_PAUSE_THRESHOLD = 5 * 60 * 1000;
const productionStartTime = global.get("productionStartTime");

if (!trackingEnabled || timeSinceLastCycle > BRIEF_PAUSE_THRESHOLD) {
  msg.kpis.availability = 0;
  global.set("lastKPIValues", null);
} else if (trackingEnabled && productionStartTime && operatingTime > 0) {
    const elapsedSec = (now - productionStartTime) / 1000;
    if (elapsedSec > 0) {
        msg.kpis.availability = (operatingTime / elapsedSec) * 100;
        msg.kpis.availability = Math.min(100, msg.kpis.availability);
    }
    global.set("lastKPIValues", msg.kpis);
} else if (trackingEnabled && productionStartTime) {
  msg.kpis.availability = 100;
} else {
  const prevKPIs = global.get("lastKPIValues") || {};
  msg.kpis.availability = prevKPIs.availability || 0;
}

// ========================================
// 4) OEE
// ----------------------------------------
msg.kpis.oee =
    (msg.kpis.availability *
     msg.kpis.performance *
     msg.kpis.quality) / 10000;

// Round nicely
for (let k of ["quality","performance","availability","oee"]) {
    msg.kpis[k] = Math.round(msg.kpis[k] * 10) / 10;
}

global.set("currentKPIs", msg.kpis);
return msg;

Process Alert for DB

// Process incoming alert
if (msg.payload && msg.payload.action === 'alert') {
    const alert = msg.payload;

    // Format timestamp for MySQL DATETIME
    const timestamp = alert.timestamp ?
        new Date(alert.timestamp).toISOString().slice(0, 19).replace('T', ' ') :
        new Date().toISOString().slice(0, 19).replace('T', ' ');

    // Prepare INSERT query
    const alertType = (alert.type || 'Unknown').replace(/'/g, "''"); // Escape quotes
    const description = (alert.description || '').replace(/'/g, "''"); // Escape quotes

    msg.topic = `
    INSERT INTO alerts_log (timestamp, alert_type, description)
    VALUES ('${timestamp}', '${alertType}', '${description}')
    `;

    node.status({
        fill: 'green',
        shape: 'dot',
        text: `Logging: ${alertType}`
    });

    // Store original message for passthrough
    msg._originalAlert = alert;

    return msg;
}

return null;

Alert Logging: Template → Process → MySQL Insert → Debug

Alerts from the UI are logged to the alerts_log table.
Table structure:
- id (auto-increment)
- timestamp (when alert occurred)
- alert_type (category)
- description (optional notes)
- created_at (when logged)

Record KPI History

// Complete Record KPI History function with robust initialization and averaging

// ========== INITIALIZATION ==========
// Initialize buffer
let buffer = global.get("kpiBuffer");
if (!buffer || !Array.isArray(buffer)) {
  buffer = [];
  global.set("kpiBuffer", buffer);
  node.warn('[KPI History] Initialized kpiBuffer');
}

// Initialize last record time
let lastRecordTime = global.get("lastKPIRecordTime");
if (!lastRecordTime || typeof lastRecordTime !== 'number') {
  // Set to 1 minute ago to ensure immediate recording on startup
  lastRecordTime = Date.now() - 60000;
  global.set("lastKPIRecordTime", lastRecordTime);
  node.warn('[KPI History] Initialized lastKPIRecordTime');
}

// ========== ACCUMULATE ==========
const kpis = msg.payload?.kpis || msg.kpis;
if (!kpis) {
  node.warn('[KPI History] No KPIs in message, skipping');
  return null;
}

buffer.push({
  timestamp: Date.now(),
  oee: kpis.oee || 0,
  availability: kpis.availability || 0,
  performance: kpis.performance || 0,
  quality: kpis.quality || 0
});

// Prevent buffer from growing too large (safety limit)
if (buffer.length > 100) {
  buffer = buffer.slice(-60); // Keep last 60 entries
  node.warn('[KPI History] Buffer exceeded 100 entries, trimmed to 60');
}

global.set("kpiBuffer", buffer);

// ========== CHECK IF TIME TO RECORD ==========
const now = Date.now();
const timeSinceLastRecord = now - lastRecordTime;
const ONE_MINUTE = 60 * 1000;

if (timeSinceLastRecord < ONE_MINUTE) {
  // Not time to record yet
  return null; // Don't send to charts yet
}

// ========== CALCULATE AVERAGES ==========
if (buffer.length === 0) {
  node.warn('[KPI History] Buffer empty at recording time, skipping');
  return null;
}

const avg = {
  oee: buffer.reduce((sum, d) => sum + d.oee, 0) / buffer.length,
  availability: buffer.reduce((sum, d) => sum + d.availability, 0) / buffer.length,
  performance: buffer.reduce((sum, d) => sum + d.performance, 0) / buffer.length,
  quality: buffer.reduce((sum, d) => sum + d.quality, 0) / buffer.length
};

node.warn(`[KPI History] Recording averaged KPIs from ${buffer.length} samples: OEE=${avg.oee.toFixed(1)}%`);

// ========== RECORD TO HISTORY ==========
// Load history arrays
let oeeHist  = global.get("realOEE")           || [];
let availHist = global.get("realAvailability") || [];
let perfHist  = global.get("realPerformance")  || [];
let qualHist  = global.get("realQuality")      || [];

// Append averaged values
oeeHist.push({  timestamp: now, value: Math.round(avg.oee * 10) / 10 });
availHist.push({ timestamp: now, value: Math.round(avg.availability * 10) / 10 });
perfHist.push({ timestamp: now, value: Math.round(avg.performance * 10) / 10 });
qualHist.push({ timestamp: now, value: Math.round(avg.quality * 10) / 10 });

// Trim arrays (avoid memory explosion)
oeeHist  = oeeHist.slice(-300);
availHist = availHist.slice(-300);
perfHist  = perfHist.slice(-300);
qualHist  = qualHist.slice(-300);

// Save
global.set("realOEE", oeeHist);
global.set("realAvailability", availHist);
global.set("realPerformance", perfHist);
global.set("realQuality", qualHist);

// Update global state
global.set("lastKPIRecordTime", now);
global.set("kpiBuffer", []); // Clear buffer

// Send to graphs
return {
  topic: "chartsData",
  payload: {
    oee: oeeHist,
    availability: availHist,
    performance: perfHist,
    quality: qualHist
  }
};

Send to template

const realOEE = global.get("realOEE");
const realAvailability = global.get("realAvailability");
const realPerformance = global.get("realPerformance");
const realQuality = global.get("realQuality");

const chartsMsg = {
    topic: "chartsData",
    payload: {
        oee: realOEE,
        availability: realAvailability,
        performance: realPerformance,
        quality: realQuality
    }
};
return [chartsMsg];

Initialize Global Variables

// Initialize Global Variables - Run on Deploy
node.warn('[INIT] Initializing global variables');

// KPI Buffer for averaging
if (!global.get("kpiBuffer")) {
  global.set("kpiBuffer", []);
  node.warn('[INIT] Set kpiBuffer to []');
}

// Last KPI record time - set to 1 min ago for immediate first record
if (!global.get("lastKPIRecordTime")) {
  global.set("lastKPIRecordTime", Date.now() - 60000);
  node.warn('[INIT] Set lastKPIRecordTime');
}

// Last machine cycle time - set to now to prevent immediate 0% availability
if (!global.get("lastMachineCycleTime")) {
  global.set("lastMachineCycleTime", Date.now());
  node.warn('[INIT] Set lastMachineCycleTime to prevent 0% availability on startup');
}

// Last KPI values
if (!global.get("lastKPIValues")) {
  global.set("lastKPIValues", {});
  node.warn('[INIT] Set lastKPIValues to {}');
}

// KPI Startup Mode - ensure clean state on deploy
global.set("kpiStartupMode", false);
node.warn('[INIT] Set kpiStartupMode to false');

// Tracking flags - ensure clean state
if (global.get("trackingEnabled") === undefined) {
  global.set("trackingEnabled", false);
}
if (global.get("productionStarted") === undefined) {
  global.set("productionStarted", false);
}

node.warn('[INIT] Global variable initialization complete');

// Trigger restore-session to check for running work orders
const restoreMsg = { action: "restore-session" };
return [null, restoreMsg];

Fetch Graph Data

// Fetch KPI History from global context

if (msg.topic !== 'fetch-graph-data' && msg.action !== 'fetch-graph-data') {
    return null;
}

// Get KPI history arrays from global context
const oeeHist = global.get("realOEE") || [];
const availHist = global.get("realAvailability") || [];
const perfHist = global.get("realPerformance") || [];
const qualHist = global.get("realQuality") || [];

node.warn(`[FETCH GRAPH] Retrieved KPI history: ${oeeHist.length} data points`);

// Filter by range if specified
const range = msg.payload?.range || '24h';
let cutoffTime;
const now = Date.now();

switch(range) {
    case '1h':
        cutoffTime = now - (1 * 60 * 60 * 1000);
        break;
    case '24h':
        cutoffTime = now - (24 * 60 * 60 * 1000);
        break;
    case '7d':
        cutoffTime = now - (7 * 24 * 60 * 60 * 1000);
        break;
    case '30d':
        cutoffTime = now - (30 * 24 * 60 * 60 * 1000);
        break;
    case '90d':
        cutoffTime = now - (90 * 24 * 60 * 60 * 1000);
        break;
    case 'all':
        cutoffTime = 0; // Show all data
        break;
    default:
        cutoffTime = now - (24 * 60 * 60 * 1000); // Default to 24h
}

// Filter arrays by cutoff time
const filterByTime = (arr) => arr.filter(point => point.timestamp >= cutoffTime);

const filteredOEE = filterByTime(oeeHist);
const filteredAvail = filterByTime(availHist);
const filteredPerf = filterByTime(perfHist);
const filteredQual = filterByTime(qualHist);

node.warn(`[FETCH GRAPH] After ${range} filter: ${filteredOEE.length} points`);

// Send to Format Graph Data
msg.topic = "kpiHistory";
msg.payload = {
    oee: filteredOEE,
    availability: filteredAvail,
    performance: filteredPerf,
    quality: filteredQual
};

return msg;

Format Graph Data

// Format Graph Data for KPI charts

// Check if we have KPI history data (from global context)
if (msg.topic === "kpiHistory" && msg.payload) {
    const kpiData = msg.payload;

    // Extract arrays
    const oeeHist = kpiData.oee || [];
    const availHist = kpiData.availability || [];
    const perfHist = kpiData.performance || [];
    const qualHist = kpiData.quality || [];

    // Build labels and data arrays
    const labels = [];
    const oeeData = [];
    const availData = [];
    const perfData = [];
    const qualData = [];

    // Use OEE timestamps as primary (they should all be the same length)
    oeeHist.forEach((point, index) => {
        const timestamp = new Date(point.timestamp);
        labels.push(timestamp.toLocaleString());

        oeeData.push(point.value || 0);
        availData.push(availHist[index]?.value || 0);
        perfData.push(perfHist[index]?.value || 0);
        qualData.push(qualHist[index]?.value || 0);
    });

    msg.graphData = {
        labels: labels,
        datasets: [
            { label: 'OEE %', data: oeeData },
            { label: 'Availability %', data: availData },
            { label: 'Performance %', data: perfData },
            { label: 'Quality %', data: qualData }
        ]
    };

    node.warn(`[GRAPH DATA] Formatted ${labels.length} KPI history points`);

    delete msg.topic;
    delete msg.payload;
    return msg;
}

// Legacy support: work_orders query data (if needed)
const rows = msg.payload || [];

if (!Array.isArray(rows) || rows.length === 0) {
    msg.graphData = {
        labels: [],
        datasets: [
            { label: 'OEE %', data: [] },
            { label: 'Availability %', data: [] },
            { label: 'Performance %', data: [] },
            { label: 'Quality %', data: [] }
        ]
    };
    delete msg.topic;
    delete msg.payload;
    return msg;
}

// If we have work_orders data, format it (though we won't use this path anymore)
const labels = [];
const goodData = [];
const scrapData = [];
const efficiencyData = [];
const qualityData = [];

rows.forEach(row => {
    const timestamp = new Date(row.updated_at);
    labels.push(timestamp.toLocaleString());

    const good = Number(row.good_parts) || 0;
    const scrap = Number(row.scrap_parts) || 0;
    const target = Number(row.target_quantity) || 0;

    goodData.push(good);
    scrapData.push(scrap);

    let eff = (row.progress_percent != null)
        ? Number(row.progress_percent)
        : (target > 0 ? (good / target) * 100 : 0);
    efficiencyData.push(Math.min(eff, 100));

    const total = good + scrap;
    const quality = total > 0 ? (good / total) * 100 : 100;
    qualityData.push(quality);
});

msg.graphData = {
    labels: labels,
    datasets: [
        { label: 'OEE %', data: efficiencyData },  // Use efficiency as fallback
        { label: 'Availability %', data: [] },
        { label: 'Performance %', data: [] },
        { label: 'Quality %', data: qualityData }
    ]
};

delete msg.topic;
delete msg.payload;

return msg;

Progress Check Handler

// Handle DB result from start-work-order progress check
if (msg._mode === "start-check-progress") {
    const order = flow.get("pendingWorkOrder");

    if (!order || !order.id) {
        node.error("No pending work order found", msg);
        return [null, null];
    }

    // Get progress from DB query result
    const dbRow = (Array.isArray(msg.payload) && msg.payload.length > 0) ? msg.payload[0] : null;
    const cycleCount = dbRow ? (Number(dbRow.cycle_count) || 0) : 0;
    const goodParts = dbRow ? (Number(dbRow.good_parts) || 0) : 0;
    const scrapParts = dbRow ? (Number(dbRow.scrap_parts) || 0) : 0;
    const targetQty = dbRow ? (Number(dbRow.target_qty) || 0) : (Number(order.target) || 0);

    node.warn(`[PROGRESS-CHECK] WO ${order.id}: cycles=${cycleCount}, good=${goodParts}, target=${targetQty}`);

    // Check if work order has existing progress
    if (cycleCount > 0 || goodParts > 0) {
        // Work order has progress - send prompt to UI
        node.warn(`[PROGRESS-CHECK] Work order has existing progress - sending prompt to UI`);

        const promptMsg = {
            _mode: "resume-prompt",
            topic: "resumePrompt",
            payload: {
                id: order.id,
                sku: order.sku || "",
                cycleCount: cycleCount,
                goodParts: goodParts,
                targetQty: targetQty,
                progressPercent: targetQty > 0 ? Math.round((goodParts / targetQty) * 100) : 0,
                // Include full order object for resume/restart actions
                order: {...order, cycle_count: cycleCount, good_parts: goodParts, scrap: scrapParts}
            }
        };

        return [null, promptMsg];
    } else {
        // No existing progress - proceed with normal start
        // But still use DB values (even if 0) to ensure DB is source of truth
        node.warn(`[PROGRESS-CHECK] No existing progress - proceeding with normal start`);

        // Update order object with DB values (makes DB the source of truth)
        order.cycle_count = cycleCount;  // Will be 0 from DB
        order.good_parts = goodParts;    // Will be 0 from DB
        order.scrap = scrapParts;        // Will be 0 from DB
        order.good = goodParts;          // For consistency
        order.target = targetQty;        // From DB

        const startMsg = {
            _mode: "start",
            startOrder: order,
            topic: "UPDATE work_orders SET status = CASE WHEN work_order_id = ? THEN 'RUNNING' ELSE 'PENDING' END, updated_at = CASE WHEN work_order_id = ? THEN NOW() ELSE updated_at END WHERE status <> 'DONE'",
            payload: [order.id, order.id]
        };

        // Initialize global state with DB values (even if 0)
        global.set("activeWorkOrder", order);
        global.set("cycleCount", cycleCount);  // Use DB value instead of hardcoded 0
        flow.set("lastMachineState", 0);
        global.set("scrapPromptIssuedFor", null);

        node.warn(`[PROGRESS-CHECK] Initialized from DB: cycles=${cycleCount}, good=${goodParts}, scrap=${scrapParts}`);

        return [startMsg, null];
    }
}

// Pass through all other messages
return [msg, null];

Anomaly Detector

// ============================================================
// ANOMALY DETECTOR - Enhanced with Tier 1 & 2 Detections
// 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,
    oeeHistory: [],
    performanceHistory: [],
    qualityHistory: []
};

// Configuration
const OEE_THRESHOLD = global.get("oeeAlertThreshold") || 90;  // Customizable in settings
const HISTORY_WINDOW = 20;  // Keep last 20 data points for trend analysis
const QUALITY_SPIKE_THRESHOLD = 5;  // Alert if scrap rate increases by 5%+

const detectedAnomalies = [];

// ============================================================
// TIER 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)`);
    }
}

// ============================================================
// TIER 1: 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`);
    }
}

// ============================================================
// TIER 2: OEE DROP DETECTION
// Trigger: OEE falls below threshold
// ============================================================
const currentOEE = Number(kpis.oee) || 0;
if (currentOEE > 0 && currentOEE < OEE_THRESHOLD) {
    let severity = 'warning';
    if (currentOEE < 75) {
        severity = 'critical';  // Critically low OEE
    }

    detectedAnomalies.push({
        anomaly_type: 'oee-drop',
        severity: severity,
        title: `OEE Below Threshold`,
        description: `OEE at ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`,
        data: {
            current_oee: currentOEE,
            threshold: OEE_THRESHOLD,
            delta: OEE_THRESHOLD - currentOEE
        },
        kpi_snapshot: {
            oee: 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] OEE drop: ${currentOEE.toFixed(1)}% (threshold: ${OEE_THRESHOLD}%)`);
}

// Update OEE history for trend analysis
anomalyState.oeeHistory.push({ timestamp: now, value: currentOEE });
if (anomalyState.oeeHistory.length > HISTORY_WINDOW) {
    anomalyState.oeeHistory.shift();  // Keep only recent history
}

// ============================================================
// TIER 2: QUALITY SPIKE DETECTION
// Trigger: Sudden increase in scrap/defect rate
// ============================================================
const totalParts = (cycle.goodParts || 0) + (cycle.scrapParts || 0);
const currentScrapRate = totalParts > 0 ? ((cycle.scrapParts || 0) / totalParts) * 100 : 0;

anomalyState.qualityHistory.push({ timestamp: now, value: currentScrapRate });
if (anomalyState.qualityHistory.length > HISTORY_WINDOW) {
    anomalyState.qualityHistory.shift();
}

// Calculate average scrap rate from history
if (anomalyState.qualityHistory.length >= 5) {
    const recentHistory = anomalyState.qualityHistory.slice(0, -1);  // Exclude current
    const avgScrapRate = recentHistory.reduce((sum, point) => sum + point.value, 0) / recentHistory.length;

    const scrapRateIncrease = currentScrapRate - avgScrapRate;

    if (scrapRateIncrease > QUALITY_SPIKE_THRESHOLD && currentScrapRate > 2) {
        let severity = 'warning';
        if (scrapRateIncrease > 10 || currentScrapRate > 15) {
            severity = 'critical';  // Major quality issue
        }

        detectedAnomalies.push({
            anomaly_type: 'quality-spike',
            severity: severity,
            title: `Quality Issue Detected`,
            description: `Scrap rate at ${currentScrapRate.toFixed(1)}% (avg: ${avgScrapRate.toFixed(1)}%, +${scrapRateIncrease.toFixed(1)}%)`,
            data: {
                current_scrap_rate: currentScrapRate,
                average_scrap_rate: avgScrapRate,
                increase: scrapRateIncrease,
                scrap_parts: cycle.scrapParts || 0,
                total_parts: totalParts
            },
            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] Quality spike: Scrap rate ${currentScrapRate.toFixed(1)}% (avg: ${avgScrapRate.toFixed(1)}%)`);
    }
}

// ============================================================
// TIER 2: PERFORMANCE DEGRADATION
// Trigger: Consistent underperformance over time
// ============================================================
const currentPerformance = Number(kpis.performance) || 0;
anomalyState.performanceHistory.push({ timestamp: now, value: currentPerformance });
if (anomalyState.performanceHistory.length > HISTORY_WINDOW) {
    anomalyState.performanceHistory.shift();
}

// Check for sustained poor performance (at least 10 data points)
if (anomalyState.performanceHistory.length >= 10) {
    const recent10 = anomalyState.performanceHistory.slice(-10);
    const avgPerformance = recent10.reduce((sum, point) => sum + point.value, 0) / recent10.length;

    // Alert if consistently below 85% performance
    if (avgPerformance > 0 && avgPerformance < 85) {
        let severity = 'warning';
        if (avgPerformance < 75) {
            severity = 'critical';  // Severely degraded performance
        }

        detectedAnomalies.push({
            anomaly_type: 'performance-degradation',
            severity: severity,
            title: `Performance Degradation`,
            description: `Performance at ${avgPerformance.toFixed(1)}% (sustained over last 10 cycles)`,
            data: {
                average_performance: avgPerformance,
                current_performance: currentPerformance,
                threshold: 85,
                sample_size: 10
            },
            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] Performance degradation: ${avgPerformance.toFixed(1)}% over last 10 cycles`);
    }
}

// ============================================================
// TIER 3: PREDICTIVE ALERTS (Trend Analysis)
// Predict issues before they become critical
// ============================================================
if (anomalyState.oeeHistory.length >= 15) {
    // Simple linear trend analysis on OEE
    const recent15 = anomalyState.oeeHistory.slice(-15);
    const firstHalf = recent15.slice(0, 7);
    const secondHalf = recent15.slice(-7);

    const avgFirstHalf = firstHalf.reduce((sum, p) => sum + p.value, 0) / firstHalf.length;
    const avgSecondHalf = secondHalf.reduce((sum, p) => sum + p.value, 0) / secondHalf.length;

    const oeeTrend = avgSecondHalf - avgFirstHalf;

    // Predict if OEE is trending downward significantly
    if (oeeTrend < -5 && avgSecondHalf > OEE_THRESHOLD * 0.95 && avgSecondHalf < OEE_THRESHOLD * 1.05) {
        detectedAnomalies.push({
            anomaly_type: 'predictive-oee-decline',
            severity: 'info',
            title: `Declining OEE Trend Detected`,
            description: `OEE trending down ${Math.abs(oeeTrend).toFixed(1)}% over last 15 cycles. Current: ${avgSecondHalf.toFixed(1)}%`,
            data: {
                trend: oeeTrend,
                first_half_avg: avgFirstHalf,
                second_half_avg: avgSecondHalf,
                prediction: 'OEE may drop below threshold soon'
            },
            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(`[PREDICTIVE] OEE trending down: ${oeeTrend.toFixed(1)}%`);
    }
}

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

Event Logger

// ============================================================
// 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
        }
    }];
}

Check Table Exists

// First check if table exists
msg.topic = "SHOW TABLES LIKE 'anomaly_events'";
msg.payload = [];
return msg;

Create Table SQL

// Table doesn't exist - create it
msg.topic = `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)
)`;
msg.payload = [];
return msg;

+        "name": "Initialize OEE Threshold (90%)",

+                "set_oee_threshold_global"

+        "id": "set_oee_threshold_global",

+        "name": "Set OEE Threshold Global",

+        "func": "// Initialize OEE alert threshold\nconst threshold = Number(msg.payload) || 90;\nglobal.set(\"oeeAlertThreshold\", threshold);\n\nnode.warn(`[CONFIG] OEE Alert Threshold set to ${threshold}%`);\n\nreturn msg;",


Perfect! I've added an initialization inject node that runs once on startup to set the OEE threshold to 90%. This will automatically initialize when Node-RED starts.

Now let me mark all tasks as completed and provide a comprehensive summary to the user.


Perfect! Let me mark all tasks complete and provide you with a comprehensive summary:


TodoWrite

Fix and verify anomaly_events table creation

Fixing and verifying anomaly_events table creation

Enhance Anomaly Detector with Tier 2 detections (OEE Drop, Quality Spike, Performance Degradation)

Enhancing Anomaly Detector with Tier 2 detections

Add predictive alerts based on trend analysis

Adding predictive alerts based on trend analysis

Create floating alert panel UI component (hideable on side)

Creating floating alert panel UI component

Create pop-up notification system requiring acknowledgment

Wire Event Logger output 2 to UI components

Wiring Event Logger output 2 to UI components

Add OEE threshold initialization (default 90%)

Test complete anomaly detection and notification flow

Testing complete anomaly detection and notification flow