[
{
"id": "cac3a4383120cb57",
"type": "tab",
"label": "Flow 1",
"disabled": false,
"info": "",
"env": []
},
{
"id": "16bb591480852f51",
"type": "group",
"z": "cac3a4383120cb57",
"name": "Start ",
"style": {
"stroke": "#92d04f",
"fill": "#addb7b",
"label": true
},
"nodes": [
"6ad64dedab2042b9",
"0f5ee343ed17976c",
"4e025693949ec4bd",
"b55c91c096a366db",
"33d1f41119e0e262",
"f98ae23b2430c206",
"0d023d87a13bf56f",
"dbc7a5ee041845ed",
"e15d6c1f78b644a2"
],
"x": 34,
"y": 339,
"w": 762,
"h": 162
},
{
"id": "bdaf9298cd8e306b",
"type": "group",
"z": "cac3a4383120cb57",
"name": "Cavity Settings ",
"style": {
"stroke": "#ff7f7f",
"fill": "#ffbfbf",
"label": true
},
"nodes": [
"e1f2a3b4c5d6e7f8",
"75dbe316f19fd44c"
],
"x": null,
"y": null,
"w": null,
"h": null
},
{
"id": "ec32d0a62eacfb22",
"type": "group",
"z": "cac3a4383120cb57",
"name": "UI/UX",
"style": {
"fill": "#d1d1d1",
"label": true
},
"nodes": [
"1821c4842945ecd8",
"f2a3b4c5d6e7f8a9",
"f3a4b5c6d7e8f9a0",
"f4a5b6c7d8e9f0a1",
"f5a6b7c8d9e0f1a2",
"f1a2b3c4d5e6f7a8",
"a7d58e15929b3d8c",
"cc81a9dbfd443d62",
"06f9769e8b0d5355",
"0a5caf3e23c68e6e",
"010de5af3ced0ae3",
"6f9de736a538d0d1",
"16af50d6fce977a8",
"2f04a72fdeb67f3f",
"8f890f97aa9257c7",
"394cfca6b72f6444"
],
"x": 54,
"y": 39,
"w": 632,
"h": 282
},
{
"id": "b7ab5e0cc02b9508",
"type": "group",
"z": "cac3a4383120cb57",
"name": "Work Orders",
"style": {
"stroke": "#9363b7",
"fill": "#dbcbe7",
"label": true
},
"nodes": [
"9bbd4fade968036d",
"65ddb4cca6787bde",
"596b390d7aaf69fb",
"f6ad294bc02618c9",
"f2bab26e27e2023d",
"0779932734d8201c",
"3772c25d07b07407",
"c2b272494952cd98",
"87d85c86e4773aa5",
"15a6b7b6d8f39fe4",
"64661fe6aa2cb83d",
"578c92e75bf0f266",
"76ce53cf1ae40e9c",
"0d6ec01f421acdef",
"fd32602c52d896e9",
"d39000415ba85495",
"dc9b9a26af05dfa8",
"dc74dbc51dd757ba"
],
"x": 774,
"y": 279,
"w": 932,
"h": 322
},
{
"id": "75dbe316f19fd44c",
"type": "group",
"z": "cac3a4383120cb57",
"g": "bdaf9298cd8e306b",
"name": "Cavities Settings",
"style": {
"stroke": "#ffff00",
"fill": "#ffffbf",
"label": true
},
"nodes": [
"e1f2a3b4c5d6e7f8",
"28c173789034639c"
],
"x": null,
"y": null,
"w": null,
"h": null
},
{
"id": "28c173789034639c",
"type": "group",
"z": "cac3a4383120cb57",
"g": "75dbe316f19fd44c",
"name": "Settings",
"style": {
"stroke": "#92d04f",
"fill": "#ffffbf",
"label": true
},
"nodes": [
"eaebd8c719c3d135",
"a1b2c3d4e5f6a7b8",
"c9d8e7f6a5b4c3d2",
"b2c3d4e5f6a7b8c9",
"7311641fd09b4d3a",
"0b5740c4a2b298b7"
],
"x": 714,
"y": 39,
"w": 722,
"h": 162
},
{
"id": "c567195d86466cd5",
"type": "ui_tab",
"name": "Home",
"icon": "dashboard",
"order": 1,
"disabled": false,
"hidden": false
},
{
"id": "f4c299235c1b719d",
"type": "ui_base",
"theme": {
"name": "theme-custom",
"lightTheme": {
"default": "#0094CE",
"baseColor": "#0094CE",
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
"edited": true,
"reset": false
},
"darkTheme": {
"default": "#097479",
"baseColor": "#000000",
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
"edited": true,
"reset": false
},
"customTheme": {
"name": "Transparent",
"default": "#4B7930",
"baseColor": "#000000",
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
"reset": false
},
"themeState": {
"base-color": {
"default": "#4B7930",
"value": "#000000",
"edited": true
},
"page-titlebar-backgroundColor": {
"value": "#000000",
"edited": false
},
"page-backgroundColor": {
"value": "#111111",
"edited": false
},
"page-sidebar-backgroundColor": {
"value": "#333333",
"edited": false
},
"group-textColor": {
"value": "#262626",
"edited": false
},
"group-borderColor": {
"value": "#000000",
"edited": true
},
"group-backgroundColor": {
"value": "#333333",
"edited": false
},
"widget-textColor": {
"value": "#eeeeee",
"edited": false
},
"widget-backgroundColor": {
"value": "#000000",
"edited": false
},
"widget-borderColor": {
"value": "#333333",
"edited": false
},
"base-font": {
"value": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"
}
},
"angularTheme": {
"primary": "indigo",
"accents": "blue",
"warn": "red",
"background": "grey",
"palette": "light"
}
},
"site": {
"name": "Node-RED Dashboard",
"hideToolbar": "true",
"allowSwipe": "false",
"lockMenu": "false",
"allowTempTheme": "true",
"dateFormat": "DD/MM/YYYY",
"sizes": {
"sx": 48,
"sy": 48,
"gx": 6,
"gy": 6,
"cx": 6,
"cy": 6,
"px": 0,
"py": 0
}
}
},
{
"id": "919b5b8d778e2b6c",
"type": "ui_group",
"name": "Default",
"tab": "c567195d86466cd5",
"order": 1,
"disp": false,
"width": "25",
"collapse": false,
"className": ""
},
{
"id": "d1a1e2f3a4b5c6d7",
"type": "ui_tab",
"name": "Work Orders",
"icon": "list",
"order": 2,
"disabled": false,
"hidden": false
},
{
"id": "a1b2c3d4e5f60718",
"type": "ui_tab",
"name": "Alerts",
"icon": "warning",
"order": 3,
"disabled": false,
"hidden": false
},
{
"id": "b2c3d4e5f6a70182",
"type": "ui_tab",
"name": "Graphs",
"icon": "show_chart",
"order": 4,
"disabled": false,
"hidden": false
},
{
"id": "c3d4e5f6a7b80192",
"type": "ui_tab",
"name": "Help",
"icon": "help",
"order": 5,
"disabled": false,
"hidden": false
},
{
"id": "d4e5f6a7b8c90123",
"type": "ui_tab",
"name": "Settings",
"icon": "settings",
"order": 6,
"disabled": false,
"hidden": false
},
{
"id": "e1f2a3b4c5d6e7f8",
"type": "ui_group",
"g": "75dbe316f19fd44c",
"name": "Work Orders Group",
"tab": "d1a1e2f3a4b5c6d7",
"order": 1,
"disp": false,
"width": 25,
"collapse": false,
"className": ""
},
{
"id": "e2f3a4b5c6d7e8f9",
"type": "ui_group",
"name": "Alerts Group",
"tab": "a1b2c3d4e5f60718",
"order": 1,
"disp": false,
"width": "25",
"collapse": false,
"className": ""
},
{
"id": "e3f4a5b6c7d8e9f0",
"type": "ui_group",
"name": "Graphs Group",
"tab": "b2c3d4e5f6a70182",
"order": 1,
"disp": false,
"width": "25",
"collapse": false,
"className": ""
},
{
"id": "e4f5a6b7c8d9e0f1",
"type": "ui_group",
"name": "Help Group",
"tab": "c3d4e5f6a7b80192",
"order": 1,
"disp": false,
"width": "25",
"collapse": false,
"className": ""
},
{
"id": "e5f6a7b8c9d0e1f2",
"type": "ui_group",
"name": "Settings Group",
"tab": "d4e5f6a7b8c90123",
"order": 1,
"disp": false,
"width": "25",
"collapse": false,
"className": ""
},
{
"id": "00d8ad2b0277f906",
"type": "MySQLdatabase",
"name": "machine_data",
"host": "10.147.20.244",
"port": "3306",
"db": "machine_data",
"tz": "",
"charset": "UTF8"
},
{
"id": "1821c4842945ecd8",
"type": "ui_template",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"group": "919b5b8d778e2b6c",
"name": "Home Template",
"order": 0,
"width": "25",
"height": "25",
"format": "\n
\n \n\n
\n \n
\n \n OEE
\n 0 %
\n \n \n Availability
\n 0 %
\n \n \n Performance
\n 0 %
\n \n \n Quality
\n 0 %
\n \n \n\n
\n
Current Work Order \n \n \n \n\n
\n \n Good Parts
\n 0
\n out of 0
\n \n\n \n Machine OFFLINE
\n Production STOPPED
\n \n\n \n {{ isProductionRunning ? 'STOP' : 'START' }} \n \n \n
\n \n
\n\n
\n
Work Order Complete \n
{{ scrapPrompt.orderId }}
\n
Produced {{ scrapPrompt.produced }} of {{ scrapPrompt.target }} pieces
\n\n
Were there any scrap parts?
\n\n \n
\n
{{ scrapPrompt.scrapCount || 0 }}
\n
{{ scrapPrompt.error }}
\n\n
\n 7 \n 8 \n 9 \n\n 4 \n 5 \n 6 \n\n 1 \n 2 \n 3 \n\n C \n 0 \n ⌫ \n
\n\n
\n \n Submit Scrap\n \n
\n
\n\n \n
\n \n
\n \n \n Remind me again if we keep overproducing\n \n
\n\n
\n No, Continue Production\n \n
\n Yes, Enter Scrap\n \n
\n
\n
\n\n\n\n",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 200,
"y": 80,
"wires": [
[
"a7d58e15929b3d8c",
"010de5af3ced0ae3"
]
]
},
{
"id": "f2a3b4c5d6e7f8a9",
"type": "ui_template",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"group": "e2f3a4b5c6d7e8f9",
"name": "Alerts Template",
"order": 0,
"width": "25",
"height": "25",
"format": "\n\n \n\n
\n \n
\n\n
\n Material Out \n Machine Stopped \n Emergency Stop \n \n\n
\n \n Alert type \n \n Quality Defect Detected \n Tooling/Mold Issue \n Temperature Out of Range \n Pressure Issue \n Cycle Time Deviation \n Safety Interlock Triggered \n Hydraulic/Pneumatic Failure \n Electrical Fault \n Sensor Malfunction \n Other \n \n
\n \n Description \n \n
\n Send Alert \n \n
\n \n
\n\n",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 200,
"y": 160,
"wires": [
[
"a7d58e15929b3d8c",
"alert_process_function"
]
]
},
{
"id": "f3a4b5c6d7e8f9a0",
"type": "ui_template",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"group": "e3f4a5b6c7d8e9f0",
"name": "Graphs Template",
"order": 0,
"width": "25",
"height": "25",
"format": "\n\n\n\n \n\n
\n \n\n
Graphs \n\n
\n \n Shift (8h) \n Day \n Week \n Month \n Year \n
\n \n\n
\n \n OEE \n
\n \n\n \n Availability \n
\n \n\n \n Performance \n
\n \n\n \n Quality \n
\n \n \n\n
\n \n
\n\n\n\n\n",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 210,
"y": 200,
"wires": [
[
"a7d58e15929b3d8c"
]
]
},
{
"id": "f4a5b6c7d8e9f0a1",
"type": "ui_template",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"group": "e4f5a6b7c8d9e0f1",
"name": "Help Template",
"order": 0,
"width": "25",
"height": "25",
"format": "\n\n \n\n
\n \n
\n\n
\n About this Dashboard \n 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.
\n \n\n
\n Getting Started with a Work Order \n 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.
\n \n\n
\n Running Production \n 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.
\n \n\n
\n Logging Incidents \n 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.
\n \n\n
\n Configuring Molds \n 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.
\n \n\n
\n Viewing Performance Data \n 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.
\n \n
\n \n
\n\n\n",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 200,
"y": 240,
"wires": [
[
"a7d58e15929b3d8c"
]
]
},
{
"id": "f5a6b7c8d9e0f1a2",
"type": "ui_template",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"group": "e5f6a7b8c9d0e1f2",
"name": "Settings Template",
"order": 0,
"width": "25",
"height": "25",
"format": "\n\n \n\n
\n \n
\n\n
\n Mold Presets \n \n
\n Manufacturer \n \n Select manufacturer... \n {{mfg}} \n \n
\n
\n Mold \n \n Select mold... \n \n
\n
\n \n \n \n
\n
Select a manufacturer and mold from the dropdowns above.
\n
\n\n \n
\n
\n \n\n
\n\n
\n Integrations \n \n
\n Connect to ERP \n
\n
\n
Can't find the mold you're looking for?
\n
Add Mold \n
\n
\n \n
\n \n
\n\n\n\n\n",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 210,
"y": 280,
"wires": [
[
"a7d58e15929b3d8c",
"0a5caf3e23c68e6e"
]
]
},
{
"id": "f1a2b3c4d5e6f7a8",
"type": "ui_template",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"group": "e1f2a3b4c5d6e7f8",
"name": "WO Template",
"order": 1,
"width": "25",
"height": "25",
"format": "\n\n\n\n",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 200,
"y": 120,
"wires": [
[
"a7d58e15929b3d8c",
"010de5af3ced0ae3"
]
]
},
{
"id": "a7d58e15929b3d8c",
"type": "function",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"name": "Tab navigation",
"func": "if (msg.ui_control && msg.ui_control.tab) {\n msg.payload = { tab: msg.ui_control.tab };\n delete msg.ui_control;\n return msg;\n}\nreturn null;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 440,
"y": 180,
"wires": [
[
"cc81a9dbfd443d62"
]
]
},
{
"id": "cc81a9dbfd443d62",
"type": "ui_ui_control",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"name": "",
"events": "all",
"x": 600,
"y": 180,
"wires": [
[]
]
},
{
"id": "06f9769e8b0d5355",
"type": "ui_template",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"group": "",
"name": "General Style",
"order": 0,
"width": 0,
"height": 0,
"format": "",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "global",
"className": "",
"x": 560,
"y": 140,
"wires": [
[]
]
},
{
"id": "6ad64dedab2042b9",
"type": "inject",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "Simula Inyectora",
"props": [
{
"p": "payload"
}
],
"repeat": "1",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"payload": "1",
"payloadType": "num",
"x": 170,
"y": 380,
"wires": [
[
"0f5ee343ed17976c"
]
]
},
{
"id": "0f5ee343ed17976c",
"type": "function",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "1,0",
"func": "// Get current global value (default to 0 if not set)\nlet estado = global.get('Estado_maquina') || 0;\nlet stop = flow.get('stop') || false;\n\nif (stop) {\n // Manual stop active → force 0, don't reschedule\n global.set('Estado_maquina', 0);\n msg.payload = 0;\n node.send(msg);\n return;\n}\n\n// Toggle between 1 and 0\nestado = estado === 1 ? 0 : 1;\n\n// Update the global variable\nglobal.set('Estado_maquina', estado);\n\n// Send it out\nmsg.payload = estado;\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 490,
"y": 400,
"wires": [
[
"0d023d87a13bf56f"
]
]
},
{
"id": "4e025693949ec4bd",
"type": "inject",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "Manual Stop",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 150,
"y": 420,
"wires": [
[
"b55c91c096a366db"
]
]
},
{
"id": "b55c91c096a366db",
"type": "change",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "",
"rules": [
{
"t": "set",
"p": "stop",
"pt": "flow",
"to": "true",
"tot": "bool"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 330,
"y": 420,
"wires": [
[
"0f5ee343ed17976c"
]
]
},
{
"id": "33d1f41119e0e262",
"type": "inject",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "Manual Start",
"props": [
{
"p": "payload"
},
{
"p": "topic",
"vt": "str"
}
],
"repeat": "",
"crontab": "",
"once": false,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 150,
"y": 460,
"wires": [
[
"f98ae23b2430c206"
]
]
},
{
"id": "f98ae23b2430c206",
"type": "change",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "set flow.start",
"rules": [
{
"t": "set",
"p": "stop",
"pt": "flow",
"to": "false",
"tot": "bool"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 320,
"y": 460,
"wires": [
[
"0f5ee343ed17976c"
]
]
},
{
"id": "eaebd8c719c3d135",
"type": "function",
"z": "cac3a4383120cb57",
"g": "28c173789034639c",
"name": "Cavities Settings",
"func": "if (msg.topic === \"moldSettings\" && msg.payload) {\n const total = Number(msg.payload.total || 0);\n const active = Number(msg.payload.active || 0);\n\n // Store globally\n global.set(\"moldTotal\", total);\n global.set(\"moldActive\", active);\n\n node.status({ fill: \"green\", shape: \"dot\", text: `Saved: ${active}/${total}` });\n\n msg.payload = { saved: true, total, active };\n return msg;\n}\n\n// Handle preset selection\nif (msg.topic === \"selectMoldPreset\" && msg.payload) {\n const preset = msg.payload;\n const total = Number(preset.theoretical_cavities || 0);\n const active = Number(preset.functional_cavities || 0);\n\n // Store globally\n global.set(\"moldTotal\", total);\n global.set(\"moldActive\", active);\n\n node.status({ fill: \"blue\", shape: \"dot\", text: `Preset: ${preset.mold_name}` });\n\n // Send to UI to update fields\n msg.topic = \"moldPresetSelected\";\n msg.payload = { total, active, presetName: preset.mold_name };\n return msg;\n}\n\nnode.status({ fill: \"red\", shape: \"ring\", text: \"Invalid payload\" });\nreturn null;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 910,
"y": 100,
"wires": [
[]
]
},
{
"id": "a1b2c3d4e5f6a7b8",
"type": "function",
"z": "cac3a4383120cb57",
"g": "28c173789034639c",
"name": "Mold Presets Handler",
"func": "const topic = msg.topic || '';\nconst payload = msg.payload || {};\n\n// ===== IGNORE NON-MOLD TOPICS SILENTLY =====\n// These are KPI/dashboard messages not meant for this handler\nconst ignoredTopics = [\n 'machineStatus',\n 'kpis',\n 'chartsData',\n 'activeWorkOrder',\n 'workOrderCycle',\n 'workOrdersList',\n 'scrapPrompt',\n 'uploadStatus'\n];\n\nif (ignoredTopics.includes(topic) || topic === '') {\n return null; // Silent ignore\n}\n\n// Log only mold-related requests\nnode.warn(`Received: ${topic}`);\n\n// CRITICAL: Use a processing lock to prevent simultaneous requests\nlet dedupeKey = topic;\nif (topic === 'addMoldPreset') {\n dedupeKey = `add_${payload.manufacturer}_${payload.mold_name}`;\n} else if (topic === 'getMoldsByManufacturer') {\n dedupeKey = `getmolds_${payload.manufacturer}`;\n}\n\nconst lockKey = `lock_${dedupeKey}`;\nconst lastRequestKey = `last_request_${dedupeKey}`;\n\n// Check if currently processing this request\nif (flow.get(lockKey) === true) {\n node.warn(`${topic} already processing - duplicate blocked`);\n return null;\n}\n\n// Check timing\nconst now = Date.now();\nconst lastRequestTime = flow.get(lastRequestKey) || 0;\nif (now - lastRequestTime < 2000) {\n node.warn(`Duplicate ${topic} request ignored (within 2s)`);\n return null;\n}\n\n// Set lock IMMEDIATELY before any async operations\nflow.set(lockKey, true);\nflow.set(lastRequestKey, now);\n\n// Release lock after 3 seconds (safety timeout)\nsetTimeout(() => {\n flow.set(lockKey, false);\n}, 3000);\n\n// Load all presets (legacy)\nif (topic === 'loadMoldPresets') {\n msg._originalTopic = 'loadMoldPresets';\n msg.topic = 'SELECT * FROM mold_presets ORDER BY manufacturer, mold_name;';\n node.warn('Querying all presets');\n return msg;\n}\n\n// Search/filter presets (legacy)\nif (topic === 'searchMoldPresets') {\n const filters = msg.payload || {};\n const searchTerm = (filters.searchTerm || '').trim().replace(/['\\\"\\\\\\\\]/g, '');\n const manufacturer = (filters.manufacturer || '').replace(/['\\\"\\\\\\\\]/g, '');\n const theoreticalCavities = filters.theoreticalCavities || '';\n\n let query = 'SELECT * FROM mold_presets WHERE 1=1';\n\n if (searchTerm) {\n const searchPattern = `%${searchTerm}%`;\n query += ` AND (mold_name LIKE '${searchPattern.replace(/'/g, \"''\")}' OR manufacturer LIKE '${searchPattern.replace(/'/g, \"''\")}')`;\n }\n\n if (manufacturer && manufacturer !== 'All') {\n query += ` AND manufacturer = '${manufacturer.replace(/'/g, \"''\")}'`;\n }\n\n if (theoreticalCavities && theoreticalCavities !== '') {\n const cavities = Number(theoreticalCavities);\n if (!isNaN(cavities)) {\n query += ` AND theoretical_cavities = ${cavities}`;\n }\n }\n\n query += ' ORDER BY manufacturer, mold_name;';\n\n msg._originalTopic = 'searchMoldPresets';\n msg.topic = query;\n return msg;\n}\n\n// Get unique manufacturers for dropdown\nif (topic === 'getManufacturers') {\n msg._originalTopic = 'getManufacturers';\n msg.topic = 'SELECT DISTINCT manufacturer FROM mold_presets ORDER BY manufacturer;';\n node.warn('Querying manufacturers');\n return msg;\n}\n\n// Get molds for a specific manufacturer\nif (topic === 'getMoldsByManufacturer') {\n const data = msg.payload || {};\n const manufacturerRaw = (data.manufacturer || '').trim();\n if (!manufacturerRaw) {\n node.warn('No manufacturer provided');\n return null;\n }\n\n const manufacturerSafe = manufacturerRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n\n msg._originalTopic = 'getMoldsByManufacturer';\n msg.topic = `SELECT * FROM mold_presets WHERE manufacturer = '${manufacturerSafe}' ORDER BY mold_name;`;\n node.warn(`Querying molds for: ${manufacturerSafe}`);\n return msg;\n}\n\n// Add a new mold preset - CRITICAL: Strong deduplication\nif (topic === 'addMoldPreset') {\n const data = msg.payload || {};\n const manufacturerRaw = (data.manufacturer || '').trim();\n const moldNameRaw = (data.mold_name || '').trim();\n const theoreticalRaw = (data.theoretical || '').trim();\n const activeRaw = (data.active || '').trim();\n\n if (!manufacturerRaw || !moldNameRaw || !theoreticalRaw || !activeRaw) {\n node.status({ fill: 'red', shape: 'ring', text: 'Missing value' });\n node.warn('Missing required fields');\n return null;\n }\n\n // Additional safety check for already-processed flag\n if (msg._addMoldProcessed) {\n node.warn('addMoldPreset already processed flag detected, ignoring');\n return null;\n }\n msg._addMoldProcessed = true;\n\n const manufacturerSafe = manufacturerRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n const moldNameSafe = moldNameRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n const theoreticalSafe = theoreticalRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n const activeSafe = activeRaw.replace(/['\\\"\\\\\\\\]/g, '').replace(/'/g, \"''\");\n\n msg._originalTopic = 'addMoldPreset';\n msg.topic =\n \"INSERT INTO mold_presets (manufacturer, mold_name, theoretical_cavities, functional_cavities) \" +\n \"VALUES ('\" + manufacturerSafe + \"', '\" + moldNameSafe + \"', \" + theoreticalSafe + \", \" + activeSafe + \");\";\n\n node.status({ fill: 'blue', shape: 'dot', text: 'Inserting mold...' });\n node.warn(`Inserting: ${manufacturerSafe} - ${moldNameSafe}`);\n return msg;\n}\n\nnode.warn(`Unknown topic: ${topic}`);\nreturn null;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 840,
"y": 160,
"wires": [
[
"c9d8e7f6a5b4c3d2"
]
]
},
{
"id": "c9d8e7f6a5b4c3d2",
"type": "mysql",
"z": "cac3a4383120cb57",
"g": "28c173789034639c",
"mydb": "00d8ad2b0277f906",
"name": "Mold Presets DB",
"x": 1050,
"y": 160,
"wires": [
[
"b2c3d4e5f6a7b8c9"
]
]
},
{
"id": "b2c3d4e5f6a7b8c9",
"type": "function",
"z": "cac3a4383120cb57",
"g": "28c173789034639c",
"name": "Process DB Results",
"func": "// Replace function in \"Process DB Results\" node\n\nconst originalTopic = msg._originalTopic || '';\nconst dbResults = Array.isArray(msg.payload) ? msg.payload : [];\n\nif (!originalTopic) {\n return null;\n}\n\n// IMPORTANT: Clear socketid to prevent loops back to sender\ndelete msg._socketid;\ndelete msg.socketid;\n\n// Manufacturers query → list for first dropdown\nif (originalTopic === 'getManufacturers') {\n const manufacturers = dbResults\n .map(row => row.manufacturer)\n .filter((mfg, index, arr) => mfg && arr.indexOf(mfg) === index)\n .sort();\n\n msg.topic = 'manufacturersList';\n msg.payload = manufacturers;\n\n node.status({ fill: 'green', shape: 'dot', text: `${manufacturers.length} manufacturers` });\n return msg;\n}\n\n// Preset lists (legacy load/search)\nif (originalTopic === 'loadMoldPresets' || originalTopic === 'searchMoldPresets') {\n const presets = dbResults.map(row => ({\n mold_name: row.mold_name || '',\n manufacturer: row.manufacturer || '',\n theoretical_cavities: Number(row.theoretical_cavities) || 0,\n functional_cavities: Number(row.functional_cavities) || 0\n }));\n\n msg.topic = 'moldPresetsList';\n msg.payload = presets;\n\n node.status({ fill: 'green', shape: 'dot', text: `${presets.length} presets found` });\n return msg;\n}\n\n// Molds for selected manufacturer\nif (originalTopic === 'getMoldsByManufacturer') {\n const presets = dbResults.map(row => ({\n mold_name: row.mold_name || '',\n manufacturer: row.manufacturer || '',\n theoretical_cavities: Number(row.theoretical_cavities) || 0,\n functional_cavities: Number(row.functional_cavities) || 0\n }));\n\n msg.topic = 'moldPresetsList';\n msg.payload = presets;\n\n node.status({ fill: 'blue', shape: 'dot', text: `${presets.length} molds for manufacturer` });\n return msg;\n}\n\n// Result of inserting a new mold\nif (originalTopic === 'addMoldPreset') {\n msg.topic = 'addMoldResult';\n msg.payload = {\n success: true,\n result: msg.payload\n };\n\n node.status({ fill: 'green', shape: 'dot', text: 'Mold added' });\n return msg;\n}\n\nnode.status({ fill: 'yellow', shape: 'ring', text: 'Unknown topic: ' + originalTopic });\nreturn null;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1200,
"y": 80,
"wires": [
[
"0b5740c4a2b298b7"
]
]
},
{
"id": "0a5caf3e23c68e6e",
"type": "link out",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"name": "link out 1",
"mode": "link",
"links": [
"7311641fd09b4d3a"
],
"x": 325,
"y": 280,
"wires": []
},
{
"id": "7311641fd09b4d3a",
"type": "link in",
"z": "cac3a4383120cb57",
"g": "28c173789034639c",
"name": "link in 1",
"links": [
"0a5caf3e23c68e6e"
],
"x": 755,
"y": 100,
"wires": [
[
"eaebd8c719c3d135",
"a1b2c3d4e5f6a7b8"
]
]
},
{
"id": "9bbd4fade968036d",
"type": "function",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "Work Order buttons",
"func": "switch (msg.action) {\n case \"upload-excel\":\n msg._mode = \"upload\";\n return [msg, null, null, null];\n case \"refresh-work-orders\":\n msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY created_at DESC;\";\n return [null, msg, null, null];\n // start/complete unchanged...\n case \"start-work-order\": {\n msg._mode = \"start\";\n const order = msg.payload || {};\n if (!order.id) {\n node.error(\"No work order id supplied for start\", msg);\n return [null, null, null, null];\n }\n msg.startOrder = order;\n\n msg.topic = `\n UPDATE work_orders\n SET\n status = CASE\n WHEN work_order_id = '${order.id}' THEN 'RUNNING'\n ELSE 'PENDING'\n END,\n updated_at = CASE\n WHEN work_order_id = '${order.id}' THEN NOW()\n ELSE updated_at\n END\n WHERE status <> 'DONE';\n `;\n\n global.set(\"activeWorkOrder\", order);\n global.set(\"cycleCount\", 0);\n flow.set(\"lastMachineState\", 0);\n global.set(\"scrapPromptIssuedFor\", null);\n return [null, null, msg, null];\n }\n case \"complete-work-order\": {\n msg._mode = \"complete\";\n const order = msg.payload || {};\n if (!order.id) {\n node.error(\"No work order id supplied for complete\", msg);\n return [null, null, null, null];\n }\n msg.completeOrder = order;\n msg.topic = `\n UPDATE work_orders\n SET status = 'DONE', updated_at = NOW()\n WHERE work_order_id = '${order.id}';\n `;\n global.set(\"activeWorkOrder\", null);\n\n // Phase 2: Clean up time tracking variables\n global.set(\"operatingTime\", 0);\n global.set(\"lastCycleTime\", null);\n global.set(\"cycleCount\", 0);\n flow.set(\"lastMachineState\", 0);\n global.set(\"scrapPromptIssuedFor\", null);\n return [null, null, null, msg];\n }\n case \"scrap-entry\": {\n const { id, scrap } = msg.payload || {};\n const scrapNum = Number(scrap) || 0;\n\n if (!id) {\n node.error(\"No work order id supplied for scrap entry\", msg);\n return [null, null, null, null];\n }\n\n // Update activeWorkOrder with accumulated scrap\n const activeOrder = global.get(\"activeWorkOrder\");\n if (activeOrder && activeOrder.id === id) {\n activeOrder.scrap = (Number(activeOrder.scrap) || 0) + scrapNum;\n global.set(\"activeWorkOrder\", activeOrder);\n }\n\n // Clear prompt flag so it can show again when target reached next time\n global.set(\"scrapPromptIssuedFor\", null);\n\n msg._mode = \"scrap-update\";\n msg.scrapEntry = { id, scrap: scrapNum };\n msg.topic = `\n UPDATE work_orders\n SET\n scrap_parts = scrap_parts + ${scrapNum},\n updated_at = NOW()\n WHERE work_order_id = '${id}';\n `;\n\n // CRITICAL: Do NOT set status='DONE', do NOT clear activeWorkOrder\n return [null, null, msg, null];\n }\n case \"scrap-skip\": {\n // User clicked \"No, Continue\" - respect their \"remind again\" preference\n const { id, remindAgain } = msg.payload || {};\n\n if (!id) {\n node.error(\"No work order id supplied for scrap skip\", msg);\n return [null, null, null, null];\n }\n\n // Only clear prompt flag if user wants to be reminded again\n // By default (unchecked), keep the flag set to prevent loop\n if (remindAgain) {\n global.set(\"scrapPromptIssuedFor\", null);\n }\n // Otherwise, leave scrapPromptIssuedFor as-is (won't prompt again)\n\n msg._mode = \"scrap-skipped\";\n return [null, null, null, null];\n }\n case \"start\": {\n // START button clicked from Home dashboard\n // Enable tracking of cycles for the active work order\n global.set(\"operatingTime\", 0.001);\n global.set(\"trackingEnabled\", true);\n\n // CRITICAL: Clear KPI buffer on production start\n // Prevents stale data from skewing averages if Node-RED was restarted mid-production\n global.set(\"kpiBuffer\", []);\n node.warn('[START] Cleared kpiBuffer for fresh production run');\n\n // Optional: Reset last record time to ensure immediate data point\n global.set(\"lastKPIRecordTime\", Date.now() - 60000);\n\n // Initialize production start time for KPI calculations\n global.set(\"productionStartTime\", Date.now());\n\n // Phase 2: Initialize operating time tracking\n global.set(\"operatingTime\", 0); // Reset operating time counter\n global.set(\"lastCycleTime\", Date.now()); // Initialize last cycle timestamp\n \n // State info added to msg.payload instead \n // Trigger initial KPI calculation\n const activeOrder = global.get(\"activeWorkOrder\") || {};\n msg._mode = \"production-state\";\n \n // Initialize payload object BEFORE setting properties\n msg.payload = msg.payload || {};\n \n // Set state flags at both msg and msg.payload levels for compatibility\n msg.trackingEnabled = true;\n msg.productionStarted = true;\n msg.machineOnline = true;\n \n msg.payload.trackingEnabled = true;\n msg.payload.productionStarted = true;\n msg.payload.machineOnline = true;\n \n return [null, msg, null, null];\n }\n case \"stop\": {\n // Manual STOP button clicked from Home dashboard\n // Disable tracking but keep work order active\n global.set(\"trackingEnabled\", false);\n return [null, null, null, null];\n }\n case \"start-tracking\": {\n const activeOrder = global.get('activeOrder') || {};\n\n // --- 1. EDGE CASE CHECK: Stop if no active order is loaded ---\n if (!activeOrder.id) {\n node.warn('[START] Cannot start tracking: No active order loaded.');\n // Return null for DB (Output 1) and send simple alert message (Output 2)\n return [null, { topic: \"alert\", payload: \"Error: No active work order loaded.\" }, null, null];\n }\n\n // --- 2. GLOBAL STATE UPDATES (Preserved Logic) ---\n // These lines were part of your successful fixes\n global.set(\"trackingEnabled\", true);\n global.set(\"kpiBuffer\", []); // Clear stale KPI data\n global.set(\"lastKPIRecordTime\", Date.now() - 60000); // Allow immediate first record\n global.set(\"lastMachineCycleTime\", Date.now()); // Set machine start time\n node.warn('[START] Cleared kpiBuffer for fresh production run');\n\n // --- 3. DATABASE MESSAGE (Output 1) ---\n // This is the CRITICAL missing piece that failed the MySQL node.\n const dbMsg = {\n // Topic must contain the SQL query string\n topic: `UPDATE work_orders SET production_start_time = ${Date.now()}, is_tracking = 1 WHERE id = '${activeOrder.id}'`,\n payload: []\n };\n\n // --- 4. UI STATE MESSAGE (Output 2) ---\n // This handles the dashboard button change\n const stateMsg = {\n topic: \"machineStatus\",\n payload: msg.payload || {} // Preserve msg.payload initialization fix\n };\n\n // Set all necessary flags for the UI to correctly display \"STOP\"\n stateMsg.payload.trackingEnabled = true;\n stateMsg.payload.productionStarted = true;\n stateMsg.payload.machineOnline = true;\n\n // --- 5. FINAL RETURN [DB_MSG, STATE_MSG, ...] ---\n // Output 1 (DB) gets dbMsg, Output 2 (State) gets stateMsg\n return [dbMsg, stateMsg, null, null];\n }\n}\n",
"outputs": 4,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 930,
"y": 380,
"wires": [
[
"15a6b7b6d8f39fe4",
"942807fcbd15f50c"
],
[
"f6ad294bc02618c9",
"00b6132848964bd9",
"e394d0275e78950c"
],
[
"f6ad294bc02618c9",
"00b6132848964bd9",
"f8f4ca6de445c0c2"
],
[
"f6ad294bc02618c9",
"0ab6a68c1ad50a91"
]
]
},
{
"id": "010de5af3ced0ae3",
"type": "link out",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"name": "link out 2",
"mode": "link",
"links": [
"65ddb4cca6787bde"
],
"x": 325,
"y": 120,
"wires": []
},
{
"id": "65ddb4cca6787bde",
"type": "link in",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "link in 2",
"links": [
"010de5af3ced0ae3"
],
"x": 815,
"y": 440,
"wires": [
[
"9bbd4fade968036d"
]
]
},
{
"id": "596b390d7aaf69fb",
"type": "function",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "Build Insert SQL",
"func": "const rows = Array.isArray(msg.payload) ? msg.payload : [];\nconst vals = rows.map(r => `(\n'${r[\"Work Order ID\"]}',\n'${r[\"SKU\"]}',\n${Number(r[\"Target Quantity\"]) || 0},\n${Number(r[\"Theoretical Cycle Time (Seconds)\"]) || 0},\n'PENDING')`).join(',\\n');\n\nmsg.topic = `\nINSERT INTO work_orders (work_order_id, sku, target_qty, cycle_time, status)\nVALUES\n${vals}\nON DUPLICATE KEY UPDATE\n sku=VALUES(sku),\n target_qty=VALUES(target_qty),\n cycle_time=VALUES(cycle_time);\n`;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1240,
"y": 360,
"wires": [
[
"f6ad294bc02618c9"
]
]
},
{
"id": "f6ad294bc02618c9",
"type": "mysql",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"mydb": "00d8ad2b0277f906",
"name": "mariaDB",
"x": 1220,
"y": 400,
"wires": [
[
"578c92e75bf0f266"
]
]
},
{
"id": "f2bab26e27e2023d",
"type": "function",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "Back to UI",
"func": "const mode = msg._mode || '';\nconst started = msg.startOrder || null;\nconst completed = msg.completeOrder || null;\n\ndelete msg._mode;\ndelete msg.startOrder;\ndelete msg.completeOrder;\ndelete msg.action;\ndelete msg.filename;\n\n// ========================================================\n// MODE: UPLOAD\n// ========================================================\nif (mode === \"upload\") {\n msg.topic = \"uploadStatus\";\n msg.payload = { message: \"✅ Work orders uploaded successfully.\" };\n return [msg, null, null, null];\n}\n\n// ========================================================\n// MODE: SELECT (Load Work Orders)\n// ========================================================\nif (mode === \"select\") {\n const rawRows = Array.isArray(msg.payload) ? msg.payload : [];\n msg.topic = \"workOrdersList\";\n msg.payload = rawRows.map(row => ({\n id: row.work_order_id ?? row.id ?? \"\",\n sku: row.sku ?? \"\",\n target: Number(row.target_qty ?? row.target ?? 0),\n good: Number(row.good_parts ?? row.good ?? 0),\n scrap: Number(row.scrap_count ?? row.scrap ?? 0),\n progressPercent: Number(row.progress_percent ?? row.progress ?? 0),\n status: (row.status ?? \"PENDING\").toUpperCase(),\n lastUpdateIso: row.updated_at ?? row.last_update ?? null,\n cycleTime: Number(row.cycle_time ?? row.theoretical_cycle_time ?? 0)\n }));\n return [msg, null, null, null];\n}\n\n// ========================================================\n// MODE: START WORK ORDER\n// ========================================================\nif (mode === \"start\") {\n const order = started || {};\n const kpis = msg.kpis || global.get(\"currentKPIs\") || {\n oee: 0, availability: 0, performance: 0, quality: 0\n };\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: {\n id: order.id || \"\",\n sku: order.sku || \"\",\n target: Number(order.target) || 0,\n good: Number(order.good) || 0,\n scrap: Number(order.scrap) || 0,\n cycleTime: Number(order.cycleTime || order.theoreticalCycleTime || 0),\n progressPercent: Number(order.progressPercent) || 0,\n lastUpdateIso: order.lastUpdateIso || null,\n kpis: kpis\n }\n };\n\n return [null, homeMsg, null, null];\n}\n\n// ========================================================\n// MODE: COMPLETE WORK ORDER\n// ========================================================\nif (mode === \"complete\") {\n const homeMsg = { topic: \"activeWorkOrder\", payload: null };\n return [null, homeMsg, null, null];\n}\n\n// ========================================================\n// MODE: CYCLE UPDATE DURING PRODUCTION\n// ========================================================\nif (mode === \"cycle\") {\n const cycle = msg.cycle || {};\n\n const workOrderMsg = {\n topic: \"workOrderCycle\",\n payload: {\n id: cycle.id || \"\",\n sku: cycle.sku || \"\",\n target: Number(cycle.target) || 0,\n good: Number(cycle.good) || 0,\n scrap: Number(cycle.scrap) || 0,\n progressPercent: Number(cycle.progressPercent) || 0,\n lastUpdateIso: cycle.lastUpdateIso || new Date().toISOString(),\n status: cycle.progressPercent >= 100 ? \"DONE\" : \"RUNNING\"\n }\n };\n\n const kpis = msg.kpis || global.get(\"currentKPIs\") || {\n oee: 0, availability: 0, performance: 0, quality: 0\n };\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: {\n id: cycle.id || \"\",\n sku: cycle.sku || \"\",\n target: Number(cycle.target) || 0,\n good: Number(cycle.good) || 0,\n scrap: Number(cycle.scrap) || 0,\n cycleTime: Number(cycle.cycleTime) || 0,\n progressPercent: Number(cycle.progressPercent) || 0,\n lastUpdateIso: cycle.lastUpdateIso || new Date().toISOString(),\n kpis: kpis\n }\n };\n\n return [workOrderMsg, homeMsg, null, null];\n}\n\n// ========================================================\n// MODE: MACHINE PRODUCTION STATE\n// ========================================================\nif (mode === \"production-state\") {\n const homeMsg = {\n topic: \"machineStatus\",\n payload: {\n machineOnline: msg.machineOnline ?? true,\n productionStarted: !!msg.productionStarted,\n trackingEnabled: msg.payload?.trackingEnabled ?? msg.trackingEnabled ?? false\n }\n };\n return [null, homeMsg, null, null];\n}\n\n// ========================================================\n// MODE: SCRAP PROMPT\n// ========================================================\nif (mode === \"scrap-prompt\") {\n const prompt = msg.scrapPrompt || {};\n\n const homeMsg = { topic: \"scrapPrompt\", payload: prompt };\n const tabMsg = { ui_control: { tab: \"Home\" } };\n\n // output1: nothing\n // output2: home template\n // output3: tab navigation\n // output4: graphs template (unused here)\n return [null, homeMsg, tabMsg, null];\n}\n\n// ========================================================\n// MODE: SCRAP UPDATE\n// ========================================================\nif (mode === \"scrap-update\") {\n const activeOrder = global.get(\"activeWorkOrder\") || {};\n const kpis = msg.kpis || global.get(\"currentKPIs\") || {\n oee: 0, availability: 0, performance: 0, quality: 0\n };\n\n const homeMsg = {\n topic: \"activeWorkOrder\",\n payload: {\n id: activeOrder.id || \"\",\n sku: activeOrder.sku || \"\",\n target: Number(activeOrder.target) || 0,\n good: Number(activeOrder.good) || 0,\n scrap: Number(activeOrder.scrap) || 0,\n cycleTime: Number(activeOrder.cycleTime) || 0,\n progressPercent: Number(activeOrder.progressPercent) || 0,\n lastUpdateIso: activeOrder.lastUpdateIso || new Date().toISOString(),\n kpis: kpis\n }\n };\n\n return [null, homeMsg, null, null];\n}\n\n// ========================================================\n// MODE: SCRAP COMPLETE\n// ========================================================\nif (mode === \"scrap-complete\") {\n const homeMsg = { topic: \"activeWorkOrder\", payload: null };\n return [null, homeMsg, null, null];\n}\n\n// ========================================================\n// MODE: CHARTS → SEND REAL DATA TO GRAPH TEMPLATE\n// ========================================================\n//if (mode === \"charts\") {\n\n// const realOEE = msg.realOEE || global.get(\"realOEE\") || [];\n// const realAvailability = msg.realAvailability || global.get(\"realAvailability\") || [];\n// const realPerformance = msg.realPerformance || global.get(\"realPerformance\") || [];\n// const realQuality = msg.realQuality || global.get(\"realQuality\") || [];\n\n// const chartsMsg = {\n// topic: \"chartsData\",\n// payload: {\n// oee: realOEE,\n// availability: realAvailability,\n// performance: realPerformance,\n// quality: realQuality\n// }\n// };\n\n // Send ONLY to output #4\n// return [null, null, null, chartsMsg];\n//}\n\n// ========================================================\n// DEFAULT\n// ========================================================\nreturn [null, null, null, null];\n",
"outputs": 4,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1550,
"y": 400,
"wires": [
[
"0779932734d8201c"
],
[
"64661fe6aa2cb83d"
],
[
"fd32602c52d896e9"
],
[]
]
},
{
"id": "0779932734d8201c",
"type": "link out",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "link out 3",
"mode": "link",
"links": [
"6f9de736a538d0d1"
],
"x": 1665,
"y": 360,
"wires": []
},
{
"id": "6f9de736a538d0d1",
"type": "link in",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"name": "link in 3",
"links": [
"0779932734d8201c"
],
"x": 95,
"y": 120,
"wires": [
[
"f1a2b3c4d5e6f7a8"
]
]
},
{
"id": "3772c25d07b07407",
"type": "book",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "",
"raw": false,
"x": 1350,
"y": 320,
"wires": [
[
"c2b272494952cd98"
]
]
},
{
"id": "c2b272494952cd98",
"type": "sheet",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "",
"sheetName": "Sheet1",
"x": 1470,
"y": 320,
"wires": [
[
"87d85c86e4773aa5"
]
]
},
{
"id": "87d85c86e4773aa5",
"type": "sheet-to-json",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "",
"raw": "false",
"range": "",
"header": "default",
"blankrows": false,
"x": 1610,
"y": 320,
"wires": [
[
"596b390d7aaf69fb"
]
]
},
{
"id": "15a6b7b6d8f39fe4",
"type": "function",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "Base64",
"func": "const filename =\n msg.filename ||\n (msg.meta && msg.meta.filename) ||\n (msg.payload && msg.payload.filename) ||\n msg.name ||\n 'upload.xlsx';\n\nconst candidates = [];\nif (typeof msg.payload === 'string') candidates.push(msg.payload);\nif (msg.payload && typeof msg.payload.payload === 'string') candidates.push(msg.payload.payload);\nif (msg.payload && typeof msg.payload.file === 'string') candidates.push(msg.payload.file);\nif (msg.payload && typeof msg.payload.base64 === 'string') candidates.push(msg.payload.base64);\nif (typeof msg.file === 'string') candidates.push(msg.file);\nif (typeof msg.data === 'string') candidates.push(msg.data);\n\nfunction stripDataUrl(s) {\n return (s && s.startsWith('data:')) ? s.split(',')[1] : s;\n}\n\nlet b64 = candidates.map(stripDataUrl).find(s => typeof s === 'string' && s.length > 0);\nif (!b64 && Buffer.isBuffer(msg.payload)) { msg.filename = filename; return msg; }\nif (!b64) { node.error('No base64 data found on msg', msg); return null; }\n\nmsg.payload = Buffer.from(b64, 'base64');\nmsg.filename = filename;\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1220,
"y": 320,
"wires": [
[
"3772c25d07b07407"
]
]
},
{
"id": "64661fe6aa2cb83d",
"type": "link out",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "link out 4",
"mode": "link",
"links": [
"16af50d6fce977a8"
],
"x": 1665,
"y": 400,
"wires": []
},
{
"id": "16af50d6fce977a8",
"type": "link in",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"name": "link in 4",
"links": [
"64661fe6aa2cb83d"
],
"x": 95,
"y": 80,
"wires": [
[
"1821c4842945ecd8"
]
]
},
{
"id": "578c92e75bf0f266",
"type": "function",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "Refresh Trigger",
"func": "if (msg._mode === \"start\" || msg._mode === \"complete\") {\n // Preserve original message for Back to UI (output 2)\n const originalMsg = {...msg};\n // Create select message for refreshing WO table (output 1)\n msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY updated_at DESC;\";\n return [msg, originalMsg];\n}\nif (msg._mode === \"cycle\" || msg._mode === \"production-state\") {\n return [null, msg];\n}\nif (msg._mode === \"scrap-prompt\") {\n return [null, msg];\n}\nif (msg._mode === \"scrap-complete\") {\n // Preserve original message for Back to UI (output 2)\n const originalMsg = {...msg};\n // Create select message for refreshing WO table (output 1)\n msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY updated_at DESC;\";\n return [msg, originalMsg];\n}\nreturn [null, msg];",
"outputs": 2,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1380,
"y": 400,
"wires": [
[
"f6ad294bc02618c9"
],
[
"f2bab26e27e2023d"
]
]
},
{
"id": "0d023d87a13bf56f",
"type": "function",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "Machine cycles",
"func": "const current = Number(msg.payload) || 0;\n\nlet zeroStreak = flow.get(\"zeroStreak\") || 0;\nzeroStreak = current === 0 ? zeroStreak + 1 : 0;\nflow.set(\"zeroStreak\", zeroStreak);\n\nconst prev = flow.get(\"lastMachineState\") ?? 0;\nflow.set(\"lastMachineState\", current);\n\nglobal.set(\"machineOnline\", true); // force ONLINE for now\n\nlet productionRunning = !!global.get(\"productionStarted\");\nlet stateChanged = false;\n\nif (current === 1 && !productionRunning) {\n productionRunning = true;\n stateChanged = true;\n} else if (current === 0 && zeroStreak >= 2 && productionRunning) {\n productionRunning = false;\n stateChanged = true;\n}\n\nglobal.set(\"productionStarted\", productionRunning);\n\nconst stateMsg = stateChanged\n ? {\n _mode: \"production-state\",\n machineOnline: true,\n productionStarted: productionRunning\n }\n : null;\n\nconst activeOrder = global.get(\"activeWorkOrder\");\nconst cavities = Number(global.get(\"moldActive\") || 0);\nif (!activeOrder || !activeOrder.id || cavities <= 0) {\n // We still want to pass along any state change even if there's no active WO.\n return [null, stateMsg, { _triggerKPI: true }];\n}\n\n// Check if tracking is enabled (START button clicked)\nvar trackingEnabled = !!global.get(\"trackingEnabled\");\nif (!trackingEnabled) {\n // Cycles are happening but we're not tracking them yet\n return [null, stateMsg, { _triggerKPI: true }];\n}\n\n// only count rising edges (0 -> 1) for production totals\nif (prev === 1 || current !== 1) {\n return [null, stateMsg, { _triggerKPI: true }];\n}\n\nlet cycles = Number(global.get(\"cycleCount\") || 0) + 1;\nglobal.set(\"cycleCount\", cycles);\n\n// ===== PHASE 2: OPERATING TIME TRACKING =====\n// Track actual operating time between cycles\nconst now = Date.now();\nconst lastCycleTime = global.get(\"lastCycleTime\") || now;\n\n// Calculate time since last cycle (in milliseconds)\nconst timeSinceLastCycle = now - lastCycleTime;\n\n\n// Accumulate operating time (in seconds)\nlet operatingTime = global.get(\"operatingTime\") || 0;\noperatingTime += (timeSinceLastCycle / 1000);\n\nglobal.set(\"operatingTime\", operatingTime);\nnode.warn(`[MACHINE CYCLE] operatingTime updated to ${operatingTime}`);\nglobal.set(\"lastCycleTime\", now);\n// ===== END OPERATING TIME TRACKING =====\n\n\n\n// Calculate good parts: total produced minus accumulated scrap\nconst scrapTotal = Number(activeOrder.scrap) || 0;\nconst totalProduced = cycles * cavities;\nconst produced = totalProduced - scrapTotal;\nconst target = Number(activeOrder.target) || 0;\nconst progress = target > 0 ? Math.min(100, Math.round((produced / target) * 100)) : 0;\n\nactiveOrder.good = produced;\nactiveOrder.progressPercent = progress;\nactiveOrder.lastUpdateIso = new Date().toISOString();\nglobal.set(\"activeWorkOrder\", activeOrder);\n\nconst promptIssued = global.get(\"scrapPromptIssuedFor\") || null;\nif (!promptIssued && target > 0 && produced >= target) {\n global.set(\"scrapPromptIssuedFor\", activeOrder.id);\n msg._mode = \"scrap-prompt\";\n msg.scrapPrompt = {\n id: activeOrder.id,\n sku: activeOrder.sku || \"\",\n target,\n produced\n };\n return [null, msg]; // bypass the DB update on this cycle\n}\n\nconst dbMsg = {\n _mode: \"cycle\",\n cycle: {\n id: activeOrder.id,\n sku: activeOrder.sku || \"\",\n target,\n good: produced,\n scrap: Number(activeOrder.scrap) || 0,\n cycleTime: Number(activeOrder.cycleTime || activeOrder.theoreticalCycleTime || 0),\n progressPercent: progress,\n lastUpdateIso: activeOrder.lastUpdateIso,\n machineOnline: true,\n productionStarted: productionRunning\n },\n topic: `\n UPDATE work_orders\n SET\n good_parts = ${produced},\n progress_percent = ${progress},\n updated_at = NOW()\n WHERE work_order_id = '${activeOrder.id}';\n `\n};\n\n// Prepare KPI trigger message (always sent)\nconst kpiTrigger = { _triggerKPI: true };\n\n// Update last machine cycle time when a successful cycle occurs\n// This is used for time-based availability logic\nvar trackingEnabled = !!global.get(\"trackingEnabled\");\nif (trackingEnabled && dbMsg) {\n // dbMsg being non-null implies a cycle was recorded\n global.set(\"lastMachineCycleTime\", Date.now());\n}\n\n// Output to 3 paths:\n// 1: DB update (only when tracking)\n// 2: State message (always)\n// 3: KPI trigger (always, for continuous updates)\nreturn [dbMsg, stateMsg, kpiTrigger];",
"outputs": 3,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 640,
"y": 400,
"wires": [
[
"dbc7a5ee041845ed"
],
[
"00b6132848964bd9"
],
[]
]
},
{
"id": "dbc7a5ee041845ed",
"type": "link out",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "link out 5",
"mode": "link",
"links": [
"76ce53cf1ae40e9c"
],
"x": 755,
"y": 380,
"wires": []
},
{
"id": "76ce53cf1ae40e9c",
"type": "link in",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "link in 5",
"links": [
"dbc7a5ee041845ed"
],
"x": 1105,
"y": 400,
"wires": [
[
"f6ad294bc02618c9"
]
]
},
{
"id": "e15d6c1f78b644a2",
"type": "link out",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "link out 6",
"mode": "link",
"links": [
"0d6ec01f421acdef"
],
"x": 755,
"y": 420,
"wires": []
},
{
"id": "0d6ec01f421acdef",
"type": "link in",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "link in 6",
"links": [
"e15d6c1f78b644a2"
],
"x": 1295,
"y": 440,
"wires": [
[
"578c92e75bf0f266"
]
]
},
{
"id": "fd32602c52d896e9",
"type": "link out",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "link out 7",
"mode": "link",
"links": [
"2f04a72fdeb67f3f"
],
"x": 1665,
"y": 440,
"wires": []
},
{
"id": "2f04a72fdeb67f3f",
"type": "link in",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"name": "link in 7",
"links": [
"fd32602c52d896e9"
],
"x": 355,
"y": 220,
"wires": [
[
"a7d58e15929b3d8c"
]
]
},
{
"id": "0b5740c4a2b298b7",
"type": "link out",
"z": "cac3a4383120cb57",
"g": "28c173789034639c",
"name": "link out 8",
"mode": "link",
"links": [
"8f890f97aa9257c7"
],
"x": 1395,
"y": 160,
"wires": []
},
{
"id": "8f890f97aa9257c7",
"type": "link in",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"name": "link in 8",
"links": [
"0b5740c4a2b298b7"
],
"x": 95,
"y": 280,
"wires": [
[
"f5a6b7c8d9e0f1a2"
]
]
},
{
"id": "00b6132848964bd9",
"type": "function",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "Calculate KPIs",
"func": "// ========================================\n// OEE KPI ENGINE – STABLE, NO DROPS, NO RESETS\n// ========================================\n\n// Pull system state\n// ========== MULTI-SOURCE HANDLING ==========\n// This function receives triggers from both Machine Cycles and Scrap Submission\n// Must execute regardless of message content as long as it receives ANY trigger\n\nvar trackingEnabled = global.get(\"trackingEnabled\");\nvar activeOrder = global.get(\"activeWorkOrder\") || {};\n\n// Guard against missing critical data\nif (!trackingEnabled || !activeOrder.id) {\n // Can't calculate meaningful KPIs without tracking or active order\n return null;\n}\n\n\nvar activeOrder = global.get(\"activeWorkOrder\") || {};\nconst cycleCount = global.get(\"cycleCount\") || 0;\nconst cavities = Number(global.get(\"moldActive\")?.cavities) || 1;\nvar trackingEnabled = global.get(\"trackingEnabled\") || false;\n\nconst prev = global.get(\"currentKPIs\") || {\n availability: 100,\n performance: 100,\n quality: 100,\n oee: 100\n};\n\nmsg.kpis = {\n quality: prev.quality,\n performance: prev.performance,\n availability: prev.availability,\n oee: prev.oee\n};\n\n// ========================================\n// 1) QUALITY\n// ----------------------------------------\nconst good = Number(activeOrder.good) || 0;\nconst scrap = Number(activeOrder.scrap) || 0;\nconst total = good + scrap;\n\nif (total > 0) {\n msg.kpis.quality = (good / total) * 100;\n} else {\n msg.kpis.quality = prev.quality ?? 100;\n}\n\nmsg.kpis.quality = Math.min(100, msg.kpis.quality);\n\n// ========================================\n// 2) PERFORMANCE\n// ----------------------------------------\nconst idealCycle = Number(activeOrder.cycleTime) || 0;\nconst operatingTime = global.get(\"operatingTime\") || 0;\n\nif (cycleCount > 0 && idealCycle > 0 && operatingTime > 0) {\n const targetCount = operatingTime / idealCycle;\n msg.kpis.performance = (cycleCount / targetCount) * 100;\n msg.kpis.performance = Math.min(100, msg.kpis.performance);\n}\nelse if (trackingEnabled) {\n msg.kpis.performance = prev.performance ?? 100;\n}\nelse {\n msg.kpis.performance = prev.performance ?? 100;\n}\n\n// ========================================\n// 3) AVAILABILITY (with time-based pause detection)\n// ----------------------------------------\nconst now = Date.now();\nconst lastCycleTime = global.get(\"lastMachineCycleTime\") || now;\nconst timeSinceLastCycle = now - lastCycleTime;\n\nconst BRIEF_PAUSE_THRESHOLD = 5 * 60 * 1000; // 5 minutes\n\nlet productionStartTime = global.get(\"productionStartTime\");\n\nnode.warn(\"AVAILABILITY CHECK ➤\");\nnode.warn(\"trackingEnabled: \" + trackingEnabled);\nnode.warn(\"now: \" + now);\nnode.warn(\"lastCycleTime: \" + lastCycleTime);\nnode.warn(\"timeSinceLastCycle: \" + timeSinceLastCycle);\nnode.warn(\"productionStartTime: \" + productionStartTime);\nnode.warn(\"operatingTime: \" + operatingTime);\n\n\nif (!trackingEnabled || timeSinceLastCycle > BRIEF_PAUSE_THRESHOLD) {\n // Legitimately stopped or long pause\n msg.kpis.availability = 0;\n global.set(\"lastKPIValues\", null); // Clear history\n} else if (trackingEnabled && productionStartTime && operatingTime > 0) {\n // Calculate normally based on actual operating time\n const elapsedSec = (now - productionStartTime) / 1000;\n if (elapsedSec > 0) {\n msg.kpis.availability = (operatingTime / elapsedSec) * 100;\n msg.kpis.availability = Math.min(100, msg.kpis.availability);\n }\n global.set(\"lastKPIValues\", msg.kpis);\n} else if (trackingEnabled && productionStartTime) {\n // Production just started, no cycles yet - assume 100% (optimistic)\n // Will be corrected once real cycles begin\n msg.kpis.availability = 100;\n node.warn('[Availability] Production starting - showing 100% until first cycle');\n} else {\n // Brief pause - maintain last known value\n const prevKPIs = global.get(\"lastKPIValues\") || {};\n msg.kpis.availability = prevKPIs.availability || 0;\n}\n\n// NOTE: lastMachineCycleTime is updated in Machine Cycles function ONLY\n// This keeps the \"machine pulse\" signal clean and separate from KPI calculation\n\n// ========================================\n// 4) OEE\n// ----------------------------------------\nmsg.kpis.oee =\n (msg.kpis.availability *\n msg.kpis.performance *\n msg.kpis.quality) / 10000;\n\n// Round nicely\nfor (let k of [\"quality\",\"performance\",\"availability\",\"oee\"]) {\n msg.kpis[k] = Math.round(msg.kpis[k] * 10) / 10;\n}\n\n// Save as new \"stable\" KPIs\nglobal.set(\"currentKPIs\", msg.kpis);\n\n// Output\nreturn msg;\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 980,
"y": 540,
"wires": [
[
"578c92e75bf0f266",
"dc9b9a26af05dfa8",
"ab31039047323f42"
]
]
},
{
"id": "alert_process_function",
"type": "function",
"z": "cac3a4383120cb57",
"name": "Process Alert for DB",
"func": "// Process incoming alert\nif (msg.payload && msg.payload.action === 'alert') {\n const alert = msg.payload;\n\n // Format timestamp for MySQL DATETIME\n const timestamp = alert.timestamp ?\n new Date(alert.timestamp).toISOString().slice(0, 19).replace('T', ' ') :\n new Date().toISOString().slice(0, 19).replace('T', ' ');\n\n // Prepare INSERT query\n const alertType = (alert.type || 'Unknown').replace(/'/g, \"''\"); // Escape quotes\n const description = (alert.description || '').replace(/'/g, \"''\"); // Escape quotes\n\n msg.topic = `\n INSERT INTO alerts_log (timestamp, alert_type, description)\n VALUES ('${timestamp}', '${alertType}', '${description}')\n `;\n\n node.status({\n fill: 'green',\n shape: 'dot',\n text: `Logging: ${alertType}`\n });\n\n // Store original message for passthrough\n msg._originalAlert = alert;\n\n return msg;\n}\n\nreturn null;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 400,
"y": 620,
"wires": [
[
"alert_insert_mysql"
]
]
},
{
"id": "alert_insert_mysql",
"type": "mysql",
"z": "cac3a4383120cb57",
"mydb": "00d8ad2b0277f906",
"name": "Log Alert to DB",
"x": 650,
"y": 620,
"wires": [
[
"alert_insert_debug"
]
]
},
{
"id": "alert_insert_debug",
"type": "debug",
"z": "cac3a4383120cb57",
"name": "Alert Logged",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 850,
"y": 620,
"wires": []
},
{
"id": "alert_flow_comment",
"type": "comment",
"z": "cac3a4383120cb57",
"name": "Alert Logging: Template → Process → MySQL Insert → Debug",
"info": "Alerts from the UI are logged to the alerts_log table.\nTable structure:\n- id (auto-increment)\n- timestamp (when alert occurred)\n- alert_type (category)\n- description (optional notes)\n- created_at (when logged)",
"x": 530,
"y": 580,
"wires": []
},
{
"id": "394cfca6b72f6444",
"type": "link in",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"name": "link in 9",
"links": [
"d39000415ba85495"
],
"x": 95,
"y": 200,
"wires": [
[
"f3a4b5c6d7e8f9a0"
]
]
},
{
"id": "d39000415ba85495",
"type": "link out",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "link out 9",
"mode": "link",
"links": [
"394cfca6b72f6444"
],
"x": 1445,
"y": 500,
"wires": []
},
{
"id": "dc9b9a26af05dfa8",
"type": "function",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "Record KPI History",
"func": "// Complete Record KPI History function with robust initialization and averaging\n\n// ========== INITIALIZATION ==========\n// Initialize buffer\nlet buffer = global.get(\"kpiBuffer\");\nif (!buffer || !Array.isArray(buffer)) {\n buffer = [];\n global.set(\"kpiBuffer\", buffer);\n node.warn('[KPI History] Initialized kpiBuffer');\n}\n\n// Initialize last record time\nlet lastRecordTime = global.get(\"lastKPIRecordTime\");\nif (!lastRecordTime || typeof lastRecordTime !== 'number') {\n // Set to 1 minute ago to ensure immediate recording on startup\n lastRecordTime = Date.now() - 60000;\n global.set(\"lastKPIRecordTime\", lastRecordTime);\n node.warn('[KPI History] Initialized lastKPIRecordTime');\n}\n\n// ========== ACCUMULATE ==========\nconst kpis = msg.payload?.kpis || msg.kpis;\nif (!kpis) {\n node.warn('[KPI History] No KPIs in message, skipping');\n return null;\n}\n\nbuffer.push({\n timestamp: Date.now(),\n oee: kpis.oee || 0,\n availability: kpis.availability || 0,\n performance: kpis.performance || 0,\n quality: kpis.quality || 0\n});\n\n// Prevent buffer from growing too large (safety limit)\nif (buffer.length > 100) {\n buffer = buffer.slice(-60); // Keep last 60 entries\n node.warn('[KPI History] Buffer exceeded 100 entries, trimmed to 60');\n}\n\nglobal.set(\"kpiBuffer\", buffer);\n\n// ========== CHECK IF TIME TO RECORD ==========\nconst now = Date.now();\nconst timeSinceLastRecord = now - lastRecordTime;\nconst ONE_MINUTE = 60 * 1000;\n\nif (timeSinceLastRecord < ONE_MINUTE) {\n // Not time to record yet\n return null; // Don't send to charts yet\n}\n\n// ========== CALCULATE AVERAGES ==========\nif (buffer.length === 0) {\n node.warn('[KPI History] Buffer empty at recording time, skipping');\n return null;\n}\n\nconst avg = {\n oee: buffer.reduce((sum, d) => sum + d.oee, 0) / buffer.length,\n availability: buffer.reduce((sum, d) => sum + d.availability, 0) / buffer.length,\n performance: buffer.reduce((sum, d) => sum + d.performance, 0) / buffer.length,\n quality: buffer.reduce((sum, d) => sum + d.quality, 0) / buffer.length\n};\n\nnode.warn(`[KPI History] Recording averaged KPIs from ${buffer.length} samples: OEE=${avg.oee.toFixed(1)}%`);\n\n// ========== RECORD TO HISTORY ==========\n// Load history arrays\nlet oeeHist = global.get(\"realOEE\") || [];\nlet availHist = global.get(\"realAvailability\") || [];\nlet perfHist = global.get(\"realPerformance\") || [];\nlet qualHist = global.get(\"realQuality\") || [];\n\n// Append averaged values\noeeHist.push({ timestamp: now, value: Math.round(avg.oee * 10) / 10 });\navailHist.push({ timestamp: now, value: Math.round(avg.availability * 10) / 10 });\nperfHist.push({ timestamp: now, value: Math.round(avg.performance * 10) / 10 });\nqualHist.push({ timestamp: now, value: Math.round(avg.quality * 10) / 10 });\n\n// Trim arrays (avoid memory explosion)\noeeHist = oeeHist.slice(-300);\navailHist = availHist.slice(-300);\nperfHist = perfHist.slice(-300);\nqualHist = qualHist.slice(-300);\n\n// Save\nglobal.set(\"realOEE\", oeeHist);\nglobal.set(\"realAvailability\", availHist);\nglobal.set(\"realPerformance\", perfHist);\nglobal.set(\"realQuality\", qualHist);\n\n// Update global state\nglobal.set(\"lastKPIRecordTime\", now);\nglobal.set(\"kpiBuffer\", []); // Clear buffer\n\n// Send to graphs\nreturn {\n topic: \"chartsData\",\n payload: {\n oee: oeeHist,\n availability: availHist,\n performance: perfHist,\n quality: qualHist\n }\n};",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1190,
"y": 500,
"wires": [
[
"d39000415ba85495"
]
]
},
{
"id": "dc74dbc51dd757ba",
"type": "function",
"z": "cac3a4383120cb57",
"g": "b7ab5e0cc02b9508",
"name": "Send to template",
"func": "const realOEE = global.get(\"realOEE\");\nconst realAvailability = global.get(\"realAvailability\");\nconst realPerformance = global.get(\"realPerformance\");\nconst realQuality = global.get(\"realQuality\");\n\nconst chartsMsg = {\n topic: \"chartsData\",\n payload: {\n oee: realOEE,\n availability: realAvailability,\n performance: realPerformance,\n quality: realQuality\n }\n};\nreturn [chartsMsg];\n",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1450,
"y": 560,
"wires": [
[]
]
},
{
"id": "fcee023b62d44e58",
"type": "inject",
"z": "cac3a4383120cb57",
"name": "Init on Deploy",
"props": [
{
"p": "payload"
}
],
"repeat": "",
"crontab": "",
"once": true,
"onceDelay": 0.1,
"topic": "",
"payload": "",
"payloadType": "date",
"x": 150,
"y": 80,
"wires": [
[
"952cd0a9a4504f2b"
]
]
},
{
"id": "952cd0a9a4504f2b",
"type": "function",
"z": "cac3a4383120cb57",
"name": "Initialize Global Variables",
"func": "// Initialize Global Variables - Run on Deploy\nnode.warn('[INIT] Initializing global variables');\n\n// KPI Buffer for averaging\nif (!global.get(\"kpiBuffer\")) {\n global.set(\"kpiBuffer\", []);\n node.warn('[INIT] Set kpiBuffer to []');\n}\n\n// Last KPI record time - set to 1 min ago for immediate first record\nif (!global.get(\"lastKPIRecordTime\")) {\n global.set(\"lastKPIRecordTime\", Date.now() - 60000);\n node.warn('[INIT] Set lastKPIRecordTime');\n}\n\n// Last machine cycle time - set to now to prevent immediate 0% availability\nif (!global.get(\"lastMachineCycleTime\")) {\n global.set(\"lastMachineCycleTime\", Date.now());\n node.warn('[INIT] Set lastMachineCycleTime to prevent 0% availability on startup');\n}\n\n// Last KPI values\nif (!global.get(\"lastKPIValues\")) {\n global.set(\"lastKPIValues\", {});\n node.warn('[INIT] Set lastKPIValues to {}');\n}\n\nnode.warn('[INIT] Global variable initialization complete');\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 380,
"y": 80,
"wires": [
[]
]
},
{
"id": "f8f4ca6de445c0c2",
"type": "debug",
"z": "cac3a4383120cb57",
"name": "Output 3 WO",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 1330,
"y": 720,
"wires": []
},
{
"id": "ab31039047323f42",
"type": "debug",
"z": "cac3a4383120cb57",
"name": "Calculate KPIS",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 1140,
"y": 800,
"wires": []
},
{
"id": "e394d0275e78950c",
"type": "debug",
"z": "cac3a4383120cb57",
"name": "Output 2 WO",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 1330,
"y": 680,
"wires": []
},
{
"id": "0ab6a68c1ad50a91",
"type": "debug",
"z": "cac3a4383120cb57",
"name": "Output 4 WO",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 1330,
"y": 760,
"wires": []
},
{
"id": "942807fcbd15f50c",
"type": "debug",
"z": "cac3a4383120cb57",
"name": "Output 1 WO",
"active": true,
"tosidebar": true,
"console": false,
"tostatus": false,
"complete": "true",
"targetType": "full",
"statusVal": "",
"statusType": "auto",
"x": 1330,
"y": 640,
"wires": []
}
]