[ { "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" ], "x": 34, "y": 19, "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" ], "x": 804, "y": 279, "w": 902, "h": 202 }, { "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" ], "x": 714, "y": 19, "w": 682, "h": 142 }, { "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
Work Order ID
\n
 
\n
\n
\n
SKU
\n
 
\n
\n
\n
Cycle Time
\n
0
\n
\n
\n
\n
\n
0%
\n
\n
\n\n
\n
\n
Good Parts
\n
0
\n
out of 0
\n
\n\n
\n
MachineOFFLINE
\n
ProductionSTOPPED
\n
\n\n
\n \n
\n
\n
\n
\n
\n
\n
\n

Work Order {{ ::scrapPrompt.orderId }}

\n

Produced {{ scrapPrompt.produced | number }} pieces (target {{ scrapPrompt.target }})

\n

Were there any scrap parts?

\n
\n \n \n
\n
\n \n \n
\n
\n
\n\n\n\n", "storeOutMessages": true, "fwdInMessages": true, "resendOnRefresh": true, "templateScope": "local", "className": "", "x": 180, "y": 60, "wires": [ [ "a7d58e15929b3d8c" ] ] }, { "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

Alerts

\n
\n\n
\n \n \n \n
\n\n
\n
\n \n \n
\n
\n \n \n
\n \n
\n
\n
\n
\n\n\n", "storeOutMessages": true, "fwdInMessages": true, "resendOnRefresh": true, "templateScope": "local", "className": "", "x": 180, "y": 140, "wires": [ [ "a7d58e15929b3d8c" ] ] }, { "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

Graphs

\n
\n\n
\n
\n

OEE – Last 24h

\n
\n
\n
\n

Availability – Last 7 days

\n
\n
\n
\n

Performance – Last 7 days

\n
\n
\n
\n

Quality – Last 7 days

\n
\n
\n
\n
\n
\n
\n\n\n", "storeOutMessages": true, "fwdInMessages": true, "resendOnRefresh": true, "templateScope": "local", "className": "", "x": 190, "y": 180, "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

Help

\n
\n\n
\n

About this Dashboard

\n

This interface centralizes Overall Equipment Effectiveness metrics, real-time production details, and critical status indicators. Each tab follows a unified layout so operators can scan performance, alerts, and configuration without relearning navigation.

\n
\n\n
\n

How to Start / Stop Production

\n

Navigate to the Work Orders tab, select the required job, and use the primary controls to begin or end production. Always log the reason for stoppages using the Alerts tab, and confirm machine readiness before resuming. Follow your facility’s standard operating procedure for approvals and sign-off.

\n
\n
\n
\n
\n\n\n", "storeOutMessages": true, "fwdInMessages": true, "resendOnRefresh": true, "templateScope": "local", "className": "", "x": 180, "y": 220, "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

Settings

\n
\n\n
\n

Mold Presets

\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n
\n \n
\n

Select a manufacturer and mold from the dropdowns above.

\n

If you can't find the mold you're looking for, add a new one:

\n \n
\n \n \n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n \n \n
\n
\n
\n
\n\n
\n

Mold Configuration

\n
\n
\n \n \n
\n
\n \n \n
\n
\n
\n\n
\n

Integrations

\n \n
\n
\n
\n
\n\n\n\n", "storeOutMessages": true, "fwdInMessages": true, "resendOnRefresh": true, "templateScope": "local", "className": "", "x": 190, "y": 260, "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
\n
\n
\n

Work Orders

\n
\n \n \n \n \n \n
\n
\n\n
\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
IDSKUTARGETGOODSCRAPPROGRESSSTATUSLAST UPDATE
\n
\n
0 items
\n
\n
\n
\n
\n\n\n", "storeOutMessages": true, "fwdInMessages": true, "resendOnRefresh": true, "templateScope": "local", "className": "", "x": 180, "y": 100, "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": 420, "y": 160, "wires": [ [ "cc81a9dbfd443d62" ] ] }, { "id": "cc81a9dbfd443d62", "type": "ui_ui_control", "z": "cac3a4383120cb57", "g": "ec32d0a62eacfb22", "name": "", "events": "all", "x": 580, "y": 160, "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": 540, "y": 120, "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": 60, "wires": [ [] ] }, { "id": "a1b2c3d4e5f6a7b8", "type": "function", "z": "cac3a4383120cb57", "g": "28c173789034639c", "name": "Mold Presets Handler", "func": "const topic = msg.topic || '';\nconst payload = msg.payload || {};\n\n// Log every incoming request\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": 120, "wires": [ [ "c9d8e7f6a5b4c3d2" ] ] }, { "id": "c9d8e7f6a5b4c3d2", "type": "mysql", "z": "cac3a4383120cb57", "g": "28c173789034639c", "mydb": "00d8ad2b0277f906", "name": "Mold Presets DB", "x": 1050, "y": 120, "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": 1270, "y": 120, "wires": [ [ "f5a6b7c8d9e0f1a2" ] ] }, { "id": "0a5caf3e23c68e6e", "type": "link out", "z": "cac3a4383120cb57", "g": "ec32d0a62eacfb22", "name": "link out 1", "mode": "link", "links": [ "7311641fd09b4d3a" ], "x": 305, "y": 260, "wires": [] }, { "id": "7311641fd09b4d3a", "type": "link in", "z": "cac3a4383120cb57", "g": "28c173789034639c", "name": "link in 1", "links": [ "0a5caf3e23c68e6e" ], "x": 755, "y": 60, "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 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 if (!id) {\n node.error(\"No work order id supplied for scrap entry\", msg);\n return [null, null, null, null];\n }\n\n msg._mode = \"scrap-complete\";\n msg.scrapEntry = { id, scrap: Number(scrap) || 0 };\n\n msg.topic = `\n UPDATE work_orders\n SET\n scrap_parts = scrap_parts + ${Number(scrap) || 0},\n good_parts = GREATEST(good_parts - ${Number(scrap) || 0}, 0),\n status = 'DONE',\n updated_at = NOW()\n WHERE work_order_id = '${id}';\n `;\n\n global.set(\"scrapPromptIssuedFor\", null);\n global.set(\"activeWorkOrder\", null);\n global.set(\"cycleCount\", 0);\n\n return [null, null, msg, null]; // route through the MySQL node (same output as start/complete)\n }\n}\n", "outputs": 4, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 970, "y": 400, "wires": [ [ "15a6b7b6d8f39fe4" ], [ "f6ad294bc02618c9" ], [ "f6ad294bc02618c9" ], [ "f6ad294bc02618c9" ] ] }, { "id": "010de5af3ced0ae3", "type": "link out", "z": "cac3a4383120cb57", "g": "ec32d0a62eacfb22", "name": "link out 2", "mode": "link", "links": [ "65ddb4cca6787bde" ], "x": 305, "y": 100, "wires": [] }, { "id": "65ddb4cca6787bde", "type": "link in", "z": "cac3a4383120cb57", "g": "b7ab5e0cc02b9508", "name": "link in 2", "links": [ "010de5af3ced0ae3" ], "x": 845, "y": 400, "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\nif (mode === \"upload\") {\n msg.topic = \"uploadStatus\";\n msg.payload = { message: \"βœ… Work orders uploaded successfully.\" };\n return [msg, null];\n}\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];\n}\n\nif (mode === \"start\") {\n const order = started || {};\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 }\n };\n return [null, homeMsg];\n}\n\nif (mode === \"complete\") {\n const homeMsg = { topic: \"activeWorkOrder\", payload: null };\n return [null, homeMsg];\n}\n\nif (mode === \"cycle\") {\n const cycle = msg.cycle || {};\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 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 }\n };\n\n return [workOrderMsg, homeMsg];\n}\nif (mode === \"production-state\") {\n const homeMsg = {\n topic: \"machineStatus\",\n payload: {\n machineOnline: msg.machineOnline ?? true,\n productionStarted: !!msg.productionStarted\n }\n };\n return [null, homeMsg];\n}\nif (mode === \"scrap-prompt\") {\n const prompt = msg.scrapPrompt || {};\n const homeMsg = { topic: \"scrapPrompt\", payload: prompt };\n const tabMsg = { ui_control: { tab: \"Home\" } };\n\n // output1: nothing, output2: Home template, output3: Tab navigation\n return [null, homeMsg, tabMsg];\n}\n\nif (mode === \"scrap-complete\") {\n const homeMsg = { topic: \"activeWorkOrder\", payload: null };\n return [null, homeMsg];\n}\nreturn [null, null];", "outputs": 3, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1570, "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": 75, "y": 100, "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": 75, "y": 60, "wires": [ [ "1821c4842945ecd8" ] ] }, { "id": "578c92e75bf0f266", "type": "function", "z": "cac3a4383120cb57", "g": "b7ab5e0cc02b9508", "name": "Refresh Trigger", "func": "if (msg._mode === \"start\" || msg._mode === \"complete\") {\n msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY updated_at DESC;\";\n return [msg, null];\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 msg._mode = \"select\";\n msg.topic = \"SELECT * FROM work_orders ORDER BY updated_at DESC;\";\n return [msg, null];\n}\nreturn [null, msg];", "outputs": 2, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 1400, "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];\n}\n\n// only count rising edges (0 -> 1) for production totals\nif (prev === 1 || current !== 1) {\n return [null, stateMsg];\n}\n\nlet cycles = Number(global.get(\"cycleCount\") || 0) + 1;\nglobal.set(\"cycleCount\", cycles);\n\nconst produced = cycles * cavities;\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\nreturn [dbMsg, stateMsg];", "outputs": 2, "timeout": 0, "noerr": 0, "initialize": "", "finalize": "", "libs": [], "x": 640, "y": 400, "wires": [ [ "dbc7a5ee041845ed" ], [ "e15d6c1f78b644a2" ] ] }, { "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": 1115, "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": 335, "y": 200, "wires": [ [ "a7d58e15929b3d8c" ] ] } ]