Files
Projects-plastic/flows.json.backup_phase2
Marcelo b66cb97f16 MVP
2025-11-28 09:11:59 -06:00

1315 lines
155 KiB
Plaintext

[
{
"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"
],
"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",
"0b5740c4a2b298b7"
],
"x": 714,
"y": 19,
"w": 722,
"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": "<style>\n@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@600;700&display=swap');\n\n/* SCALE: align 100% zoom with prior 125% appearance */\n:root {\n #oee md-content,\n #oee .nr-dashboard-template,\n #oee .nr-dashboard-template md-content,\n #oee .nr-dashboard-cardpanel,\n #oee .nr-dashboard-cardpanel md-card,\n #oee .nr-dashboard-cardpanel md-card-content {\n background: transparent !important;\n box-shadow: none !important;\n }\n font-size: 125%;\n --bg-0: #0c1117;\n --bg-1: #131a23;\n --panel: #151e2b;\n --panel-hi: #1a2433;\n --text: #ecf3ff;\n --muted: #96a5b7;\n --accent: #2ea3ff;\n --accent-2: #1f8cf3;\n --good: #24d06f;\n --warn: #ffd100;\n --bad: #ff4d4f;\n --radius: 0.75rem;\n --shadow: 0 0.5rem 1rem rgba(0, 0, 0, .35), inset 0 0.0625rem 0 rgba(255, 255, 255, .05);\n --sidebar-width: 3.75rem;\n --sidebar-gap: clamp(0.75rem, 1.2vh, 1rem);\n --content-max: 100rem;\n --content-pad: clamp(1rem, 1.2vw, 1.4rem);\n --section-gap: clamp(0.75rem, 1vw, 1rem);\n --card-pad: clamp(1rem, 1.1vw, 1.25rem);\n --card-pad-lg: clamp(1.1rem, 1.2vw, 1.35rem);\n --fs-page-title: clamp(1.2rem, 1vw + 0.35rem, 1.7rem);\n --fs-section-title: clamp(0.95rem, 0.9vw + 0.25rem, 1.25rem);\n --fs-label: clamp(0.85rem, 0.7vw + 0.3rem, 1.05rem);\n --fs-label-lg: clamp(0.95rem, 0.8vw + 0.32rem, 1.15rem);\n --fs-kpi: clamp(2.25rem, 2.5vw, 3.25rem);\n --fs-kpi-unit: clamp(1.25rem, 1.4vw, 1.5rem);\n --fs-body: clamp(0.8rem, 0.7vw + 0.28rem, 1rem);\n --progress-height: 0.875rem;\n --start-min-height: 9.375rem;\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody,\n#oee {\n width: 100vw;\n height: 100vh;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n overflow-x: hidden;\n background: var(--bg-0);\n color: var(--text);\n font-family: 'Poppins', system-ui, 'Segoe UI', 'Roboto', sans-serif;\n font-weight: 600;\n}\n\nbody {\n overflow: hidden;\n}\n\nbody > md-content,\nbody > md-content > md-content,\n.nr-dashboard-template,\n.nr-dashboard-template md-content,\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-content {\n background: transparent !important;\n height: 100%;\n overflow: hidden;\n}\n\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-card,\n.nr-dashboard-cardpanel md-card-content {\n background: transparent !important;\n box-shadow: none !important;\n padding: 0 !important;\n}\n\n/* LAYOUT: lock sidebar + content grid across tabs */\n#oee {\n position: fixed;\n inset: 0;\n display: flex;\n overflow: hidden;\n}\n\n.sidebar {\n width: var(--sidebar-width);\n background: #0b1119;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n padding: clamp(0.625rem, 1.4vh, 0.9rem) clamp(0.5rem, 0.8vw, 0.75rem);\n}\n\n.side-top {\n display: flex;\n flex-direction: column;\n gap: var(--sidebar-gap);\n align-items: center;\n}\n\n.sb-btn {\n width: 2.75rem;\n height: 2.75rem;\n border-radius: 0.75rem;\n background: #0f1721;\n border: 1px solid #19222e;\n display: grid;\n place-items: center;\n color: #8fb3d9;\n cursor: pointer;\n transition: 0.16s box-shadow, 0.16s transform, 0.16s border-color;\n}\n\n.sb-btn.active {\n border-color: #2fd289;\n box-shadow: 0 0 0 0.125rem rgba(36, 208, 111, .25), 0 0.625rem 1.125rem rgba(36, 208, 111, .28);\n color: #2fd289;\n}\n\n.sb-ico {\n font-size: clamp(1.1rem, 1.2vw, 1.25rem);\n line-height: 1;\n}\n\n.sb-foot {\n font-size: clamp(0.6rem, 0.6vw, 0.7rem);\n color: #6c7b8d;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.main {\n flex: 1;\n overflow: auto;\n padding: var(--content-pad);\n}\n\n.container {\n max-width: var(--content-max);\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: var(--section-gap);\n}\n\n.card {\n background: linear-gradient(160deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n}\n\n.label {\n color: var(--text);\n text-transform: uppercase;\n letter-spacing: 0.04em;\n font-weight: 700;\n font-size: var(--fs-label);\n text-align: center;\n}\n\n.kpis {\n display: grid;\n grid-template-columns: repeat(4, minmax(11.25rem, 1fr));\n gap: var(--section-gap);\n}\n\n.kpi {\n height: 6.25rem;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n gap: 0.25rem;\n padding: var(--card-pad);\n}\n\n.kpi .label {\n font-size: var(--fs-label-lg);\n}\n\n.kval {\n display: flex;\n align-items: flex-end;\n gap: 0.3rem;\n color: var(--good);\n font-weight: 700;\n}\n\n.knum {\n font-size: var(--fs-kpi);\n line-height: 1;\n}\n\n.kunit {\n font-size: var(--fs-kpi-unit);\n line-height: 1;\n}\n\n.panel {\n padding: var(--card-pad-lg);\n display: flex;\n flex-direction: column;\n gap: clamp(0.75rem, 0.9vw, 0.95rem);\n}\n\n.panel-title {\n font-size: var(--fs-section-title);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n font-weight: 700;\n color: var(--text);\n margin: 0;\n padding: 0;\n background: transparent !important;\n border: none;\n box-shadow: none;\n}\n\n.panel-strip {\n background: transparent;\n border: none;\n padding: 0;\n margin: 0;\n}\n\n.row-3 {\n display: flex;\n gap: var(--section-gap);\n background: transparent;\n}\n\n.mini {\n flex: 1;\n min-height: 6.25rem;\n padding: var(--card-pad);\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n}\n\n.mini .val {\n font-size: var(--fs-body);\n color: var(--text);\n font-weight: 700;\n}\n\n.progress {\n display: flex;\n align-items: center;\n gap: 0.4rem;\n}\n\n.track {\n flex: 1;\n height: var(--progress-height);\n background: #111a24;\n border-radius: 999px;\n overflow: hidden;\n box-shadow: inset 0 0.0625rem 0 rgba(255, 255, 255, .05);\n}\n\n.fill {\n height: 100%;\n width: 0;\n background: var(--good);\n transition: width 0.35s ease;\n}\n\n.pct {\n min-width: 3rem;\n text-align: right;\n color: var(--text);\n font-weight: 700;\n font-size: var(--fs-body);\n}\n\n.bottom {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--section-gap);\n}\n\n.gparts,\n.status,\n.start-wrap {\n min-height: clamp(9.375rem, 18vw, 11.25rem);\n padding: var(--card-pad-lg);\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n}\n\n.gnum {\n font-size: clamp(2.5rem, 3.2vw, 3.5rem);\n color: var(--good);\n font-weight: 700;\n}\n\n.gmeta {\n color: var(--muted);\n font-size: clamp(0.75rem, 0.9vw, 0.85rem);\n}\n\n.status {\n justify-content: center;\n gap: clamp(0.75rem, 1vw, 1rem);\n}\n\n.st-row {\n background: #121c28;\n border-radius: 0.625rem;\n border: 1px solid #243244;\n padding: 0.9rem 1rem;\n display: flex;\n align-items: center;\n justify-content: space-between;\n}\n\n.st-name {\n color: var(--text);\n font-weight: 600;\n font-size: var(--fs-body);\n}\n\n.st-val {\n font-weight: 700;\n font-size: clamp(1.25rem, 1.4vw, 1.5rem);\n text-transform: uppercase;\n}\n\n.st-on {\n color: var(--good);\n}\n\n.st-off {\n color: var(--bad);\n}\n\n.start-wrap {\n align-items: center;\n justify-content: center;\n padding: 0;\n}\n\n.start {\n width: 100%;\n height: 100%;\n min-height: var(--start-min-height);\n border-radius: var(--radius);\n border: none;\n background: linear-gradient(180deg, #32ff7e 0%, #2fd289 100%);\n color: var(--text);\n font-weight: 700;\n font-size: clamp(1.4rem, 2vw, 2rem);\n letter-spacing: 0.08em;\n text-transform: uppercase;\n box-shadow: 0 0 1.25rem rgba(36, 208, 111, .5), inset 0 0.0625rem 0 rgba(255, 255, 255, .1);\n cursor: pointer;\n transition: 0.16s transform, 0.16s filter;\n}\n\n.start:hover {\n transform: translateY(-0.05rem);\n filter: brightness(1.08);\n}\n.modal {\nposition: fixed;\ninset: 0;\ndisplay: flex;\nalign-items: center;\njustify-content: center;\nbackground: rgba(12, 17, 23, 0.82);\nbackdrop-filter: blur(4px);\nz-index: 999;\npadding: 2rem;\n}\n\n.modal.ng-hide {\ndisplay: none !important;\n}\n\n.modal-card {\nwidth: clamp(18rem, 32vw, 26rem);\nbackground: rgba(20, 30, 44, 0.94);\nborder-radius: 1rem;\nborder: 1px solid rgba(46, 163, 255, 0.35);\nbox-shadow: 0 1.25rem 2.5rem rgba(0, 0, 0, 0.45);\npadding: 1.75rem;\ndisplay: flex;\nflex-direction: column;\ngap: 0.75rem;\ncolor: var(--text);\n}\n\n.prompt-actions,\n.prompt-entry {\ndisplay: flex;\ngap: 0.75rem;\njustify-content: center;\n}\n\n.prompt-no,\n.prompt-yes,\n.prompt-entry button {\nflex: 1;\npadding: 0.9rem 1rem;\nborder-radius: 0.75rem;\nborder: none;\nfont-weight: 700;\ntext-transform: uppercase;\ncursor: pointer;\ntransition: transform 0.16s ease, filter 0.16s ease;\n}\n\n.prompt-no {\nbackground: rgba(36, 208, 111, 0.85);\ncolor: var(--text);\n}\n\n.prompt-yes,\n.prompt-entry button {\nbackground: rgba(46, 163, 255, 0.85);\ncolor: var(--text);\n}\n\n.prompt-entry input {\nflex: 1;\npadding: 0.85rem 1rem;\nborder-radius: 0.75rem;\nborder: 1px solid rgba(46, 163, 255, 0.35);\nbackground: rgba(15, 22, 33, 0.9);\ncolor: var(--text);\nfont-size: 1rem;\n}\n\n.start:active {\n transform: none;\n filter: brightness(0.96);\n}\n\n@media (max-width: 68.75rem) {\n .kpis {\n grid-template-columns: repeat(2, minmax(11.25rem, 1fr));\n }\n .row-3 {\n flex-direction: column;\n }\n .bottom {\n grid-template-columns: 1fr;\n }\n}\n\n\n/* Override Node-RED Dashboard's gray backgrounds on text elements */\n.nr-dashboard-template p,\n.nr-dashboard-template h1,\n.nr-dashboard-template h2,\n.nr-dashboard-template h3,\n.nr-dashboard-template h4,\n.nr-dashboard-template label {\n background-color: transparent !important;\n}\n\n/* Scrap modal sizing - responsive */\n.modal-card-scrap {\n width: min(90vw, 28rem);\n max-height: 85vh;\n overflow-y: auto;\n}\n\n.wo-info {\n color: var(--accent);\n font-size: var(--fs-section-title);\n margin: 0.5rem 0;\n}\n\n.wo-summary {\n color: var(--muted);\n margin: 0.25rem 0 0.75rem;\n}\n\n.scrap-question {\n font-size: var(--fs-label-lg);\n text-align: center;\n margin-bottom: 0.75rem;\n color: var(--text);\n}\n\n/* Numpad styling */\n.numpad-container {\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n}\n\n.numpad-display {\n background: #0f1721;\n border: 2px solid var(--accent);\n border-radius: var(--radius);\n padding: 0.5rem;\n font-size: clamp(1rem, 4vw, 1.5rem);\n font-weight: 700;\n text-align: right;\n color: var(--text);\n min-height: 2rem;\n display: flex;\n align-items: center;\n justify-content: flex-end;\n word-break: break-all;\n overflow-wrap: anywhere;\n}\n\n.numpad-error {\n background: rgba(255, 77, 79, 0.2);\n border: 1px solid var(--bad);\n border-radius: var(--radius);\n padding: 0.5rem;\n color: var(--bad);\n text-align: center;\n font-size: var(--fs-body);\n margin-top: -0.3rem;\n}\n\n.numpad-grid {\n display: grid;\n grid-template-columns: repeat(3, 1fr);\n gap: 0.4rem;\n}\n\n.numpad-btn {\n background: linear-gradient(160deg, #1a2433 0%, #151e2b 100%);\n border: 1px solid #243244;\n border-radius: var(--radius);\n color: var(--text);\n font-size: clamp(1rem, 3vw, 1.4rem);\n font-weight: 700;\n padding: 0.5rem;\n cursor: pointer;\n transition: 0.16s transform, 0.16s filter;\n min-height: 2.5rem;\n}\n\n.numpad-btn:active {\n transform: scale(0.95);\n filter: brightness(1.2);\n}\n\n.numpad-clear,\n.numpad-back {\n background: linear-gradient(160deg, #2a1a1a 0%, #1f1515 100%);\n color: var(--warn);\n}\n\n.numpad-actions {\n display: flex;\n gap: 0.75rem;\n margin-top: 0.5rem;\n}\n\n.scrap-submit-btn {\n flex: 1;\n background: linear-gradient(180deg, #32ff7e 0%, #2fd289 100%);\n box-shadow: 0 0 1.25rem rgba(36, 208, 111, .5);\n border: none;\n border-radius: var(--radius);\n color: var(--text);\n padding: 0.5rem;\n font-weight: 700;\n font-size: clamp(0.85rem, 2vw, 1rem);\n text-transform: uppercase;\n cursor: pointer;\n transition: 0.16s transform, 0.16s filter;\n}\n\n.scrap-submit-btn:active {\n transform: scale(0.98);\n filter: brightness(1.1);\n}\n\n/* Updated button styles */\n.prompt-continue {\n flex: 1;\n padding: 1.2rem;\n border-radius: var(--radius);\n border: none;\n font-weight: 700;\n font-size: var(--fs-label);\n text-transform: uppercase;\n cursor: pointer;\n background: linear-gradient(180deg, #2ea3ff 0%, #1f8cf3 100%);\n color: var(--text);\n transition: 0.16s transform, 0.16s filter;\n}\n\n.prompt-continue:active {\n transform: scale(0.98);\n filter: brightness(1.1);\n}\n\n.prompt-yes {\n flex: 1;\n padding: 1.2rem;\n border-radius: var(--radius);\n border: none;\n font-weight: 700;\n font-size: var(--fs-label);\n text-transform: uppercase;\n cursor: pointer;\n background: linear-gradient(180deg, #ffd100 0%, #e6bd00 100%);\n color: var(--bg-0);\n transition: 0.16s transform, 0.16s filter;\n}\n\n.prompt-yes:active {\n transform: scale(0.98);\n filter: brightness(1.1);\n}\n\n/* STOP button styling */\n.start.stop-btn {\n background: linear-gradient(180deg, #ff4d4f 0%, #e63946 100%);\n box-shadow: 0 0 1.25rem rgba(255, 77, 79, .5);\n}\n\n.start.stop-btn:hover {\n filter: brightness(1.15);\n}\n\n/* Disabled START button */\n.start:disabled {\n background: linear-gradient(180deg, #3a3a3a 0%, #2a2a2a 100%);\n box-shadow: none;\n cursor: not-allowed;\n opacity: 0.5;\n}\n\n.start:disabled:hover {\n transform: none;\n filter: none;\n}\n\n</style>\n<div id=\"oee\">\n <aside class=\"sidebar\">\n <div class=\"side-top\">\n <button class=\"sb-btn active\" data-label=\"Home\" ng-click=\"gotoTab('Home')\"><span class=\"sb-ico\">\ud83c\udfe0</span></button>\n <button class=\"sb-btn\" data-label=\"Work Orders\" ng-click=\"gotoTab('Work Orders')\"><span class=\"sb-ico\">\ud83d\udccb</span></button>\n <button class=\"sb-btn\" data-label=\"Alerts\" ng-click=\"gotoTab('Alerts')\"><span class=\"sb-ico\">\u26a0\ufe0f</span></button>\n <button class=\"sb-btn\" data-label=\"Graphs\" ng-click=\"gotoTab('Graphs')\"><span class=\"sb-ico\">\ud83d\udcca</span></button>\n <button class=\"sb-btn\" data-label=\"Help\" ng-click=\"gotoTab('Help')\"><span class=\"sb-ico\">\u2753</span></button>\n <button class=\"sb-btn\" data-label=\"Settings\" ng-click=\"gotoTab('Settings')\"><span class=\"sb-ico\">\u2699\ufe0f</span></button>\n </div>\n <div class=\"sb-foot\">OEE V1.0</div>\n </aside>\n\n <main class=\"main\">\n <div class=\"container\">\n <section class=\"kpis\">\n <article class=\"card kpi\">\n <div class=\"label\">OEE</div>\n <div class=\"kval\"><span id=\"kpi-oee-value\" class=\"knum\">0</span><span class=\"kunit\">%</span></div>\n </article>\n <article class=\"card kpi\">\n <div class=\"label\">Availability</div>\n <div class=\"kval\"><span id=\"kpi-availability-value\" class=\"knum\">0</span><span class=\"kunit\">%</span></div>\n </article>\n <article class=\"card kpi\">\n <div class=\"label\">Performance</div>\n <div class=\"kval\"><span id=\"kpi-performance-value\" class=\"knum\">0</span><span class=\"kunit\">%</span></div>\n </article>\n <article class=\"card kpi\">\n <div class=\"label\">Quality</div>\n <div class=\"kval\"><span id=\"kpi-quality-value\" class=\"knum\">0</span><span class=\"kunit\">%</span></div>\n </article>\n </section>\n\n <section class=\"card panel\">\n <div class=\"panel-strip\"><h2 class=\"panel-title\">Current Work Order</h2></div>\n <div class=\"row-3\">\n <div class=\"card mini\">\n <div class=\"label\">Work Order ID</div>\n <div id=\"workorder-id\" class=\"val\">&nbsp;</div>\n </div>\n <div class=\"card mini\">\n <div class=\"label\">SKU</div>\n <div id=\"workorder-sku\" class=\"val\">&nbsp;</div>\n </div>\n <div class=\"card mini\">\n <div class=\"label\">Cycle Time</div>\n <div id=\"workorder-cycle\" class=\"val\">0</div>\n </div>\n </div>\n <div class=\"progress\">\n <div class=\"track\"><div id=\"workorder-progress-fill\" class=\"fill\"></div></div>\n <div id=\"workorder-progress-percent\" class=\"pct\">0%</div>\n </div>\n </section>\n\n <section class=\"bottom\">\n <article class=\"card gparts\">\n <div class=\"label\">Good Parts</div>\n <div id=\"good-parts-value\" class=\"gnum\">0</div>\n <div id=\"good-parts-meta\" class=\"gmeta\">out of 0</div>\n </article>\n\n <article class=\"card status\">\n <div class=\"st-row\"><span class=\"st-name\">Machine</span><span id=\"machine-state\" class=\"st-val st-off\">OFFLINE</span></div>\n <div class=\"st-row\"><span class=\"st-name\">Production</span><span id=\"production-state\" class=\"st-val st-off\">STOPPED</span></div>\n </article>\n\n <article class=\"card start-wrap\">\n <button id=\"start-button\" type=\"button\" class=\"start\" ng-click=\"toggleStartStop()\" ng-class=\"{'stop-btn': isProductionRunning}\" ng-disabled=\"!hasActiveOrder && !isProductionRunning\">{{ isProductionRunning ? 'STOP' : 'START' }}</button>\n </article>\n </section>\n </div>\n </main>\n</div>\n<div id=\"scrap-modal\" class=\"modal\" ng-show=\"scrapPrompt.show\">\n <div class=\"modal-card modal-card-scrap\">\n <h2>Work Order Complete</h2>\n <p class=\"wo-info\">{{ scrapPrompt.orderId }}</p>\n <p class=\"wo-summary\">Produced <strong>{{ scrapPrompt.produced }}</strong> of <strong>{{ scrapPrompt.target }}</strong> pieces</p>\n\n <div class=\"scrap-question\">Were there any scrap parts?</div>\n\n <!-- Numpad interface -->\n <div class=\"numpad-container\" ng-if=\"scrapPrompt.enterMode\">\n <div class=\"numpad-display\">{{ scrapPrompt.scrapCount || 0 }}</div>\n <div class=\"numpad-error\" ng-if=\"scrapPrompt.error\">{{ scrapPrompt.error }}</div>\n\n <div class=\"numpad-grid\">\n <button class=\"numpad-btn\" ng-click=\"addDigit(7)\">7</button>\n <button class=\"numpad-btn\" ng-click=\"addDigit(8)\">8</button>\n <button class=\"numpad-btn\" ng-click=\"addDigit(9)\">9</button>\n\n <button class=\"numpad-btn\" ng-click=\"addDigit(4)\">4</button>\n <button class=\"numpad-btn\" ng-click=\"addDigit(5)\">5</button>\n <button class=\"numpad-btn\" ng-click=\"addDigit(6)\">6</button>\n\n <button class=\"numpad-btn\" ng-click=\"addDigit(1)\">1</button>\n <button class=\"numpad-btn\" ng-click=\"addDigit(2)\">2</button>\n <button class=\"numpad-btn\" ng-click=\"addDigit(3)\">3</button>\n\n <button class=\"numpad-btn numpad-clear\" ng-click=\"clearScrap()\">C</button>\n <button class=\"numpad-btn\" ng-click=\"addDigit(0)\">0</button>\n <button class=\"numpad-btn numpad-back\" ng-click=\"backspace()\">\u232b</button>\n </div>\n\n <div class=\"numpad-actions\">\n <button class=\"scrap-submit-btn\" ng-click=\"submitScrapValidated(scrapPrompt.scrapCount)\">\n Submit Scrap\n </button>\n </div>\n </div>\n\n <!-- Initial choice buttons -->\n <div class=\"prompt-actions\" ng-if=\"!scrapPrompt.enterMode\">\n <!-- Don't ask again checkbox -->\n <div style=\"display: flex; align-items: center; justify-content: center; gap: 0.5rem; margin-bottom: 0.75rem;\">\n <input type=\"checkbox\" id=\"remind-checkbox\" ng-model=\"scrapPrompt.remindAgain\" style=\"width: 1.2rem; height: 1.2rem; cursor: pointer;\">\n <label for=\"remind-checkbox\" style=\"cursor: pointer; margin: 0; font-size: var(--fs-body); color: var(--muted);\">\n Remind me again if we keep overproducing\n </label>\n </div>\n\n <button class=\"prompt-continue\" ng-click=\"skipScrap()\">\n No, Continue Production\n </button>\n <button class=\"prompt-yes\" ng-click=\"openScrapEntry()\">\n Yes, Enter Scrap\n </button>\n </div>\n </div>\n</div>\n\n<script>\n(function(scope) {\n scope.gotoTab = function(tabName) {\n scope.send({ ui_control: { tab: tabName } });\n };\n\n window.kpiOeePercent = window.kpiOeePercent || 0;\n window.kpiAvailabilityPercent = window.kpiAvailabilityPercent || 0;\n window.kpiPerformancePercent = window.kpiPerformancePercent || 0;\n window.kpiQualityPercent = window.kpiQualityPercent || 0;\n window.currentWorkOrderId = window.currentWorkOrderId || \"\";\n window.currentSku = window.currentSku || \"\";\n window.currentCycleTime = window.currentCycleTime || 0;\n window.currentProgressPercent = window.currentProgressPercent || 0;\n window.goodPartsCount = window.goodPartsCount || 0;\n window.goodPartsTarget = window.goodPartsTarget || 0;\n window.machineOnline = typeof window.machineOnline === \"boolean\" ? window.machineOnline : false;\n window.productionStarted = typeof window.productionStarted === \"boolean\" ? window.productionStarted : false;\n\n var elements = {\n kpiOee: document.getElementById(\"kpi-oee-value\"),\n kpiAvailability: document.getElementById(\"kpi-availability-value\"),\n kpiPerformance: document.getElementById(\"kpi-performance-value\"),\n kpiQuality: document.getElementById(\"kpi-quality-value\"),\n workOrderId: document.getElementById(\"workorder-id\"),\n workOrderSku: document.getElementById(\"workorder-sku\"),\n workOrderCycle: document.getElementById(\"workorder-cycle\"),\n progressFill: document.getElementById(\"workorder-progress-fill\"),\n progressPercent: document.getElementById(\"workorder-progress-percent\"),\n goodPartsValue: document.getElementById(\"good-parts-value\"),\n goodPartsMeta: document.getElementById(\"good-parts-meta\"),\n machineState: document.getElementById(\"machine-state\"),\n productionState: document.getElementById(\"production-state\"),\n startButton: document.getElementById(\"start-button\")\n };\n\n function setText(el, value) {\n if (el) {\n el.textContent = value;\n }\n }\n\n function setNumber(el, value) {\n setText(el, String(Math.max(0, Math.round(Number(value) || 0))));\n }\n\n function clampPercent(value) {\n var num = Number(value) || 0;\n return Math.max(0, Math.min(100, Math.round(num)));\n }\n\n function updateState(el, isActive, onLabel, offLabel) {\n if (!el) {\n return;\n }\n el.textContent = isActive ? onLabel : offLabel;\n el.classList.remove(\"st-on\", \"st-off\");\n el.classList.add(isActive ? \"st-on\" : \"st-off\");\n }\n\n function renderDashboard() {\n setNumber(elements.kpiOee, window.kpiOeePercent);\n setNumber(elements.kpiAvailability, window.kpiAvailabilityPercent);\n setNumber(elements.kpiPerformance, window.kpiPerformancePercent);\n setNumber(elements.kpiQuality, window.kpiQualityPercent);\n\n setText(elements.workOrderId, window.currentWorkOrderId || \"\");\n setText(elements.workOrderSku, window.currentSku || \"\");\n setText(elements.workOrderCycle, window.currentCycleTime ? String(window.currentCycleTime) : \"0\");\n\n var progress = clampPercent(window.currentProgressPercent);\n if (elements.progressFill) {\n elements.progressFill.style.width = progress + \"%\";\n }\n setText(elements.progressPercent, progress + \"%\");\n\n setText(elements.goodPartsValue, String(window.goodPartsCount || 0));\n setText(elements.goodPartsMeta, \"out of \" + String(window.goodPartsTarget || 0));\n\n updateState(elements.machineState, !!window.machineOnline, \"ONLINE\", \"OFFLINE\");\n updateState(elements.productionState, !!window.productionStarted, \"STARTED\", \"STOPPED\");\n }\n\n scope.renderDashboard = renderDashboard;\n\n scope.scrapPrompt = {\n show: false,\n enterMode: false,\n orderId: '',\n sku: '',\n target: 0,\n produced: 0,\n scrapCount: 0,\n remindAgain: false\n };\n \n scope.submitScrap = function(scrapCount) {\n scrapCount = Math.max(0, Number(scrapCount) || 0);\n scope.send({\n action: 'scrap-entry',\n payload: {\n id: scope.scrapPrompt.orderId,\n scrap: scrapCount\n }\n });\n scope.scrapPrompt.show = false;\n scope.scrapPrompt.enterMode = false;\n scope.scrapPrompt.remindAgain = false;\n };\n\n // Numpad digit entry\n scope.addDigit = function(digit) {\n scope.scrapPrompt.error = '';\n const current = String(scope.scrapPrompt.scrapCount || 0);\n const newValue = current === '0' ? String(digit) : current + String(digit);\n scope.scrapPrompt.scrapCount = Number(newValue);\n };\n\n // Clear scrap count\n scope.clearScrap = function() {\n scope.scrapPrompt.scrapCount = 0;\n scope.scrapPrompt.error = '';\n };\n\n // Backspace\n scope.backspace = function() {\n scope.scrapPrompt.error = '';\n const current = String(scope.scrapPrompt.scrapCount || 0);\n const newValue = current.length > 1 ? current.slice(0, -1) : '0';\n scope.scrapPrompt.scrapCount = Number(newValue);\n };\n\n // Open scrap entry (validate and show numpad)\n scope.openScrapEntry = function() {\n scope.scrapPrompt.enterMode = true;\n scope.scrapPrompt.scrapCount = 0;\n scope.scrapPrompt.error = '';\n };\n\n // Skip scrap (No, Continue)\n scope.skipScrap = function() {\n scope.send({\n action: 'scrap-skip',\n payload: {\n id: scope.scrapPrompt.orderId,\n remindAgain: !!scope.scrapPrompt.remindAgain\n }\n });\n scope.scrapPrompt.show = false;\n scope.scrapPrompt.enterMode = false;\n scope.scrapPrompt.remindAgain = false;\n scope.scrapPrompt.error = '';\n };\n\n // START/STOP toggle\n scope.isProductionRunning = false;\n scope.hasActiveOrder = false;\n\n scope.toggleStartStop = function() {\n if (scope.isProductionRunning) {\n // STOP production - pause tracking (work order stays active)\n scope.send({ action: \"stop\" });\n scope.isProductionRunning = false;\n // Keep hasActiveOrder=true so button stays enabled (resume pattern)\n } else {\n // START/RESUME production - send start action to backend\n scope.send({ action: \"start\" });\n scope.isProductionRunning = true;\n }\n };\n\n // Submit scrap with validation\n scope.submitScrapValidated = function(scrapCount) {\n scrapCount = Math.max(0, Number(scrapCount) || 0);\n\n // Validate: scrap can't exceed produced quantity\n if (scrapCount > scope.scrapPrompt.produced) {\n scope.scrapPrompt.error = 'Scrap cannot exceed produced quantity (' + scope.scrapPrompt.produced + ')';\n return;\n }\n\n scope.send({\n action: 'scrap-entry',\n payload: {\n id: scope.scrapPrompt.orderId,\n scrap: scrapCount\n }\n });\n scope.scrapPrompt.show = false;\n scope.scrapPrompt.enterMode = false;\n scope.scrapPrompt.remindAgain = false;\n scope.scrapPrompt.scrapCount = 0;\n scope.scrapPrompt.error = '';\n };\n\n\n renderDashboard();\n\n if (elements.startButton) {\n elements.startButton.addEventListener(\"click\", function() {\n scope.send({ action: \"start\" });\n });\n }\n})(scope);\n\n(function ensureNoMaterialTint(){\n const root = document.getElementById('oee');\n if (!root) return;\n\n const targets = root.querySelectorAll([\n '.md-toolbar',\n 'md-toolbar',\n '.md-subheader',\n 'md-subheader',\n '.md-toolbar-tools',\n '.md-card-title'\n ].join(','));\n\n targets.forEach(el => {\n if (el && el.style) {\n if (el.style.background) {\n el.style.background = 'transparent';\n }\n if (el.style.backgroundColor) {\n el.style.backgroundColor = 'transparent';\n }\n }\n if (el && el.classList) {\n el.classList.remove(\n 'md-whiteframe-1dp','md-whiteframe-2dp','md-whiteframe-3dp',\n 'md-whiteframe-z1','md-whiteframe-z2','md-whiteframe-z3'\n );\n }\n });\n\n if (!document.getElementById('oee-theme-sentinel')) {\n const style = document.createElement('style');\n style.id = 'oee-theme-sentinel';\n style.textContent = `\n #oee .md-toolbar::before, #oee .md-toolbar::after,\n #oee .md-subheader::before, #oee .md-subheader::after,\n #oee md-toolbar::before, #oee md-toolbar::after,\n #oee md-subheader::before, #oee md-subheader::after {\n display: none !important;\n content: none !important;\n }\n `;\n document.head.appendChild(style);\n }\n})();\n\n\n// optional render trigger\nscope.$evalAsync(scope.renderDashboard);\n</script>\n<script>\n(function killMaterialTintsLive(){\nconst root = document.getElementById('oee');\nif (!root) return;\n\nconst zap = (el) => {\nif (!el) return;\nel.style.background = 'transparent';\nel.style.backgroundColor = 'transparent';\nel.style.boxShadow = 'none';\n};\n\nconst sel = [\n'.md-subheader','md-subheader','.md-toolbar','md-toolbar',\n'.md-card-title','.md-sticky-clone','._md-sticky-clone',\n'.nr-dashboard-template md-content','md-content'\n].join(',');\n\n// Clean anything present now\nroot.querySelectorAll(sel).forEach(zap);\n\n// Clean future clones dynamically\nconst mo = new MutationObserver((muts) => {\nfor (const m of muts) {\nm.addedNodes && m.addedNodes.forEach(n => {\nif (!(n instanceof HTMLElement)) return;\nif (n.matches && n.matches(sel)) zap(n);\nn.querySelectorAll && n.querySelectorAll(sel).forEach(zap);\n});\n}\n});\nmo.observe(document.body, { childList: true, subtree: true });\n})();\n\n\n\n (function(scope) {\n scope.$watch('msg', function(msg) {\n if (!msg) {\n return;\n }\n if (msg.topic === 'machineStatus') {\n\n // ===== UPDATE KPI DISPLAYS =====\n if (msg.payload && msg.payload.kpis) {\n var kpis = msg.payload.kpis;\n\n // Update OEE\n if (window.kpiOeePercent !== kpis.oee) {\n window.kpiOeePercent = kpis.oee;\n var oeeEl = document.getElementById('kpi-oee-value');\n if (oeeEl) oeeEl.textContent = kpis.oee.toFixed(1) + '%';\n }\n\n // Update Availability\n if (window.kpiAvailabilityPercent !== kpis.availability) {\n window.kpiAvailabilityPercent = kpis.availability;\n var availEl = document.getElementById('kpi-availability-value');\n if (availEl) availEl.textContent = kpis.availability.toFixed(1) + '%';\n }\n\n // Update Performance\n if (window.kpiPerformancePercent !== kpis.performance) {\n window.kpiPerformancePercent = kpis.performance;\n var perfEl = document.getElementById('kpi-performance-value');\n if (perfEl) perfEl.textContent = kpis.performance.toFixed(1) + '%';\n }\n\n // Update Quality\n if (window.kpiQualityPercent !== kpis.quality) {\n window.kpiQualityPercent = kpis.quality;\n var qualEl = document.getElementById('kpi-quality-value');\n if (qualEl) qualEl.textContent = kpis.quality.toFixed(1) + '%';\n }\n }\n\n window.machineOnline = msg.payload.machineOnline;\n window.productionStarted = msg.payload.productionStarted;\n scope.renderDashboard();\n return;\n }\n if (msg.topic === 'activeWorkOrder') {\n const order = msg.payload || {};\n if (!order || !order.id) {\n window.currentWorkOrderId = \"\";\n window.currentSku = \"\";\n window.currentCycleTime = 0;\n window.currentProgressPercent = 0;\n window.goodPartsCount = 0;\n window.goodPartsTarget = 0;\n } else {\n window.currentWorkOrderId = order.id || \"\";\n window.currentSku = order.sku || \"\";\n window.currentCycleTime = Number(order.cycleTime) || 0;\n window.currentProgressPercent = Number(order.progressPercent) || 0;\n window.goodPartsCount = Number(order.good) || 0;\n window.goodPartsTarget = Number(order.target) || 0;\n }\n \n // ===== UPDATE KPI DISPLAYS =====\n if (order.kpis) {\n var kpis = order.kpis;\n \n // Update OEE\n window.kpiOeePercent = kpis.oee || 0;\n var oeeEl = document.getElementById('kpi-oee-value');\n if (oeeEl) oeeEl.textContent = (kpis.oee || 0).toFixed(1) + '%';\n \n // Update Availability\n window.kpiAvailabilityPercent = kpis.availability || 0;\n var availEl = document.getElementById('kpi-availability-value');\n if (availEl) availEl.textContent = (kpis.availability || 0).toFixed(1) + '%';\n \n // Update Performance\n window.kpiPerformancePercent = kpis.performance || 0;\n var perfEl = document.getElementById('kpi-performance-value');\n if (perfEl) perfEl.textContent = (kpis.performance || 0).toFixed(1) + '%';\n \n // Update Quality\n window.kpiQualityPercent = kpis.quality || 0;\n var qualEl = document.getElementById('kpi-quality-value');\n if (qualEl) qualEl.textContent = (kpis.quality || 0).toFixed(1) + '%';\n }\n \n // Update START button availability\n scope.hasActiveOrder = !!(order && order.id);\n if (!order.id) {\n scope.isProductionRunning = false;\n }\n scope.renderDashboard();\n }\n if (!scope.scrapPrompt) {\n scope.scrapPrompt = {\n show: false,\n enterMode: false,\n orderId: '',\n sku: '',\n target: 0,\n produced: 0,\n scrapCount: 0,\n remindAgain: false\n };\n }\n \n if (msg.topic === 'scrapPrompt') {\n var p = msg.payload || {};\n \n scope.scrapPrompt.show = true;\n scope.scrapPrompt.enterMode = false;\n scope.scrapPrompt.remindAgain = false;\n scope.scrapPrompt.orderId = p.id || '';\n scope.scrapPrompt.sku = p.sku || '';\n scope.scrapPrompt.target = Number(p.target) || 0;\n scope.scrapPrompt.produced = Number(p.produced) || 0;\n scope.scrapPrompt.scrapCount = 0;\n \n scope.gotoTab('Home'); // make sure we're on Home\n scope.$evalAsync(); // trigger Angular digest so ng-show updates\n return;\n }\n scope.submitScrap = function(scrapCount) {\n scrapCount = Math.max(0, Number(scrapCount) || 0);\n scope.send({\n action: 'scrap-entry',\n payload: {\n id: scope.scrapPrompt.orderId,\n scrap: scrapCount\n }\n });\n scope.scrapPrompt.show = false;\n scope.scrapPrompt.enterMode = false;\n scope.scrapPrompt.remindAgain = false;\n };\n });\n})(scope);\n\n\n</script>\n",
"storeOutMessages": true,
"fwdInMessages": true,
"resendOnRefresh": true,
"templateScope": "local",
"className": "",
"x": 180,
"y": 60,
"wires": [
[
"a7d58e15929b3d8c",
"010de5af3ced0ae3"
]
]
},
{
"id": "f2a3b4c5d6e7f8a9",
"type": "ui_template",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"group": "e2f3a4b5c6d7e8f9",
"name": "Alerts Template",
"order": 0,
"width": "25",
"height": "25",
"format": "<style>\n@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@600;700&display=swap');\n\n/* SCALE: align 100% zoom with prior 125% appearance */\n:root {\n font-size: 125%;\n --bg-0: #0c1117;\n --bg-1: #131a23;\n --panel: #151e2b;\n --panel-hi: #1a2433;\n --text: #ecf3ff;\n --muted: #96a5b7;\n --accent: #2ea3ff;\n --accent-2: #1f8cf3;\n --good: #24d06f;\n --warn: #ffd100;\n --bad: #ff4d4f;\n --radius: 0.75rem;\n --shadow: 0 0.5rem 1rem rgba(0, 0, 0, .35), inset 0 0.0625rem 0 rgba(255, 255, 255, .05);\n --sidebar-width: 3.75rem;\n --sidebar-gap: clamp(0.75rem, 1.2vh, 1rem);\n --content-max: 80rem;\n --content-pad: clamp(1rem, 1.2vw, 1.4rem);\n --section-gap: clamp(0.75rem, 1vw, 1rem);\n --card-pad: clamp(1rem, 1.1vw, 1.25rem);\n --fs-page-title: clamp(1.2rem, 1vw + 0.35rem, 1.7rem);\n --fs-section-title: clamp(0.95rem, 0.9vw + 0.25rem, 1.25rem);\n --fs-body: clamp(0.8rem, 0.7vw + 0.28rem, 1rem);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody,\n#oee {\n width: 100vw;\n height: 100vh;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n overflow-x: hidden;\n background: var(--bg-0);\n color: var(--text);\n font-family: 'Poppins', system-ui, 'Segoe UI', 'Roboto', sans-serif;\n font-weight: 600;\n}\n\nbody {\n overflow: hidden;\n}\n\nbody > md-content,\nbody > md-content > md-content,\n.nr-dashboard-template,\n.nr-dashboard-template md-content,\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-content {\n background: transparent !important;\n height: 100%;\n overflow: hidden;\n}\n\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-card,\n.nr-dashboard-cardpanel md-card-content {\n background: transparent !important;\n box-shadow: none !important;\n padding: 0 !important;\n}\n\n/* LAYOUT: lock sidebar + content grid across tabs */\n#oee {\n position: fixed;\n inset: 0;\n display: flex;\n overflow: hidden;\n}\n\n.sidebar {\n width: var(--sidebar-width);\n background: #0b1119;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n padding: clamp(0.625rem, 1.4vh, 0.9rem) clamp(0.5rem, 0.8vw, 0.75rem);\n}\n\n.side-top {\n display: flex;\n flex-direction: column;\n gap: var(--sidebar-gap);\n align-items: center;\n}\n\n.sb-btn {\n width: 2.75rem;\n height: 2.75rem;\n border-radius: 0.75rem;\n background: #0f1721;\n border: 1px solid #19222e;\n display: grid;\n place-items: center;\n color: #8fb3d9;\n cursor: pointer;\n transition: 0.16s box-shadow, 0.16s transform, 0.16s border-color;\n}\n\n.sb-btn.active {\n border-color: #2fd289;\n box-shadow: 0 0 0 0.125rem rgba(36, 208, 111, .25), 0 0.625rem 1.125rem rgba(36, 208, 111, .28);\n color: #2fd289;\n}\n\n.sb-ico {\n font-size: clamp(1.1rem, 1.2vw, 1.25rem);\n line-height: 1;\n}\n\n.sb-foot {\n font-size: clamp(0.6rem, 0.6vw, 0.7rem);\n color: #6c7b8d;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.main {\n flex: 1;\n overflow: auto;\n padding: var(--content-pad);\n}\n\n.container {\n max-width: var(--content-max);\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: var(--section-gap);\n}\n\n.card {\n background: linear-gradient(160deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n}\n\n/* HEADER: maintain title weight + spacing */\n.page-heading {\n display: flex;\n align-items: center;\n padding: 0 0.25rem;\n}\n\n.page-title {\n margin: 0;\n font-size: var(--fs-page-title);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text);\n}\n\n/* QUICK ALERTS: button row stays balanced on any width */\n.quick-alerts {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(13.75rem, 1fr));\n gap: var(--section-gap);\n padding: var(--card-pad);\n}\n\n.quick-alert-btn {\n background: linear-gradient(180deg, #32ff7e 0%, #2fd289 100%);\n box-shadow: 0 0 1.25rem rgba(36, 208, 111, .5), inset 0 0.0625rem 0 rgba(255, 255, 255, .1);\n border: none;\n border-radius: var(--radius);\n color: var(--text);\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-weight: 700;\n padding: clamp(1.1rem, 1.8vw, 1.4rem);\n cursor: pointer;\n transition: 0.16s transform, 0.16s filter;\n}\n\n.quick-alert-btn:hover {\n transform: translateY(-0.05rem);\n filter: brightness(1.08);\n}\n\n.quick-alert-btn:active {\n transform: none;\n filter: brightness(0.96);\n}\n\n/* FORM CARD: consistent padding + typography */\n.alert-card {\n padding: var(--card-pad);\n display: flex;\n flex-direction: column;\n gap: clamp(0.9rem, 1vw, 1.1rem);\n}\n\n.form-group {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.form-group label {\n font-size: var(--fs-section-title);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n}\n\n.form-group select,\n.form-group textarea {\n background: #121c28;\n border: 1px solid #243244;\n border-radius: var(--radius);\n color: var(--text);\n padding: 0.8rem 0.9rem;\n font-family: 'Poppins', system-ui, 'Segoe UI', 'Roboto', sans-serif;\n font-weight: 600;\n font-size: var(--fs-body);\n}\n\ntextarea {\n resize: vertical;\n min-height: 7.5rem;\n}\n\n.alert-send {\n align-self: flex-end;\n background: linear-gradient(180deg, #32ff7e 0%, #2fd289 100%);\n box-shadow: 0 0 1.25rem rgba(36, 208, 111, .5), inset 0 0.0625rem 0 rgba(255, 255, 255, .1);\n border: none;\n border-radius: var(--radius);\n color: var(--text);\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-weight: 700;\n padding: 0.8rem 1.4rem;\n cursor: pointer;\n transition: 0.16s transform, 0.16s filter;\n}\n\n.alert-send:hover {\n transform: translateY(-0.05rem);\n filter: brightness(1.08);\n}\n\n.alert-send:active {\n transform: none;\n filter: brightness(0.96);\n}\n\n\n/* Override Node-RED Dashboard's gray backgrounds on text elements */\n.nr-dashboard-template p,\n.nr-dashboard-template h1,\n.nr-dashboard-template h2,\n.nr-dashboard-template h3,\n.nr-dashboard-template h4,\n.nr-dashboard-template label {\n background-color: transparent !important;\n}\n</style>\n<div id=\"oee\">\n <aside class=\"sidebar\">\n <div class=\"side-top\">\n <button class=\"sb-btn\" data-label=\"Home\" ng-click=\"gotoTab('Home')\"><span class=\"sb-ico\">\ud83c\udfe0</span></button>\n <button class=\"sb-btn\" data-label=\"Work Orders\" ng-click=\"gotoTab('Work Orders')\"><span class=\"sb-ico\">\ud83d\udccb</span></button>\n <button class=\"sb-btn active\" data-label=\"Alerts\" ng-click=\"gotoTab('Alerts')\"><span class=\"sb-ico\">\u26a0\ufe0f</span></button>\n <button class=\"sb-btn\" data-label=\"Graphs\" ng-click=\"gotoTab('Graphs')\"><span class=\"sb-ico\">\ud83d\udcca</span></button>\n <button class=\"sb-btn\" data-label=\"Help\" ng-click=\"gotoTab('Help')\"><span class=\"sb-ico\">\u2753</span></button>\n <button class=\"sb-btn\" data-label=\"Settings\" ng-click=\"gotoTab('Settings')\"><span class=\"sb-ico\">\u2699\ufe0f</span></button>\n </div>\n <div class=\"sb-foot\">OEE V1.0</div>\n </aside>\n\n <main class=\"main\">\n <div class=\"container\">\n <section class=\"page-heading\">\n <h1 class=\"page-title\">Alerts</h1>\n </section>\n\n <section class=\"card quick-alerts\">\n <button class=\"quick-alert-btn\" data-alert=\"Machine stopped\">Machine stopped</button>\n <button class=\"quick-alert-btn\" data-alert=\"Fault in machine\">Fault in machine</button>\n <button class=\"quick-alert-btn\" data-alert=\"No material\">No material</button>\n </section>\n\n <section class=\"card alert-card\">\n <div class=\"form-group\">\n <label for=\"alert-type\">Alert type</label>\n <select id=\"alert-type\">\n <option>Machine stopped</option>\n <option>Fault in machine</option>\n <option>No material</option>\n <option>Other</option>\n </select>\n </div>\n <div class=\"form-group\">\n <label for=\"alert-description\">Description</label>\n <textarea id=\"alert-description\" rows=\"5\" placeholder=\"Add context or instructions (optional)\"></textarea>\n </div>\n <button id=\"alert-send\" class=\"alert-send\" type=\"button\">Send Alert</button>\n </section>\n </div>\n </main>\n</div>\n\n<script>\n(function(scope) {\n scope.gotoTab = function(tabName) {\n scope.send({ ui_control: { tab: tabName } });\n };\n\n var quickButtons = document.querySelectorAll('[data-alert]');\n quickButtons.forEach(function(btn) {\n btn.addEventListener('click', function() {\n var type = btn.getAttribute('data-alert');\n scope.send({ action: 'alert', type: type });\n });\n });\n\n var sendButton = document.getElementById('alert-send');\n if (sendButton) {\n sendButton.addEventListener('click', function() {\n var type = document.getElementById('alert-type').value;\n var description = (document.getElementById('alert-description').value || '').trim();\n scope.send({ action: 'alert', type: type, description: description });\n });\n }\n})(scope);\n\n(function ensureNoMaterialTint(){\n const root = document.getElementById('oee');\n if (!root) return;\n\n const targets = root.querySelectorAll([\n '.md-toolbar',\n 'md-toolbar',\n '.md-subheader',\n 'md-subheader',\n '.md-toolbar-tools',\n '.md-card-title'\n ].join(','));\n\n targets.forEach(el => {\n if (el && el.style) {\n if (el.style.background) {\n el.style.background = 'transparent';\n }\n if (el.style.backgroundColor) {\n el.style.backgroundColor = 'transparent';\n }\n }\n if (el && el.classList) {\n el.classList.remove(\n 'md-whiteframe-1dp','md-whiteframe-2dp','md-whiteframe-3dp',\n 'md-whiteframe-z1','md-whiteframe-z2','md-whiteframe-z3'\n );\n }\n });\n\n if (!document.getElementById('oee-theme-sentinel')) {\n const style = document.createElement('style');\n style.id = 'oee-theme-sentinel';\n style.textContent = `\n #oee .md-toolbar::before, #oee .md-toolbar::after,\n #oee .md-subheader::before, #oee .md-subheader::after,\n #oee md-toolbar::before, #oee md-toolbar::after,\n #oee md-subheader::before, #oee md-subheader::after {\n display: none !important;\n content: none !important;\n }\n `;\n document.head.appendChild(style);\n }\n})();\n\n\n// optional render trigger\nscope.$evalAsync(function() {});\n</script>\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": "<style>\n@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@600;700&display=swap');\n\n/* SCALE: align 100% zoom with prior 125% appearance */\n:root {\n font-size: 125%;\n --bg-0: #0c1117;\n --bg-1: #131a23;\n --panel: #151e2b;\n --panel-hi: #1a2433;\n --text: #ecf3ff;\n --muted: #96a5b7;\n --accent: #2ea3ff;\n --accent-2: #1f8cf3;\n --good: #24d06f;\n --warn: #ffd100;\n --bad: #ff4d4f;\n --radius: 0.75rem;\n --shadow: 0 0.5rem 1rem rgba(0, 0, 0, .35), inset 0 0.0625rem 0 rgba(255, 255, 255, .05);\n --sidebar-width: 3.75rem;\n --sidebar-gap: clamp(0.75rem, 1.2vh, 1rem);\n --content-max: 100rem;\n --content-pad: clamp(1rem, 1.2vw, 1.4rem);\n --section-gap: clamp(0.75rem, 1vw, 1rem);\n --card-pad: clamp(1rem, 1.1vw, 1.25rem);\n --fs-page-title: clamp(1.2rem, 1vw + 0.35rem, 1.7rem);\n --fs-section-title: clamp(0.95rem, 0.9vw + 0.25rem, 1.25rem);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody,\n#oee {\n width: 100vw;\n height: 100vh;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n overflow-x: hidden;\n background: var(--bg-0);\n color: var(--text);\n font-family: 'Poppins', system-ui, 'Segoe UI', 'Roboto', sans-serif;\n font-weight: 600;\n}\n\nbody {\n overflow: hidden;\n}\n\nbody > md-content,\nbody > md-content > md-content,\n.nr-dashboard-template,\n.nr-dashboard-template md-content,\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-content {\n background: transparent !important;\n height: 100%;\n overflow: hidden;\n}\n\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-card,\n.nr-dashboard-cardpanel md-card-content {\n background: transparent !important;\n box-shadow: none !important;\n padding: 0 !important;\n}\n\n/* LAYOUT: lock sidebar + content grid across tabs */\n#oee {\n position: fixed;\n inset: 0;\n display: flex;\n overflow: hidden;\n}\n\n.sidebar {\n width: var(--sidebar-width);\n background: #0b1119;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n padding: clamp(0.625rem, 1.4vh, 0.9rem) clamp(0.5rem, 0.8vw, 0.75rem);\n}\n\n.side-top {\n display: flex;\n flex-direction: column;\n gap: var(--sidebar-gap);\n align-items: center;\n}\n\n.sb-btn {\n width: 2.75rem;\n height: 2.75rem;\n border-radius: 0.75rem;\n background: #0f1721;\n border: 1px solid #19222e;\n display: grid;\n place-items: center;\n color: #8fb3d9;\n cursor: pointer;\n transition: 0.16s box-shadow, 0.16s transform, 0.16s border-color;\n}\n\n.sb-btn.active {\n border-color: #2fd289;\n box-shadow: 0 0 0 0.125rem rgba(36, 208, 111, .25), 0 0.625rem 1.125rem rgba(36, 208, 111, .28);\n color: #2fd289;\n}\n\n.sb-ico {\n font-size: clamp(1.1rem, 1.2vw, 1.25rem);\n line-height: 1;\n}\n\n.sb-foot {\n font-size: clamp(0.6rem, 0.6vw, 0.7rem);\n color: #6c7b8d;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.main {\n flex: 1;\n overflow: auto;\n padding: var(--content-pad);\n}\n\n.container {\n max-width: var(--content-max);\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: var(--section-gap);\n}\n\n.card {\n background: linear-gradient(160deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n}\n\n/* HEADER: maintain title styling */\n.page-heading {\n display: flex;\n align-items: center;\n padding: 0 0.25rem;\n}\n\n.page-title {\n margin: 0;\n font-size: var(--fs-page-title);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text);\n}\n\n/* CHART GRID: preserve 2x2 symmetry without wrapping glitches */\n.chart-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(18rem, 1fr));\n gap: var(--section-gap);\n}\n\n.chart-card {\n display: flex;\n flex-direction: column;\n gap: clamp(0.85rem, 1vw, 1rem);\n padding: var(--card-pad);\n}\n\n.chart-title {\n margin: 0;\n font-size: var(--fs-section-title);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text);\n}\n\n.chart-placeholder {\n flex: 1;\n border-radius: var(--radius);\n background: #121c28;\n border: 1px dashed rgba(58, 96, 136, 0.35);\n min-height: 13.75rem;\n}\n\n\n/* Override Node-RED Dashboard's gray backgrounds on text elements */\n.nr-dashboard-template p,\n.nr-dashboard-template h1,\n.nr-dashboard-template h2,\n.nr-dashboard-template h3,\n.nr-dashboard-template h4,\n.nr-dashboard-template label {\n background-color: transparent !important;\n}\n</style>\n<div id=\"oee\">\n <aside class=\"sidebar\">\n <div class=\"side-top\">\n <button class=\"sb-btn\" data-label=\"Home\" ng-click=\"gotoTab('Home')\"><span class=\"sb-ico\">\ud83c\udfe0</span></button>\n <button class=\"sb-btn\" data-label=\"Work Orders\" ng-click=\"gotoTab('Work Orders')\"><span class=\"sb-ico\">\ud83d\udccb</span></button>\n <button class=\"sb-btn\" data-label=\"Alerts\" ng-click=\"gotoTab('Alerts')\"><span class=\"sb-ico\">\u26a0\ufe0f</span></button>\n <button class=\"sb-btn active\" data-label=\"Graphs\" ng-click=\"gotoTab('Graphs')\"><span class=\"sb-ico\">\ud83d\udcca</span></button>\n <button class=\"sb-btn\" data-label=\"Help\" ng-click=\"gotoTab('Help')\"><span class=\"sb-ico\">\u2753</span></button>\n <button class=\"sb-btn\" data-label=\"Settings\" ng-click=\"gotoTab('Settings')\"><span class=\"sb-ico\">\u2699\ufe0f</span></button>\n </div>\n <div class=\"sb-foot\">OEE V1.0</div>\n </aside>\n\n <main class=\"main\">\n <div class=\"container\">\n <section class=\"page-heading\">\n <h1 class=\"page-title\">Graphs</h1>\n </section>\n\n <section class=\"chart-grid\">\n <article class=\"card chart-card\">\n <h2 class=\"chart-title\">OEE \u2013 Last 24h</h2>\n <div class=\"chart-placeholder\" id=\"chart-oee\"></div>\n </article>\n <article class=\"card chart-card\">\n <h2 class=\"chart-title\">Availability \u2013 Last 7 days</h2>\n <div class=\"chart-placeholder\" id=\"chart-availability\"></div>\n </article>\n <article class=\"card chart-card\">\n <h2 class=\"chart-title\">Performance \u2013 Last 7 days</h2>\n <div class=\"chart-placeholder\" id=\"chart-performance\"></div>\n </article>\n <article class=\"card chart-card\">\n <h2 class=\"chart-title\">Quality \u2013 Last 7 days</h2>\n <div class=\"chart-placeholder\" id=\"chart-quality\"></div>\n </article>\n </section>\n </div>\n </main>\n</div>\n\n<script>\n(function(scope) {\n scope.gotoTab = function(tabName) {\n scope.send({ ui_control: { tab: tabName } });\n };\n})(scope);\n\n(function ensureNoMaterialTint(){\n const root = document.getElementById('oee');\n if (!root) return;\n\n const targets = root.querySelectorAll([\n '.md-toolbar',\n 'md-toolbar',\n '.md-subheader',\n 'md-subheader',\n '.md-toolbar-tools',\n '.md-card-title'\n ].join(','));\n\n targets.forEach(el => {\n if (el && el.style) {\n if (el.style.background) {\n el.style.background = 'transparent';\n }\n if (el.style.backgroundColor) {\n el.style.backgroundColor = 'transparent';\n }\n }\n if (el && el.classList) {\n el.classList.remove(\n 'md-whiteframe-1dp','md-whiteframe-2dp','md-whiteframe-3dp',\n 'md-whiteframe-z1','md-whiteframe-z2','md-whiteframe-z3'\n );\n }\n });\n\n if (!document.getElementById('oee-theme-sentinel')) {\n const style = document.createElement('style');\n style.id = 'oee-theme-sentinel';\n style.textContent = `\n #oee .md-toolbar::before, #oee .md-toolbar::after,\n #oee .md-subheader::before, #oee .md-subheader::after,\n #oee md-toolbar::before, #oee md-toolbar::after,\n #oee md-subheader::before, #oee md-subheader::after {\n display: none !important;\n content: none !important;\n }\n `;\n document.head.appendChild(style);\n }\n})();\n\n\n// optional render trigger\nscope.$evalAsync(function() {});\n</script>\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": "<style>\n@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@600;700&display=swap');\n\n/* SCALE: align 100% zoom with prior 125% appearance */\n:root {\n font-size: 125%;\n --bg-0: #0c1117;\n --bg-1: #131a23;\n --panel: #151e2b;\n --panel-hi: #1a2433;\n --text: #ecf3ff;\n --muted: #96a5b7;\n --accent: #2ea3ff;\n --accent-2: #1f8cf3;\n --good: #24d06f;\n --warn: #ffd100;\n --bad: #ff4d4f;\n --radius: 0.75rem;\n --shadow: 0 0.5rem 1rem rgba(0, 0, 0, .35), inset 0 0.0625rem 0 rgba(255, 255, 255, .05);\n --sidebar-width: 3.75rem;\n --sidebar-gap: clamp(0.75rem, 1.2vh, 1rem);\n --content-max: 70rem;\n --content-pad: clamp(1rem, 1.2vw, 1.4rem);\n --section-gap: clamp(0.75rem, 1vw, 1rem);\n --card-pad: clamp(1rem, 1.1vw, 1.25rem);\n --fs-page-title: clamp(1.2rem, 1vw + 0.35rem, 1.7rem);\n --fs-section-title: clamp(1rem, 0.9vw + 0.3rem, 1.35rem);\n --fs-body: clamp(0.85rem, 0.7vw + 0.3rem, 1.05rem);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody,\n#oee {\n width: 100vw;\n height: 100vh;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n overflow-x: hidden;\n background: var(--bg-0);\n color: var(--text);\n font-family: 'Poppins', system-ui, 'Segoe UI', 'Roboto', sans-serif;\n font-weight: 600;\n}\n\nbody {\n overflow: hidden;\n}\n\nbody > md-content,\nbody > md-content > md-content,\n.nr-dashboard-template,\n.nr-dashboard-template md-content,\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-content {\n background: transparent !important;\n height: 100%;\n overflow: hidden;\n}\n\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-card,\n.nr-dashboard-cardpanel md-card-content {\n background: transparent !important;\n box-shadow: none !important;\n padding: 0 !important;\n}\n\n/* LAYOUT: lock sidebar + content grid across tabs */\n#oee {\n position: fixed;\n inset: 0;\n display: flex;\n overflow: hidden;\n}\n\n.sidebar {\n width: var(--sidebar-width);\n background: #0b1119;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n padding: clamp(0.625rem, 1.4vh, 0.9rem) clamp(0.5rem, 0.8vw, 0.75rem);\n}\n\n.side-top {\n display: flex;\n flex-direction: column;\n gap: var(--sidebar-gap);\n align-items: center;\n}\n\n.sb-btn {\n width: 2.75rem;\n height: 2.75rem;\n border-radius: 0.75rem;\n background: #0f1721;\n border: 1px solid #19222e;\n display: grid;\n place-items: center;\n color: #8fb3d9;\n cursor: pointer;\n transition: 0.16s box-shadow, 0.16s transform, 0.16s border-color;\n}\n\n.sb-btn.active {\n border-color: #2fd289;\n box-shadow: 0 0 0 0.125rem rgba(36, 208, 111, .25), 0 0.625rem 1.125rem rgba(36, 208, 111, .28);\n color: #2fd289;\n}\n\n.sb-ico {\n font-size: clamp(1.1rem, 1.2vw, 1.25rem);\n line-height: 1;\n}\n\n.sb-foot {\n font-size: clamp(0.6rem, 0.6vw, 0.7rem);\n color: #6c7b8d;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.main {\n flex: 1;\n overflow: auto;\n padding: var(--content-pad);\n}\n\n.container {\n max-width: var(--content-max);\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: var(--section-gap);\n}\n\n.card {\n background: linear-gradient(160deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n}\n\n/* HEADER: maintain title styling */\n.page-heading {\n display: flex;\n align-items: center;\n padding: 0 0.25rem;\n}\n\n.page-title {\n margin: 0;\n font-size: var(--fs-page-title);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text);\n}\n\n/* HELP CARDS: keep text hierarchy + spacing clean */\n.help-card {\n display: flex;\n flex-direction: column;\n gap: clamp(0.9rem, 1vw, 1.1rem);\n padding: var(--card-pad);\n}\n\n.help-title {\n margin: 0;\n font-size: var(--fs-section-title);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text);\n}\n\n.help-body {\n margin: 0;\n line-height: 1.65;\n color: var(--muted);\n font-size: var(--fs-body);\n font-weight: 500;\n}\n\n\n/* Override Node-RED Dashboard's gray backgrounds on text elements */\n.nr-dashboard-template p,\n.nr-dashboard-template h1,\n.nr-dashboard-template h2,\n.nr-dashboard-template h3,\n.nr-dashboard-template h4,\n.nr-dashboard-template label {\n background-color: transparent !important;\n}\n</style>\n<div id=\"oee\">\n <aside class=\"sidebar\">\n <div class=\"side-top\">\n <button class=\"sb-btn\" data-label=\"Home\" ng-click=\"gotoTab('Home')\"><span class=\"sb-ico\">\ud83c\udfe0</span></button>\n <button class=\"sb-btn\" data-label=\"Work Orders\" ng-click=\"gotoTab('Work Orders')\"><span class=\"sb-ico\">\ud83d\udccb</span></button>\n <button class=\"sb-btn\" data-label=\"Alerts\" ng-click=\"gotoTab('Alerts')\"><span class=\"sb-ico\">\u26a0\ufe0f</span></button>\n <button class=\"sb-btn\" data-label=\"Graphs\" ng-click=\"gotoTab('Graphs')\"><span class=\"sb-ico\">\ud83d\udcca</span></button>\n <button class=\"sb-btn active\" data-label=\"Help\" ng-click=\"gotoTab('Help')\"><span class=\"sb-ico\">\u2753</span></button>\n <button class=\"sb-btn\" data-label=\"Settings\" ng-click=\"gotoTab('Settings')\"><span class=\"sb-ico\">\u2699\ufe0f</span></button>\n </div>\n <div class=\"sb-foot\">OEE V1.0</div>\n </aside>\n\n <main class=\"main\">\n <div class=\"container\">\n <section class=\"page-heading\">\n <h1 class=\"page-title\">Help</h1>\n </section>\n\n <section class=\"card help-card\">\n <h2 class=\"help-title\">About this Dashboard</h2>\n <p class=\"help-body\">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.</p>\n </section>\n\n <section class=\"card help-card\">\n <h2 class=\"help-title\">How to Start / Stop Production</h2>\n <p class=\"help-body\">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\u2019s standard operating procedure for approvals and sign-off.</p>\n </section>\n </div>\n </main>\n</div>\n\n<script>\n(function(scope) {\n scope.gotoTab = function(tabName) {\n scope.send({ ui_control: { tab: tabName } });\n };\n})(scope);\n\n(function ensureNoMaterialTint(){\n const root = document.getElementById('oee');\n if (!root) return;\n\n const targets = root.querySelectorAll([\n '.md-toolbar',\n 'md-toolbar',\n '.md-subheader',\n 'md-subheader',\n '.md-toolbar-tools',\n '.md-card-title'\n ].join(','));\n\n targets.forEach(el => {\n if (el && el.style) {\n if (el.style.background) {\n el.style.background = 'transparent';\n }\n if (el.style.backgroundColor) {\n el.style.backgroundColor = 'transparent';\n }\n }\n if (el && el.classList) {\n el.classList.remove(\n 'md-whiteframe-1dp','md-whiteframe-2dp','md-whiteframe-3dp',\n 'md-whiteframe-z1','md-whiteframe-z2','md-whiteframe-z3'\n );\n }\n });\n\n if (!document.getElementById('oee-theme-sentinel')) {\n const style = document.createElement('style');\n style.id = 'oee-theme-sentinel';\n style.textContent = `\n #oee .md-toolbar::before, #oee .md-toolbar::after,\n #oee .md-subheader::before, #oee .md-subheader::after,\n #oee md-toolbar::before, #oee md-toolbar::after,\n #oee md-subheader::before, #oee md-subheader::after {\n display: none !important;\n content: none !important;\n }\n `;\n document.head.appendChild(style);\n }\n})();\n\n\n// optional render trigger\nscope.$evalAsync(function() {});\n</script>\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": "<style>\n@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@600;700&display=swap');\n\n/* SCALE: align 100% zoom with prior 125% appearance */\n:root {\n font-size: 125%;\n --bg-0: #0c1117;\n --bg-1: #131a23;\n --panel: #151e2b;\n --panel-hi: #1a2433;\n --text: #ecf3ff;\n --muted: #96a5b7;\n --accent: #2ea3ff;\n --accent-2: #1f8cf3;\n --good: #24d06f;\n --warn: #ffd100;\n --bad: #ff4d4f;\n --radius: 0.75rem;\n --shadow: 0 0.5rem 1rem rgba(0, 0, 0, .35), inset 0 0.0625rem 0 rgba(255, 255, 255, .05);\n --sidebar-width: 3.75rem;\n --sidebar-gap: clamp(0.75rem, 1.2vh, 1rem);\n --content-max: 70rem;\n --content-pad: clamp(1rem, 1.2vw, 1.4rem);\n --section-gap: clamp(0.75rem, 1vw, 1rem);\n --card-pad: clamp(1rem, 1.1vw, 1.25rem);\n --fs-page-title: clamp(1.2rem, 1vw + 0.35rem, 1.7rem);\n --fs-section-title: clamp(1rem, 0.9vw + 0.3rem, 1.35rem);\n --fs-body: clamp(0.8rem, 0.7vw + 0.28rem, 1rem);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody,\n#oee {\n width: 100vw;\n height: 100vh;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n overflow-x: hidden;\n background: var(--bg-0);\n color: var(--text);\n font-family: 'Poppins', system-ui, 'Segoe UI', 'Roboto', sans-serif;\n font-weight: 600;\n}\n\nbody {\n overflow: hidden;\n}\n\nbody > md-content,\nbody > md-content > md-content,\n.nr-dashboard-template,\n.nr-dashboard-template md-content,\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-content {\n background: transparent !important;\n height: 100%;\n overflow: hidden;\n}\n\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-card,\n.nr-dashboard-cardpanel md-card-content {\n background: transparent !important;\n box-shadow: none !important;\n padding: 0 !important;\n}\n\n/* LAYOUT: lock sidebar + content grid across tabs */\n#oee {\n position: fixed;\n inset: 0;\n display: flex;\n overflow: hidden;\n}\n\n.sidebar {\n width: var(--sidebar-width);\n background: #0b1119;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n padding: clamp(0.625rem, 1.4vh, 0.9rem) clamp(0.5rem, 0.8vw, 0.75rem);\n}\n\n.side-top {\n display: flex;\n flex-direction: column;\n gap: var(--sidebar-gap);\n align-items: center;\n}\n\n.sb-btn {\n width: 2.75rem;\n height: 2.75rem;\n border-radius: 0.75rem;\n background: #0f1721;\n border: 1px solid #19222e;\n display: grid;\n place-items: center;\n color: #8fb3d9;\n cursor: pointer;\n transition: 0.16s box-shadow, 0.16s transform, 0.16s border-color;\n}\n\n.sb-btn.active {\n border-color: #2fd289;\n box-shadow: 0 0 0 0.125rem rgba(36, 208, 111, .25), 0 0.625rem 1.125rem rgba(36, 208, 111, .28);\n color: #2fd289;\n}\n\n.sb-ico {\n font-size: clamp(1.1rem, 1.2vw, 1.25rem);\n line-height: 1;\n}\n\n.sb-foot {\n font-size: clamp(0.6rem, 0.6vw, 0.7rem);\n color: #6c7b8d;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.main {\n flex: 1;\n overflow: auto;\n padding: var(--content-pad);\n}\n\n.container {\n max-width: var(--content-max);\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: var(--section-gap);\n}\n\n.card {\n background: linear-gradient(160deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n}\n\n/* HEADER: maintain title styling */\n.page-heading {\n display: flex;\n align-items: center;\n padding: 0 0.25rem;\n}\n\n.page-title {\n margin: 0;\n font-size: var(--fs-page-title);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text);\n}\n\n.section-title {\n margin: 0;\n font-size: var(--fs-section-title);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text);\n}\n\n/* SETTINGS CARDS: clean grid + balanced inputs */\n.mold-card,\n.integrations-card {\n display: flex;\n flex-direction: column;\n gap: clamp(0.9rem, 1vw, 1.1rem);\n padding: var(--card-pad);\n}\n\n.form-grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(13.75rem, 1fr));\n gap: clamp(0.75rem, 1vw, 1rem);\n}\n\n.form-field {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.form-field label {\n font-size: var(--fs-body);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--text);\n}\n\n.form-field input,\n.form-field select {\n background: #121c28;\n border: 1px solid #243244;\n border-radius: var(--radius);\n color: var(--text);\n padding: 0.75rem 0.9rem;\n font-family: 'Poppins', system-ui, 'Segoe UI', 'Roboto', sans-serif;\n font-weight: 600;\n font-size: var(--fs-body);\n}\n\n.integrations-button {\n width: fit-content;\n background: #121c28;\n border: 1px dashed rgba(150, 165, 183, .45);\n border-radius: var(--radius);\n color: rgba(150, 165, 183, .8);\n padding: 0.75rem 1.5rem;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n font-weight: 700;\n cursor: not-allowed;\n}\n.preset-add-form {\n margin-top: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.preset-add-actions {\n display: flex;\n gap: 0.75rem;\n justify-content: flex-end;\n}\n\n/* PRESET SEARCH STYLES */\n.preset-card {\n display: flex;\n flex-direction: column;\n gap: clamp(0.9rem, 1vw, 1.1rem);\n padding: var(--card-pad);\n}\n\n.preset-filters {\n display: grid;\n grid-template-columns: 2fr 1fr 1fr;\n gap: clamp(0.75rem, 1vw, 1rem);\n}\n\n.preset-results {\n max-height: 20rem;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n.preset-item {\n background: #121c28;\n border: 1px solid #243244;\n border-radius: var(--radius);\n padding: 1rem;\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 1rem;\n}\n\n.preset-info {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n}\n\n.preset-name {\n font-size: var(--fs-body);\n font-weight: 700;\n color: var(--text);\n}\n\n.preset-details {\n display: flex;\n gap: 1rem;\n font-size: clamp(0.75rem, 0.7vw + 0.25rem, 0.9rem);\n color: var(--muted);\n}\n\n.preset-badge {\n display: inline-block;\n background: rgba(46, 163, 255, 0.2);\n color: var(--accent);\n padding: 0.25rem 0.5rem;\n border-radius: 0.375rem;\n font-size: clamp(0.7rem, 0.6vw + 0.2rem, 0.85rem);\n font-weight: 600;\n}\n\n.preset-select-btn {\n background: linear-gradient(180deg, #32ff7e 0%, #2fd289 100%);\n box-shadow: 0 0 1.25rem rgba(36, 208, 111, .5), inset 0 0.0625rem 0 rgba(255, 255, 255, .1);\n border: none;\n border-radius: var(--radius);\n color: var(--text);\n padding: 0.6rem 1.2rem;\n font-weight: 700;\n font-size: var(--fs-body);\n text-transform: uppercase;\n letter-spacing: 0.05em;\n cursor: pointer;\n transition: 0.16s transform, 0.16s filter;\n}\n\n.preset-select-btn:hover {\n transform: translateY(-0.05rem);\n filter: brightness(1.08);\n}\n\n.preset-select-btn:active {\n transform: none;\n filter: brightness(0.96);\n}\n\n.preset-empty {\n text-align: center;\n color: var(--muted);\n padding: 2rem;\n font-size: var(--fs-body);\n}\n\n\n/* Override Node-RED Dashboard's gray backgrounds on text elements */\n.nr-dashboard-template p,\n.nr-dashboard-template h1,\n.nr-dashboard-template h2,\n.nr-dashboard-template h3,\n.nr-dashboard-template h4,\n.nr-dashboard-template label {\n background-color: transparent !important;\n}\n</style>\n<div id=\"oee\">\n <aside class=\"sidebar\">\n <div class=\"side-top\">\n <button class=\"sb-btn\" data-label=\"Home\" ng-click=\"gotoTab('Home')\"><span class=\"sb-ico\">\ud83c\udfe0</span></button>\n <button class=\"sb-btn\" data-label=\"Work Orders\" ng-click=\"gotoTab('Work Orders')\"><span class=\"sb-ico\">\ud83d\udccb</span></button>\n <button class=\"sb-btn\" data-label=\"Alerts\" ng-click=\"gotoTab('Alerts')\"><span class=\"sb-ico\">\u26a0\ufe0f</span></button>\n <button class=\"sb-btn\" data-label=\"Graphs\" ng-click=\"gotoTab('Graphs')\"><span class=\"sb-ico\">\ud83d\udcca</span></button>\n <button class=\"sb-btn\" data-label=\"Help\" ng-click=\"gotoTab('Help')\"><span class=\"sb-ico\">\u2753</span></button>\n <button class=\"sb-btn active\" data-label=\"Settings\" ng-click=\"gotoTab('Settings')\"><span class=\"sb-ico\">\u2699\ufe0f</span></button>\n </div>\n <div class=\"sb-foot\">OEE V1.0</div>\n </aside>\n\n <main class=\"main\">\n <div class=\"container\">\n <section class=\"page-heading\">\n <h1 class=\"page-title\">Settings</h1>\n </section>\n\n <section class=\"card preset-card\">\n <h2 class=\"section-title\">Mold Presets</h2>\n <div class=\"preset-filters\">\n <div class=\"form-field\">\n <label for=\"preset-manufacturer\">Manufacturer</label>\n <select id=\"preset-manufacturer\"\n ng-model=\"selectedManufacturer\"\n ng-change=\"onManufacturerChange()\">\n <option value=\"\">Select manufacturer...</option>\n <option ng-repeat=\"mfg in manufacturers\" value=\"{{mfg}}\">{{mfg}}</option>\n </select>\n </div>\n <div class=\"form-field\">\n <label for=\"preset-mold\">Mold</label>\n <select id=\"preset-mold\"\n ng-model=\"selectedMold\"\n ng-options=\"preset as preset.mold_name for preset in moldsForManufacturer | orderBy:'mold_name'\"\n ng-change=\"onMoldChange()\">\n <option value=\"\">Select mold...</option>\n </select>\n </div>\n </div>\n \n <div class=\"preset-results\" id=\"preset-results\">\n <!-- Default state: info + Add Mold button -->\n <div class=\"preset-empty\" ng-if=\"!showAddMoldForm\">\n <p>Select a manufacturer and mold from the dropdowns above.</p>\n <p>If you can't find the mold you're looking for, add a new one:</p>\n <button class=\"preset-select-btn\" type=\"button\" ng-click=\"openAddMold()\">Add Mold</button>\n </div>\n \n <!-- Add Mold form -->\n <div class=\"preset-add-form\" ng-if=\"showAddMoldForm\">\n <div class=\"form-grid\">\n <div class=\"form-field\">\n <label for=\"new-mold-manufacturer\">Manufacturer</label>\n <input id=\"new-mold-manufacturer\"\n type=\"text\"\n ng-model=\"newMold.manufacturer\"\n placeholder=\"Type manufacturer...\" />\n </div>\n <div class=\"form-field\">\n <label for=\"new-mold-name\">Mold Name</label>\n <input id=\"new-mold-name\"\n type=\"text\"\n ng-model=\"newMold.mold_name\"\n placeholder=\"Type mold name...\" />\n </div>\n <div class=\"form-field\">\n <label for=\"new-mold-cavities\">Mold Cavities</label>\n <input id=\"new-mold-cavities\"\n type=\"text\"\n ng-model=\"newMold.cavities\"\n placeholder=\"Type Theoretical Cavities...\" />\n </div>\n <div class=\"form-field\">\n <label for=\"new-mold-active\">Active Cavities</label>\n <input id=\"new-mold-active\"\n type=\"text\"\n ng-model=\"newMold.active\"\n placeholder=\"Type Active Cavities...\" />\n </div>\n </div>\n <div class=\"preset-add-actions\">\n <button class=\"preset-select-btn\" \n type=\"button\" \n ng-click=\"saveNewMold()\"\n ng-disabled=\"isSavingMold\">\n {{ isSavingMold ? 'Saving...' : 'Save' }}\n </button>\n <button class=\"preset-select-btn\" \n type=\"button\" \n ng-click=\"cancelAddMold()\"\n ng-disabled=\"isSavingMold\">\n Cancel\n </button>\n </div>\n </div>\n </div>\n </section>\n\n <section class=\"card mold-card\">\n <h2 class=\"form-field\">Mold Configuration</h2>\n <div class=\"form-grid\">\n <div class=\"form-field\">\n <label for=\"mold-active\">Mold Cavities</label>\n <input id=\"mold-total\" type=\"number\" ng-model=\"moldTotal\" ng-change=\"updateMoldSettings()\" />\n </div>\n <div class=\"form-field\">\n <label for=\"mold-active\">Active Cavities</label>\n <input id=\"mold-active\" type=\"number\" ng-model=\"moldActive\" ng-change=\"updateMoldSettings()\" />\n </div>\n </div>\n </section>\n\n <section class=\"card integrations-card\">\n <h2 class=\"section-title\">Integrations</h2>\n <button class=\"integrations-button\" type=\"button\" disabled>Connect to ERP</button>\n </section>\n </div>\n </main>\n</div>\n\n<script>\n (function(scope) {\n // Initialize state\n scope.moldTotal = 0;\n scope.moldActive = 0;\n scope.selectedManufacturer = '';\n scope.moldsForManufacturer = [];\n scope.selectedMold = null;\n scope.manufacturers = [];\n scope.showAddMoldForm = false;\n scope.newMold = { manufacturer: '', mold_name: '', cavities: '', active: '' };\n scope.isSavingMold = false;\n \n // Debounce timers\n scope._moldSettingsDebounceTimer = null;\n scope._saveMoldTimeout = null;\n \n // Track processed messages by topic + timestamp to prevent loops\n scope._processedMessages = {};\n \n // Clean old processed messages every 10 seconds\n setInterval(function() {\n const now = Date.now();\n Object.keys(scope._processedMessages).forEach(function(key) {\n if (now - scope._processedMessages[key] > 10000) {\n delete scope._processedMessages[key];\n }\n });\n }, 10000);\n \n // Navigation\n scope.gotoTab = function(tabName) {\n scope.isSavingMold = false;\n scope.send({ ui_control: { tab: tabName } });\n };\n \n // Load manufacturers on init\n scope.loadManufacturers = function() {\n console.log('Loading manufacturers...');\n scope.send({ topic: 'getManufacturers' });\n };\n \n // When manufacturer changes\n scope.onManufacturerChange = function() {\n scope.selectedMold = null;\n scope.moldsForManufacturer = [];\n \n const manufacturer = scope.selectedManufacturer || '';\n if (!manufacturer) {\n return;\n }\n \n console.log('Loading molds for:', manufacturer);\n scope.send({\n topic: 'getMoldsByManufacturer',\n payload: { manufacturer: manufacturer }\n });\n };\n \n // When mold changes\n scope.onMoldChange = function() {\n if (!scope.selectedMold) return;\n scope.selectPreset(scope.selectedMold);\n };\n \n // Select a preset\n scope.selectPreset = function(preset) {\n scope.send({\n topic: 'selectMoldPreset',\n payload: preset\n });\n \n scope.moldTotal = preset.theoretical_cavities;\n scope.moldActive = preset.functional_cavities;\n };\n \n // Update mold settings with debounce\n scope.updateMoldSettings = function() {\n if (scope._moldSettingsDebounceTimer) {\n clearTimeout(scope._moldSettingsDebounceTimer);\n }\n \n scope._moldSettingsDebounceTimer = setTimeout(function() {\n const total = Number(scope.moldTotal || 0);\n const active = Number(scope.moldActive || 0);\n \n if (total === (scope._lastMoldTotal || 0) && active === (scope._lastMoldActive || 0)) {\n return;\n }\n \n scope._lastMoldTotal = total;\n scope._lastMoldActive = active;\n \n scope.send({\n topic: 'moldSettings',\n payload: { total, active }\n });\n }, 500);\n };\n \n // Add mold form\n scope.openAddMold = function() {\n scope.showAddMoldForm = true;\n scope.newMold = {\n manufacturer: scope.selectedManufacturer || '',\n mold_name: '',\n cavities: '',\n active: ''\n };\n };\n \n scope.cancelAddMold = function() {\n scope.showAddMoldForm = false;\n scope.isSavingMold = false;\n scope.newMold = { manufacturer: '', mold_name: '', cavities: '', active: '' };\n };\n \n // Save new mold with proper debouncing\n scope.saveNewMold = function() {\n if (scope.isSavingMold) {\n console.log('Already saving, ignoring click');\n return;\n }\n \n const manufacturer = (scope.newMold.manufacturer || '').trim();\n const moldName = (scope.newMold.mold_name || '').trim();\n const theoretical = (scope.newMold.cavities || '').trim();\n const active = (scope.newMold.active || '').trim();\n \n if (!manufacturer || !moldName || !theoretical || !active) {\n alert('Please enter all values.');\n return;\n }\n \n scope.isSavingMold = true;\n \n // Clear any pending timeout\n if (scope._saveMoldTimeout) {\n clearTimeout(scope._saveMoldTimeout);\n }\n \n // Send after 100ms to consolidate rapid clicks\n scope._saveMoldTimeout = setTimeout(function() {\n console.log('Sending addMoldPreset request');\n scope.send({\n topic: 'addMoldPreset',\n payload: {\n manufacturer: manufacturer,\n mold_name: moldName,\n theoretical: theoretical,\n active: active\n }\n });\n scope._saveMoldTimeout = null;\n }, 100);\n };\n \n // Watch for incoming messages - FIXED\n scope.$watch('msg', function(newMsg, oldMsg) {\n if (!newMsg || !newMsg.topic) return;\n \n // Create unique key for this message\n const msgKey = newMsg.topic + '_' + (newMsg.payload ? JSON.stringify(newMsg.payload).substring(0, 100) : '');\n const now = Date.now();\n \n // Check if we've processed this message recently (within 1 second)\n if (scope._processedMessages[msgKey] && (now - scope._processedMessages[msgKey]) < 1000) {\n console.log('Duplicate message ignored:', newMsg.topic);\n return;\n }\n \n // Mark as processed\n scope._processedMessages[msgKey] = now;\n \n console.log('Processing message:', newMsg.topic, newMsg.payload);\n \n // Handle different message types\n if (newMsg.topic === 'manufacturersList') {\n scope.manufacturers = Array.isArray(newMsg.payload) ? newMsg.payload : [];\n console.log('Manufacturers loaded:', scope.manufacturers.length);\n return;\n }\n \n if (newMsg.topic === 'moldPresetsList') {\n scope.moldsForManufacturer = Array.isArray(newMsg.payload) ? newMsg.payload : [];\n console.log('Molds loaded:', scope.moldsForManufacturer.length);\n return;\n }\n \n if (newMsg.topic === 'moldPresetSelected') {\n const data = newMsg.payload || {};\n scope.moldTotal = data.total || 0;\n scope.moldActive = data.active || 0;\n console.log('Preset selected:', data);\n return;\n }\n \n if (newMsg.topic === 'addMoldResult') {\n console.log('Mold added successfully');\n scope.isSavingMold = false;\n scope.showAddMoldForm = false;\n scope.newMold = { manufacturer: '', mold_name: '', cavities: '', active: '' };\n \n // Refresh the molds list if manufacturer is selected\n if (scope.selectedManufacturer) {\n setTimeout(function() {\n scope.onManufacturerChange();\n }, 500);\n }\n return;\n }\n });\n \n // Initialize by loading manufacturers\n console.log('Initializing Settings page...');\n scope.loadManufacturers();\n \n})(scope);\n\n// Material theme cleanup\n(function ensureNoMaterialTint(){\n const root = document.getElementById('oee');\n if (!root) return;\n\n const targets = root.querySelectorAll([\n '.md-toolbar',\n 'md-toolbar',\n '.md-subheader',\n 'md-subheader',\n '.md-toolbar-tools',\n '.md-card-title'\n ].join(','));\n\n targets.forEach(el => {\n if (el && el.style) {\n if (el.style.background) {\n el.style.background = 'transparent';\n }\n if (el.style.backgroundColor) {\n el.style.backgroundColor = 'transparent';\n }\n }\n if (el && el.classList) {\n el.classList.remove(\n 'md-whiteframe-1dp','md-whiteframe-2dp','md-whiteframe-3dp',\n 'md-whiteframe-z1','md-whiteframe-z2','md-whiteframe-z3'\n );\n }\n });\n\n if (!document.getElementById('oee-theme-sentinel')) {\n const style = document.createElement('style');\n style.id = 'oee-theme-sentinel';\n style.textContent = `\n #oee .md-toolbar::before, #oee .md-toolbar::after,\n #oee .md-subheader::before, #oee .md-subheader::after,\n #oee md-toolbar::before, #oee md-toolbar::after,\n #oee md-subheader::before, #oee md-subheader::after {\n display: none !important;\n content: none !important;\n }\n `;\n document.head.appendChild(style);\n }\n})();\n</script>\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": "<style>\n@import url('https://fonts.googleapis.com/css2?family=Poppins:wght@600;700&display=swap');\n\n/* SCALE: align 100% zoom with prior 125% appearance */\n:root {\n font-size: 125%;\n --bg-0: #0c1117;\n --bg-1: #131a23;\n --panel: #151e2b;\n --panel-hi: #1a2433;\n --text: #ecf3ff;\n --muted: #96a5b7;\n --accent: #2ea3ff;\n --accent-2: #1f8cf3;\n --good: #24d06f;\n --warn: #ffd100;\n --bad: #ff4d4f;\n --radius: 0.75rem;\n --shadow: 0 0.5rem 1rem rgba(0, 0, 0, .35), inset 0 0.0625rem 0 rgba(255, 255, 255, .05);\n --sidebar-width: 3.75rem;\n --sidebar-gap: clamp(0.75rem, 1.2vh, 1rem);\n --content-max: 100rem;\n --content-pad: clamp(1rem, 1.2vw, 1.4rem);\n --section-gap: clamp(0.75rem, 1vw, 1rem);\n --card-pad: clamp(1rem, 1.1vw, 1.25rem);\n --card-pad-lg: clamp(1.1rem, 1.2vw, 1.35rem);\n --fs-page-title: clamp(1.2rem, 1vw + 0.35rem, 1.7rem);\n --fs-section-title: clamp(0.95rem, 0.9vw + 0.25rem, 1.25rem);\n --fs-label: clamp(0.85rem, 0.7vw + 0.3rem, 1.05rem);\n --fs-body: clamp(0.8rem, 0.7vw + 0.28rem, 1rem);\n --progress-height: 0.875rem;\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody,\n#oee {\n width: 100vw;\n height: 100vh;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n overflow-x: hidden;\n background: var(--bg-0);\n color: var(--text);\n font-family: 'Poppins', system-ui, 'Segoe UI', 'Roboto', sans-serif;\n font-weight: 600;\n}\n\nbody {\n overflow: hidden;\n}\n\nbody > md-content,\nbody > md-content > md-content,\n.nr-dashboard-template,\n.nr-dashboard-template md-content,\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-content {\n background: transparent !important;\n height: 100%;\n overflow: hidden;\n}\n\n.nr-dashboard-cardpanel,\n.nr-dashboard-cardpanel md-card,\n.nr-dashboard-cardpanel md-card-content {\n background: transparent !important;\n box-shadow: none !important;\n padding: 0 !important;\n}\n\n/* LAYOUT: lock sidebar + content grid across tabs */\n#oee {\n position: fixed;\n inset: 0;\n display: flex;\n overflow: hidden;\n}\n\n.sidebar {\n width: var(--sidebar-width);\n background: #0b1119;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: space-between;\n padding: clamp(0.625rem, 1.4vh, 0.9rem) clamp(0.5rem, 0.8vw, 0.75rem);\n}\n\n.side-top {\n display: flex;\n flex-direction: column;\n gap: var(--sidebar-gap);\n align-items: center;\n}\n\n.sb-btn {\n width: 2.75rem;\n height: 2.75rem;\n border-radius: 0.75rem;\n background: #0f1721;\n border: 1px solid #19222e;\n display: grid;\n place-items: center;\n color: #8fb3d9;\n cursor: pointer;\n transition: 0.16s box-shadow, 0.16s transform, 0.16s border-color;\n}\n\n.sb-btn.active {\n border-color: #2fd289;\n box-shadow: 0 0 0 0.125rem rgba(36, 208, 111, .25), 0 0.625rem 1.125rem rgba(36, 208, 111, .28);\n color: #2fd289;\n}\n\n.sb-ico {\n font-size: clamp(1.1rem, 1.2vw, 1.25rem);\n line-height: 1;\n}\n\n.sb-foot {\n font-size: clamp(0.6rem, 0.6vw, 0.7rem);\n color: #6c7b8d;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n}\n\n.main {\n flex: 1;\n overflow: auto;\n padding: var(--content-pad);\n}\n\n.container {\n max-width: var(--content-max);\n margin: 0 auto;\n display: flex;\n flex-direction: column;\n gap: var(--section-gap);\n}\n\n.card {\n background: linear-gradient(160deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius);\n box-shadow: var(--shadow);\n}\n\n/* HEADER: keep title and CTA row on one line */\n.page-heading {\n display: flex;\n align-items: center;\n gap: var(--section-gap);\n padding: 0 0.25rem;\n flex-wrap: wrap;\n}\n\n.page-title {\n margin: 0;\n font-size: var(--fs-page-title);\n letter-spacing: 0.05em;\n text-transform: uppercase;\n color: var(--text);\n}\n\n.page-actions {\n margin-left: auto;\n display: flex;\n gap: clamp(0.65rem, 1vw, 0.9rem);\n flex-wrap: wrap;\n}\n\n.action-btn {\n background: linear-gradient(180deg, #32ff7e 0%, #2fd289 100%);\n box-shadow: 0 0 1.25rem rgba(36, 208, 111, .5), inset 0 0.0625rem 0 rgba(255, 255, 255, .1);\n border: none;\n border-radius: var(--radius);\n color: var(--text);\n text-transform: uppercase;\n letter-spacing: 0.08em;\n font-weight: 700;\n padding: clamp(0.75rem, 1vw, 0.9rem) clamp(1rem, 1.4vw, 1.25rem);\n cursor: pointer;\n transition: 0.16s transform, 0.16s filter;\n min-width: clamp(8.75rem, 12vw, 10rem);\n}\n\n.action-btn:hover {\n transform: translateY(-0.05rem);\n filter: brightness(1.08);\n}\n\n.action-btn:active {\n transform: none;\n filter: brightness(0.96);\n}\n\n/* TABLE: keep density + typography aligned with Home */\n.table-card {\npadding: var(--card-pad-lg);\ndisplay: flex;\nflex-direction: column;\ngap: clamp(0.75rem, 0.9vw, 0.95rem);\n}\n\n.table-scroll {\noverflow: auto;\nborder-radius: 0.625rem;\nborder: 1px solid rgba(31, 44, 60, 0.55);\nbackground: rgba(13, 19, 27, 0.6);\n}\n\n.workorders-table {\nwidth: 100%;\ntable-layout: fixed;\nborder-collapse: separate;\nborder-spacing: 0;\nmin-width: 52rem;\nfont-variant-numeric: tabular-nums;\n}\n\n.workorders-table thead th {\nposition: sticky;\ntop: 0;\nz-index: 2;\ncolor: var(--text);\ntext-transform: uppercase;\nletter-spacing: 0.04em;\nfont-size: clamp(0.75rem, 0.9vw, 0.85rem);\npadding: clamp(0.65rem, 0.9vw, 0.9rem);\nbackground: rgba(21, 30, 43, 0.9);\ntext-align: left;\nborder-bottom: 1px solid rgba(47, 67, 95, 0.55);\n}\n\n.workorders-table tbody td {\npadding: clamp(0.65rem, 0.95vw, 0.95rem);\ncolor: var(--text);\nfont-size: var(--fs-body);\nbackground: rgba(17, 24, 34, 0.7);\nborder-bottom: 1px solid rgba(31, 44, 60, 0.45);\n}\n\n.workorders-table thead th:not(:last-child),\n.workorders-table tbody td:not(:last-child) {\nborder-right: 1px solid rgba(31, 44, 60, 0.35);\n}\n\n.workorders-table tbody tr:nth-child(odd) td {\nbackground: rgba(17, 27, 39, 0.82);\n}\n\n.workorders-table tbody tr:hover td {\nbackground: rgba(36, 56, 80, 0.55);\n}\n\n.workorders-table thead th:nth-child(3),\n.workorders-table thead th:nth-child(4),\n.workorders-table thead th:nth-child(5),\n.workorders-table thead th:nth-child(8),\n.workorders-table tbody td:nth-child(3),\n.workorders-table tbody td:nth-child(4),\n.workorders-table tbody td:nth-child(5),\n.workorders-table tbody td:nth-child(8) {\ntext-align: right;\n}\n\n.progress-cell {\nmin-width: 9.5rem;\n}\n\n.progress-bar {\nheight: var(--progress-height);\nborder-radius: 999px;\nbackground: #111a24;\nborder: 1px solid #243244;\noverflow: hidden;\n}\n\n.progress-bar__fill {\nheight: 100%;\nwidth: 0;\ntransition: width 0.4s ease;\n}\n.workorders-table tbody tr {\ncursor: pointer;\ntransition: background 0.18s ease, border-color 0.18s ease;\n}\n\n.workorders-table tbody tr.row-selected td {\nbackground: rgba(46, 163, 255, 0.18);\nborder-bottom-color: rgba(46, 163, 255, 0.45);\nbox-shadow: inset 0 0 0 1px rgba(46, 163, 255, 0.28);\n}\n\n.progress-bar__fill--pending {\n background: linear-gradient(90deg, rgba(150, 165, 183, .5), rgba(150, 165, 183, .8));\n}\n\n.progress-bar__fill--running {\n background: linear-gradient(90deg, rgba(46, 163, 255, .6), rgba(46, 163, 255, .85));\n}\n\n.progress-bar__fill--done {\n background: linear-gradient(90deg, rgba(36, 208, 111, .75), rgba(36, 208, 111, .95));\n}\n\n.status-badge {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0.375rem 0.875rem;\n border-radius: 999px;\n font-size: clamp(0.75rem, 0.9vw, 0.85rem);\n letter-spacing: 0.2em;\n text-transform: uppercase;\n font-weight: 700;\n}\n\n.status-badge--pending {\n color: var(--muted);\n background: rgba(20, 28, 40, .65);\n border: 1px solid rgba(150, 165, 183, .35);\n}\n\n.status-badge--running {\n color: var(--text);\n background: rgba(30, 52, 73, .7);\n border: 1px solid rgba(46, 163, 255, .45);\n}\n\n.status-badge--done {\n color: var(--good);\n background: rgba(20, 56, 44, .7);\n border: 1px solid rgba(36, 208, 111, .45);\n}\n\n.table-footer {\n display: flex;\n justify-content: flex-end;\n font-size: clamp(0.75rem, 0.9vw, 0.85rem);\n color: var(--muted);\n}\n\n\n@media (max-width: 68.75rem) {\n .page-actions {\n width: 100%;\n justify-content: flex-start;\n }\n}\n\n\n/* Override Node-RED Dashboard's gray backgrounds on text elements */\n.nr-dashboard-template p,\n.nr-dashboard-template h1,\n.nr-dashboard-template h2,\n.nr-dashboard-template h3,\n.nr-dashboard-template h4,\n.nr-dashboard-template label {\n background-color: transparent !important;\n}\n</style>\n<div id=\"oee\">\n <aside class=\"sidebar\">\n <div class=\"side-top\">\n <button class=\"sb-btn\" data-label=\"Home\" ng-click=\"gotoTab('Home')\"><span class=\"sb-ico\">\ud83c\udfe0</span></button>\n <button class=\"sb-btn active\" data-label=\"Work Orders\" ng-click=\"gotoTab('Work Orders')\"><span class=\"sb-ico\">\ud83d\udccb</span></button>\n <button class=\"sb-btn\" data-label=\"Alerts\" ng-click=\"gotoTab('Alerts')\"><span class=\"sb-ico\">\u26a0\ufe0f</span></button>\n <button class=\"sb-btn\" data-label=\"Graphs\" ng-click=\"gotoTab('Graphs')\"><span class=\"sb-ico\">\ud83d\udcca</span></button>\n <button class=\"sb-btn\" data-label=\"Help\" ng-click=\"gotoTab('Help')\"><span class=\"sb-ico\">\u2753</span></button>\n <button class=\"sb-btn\" data-label=\"Settings\" ng-click=\"gotoTab('Settings')\"><span class=\"sb-ico\">\u2699\ufe0f</span></button>\n </div>\n <div class=\"sb-foot\">OEE V1.0</div>\n </aside>\n\n <main class=\"main\">\n <div class=\"container\">\n <section class=\"page-heading\">\n <h1 class=\"page-title\">Work Orders</h1>\n <div class=\"page-actions\">\n <button class=\"action-btn\" data-action=\"upload-excel\">Upload Excel</button>\n <button class=\"action-btn\" data-action=\"start-work-order\">Start</button>\n <button class=\"action-btn\" data-action=\"complete-work-order\">Done</button>\n <button class=\"action-btn\" data-action=\"refresh-work-orders\">Refresh</button>\n <input type=\"file\" id=\"wo-file\" accept=\".xlsx\" style=\"display:none\" />\n </div>\n </section>\n\n <section class=\"card table-card\">\n <div class=\"table-scroll\">\n <table class=\"workorders-table\">\n <thead>\n <tr>\n <th>ID</th>\n <th>SKU</th>\n <th>TARGET</th>\n <th>GOOD</th>\n <th>SCRAP</th>\n <th>PROGRESS</th>\n <th>STATUS</th>\n <th>LAST UPDATE</th>\n </tr>\n </thead>\n <tbody id=\"workorders-body\"></tbody>\n </table>\n </div>\n <div class=\"table-footer\"><span id=\"workorders-count\">0 items</span></div>\n </section>\n </div>\n </main>\n</div>\n\n<script>\n(function(scope) {\n scope.gotoTab = function(tabName) {\n scope.send({ ui_control: { tab: tabName } });\n };\n\n window.workOrders = window.workOrders || [\n { id:\"WO-101\", sku:\"ST-003\", target:250, good:0, scrap:0, progressPercent:0, status:\"PENDING\", lastUpdateIso:\"\" }\n ];\n\n var elements = {\n tbody: document.getElementById('workorders-body'),\n count: document.getElementById('workorders-count'),\n buttons: document.querySelectorAll('[data-action]')\n };\n\n function formatNumber(value) {\n var num = Number(value);\n return Number.isFinite(num) ? num.toLocaleString() : '0';\n }\n\n function formatDate(isoString) {\n if (!isoString) {\n return '\u2014';\n }\n var date = new Date(isoString);\n if (Number.isNaN(date.valueOf())) {\n return isoString;\n }\n return date.toLocaleString();\n }\n\n function progressFillClass(status) {\n switch ((status || '').toUpperCase()) {\n case 'DONE': return 'progress-bar__fill--done';\n case 'RUNNING': return 'progress-bar__fill--running';\n default: return 'progress-bar__fill--pending';\n }\n }\n\n function statusBadgeClass(status) {\n switch ((status || '').toUpperCase()) {\n case 'DONE': return 'status-badge status-badge--done';\n case 'RUNNING': return 'status-badge status-badge--running';\n default: return 'status-badge status-badge--pending';\n }\n }\n\n function renderWorkOrders() {\n var orders = Array.isArray(window.workOrders) ? window.workOrders : [];\n orders = orders.filter(function(order) {\n return (order.status || '').toUpperCase() !== 'DONE';\n });\n var selectedId = scope.selectedOrder ? scope.selectedOrder.id : null;\n \n if (elements.tbody) {\n elements.tbody.textContent = '';\n scope.selectedRow = null;\n \n orders.forEach(function(order) {\n var tr = document.createElement('tr');\n tr.dataset.orderId = order.id || '';\n \n function appendCell(content, alignRight) {\n var td = document.createElement('td');\n if (alignRight) td.style.textAlign = 'right';\n if (content instanceof Node) {\n td.appendChild(content);\n } else {\n td.textContent = content;\n }\n tr.appendChild(td);\n }\n \n appendCell(order.id || '\u2014');\n appendCell(order.sku || '\u2014');\n appendCell(formatNumber(order.target), true);\n appendCell(formatNumber(order.good), true);\n appendCell(formatNumber(order.scrap), true);\n \n var progressCell = document.createElement('td');\n progressCell.className = 'progress-cell';\n var bar = document.createElement('div');\n bar.className = 'progress-bar';\n var fill = document.createElement('div');\n fill.className = 'progress-bar__fill ' + progressFillClass(order.status);\n var percent = Math.max(0, Math.min(100, Math.round(Number(order.progressPercent) || 0)));\n fill.style.width = percent + '%';\n bar.appendChild(fill);\n progressCell.appendChild(bar);\n tr.appendChild(progressCell);\n \n var statusCell = document.createElement('td');\n var badge = document.createElement('span');\n badge.className = statusBadgeClass(order.status);\n badge.textContent = (order.status || 'PENDING').toUpperCase();\n statusCell.appendChild(badge);\n tr.appendChild(statusCell);\n \n appendCell(formatDate(order.lastUpdateIso));\n \n tr.addEventListener('click', function() {\n if (scope.selectedRow && scope.selectedRow !== tr) {\n scope.selectedRow.classList.remove('row-selected');\n }\n scope.selectedRow = tr;\n scope.selectedOrder = order;\n tr.classList.add('row-selected');\n scope.$applyAsync();\n });\n \n if (selectedId && selectedId === (order.id || '')) {\n tr.classList.add('row-selected');\n scope.selectedRow = tr;\n }\n \n elements.tbody.appendChild(tr);\n });\n }\n \n if (elements.count) {\n var countText = orders.length + (orders.length === 1 ? ' item' : ' items');\n elements.count.textContent = countText;\n }\n }\n\n scope.renderWorkOrders = renderWorkOrders;\n renderWorkOrders();\n\nif (elements.buttons) {\nelements.buttons.forEach(function(btn) {\nbtn.addEventListener('click', function() {\nconst action = btn.getAttribute(\"data-action\");\nswitch (action) {\ncase \"upload-excel\": handleUploadExcel(); break;\ncase \"refresh-work-orders\": handleRefresh(); break;\ncase \"start-work-order\": handleStart(); break;\ncase \"complete-work-order\": handleComplete(); break;\ndefault: scope.send({ action }); break;\n}\n});\n});\n}\n function handleUploadExcel() {\n const input = document.getElementById(\"wo-file\");\n input.value = \"\"; // reset\n input.onchange = function (event) {\n const file = event.target.files[0];\n if (!file) return;\n const reader = new FileReader();\n reader.onload = function (e) {\n const base64 = e.target.result.split(\",\")[1];\n // Send Base64 file to Node-RED\n scope.send({\n action: \"upload-excel\",\n filename: file.name,\n payload: base64\n });\n };\n reader.readAsDataURL(file);\n };\n input.click();\n}\n\n// 2\ufe0f\u20e3 Refresh work orders\nfunction handleRefresh() {\n scope.send({ action: \"refresh-work-orders\" });\n}\n\n// 3\ufe0f\u20e3 Start a work order\nfunction handleStart() {\nif (!scope.selectedOrder) return alert(\"Please select a work order first.\");\n scope.send({ action: \"start-work-order\", payload: scope.selectedOrder });\n handleRefresh(); // fetch latest statuses immediately\n}\n\n// 4\ufe0f\u20e3 Complete a work order\nfunction handleComplete() {\nif (!scope.selectedOrder) return alert(\"No active work order selected.\");\n scope.send({ action: \"complete-work-order\", payload: scope.selectedOrder });\n handleRefresh(); // keep table in sync after completion\n}\n\n// 5\ufe0f\u20e3 Listen to messages from Node-RED\nscope.$watch(\"msg\", function(msg) {\n if (!msg) return;\n if (msg.topic === \"workOrdersList\") {\n // Update frontend with DB data\n window.workOrders = msg.payload || [];\n scope.renderWorkOrders();\n }\n if (msg.topic === \"uploadStatus\") {\n alert(msg.payload.message || \"Upload complete\");\n handleRefresh();\n }\n});\n\nfunction handleUploadExcel() {\nconst input = document.getElementById(\"wo-file\");\ninput.value = \"\"; // reset\ninput.onchange = function (event) {\nconst file = event.target.files[0];\nif (!file) return;\nconst reader = new FileReader();\nreader.onload = function (e) {\nconst base64 = e.target.result.split(\",\")[1];\n// Send Base64 file to Node-RED\nscope.send({\naction: \"upload-excel\",\nfilename: file.name,\npayload: base64\n});\n};\nreader.readAsDataURL(file);\n};\ninput.click();\n}\n\n// 2\ufe0f\u20e3 Refresh work orders\nfunction handleRefresh() {\nscope.send({ action: \"refresh-work-orders\" });\n}\n\n// 3\ufe0f\u20e3 Start a work order\nfunction handleStart() {\nif (!scope.selectedOrder) return alert(\"Please select a work order first.\");\nscope.send({ action: \"start-work-order\", payload: scope.selectedOrder });\n}\n\n// 4\ufe0f\u20e3 Complete a work order\nfunction handleComplete() {\nif (!scope.selectedOrder) return alert(\"No active work order selected.\");\nscope.send({ action: \"complete-work-order\", payload: scope.selectedOrder });\n}\n\n// 5\ufe0f\u20e3 Listen to messages from Node-RED\n scope.$watch(\"msg\", function(msg) {\n if (!msg) return;\n \n if (msg.topic === \"workOrderCycle\") {\n var update = msg.payload || {};\n var orders = Array.isArray(window.workOrders) ? window.workOrders.slice() : [];\n var found = false;\n \n orders = orders.map(function(order) {\n if ((order.id || \"\") === (update.id || \"\")) {\n found = true;\n return Object.assign({}, order, {\n good: Number(update.good) || 0,\n scrap: Number(update.scrap) || 0,\n target: Number(update.target) || 0,\n progressPercent: Number(update.progressPercent) || 0,\n status: update.status ? update.status.toUpperCase() : (order.status || \"PENDING\"),\n lastUpdateIso: update.lastUpdateIso || order.lastUpdateIso\n });\n }\n return order;\n });\n \n if (!found && update.id) {\n orders.push(update);\n }\n \n window.workOrders = orders;\n scope.renderWorkOrders();\n return;\n }\n \n if (msg.topic === \"workOrdersList\") {\n window.workOrders = msg.payload || [];\n scope.renderWorkOrders();\n }\n \n if (msg.topic === \"uploadStatus\") {\n alert(msg.payload.message || \"Upload complete\");\n handleRefresh();\n }\n });\n\n\n})(scope);\n\n(function ensureNoMaterialTint(){\n const root = document.getElementById('oee');\n if (!root) return;\n\n const targets = root.querySelectorAll([\n '.md-toolbar',\n 'md-toolbar',\n '.md-subheader',\n 'md-subheader',\n '.md-toolbar-tools',\n '.md-card-title'\n ].join(','));\n\n targets.forEach(el => {\n if (el && el.style) {\n if (el.style.background) {\n el.style.background = 'transparent';\n }\n if (el.style.backgroundColor) {\n el.style.backgroundColor = 'transparent';\n }\n }\n if (el && el.classList) {\n el.classList.remove(\n 'md-whiteframe-1dp','md-whiteframe-2dp','md-whiteframe-3dp',\n 'md-whiteframe-z1','md-whiteframe-z2','md-whiteframe-z3'\n );\n }\n });\n\n if (!document.getElementById('oee-theme-sentinel')) {\n const style = document.createElement('style');\n style.id = 'oee-theme-sentinel';\n style.textContent = `\n #oee .md-toolbar::before, #oee .md-toolbar::after,\n #oee .md-subheader::before, #oee .md-subheader::after,\n #oee md-toolbar::before, #oee md-toolbar::after,\n #oee md-subheader::before, #oee md-subheader::after {\n display: none !important;\n content: none !important;\n }\n `;\n document.head.appendChild(style);\n }\n})();\n\n\n// optional render trigger\nscope.$evalAsync(scope.renderWorkOrders);\n</script>\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": "<style>\n ::selection {\n background-color: blue; /* Or a specific blue hex code like #0000FF */\n color: white; /* Text color when selected */\n }\n /* Removes shadows and background tints from UI groups/cards */\n .nr-dashboard-theme ui-card-panel,\n body.nr-dashboard-theme md-content md-card {\n box-shadow: none !important;\n background-color: transparent !important;\n /* or set your desired background color */\n border-radius: 0px !important;\n /* Removes rounded corners if present */\n text-shadow: none !important;\n }\n\n /* Removes tints/shadows from specific widgets like buttons */\n .nr-dashboard-theme .nr-dashboard-button .md-button {\n box-shadow: none !important;\n background-color: transparent !important;\n }\n\n /* Adjust the main background of the page if needed */\n .masonry-container {\n background: none !important;\n /* Removes any linear gradients or colors */\n }\n\n /* ... your existing CSS rules ... */\n \n /* Change the background color when text is highlighted by the user */\n ::selection {\n background-color: transparent !important; /* Makes the background clear */\n color: inherit !important; /* Keeps the original text color */\n }\n \n /* For older browsers (Firefox specific) */\n ::-moz-selection {\n background-color: transparent !important;\n color: inherit !important;\n }\n\n /* ... your previous CSS rules ... */\n \n /* ... your previous CSS rules ... */\n \n /* Override the background color for the 'How to Start / Stop Production' title */\n .help-title {\n background-color: transparent !important;\n }\n .card help-card {\n background-color: transparent !important;\n }\n .panel-title {\n background-color: transparent !important;\n }\n\n/* Override Node-RED Dashboard's gray backgrounds on text elements */\n.nr-dashboard-template p,\n.nr-dashboard-template h1,\n.nr-dashboard-template h2,\n.nr-dashboard-template h3,\n.nr-dashboard-template h4,\n.nr-dashboard-template label {\n background-color: transparent !important;\n}\n</style>",
"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 \u2192 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 \u2192 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": [
[
"0b5740c4a2b298b7"
]
]
},
{
"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 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(\"trackingEnabled\", true);\n\n // Initialize production start time for KPI calculations\n global.set(\"productionStartTime\", Date.now());\n return [null, null, 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}\n",
"outputs": 4,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 970,
"y": 400,
"wires": [
[
"15a6b7b6d8f39fe4"
],
[
"f6ad294bc02618c9"
],
[
"f6ad294bc02618c9",
"00b6132848964bd9"
],
[
"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: \"\u2705 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 // Get KPIs from global or from msg\n const kpis = msg.kpis || global.get(\"currentKPIs\") || { oee: 0, availability: 0, performance: 0, quality: 0 };\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 return [null, homeMsg];\n}\n\nif (mode === \"complete\") {\n // Get KPIs from global or from msg\n const kpis = msg.kpis || global.get(\"currentKPIs\") || { oee: 0, availability: 0, performance: 0, quality: 0 };\n\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 // Get KPIs from global or from msg\n const kpis = msg.kpis || global.get(\"currentKPIs\") || { oee: 0, availability: 0, performance: 0, quality: 0 };\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];\n}\nif (mode === \"production-state\") {\n // Get KPIs from global or from msg\n const kpis = msg.kpis || global.get(\"currentKPIs\") || { oee: 0, availability: 0, performance: 0, quality: 0 };\n\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 // Get KPIs from global or from msg\n const kpis = msg.kpis || global.get(\"currentKPIs\") || { oee: 0, availability: 0, performance: 0, quality: 0 };\n\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-update\") {\n // Scrap was just submitted - send updated KPIs to UI\n const activeOrder = global.get(\"activeWorkOrder\") || {};\n const kpis = msg.kpis || global.get(\"currentKPIs\") || { oee: 0, availability: 0, performance: 0, quality: 0 };\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 return [null, homeMsg];\n}\n\nif (mode === \"scrap-complete\") {\n // Get KPIs from global or from msg\n const kpis = msg.kpis || global.get(\"currentKPIs\") || { oee: 0, availability: 0, performance: 0, quality: 0 };\n\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 // 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": 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// Check if tracking is enabled (START button clicked)\nconst trackingEnabled = !!global.get(\"trackingEnabled\");\nif (!trackingEnabled) {\n // Cycles are happening but we're not tracking them yet\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\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\nreturn [dbMsg, stateMsg];",
"outputs": 2,
"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": 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"
]
]
},
{
"id": "0b5740c4a2b298b7",
"type": "link out",
"z": "cac3a4383120cb57",
"g": "28c173789034639c",
"name": "link out 8",
"mode": "link",
"links": [
"8f890f97aa9257c7"
],
"x": 1395,
"y": 120,
"wires": []
},
{
"id": "8f890f97aa9257c7",
"type": "link in",
"z": "cac3a4383120cb57",
"g": "ec32d0a62eacfb22",
"name": "link in 8",
"links": [
"0b5740c4a2b298b7"
],
"x": 75,
"y": 260,
"wires": [
[
"f5a6b7c8d9e0f1a2"
]
]
},
{
"id": "00b6132848964bd9",
"type": "function",
"z": "cac3a4383120cb57",
"g": "16bb591480852f51",
"name": "Calculate KPIs",
"func": "// ========================================\n// OEE KPI CALCULATOR - PHASE 1\n// Industry Standard: OEE = Availability \u00d7 Performance \u00d7 Quality\n// ========================================\n\nconst activeOrder = global.get(\"activeWorkOrder\") || {};\nconst cycleCount = global.get(\"cycleCount\") || 0;\nconst cavities = Number(global.get(\"moldActive\")?.cavities) || 1;\nconst trackingEnabled = global.get(\"trackingEnabled\") || false;\n\n// Initialize KPI object\nmsg.kpis = {\n quality: 0,\n performance: 0,\n availability: 0,\n oee: 0\n};\n\n// ========================================\n// 1. QUALITY CALCULATION\n// Formula: (Good Parts / Total Parts) \u00d7 100%\n// ========================================\nconst goodParts = Number(activeOrder.good) || 0;\nconst scrapParts = Number(activeOrder.scrap) || 0;\nconst totalParts = goodParts + scrapParts;\n\nif (totalParts > 0) {\n msg.kpis.quality = (goodParts / totalParts) * 100;\n} else {\n msg.kpis.quality = 100; // No production yet = perfect quality\n}\n\n// Cap at 100% per OEE standard\nmsg.kpis.quality = Math.min(100, msg.kpis.quality);\n\n// ========================================\n// 2. PERFORMANCE CALCULATION (Simplified)\n// Formula: (Ideal Time / Actual Time) \u00d7 100%\n// ========================================\nconst idealCycleTime = Number(activeOrder.cycleTime) || 0; // seconds per cycle\nlet productionStartTime = global.get(\"productionStartTime\");\n\nif (cycleCount > 0 && idealCycleTime > 0 && productionStartTime) {\n // Calculate elapsed time since production started\n const elapsedTimeMs = Date.now() - productionStartTime;\n const elapsedTimeSec = elapsedTimeMs / 1000;\n\n // Calculate ideal time to produce current quantity\n const idealTotalTime = cycleCount * idealCycleTime;\n\n // Performance = (Ideal Time / Actual Time) \u00d7 100%\n if (elapsedTimeSec > 0) {\n msg.kpis.performance = (idealTotalTime / elapsedTimeSec) * 100;\n\n // Cap at 100% per OEE standard\n msg.kpis.performance = Math.min(100, msg.kpis.performance);\n }\n} else if (trackingEnabled && productionStartTime) {\n // Production started but no cycles yet - show 100% placeholder\n msg.kpis.performance = 100;\n} else {\n msg.kpis.performance = 0; // No production yet\n}\n\n// ========================================\n// 3. AVAILABILITY CALCULATION (Placeholder)\n// Phase 1: Simplified - assumes 100% when running\n// Phase 2 will add proper downtime tracking\n// ========================================\nif (trackingEnabled && productionStartTime) {\n msg.kpis.availability = 100.0; // Placeholder\n} else {\n msg.kpis.availability = 0;\n}\n\n// ========================================\n// 4. OEE CALCULATION\n// Formula: (Availability \u00d7 Performance \u00d7 Quality) / 10,000\n// ========================================\nmsg.kpis.oee = (msg.kpis.availability * msg.kpis.performance * msg.kpis.quality) / 10000;\n\n// Round all values to 1 decimal place\nmsg.kpis.quality = Math.round(msg.kpis.quality * 10) / 10;\nmsg.kpis.performance = Math.round(msg.kpis.performance * 10) / 10;\nmsg.kpis.availability = Math.round(msg.kpis.availability * 10) / 10;\nmsg.kpis.oee = Math.round(msg.kpis.oee * 10) / 10;\n\n// Store KPIs globally for access by other nodes\nglobal.set(\"currentKPIs\", msg.kpis);\n\n// Debug logging (comment out in production)\n// node.warn(`KPIs: OEE=${msg.kpis.oee}% A=${msg.kpis.availability}% P=${msg.kpis.performance}% Q=${msg.kpis.quality}%`);\n\nreturn msg;",
"outputs": 1,
"timeout": 0,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 840,
"y": 500,
"wires": [
[
"578c92e75bf0f266"
]
]
}
]