367 lines
72 KiB
Plaintext
367 lines
72 KiB
Plaintext
[
|
||
{
|
||
"id": "ea088d9e256f2e4a",
|
||
"type": "tab",
|
||
"label": "Raspi",
|
||
"disabled": false,
|
||
"info": "",
|
||
"env": []
|
||
},
|
||
{
|
||
"id": "ea803f6a2965854b",
|
||
"type": "mqtt-broker",
|
||
"name": "Broker fuera casa",
|
||
"broker": "mqtt.maliountech.com.mx",
|
||
"port": "1883",
|
||
"clientid": "raspi-maquina1",
|
||
"autoConnect": true,
|
||
"usetls": false,
|
||
"protocolVersion": 4,
|
||
"keepalive": "30",
|
||
"cleansession": false,
|
||
"autoUnsubscribe": true,
|
||
"birthTopic": "",
|
||
"birthQos": "0",
|
||
"birthRetain": "false",
|
||
"birthPayload": "",
|
||
"birthMsg": {},
|
||
"closeTopic": "",
|
||
"closeQos": "0",
|
||
"closeRetain": "false",
|
||
"closePayload": "",
|
||
"closeMsg": {},
|
||
"willTopic": "",
|
||
"willQos": "0",
|
||
"willRetain": "false",
|
||
"willPayload": "",
|
||
"willMsg": {},
|
||
"userProps": "",
|
||
"sessionExpiry": ""
|
||
},
|
||
{
|
||
"id": "615493954251cda9",
|
||
"type": "ui_tab",
|
||
"name": "Home",
|
||
"icon": "dashboard",
|
||
"disabled": false,
|
||
"hidden": false
|
||
},
|
||
{
|
||
"id": "6fe5cca8f548047a",
|
||
"type": "ui_group",
|
||
"name": "HMI Dashboard",
|
||
"tab": "615493954251cda9",
|
||
"order": 1,
|
||
"disp": false,
|
||
"width": "25",
|
||
"collapse": false,
|
||
"className": ""
|
||
},
|
||
{
|
||
"id": "fa0582493cd77ae7",
|
||
"type": "ui_base",
|
||
"theme": {
|
||
"name": "theme-dark",
|
||
"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": "#097479",
|
||
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif",
|
||
"edited": true,
|
||
"reset": false
|
||
},
|
||
"customTheme": {
|
||
"name": "Untitled Theme 1",
|
||
"default": "#4B7930",
|
||
"baseColor": "#4B7930",
|
||
"baseFont": "-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif"
|
||
},
|
||
"themeState": {
|
||
"base-color": {
|
||
"default": "#097479",
|
||
"value": "#097479",
|
||
"edited": false
|
||
},
|
||
"page-titlebar-backgroundColor": {
|
||
"value": "#097479",
|
||
"edited": false
|
||
},
|
||
"page-backgroundColor": {
|
||
"value": "#111111",
|
||
"edited": false
|
||
},
|
||
"page-sidebar-backgroundColor": {
|
||
"value": "#333333",
|
||
"edited": false
|
||
},
|
||
"group-textColor": {
|
||
"value": "#0eb8c0",
|
||
"edited": false
|
||
},
|
||
"group-borderColor": {
|
||
"value": "#555555",
|
||
"edited": false
|
||
},
|
||
"group-backgroundColor": {
|
||
"value": "#333333",
|
||
"edited": false
|
||
},
|
||
"widget-textColor": {
|
||
"value": "#eeeeee",
|
||
"edited": false
|
||
},
|
||
"widget-backgroundColor": {
|
||
"value": "#097479",
|
||
"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": "d9407fca93d82949",
|
||
"type": "function",
|
||
"z": "ea088d9e256f2e4a",
|
||
"name": "Machine status function",
|
||
"func": "// Get previous state\nconst previousStatus = global.get(\"machine_status\") || {\n state: 'Running',\n isOnline: true,\n isRunning: true\n};\n\nlet state = previousStatus.state;\n\n// Random transitions\nif (Math.random() < 0.5) {\n state = state === 'Running' ? 'Stopped' : 'Running';\n}\n\n// Create status data\nconst statusData = {\n state: state,\n isOnline: state !== 'Error',\n isRunning: state === 'Running',\n timestamp: new Date().toISOString()\n};\n\n// Store in global\nglobal.set(\"machine_status\", statusData);\n\n// Send to UI\nmsg.payload = statusData;\nmsg.topic = \"pimsa/maquina1/status\";\nreturn msg;",
|
||
"outputs": 1,
|
||
"timeout": 0,
|
||
"noerr": 0,
|
||
"initialize": "",
|
||
"finalize": "",
|
||
"libs": [],
|
||
"x": 470,
|
||
"y": 120,
|
||
"wires": [
|
||
[
|
||
"aaf07ca380495b06",
|
||
"c195f94afd7183e6"
|
||
]
|
||
]
|
||
},
|
||
{
|
||
"id": "547062128fa6337a",
|
||
"type": "inject",
|
||
"z": "ea088d9e256f2e4a",
|
||
"name": "Machine Data Timer",
|
||
"props": [
|
||
{
|
||
"p": "payload"
|
||
},
|
||
{
|
||
"p": "topic",
|
||
"vt": "str"
|
||
}
|
||
],
|
||
"repeat": "1",
|
||
"crontab": "",
|
||
"once": false,
|
||
"onceDelay": 0.1,
|
||
"topic": "",
|
||
"payload": "",
|
||
"payloadType": "date",
|
||
"x": 140,
|
||
"y": 300,
|
||
"wires": [
|
||
[
|
||
"d9407fca93d82949",
|
||
"207fdc449918affd",
|
||
"031c7ccb3125177b",
|
||
"2a35e2907dc04de0"
|
||
]
|
||
]
|
||
},
|
||
{
|
||
"id": "4f66d69c5bce7ba0",
|
||
"type": "mqtt out",
|
||
"z": "ea088d9e256f2e4a",
|
||
"name": "MQTT Out - Work Order",
|
||
"topic": "pimsa/erp/workorder/current",
|
||
"qos": "1",
|
||
"retain": "true",
|
||
"respTopic": "",
|
||
"contentType": "",
|
||
"userProps": "",
|
||
"correl": "",
|
||
"expiry": "",
|
||
"broker": "ea803f6a2965854b",
|
||
"x": 810,
|
||
"y": 540,
|
||
"wires": []
|
||
},
|
||
{
|
||
"id": "aaf07ca380495b06",
|
||
"type": "mqtt out",
|
||
"z": "ea088d9e256f2e4a",
|
||
"name": "MQTT Out - Machine Status",
|
||
"topic": "pimsa/maquina1/status",
|
||
"qos": "1",
|
||
"retain": "true",
|
||
"respTopic": "",
|
||
"contentType": "",
|
||
"userProps": "",
|
||
"correl": "",
|
||
"expiry": "",
|
||
"broker": "ea803f6a2965854b",
|
||
"x": 820,
|
||
"y": 120,
|
||
"wires": []
|
||
},
|
||
{
|
||
"id": "fee4e024db51e661",
|
||
"type": "mqtt out",
|
||
"z": "ea088d9e256f2e4a",
|
||
"name": "MQTT Out - Machine OEE",
|
||
"topic": "pimsa/maquina1/oee",
|
||
"qos": "0",
|
||
"retain": "false",
|
||
"respTopic": "",
|
||
"contentType": "",
|
||
"userProps": "",
|
||
"correl": "",
|
||
"expiry": "",
|
||
"broker": "ea803f6a2965854b",
|
||
"x": 800,
|
||
"y": 260,
|
||
"wires": []
|
||
},
|
||
{
|
||
"id": "12c37a80895dcc3e",
|
||
"type": "mqtt out",
|
||
"z": "ea088d9e256f2e4a",
|
||
"name": "MQTT Out - Production",
|
||
"topic": "pimsa/maquina1/production",
|
||
"qos": "1",
|
||
"retain": "false",
|
||
"respTopic": "",
|
||
"contentType": "",
|
||
"userProps": "",
|
||
"correl": "",
|
||
"expiry": "",
|
||
"broker": "ea803f6a2965854b",
|
||
"x": 810,
|
||
"y": 400,
|
||
"wires": []
|
||
},
|
||
{
|
||
"id": "207fdc449918affd",
|
||
"type": "function",
|
||
"z": "ea088d9e256f2e4a",
|
||
"name": "OEE Function",
|
||
"func": "// OEE Function\nconst oeeData = {\n oee: 85 + Math.random() * 15,\n availability: 90 + Math.random() * 10,\n performance: 80 + Math.random() * 20,\n quality: 95 + Math.random() * 5,\n timestamp: new Date().toISOString()\n};\n\n// Store in global context (NEW!)\nglobal.set(\"oeeData\", oeeData);\n\nmsg.payload = oeeData;\nmsg.topic = \"pimsa/maquina1/oee\";\nreturn msg;",
|
||
"outputs": 1,
|
||
"timeout": 0,
|
||
"noerr": 0,
|
||
"initialize": "",
|
||
"finalize": "",
|
||
"libs": [],
|
||
"x": 440,
|
||
"y": 260,
|
||
"wires": [
|
||
[
|
||
"fee4e024db51e661",
|
||
"c195f94afd7183e6"
|
||
]
|
||
]
|
||
},
|
||
{
|
||
"id": "031c7ccb3125177b",
|
||
"type": "function",
|
||
"z": "ea088d9e256f2e4a",
|
||
"name": "Production Function",
|
||
"func": "// Production Function with Auto-Reset for Demo\nconst targetQty = 2000;\n\n// Initialize or get current values\nlet goodParts = context.get('goodParts') || 100; // Start at 100 instead of 1200\nlet scrapParts = context.get('scrapParts') || 5;\n\n// Increment production\ngoodParts += Math.floor(Math.random() * 3) + 1;\nscrapParts += Math.floor(Math.random() * 2);\n\n// Calculate progress\nconst progress = Math.min(100, (goodParts / targetQty) * 100);\n\n// Auto-reset when reaching 100% (for continuous demo)\nif (goodParts >= targetQty) {\n goodParts = 100; // Reset to beginning\n scrapParts = 5;\n node.warn(\"Production cycle completed! Resetting for next work order...\");\n}\n\n// Save updated values\ncontext.set('goodParts', goodParts);\ncontext.set('scrapParts', scrapParts);\n\n// Create production data object\nconst productionData = {\n goodParts: goodParts,\n scrapParts: scrapParts,\n totalParts: goodParts + scrapParts,\n progress: progress,\n cycleTime: (45 + Math.random() * 10).toFixed(1) + \"s\",\n timestamp: new Date().toISOString()\n};\n\n// Store in global context (NEW!)\nglobal.set(\"productionData\", productionData);\n\nmsg.payload = productionData;\nmsg.topic = \"pimsa/maquina1/production\";\nreturn msg;",
|
||
"outputs": 1,
|
||
"timeout": 0,
|
||
"noerr": 0,
|
||
"initialize": "",
|
||
"finalize": "",
|
||
"libs": [],
|
||
"x": 460,
|
||
"y": 400,
|
||
"wires": [
|
||
[
|
||
"12c37a80895dcc3e",
|
||
"c195f94afd7183e6"
|
||
]
|
||
]
|
||
},
|
||
{
|
||
"id": "2a35e2907dc04de0",
|
||
"type": "function",
|
||
"z": "ea088d9e256f2e4a",
|
||
"name": "Work Order Function",
|
||
"func": "// Work Order Function\nconst workOrder = {\n id: 'WO-2024-001',\n sku: 'SKU-ABC123',\n description: 'Plastic Housing - Black',\n targetQty: 2000,\n dueDate: '2024-10-15'\n};\n\n// Create work order data object\nconst workOrderData = {\n workOrder: workOrder.id,\n sku: workOrder.sku,\n description: workOrder.description,\n targetQty: workOrder.targetQty,\n completedQty: context.get('goodParts') || 1200,\n dueDate: workOrder.dueDate,\n timestamp: new Date().toISOString()\n};\n\n// Store in global context (NEW!)\nglobal.set(\"workOrderData\", workOrderData);\n\nmsg.payload = workOrderData;\nmsg.topic = \"pimsa/erp/workorder/current\";\nreturn msg;",
|
||
"outputs": 1,
|
||
"timeout": 0,
|
||
"noerr": 0,
|
||
"initialize": "",
|
||
"finalize": "",
|
||
"libs": [],
|
||
"x": 460,
|
||
"y": 540,
|
||
"wires": [
|
||
[
|
||
"4f66d69c5bce7ba0",
|
||
"c195f94afd7183e6"
|
||
]
|
||
]
|
||
},
|
||
{
|
||
"id": "c195f94afd7183e6",
|
||
"type": "ui_template",
|
||
"z": "ea088d9e256f2e4a",
|
||
"group": "6fe5cca8f548047a",
|
||
"name": "",
|
||
"order": 0,
|
||
"width": "25",
|
||
"height": "25",
|
||
"format": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Industrial HMI Dashboard</title>\n <script src=\"https://unpkg.com/mqtt@4.3.7/dist/mqtt.min.js\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js\"></script>\n <style>\n /* Reset and base styles */\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n\n html,\n body {\n height: 100%;\n overflow: hidden;\n }\n\n /* CSS Variables */\n #hmi-dashboard {\n --bg-0: #0e1116;\n --bg-1: #141a1f;\n --panel: #1a2230;\n --panel-hi: #222c3e;\n --text: #eaf1fb;\n --muted: #96a5b7;\n --accent: #2ea3ff;\n --accent-2: #1f8cf3;\n --good: #2ecc71;\n --bad: #ff4d4f;\n --warning: #f6c049;\n --shadow-1: 0 8px 16px rgba(0, 0, 0, 0.35);\n --shadow-2: inset 0 1px 0 rgba(255, 255, 255, 0.05), 0 2px 6px rgba(0, 0, 0, 0.3);\n --radius-lg: 16px;\n --radius-md: 12px;\n --kpi-min-h: 120px;\n --gap: 12px;\n --pad: 12px;\n --ring: 0 0 0 3px rgba(46, 163, 255, 0.45);\n color: var(--text);\n background: var(--bg-0);\n width: 100%;\n height: 100vh;\n -webkit-tap-highlight-color: transparent;\n user-select: none;\n font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, \"Noto Sans\", \"Helvetica Neue\", Arial;\n }\n\n /* Sidebar Styles */\n #sidebar {\n width: 60px;\n height: 100vh;\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-right: 1px solid rgba(255, 255, 255, 0.1);\n box-shadow: var(--shadow-1);\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: var(--gap) 0;\n gap: var(--gap);\n position: fixed;\n left: 0;\n top: 0;\n z-index: 1000;\n }\n\n .sidebar-item {\n width: 48px;\n height: 48px;\n border: none;\n background: rgba(255, 255, 255, 0.05);\n border-radius: var(--radius-md);\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: pointer;\n transition: all 120ms ease;\n position: relative;\n font-size: 24px;\n color: var(--muted);\n border: 1px solid rgba(255, 255, 255, 0.08);\n }\n\n .sidebar-item:hover {\n background: rgba(255, 255, 255, 0.1);\n color: var(--text);\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);\n }\n\n .sidebar-item.active {\n background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);\n color: white;\n box-shadow: 0 4px 12px rgba(46, 163, 255, 0.3);\n }\n\n .sidebar-item:active {\n transform: translateY(0) scale(0.95);\n }\n\n .sidebar-item.warning-icon {\n color: var(--bad);\n }\n\n .sidebar-item.warning-icon:hover {\n color: #ff6b6b;\n background: rgba(255, 77, 79, 0.1);\n }\n\n /* Tooltip */\n .sidebar-item::after {\n content: attr(data-tooltip);\n position: absolute;\n left: 60px;\n top: 50%;\n transform: translateY(-50%);\n background: var(--panel-hi);\n color: var(--text);\n padding: 8px 12px;\n border-radius: var(--radius-md);\n font-size: 12px;\n font-weight: 600;\n white-space: nowrap;\n opacity: 0;\n visibility: hidden;\n transition: all 120ms ease;\n pointer-events: none;\n border: 1px solid rgba(255, 255, 255, 0.1);\n box-shadow: var(--shadow-1);\n z-index: 1001;\n }\n\n .sidebar-item:hover::after {\n opacity: 1;\n visibility: visible;\n transform: translateY(-50%) translateX(4px);\n }\n\n /* Main Layout */\n .main-layout {\n margin-left: 60px;\n width: calc(100% - 60px);\n height: 100vh;\n overflow-y: auto;\n }\n\n /* View Container */\n .view {\n display: none;\n animation: fadeIn 0.3s ease;\n }\n\n .view.active {\n display: block;\n }\n\n @keyframes fadeIn {\n from {\n opacity: 0;\n transform: translateY(10px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n }\n\n /* Common Grid for all views */\n .view-grid {\n display: grid;\n grid-template-rows: auto 1fr;\n gap: var(--gap);\n height: 100%;\n padding: var(--pad);\n }\n\n /* Page Header */\n .page-header {\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-1), var(--shadow-2);\n padding: 20px;\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n .page-title {\n font-size: clamp(24px, 3vw, 32px);\n font-weight: 700;\n display: flex;\n align-items: center;\n gap: 12px;\n }\n\n .page-title-icon {\n font-size: clamp(28px, 3.5vw, 38px);\n }\n\n /* Buttons */\n .btn {\n padding: 10px 20px;\n border: none;\n border-radius: var(--radius-md);\n font-size: 14px;\n font-weight: 600;\n cursor: pointer;\n transition: all 120ms ease;\n display: inline-flex;\n align-items: center;\n gap: 8px;\n }\n\n .btn-primary {\n background: linear-gradient(135deg, var(--accent) 0%, var(--accent-2) 100%);\n color: white;\n box-shadow: 0 4px 12px rgba(46, 163, 255, 0.3);\n }\n\n .btn-primary:hover {\n transform: translateY(-2px);\n box-shadow: 0 6px 16px rgba(46, 163, 255, 0.4);\n }\n\n .btn-success {\n background: var(--good);\n color: white;\n }\n\n .btn-danger {\n background: var(--bad);\n color: white;\n }\n\n .btn-secondary {\n background: rgba(255, 255, 255, 0.1);\n color: var(--text);\n border: 1px solid rgba(255, 255, 255, 0.2);\n }\n\n /* Cards */\n .card {\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-1), var(--shadow-2);\n padding: 20px;\n }\n\n .card-header {\n font-size: 18px;\n font-weight: 700;\n margin-bottom: 16px;\n padding-bottom: 12px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n }\n\n /* Forms */\n .form-group {\n margin-bottom: 16px;\n }\n\n .form-label {\n display: block;\n margin-bottom: 8px;\n font-size: 14px;\n font-weight: 600;\n color: var(--muted);\n }\n\n .form-input,\n .form-select,\n .form-textarea {\n width: 100%;\n padding: 12px;\n background: rgba(255, 255, 255, 0.05);\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: var(--radius-md);\n color: var(--text);\n font-size: 14px;\n font-family: inherit;\n transition: all 120ms ease;\n }\n\n .form-input:focus,\n .form-select:focus,\n .form-textarea:focus {\n outline: none;\n border-color: var(--accent);\n box-shadow: var(--ring);\n }\n\n .form-textarea {\n min-height: 100px;\n resize: vertical;\n }\n\n /* Tables */\n .table {\n width: 100%;\n border-collapse: collapse;\n }\n\n .table th {\n background: rgba(255, 255, 255, 0.05);\n padding: 12px;\n text-align: left;\n font-size: 12px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--muted);\n border-bottom: 2px solid rgba(255, 255, 255, 0.1);\n }\n\n .table td {\n padding: 12px;\n border-bottom: 1px solid rgba(255, 255, 255, 0.05);\n }\n\n .table tr:hover {\n background: rgba(255, 255, 255, 0.02);\n }\n\n /* Status badges */\n .badge {\n display: inline-block;\n padding: 4px 12px;\n border-radius: 12px;\n font-size: 12px;\n font-weight: 600;\n }\n\n .badge-success {\n background: var(--good);\n color: white;\n }\n\n .badge-danger {\n background: var(--bad);\n color: white;\n }\n\n .badge-warning {\n background: var(--warning);\n color: #000;\n }\n\n .badge-info {\n background: var(--accent);\n color: white;\n }\n\n /* Notification badge on sidebar */\n .notification-count {\n position: absolute;\n top: -4px;\n right: -4px;\n background: var(--bad);\n color: white;\n font-size: 10px;\n font-weight: 700;\n padding: 2px 6px;\n border-radius: 10px;\n min-width: 18px;\n text-align: center;\n }\n\n /* Include dashboard-specific styles from original */\n .top-kpis {\n display: grid;\n grid-template-columns: repeat(4, minmax(0, 1fr));\n gap: var(--gap);\n }\n\n .kpi {\n appearance: none;\n border: 0;\n border-radius: var(--radius-lg);\n min-height: var(--kpi-min-h);\n padding: 14px;\n text-align: left;\n background: linear-gradient(160deg, var(--panel) 0%, var(--panel-hi) 100%);\n color: var(--text);\n box-shadow: var(--shadow-1), var(--shadow-2);\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n cursor: pointer;\n position: relative;\n overflow: hidden;\n transition: transform 120ms ease;\n }\n\n .kpi:hover {\n transform: translateY(-2px);\n }\n\n .kpi::after {\n content: \"\";\n position: absolute;\n inset: 0;\n background: radial-gradient(1200px 1200px at 0% 0%, rgba(46, 163, 255, 0.07), transparent 50%);\n pointer-events: none;\n }\n\n .kpi-label {\n font-size: clamp(12px, 1.6vw, 18px);\n letter-spacing: 0.04em;\n text-transform: uppercase;\n color: var(--muted);\n }\n\n .kpi-value {\n font-variant-numeric: tabular-nums;\n font-weight: 700;\n line-height: 1.1;\n font-size: clamp(28px, 4.2vw, 48px);\n }\n\n /* Work Order Section */\n .workorder {\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-1), var(--shadow-2);\n padding: 16px;\n }\n\n .wo-header {\n font-size: clamp(14px, 2vw, 20px);\n font-weight: 700;\n margin-bottom: 12px;\n }\n\n .wo-grid {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: var(--gap);\n }\n\n .wo-item {\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: var(--radius-md);\n padding: 12px;\n display: flex;\n flex-direction: column;\n gap: 6px;\n }\n\n .wo-label {\n font-size: clamp(11px, 1.4vw, 14px);\n text-transform: uppercase;\n color: var(--muted);\n letter-spacing: 0.06em;\n }\n\n .wo-value {\n font-size: clamp(16px, 2.2vw, 24px);\n font-variant-numeric: tabular-nums;\n font-weight: 600;\n }\n\n .wo-progress {\n grid-column: 1 / -1;\n }\n\n .progress-track {\n position: relative;\n height: clamp(24px, 4vw, 36px);\n width: 100%;\n background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02));\n border: 1px solid rgba(255, 255, 255, 0.1);\n border-radius: 999px;\n overflow: hidden;\n }\n\n .progress-fill {\n position: absolute;\n inset: 0 auto 0 0;\n width: 0%;\n background: linear-gradient(90deg, var(--accent) 0%, var(--accent-2) 100%);\n transition: width 250ms ease;\n }\n\n .progress-text {\n position: absolute;\n inset: 0;\n display: grid;\n place-items: center;\n font-weight: 700;\n font-size: clamp(13px, 2vw, 18px);\n }\n\n /* Bottom Counters */\n .bottom {\n display: grid;\n grid-template-columns: 1fr 1fr minmax(220px, 0.8fr);\n gap: var(--gap);\n }\n\n .counter {\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-1), var(--shadow-2);\n padding: 14px;\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 10px;\n }\n\n .counter-label {\n font-size: clamp(13px, 1.6vw, 16px);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n color: var(--muted);\n }\n\n .counter-value {\n font-size: clamp(34px, 6vw, 64px);\n font-weight: 800;\n font-variant-numeric: tabular-nums;\n line-height: 1;\n }\n\n .counter.good .counter-value {\n color: var(--good);\n }\n\n .counter.scrap .counter-value {\n color: var(--bad);\n }\n\n .status {\n background: linear-gradient(180deg, var(--panel) 0%, var(--panel-hi) 100%);\n border-radius: var(--radius-lg);\n box-shadow: var(--shadow-1), var(--shadow-2);\n padding: 14px;\n display: grid;\n grid-auto-rows: minmax(44px, auto);\n align-content: center;\n gap: 10px;\n }\n\n .status-item {\n display: grid;\n grid-template-columns: 28px 1fr auto;\n align-items: center;\n gap: 10px;\n padding: 8px 10px;\n border-radius: 10px;\n background: rgba(255, 255, 255, 0.03);\n border: 1px solid rgba(255, 255, 255, 0.06);\n }\n\n .led {\n width: 18px;\n height: 18px;\n border-radius: 999px;\n background: #606b78;\n box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.25);\n }\n\n .led.online {\n background: var(--good);\n box-shadow: 0 0 12px rgba(46, 204, 113, 0.7);\n }\n\n .led.running {\n background: var(--accent);\n box-shadow: 0 0 12px rgba(46, 163, 255, 0.7);\n }\n\n .status-label {\n font-weight: 600;\n color: var(--text);\n font-size: clamp(14px, 1.8vw, 18px);\n }\n\n .status-value {\n color: var(--muted);\n font-size: clamp(12px, 1.6vw, 14px);\n }\n\n /* Footer */\n .footer {\n display: flex;\n justify-content: flex-end;\n align-items: center;\n padding: 8px 4px 2px;\n color: var(--muted);\n font-size: clamp(12px, 1.6vw, 14px);\n }\n\n /* Dashboard specific grid */\n .dashboard-grid {\n display: grid;\n grid-template-rows: auto auto 1fr auto;\n gap: var(--gap);\n height: 100vh;\n padding: var(--pad);\n box-sizing: border-box;\n }\n\n /* Chart container */\n .chart-container {\n position: relative;\n height: 300px;\n margin: 16px 0;\n }\n </style>\n</head>\n\n<body>\n <div id=\"hmi-dashboard\">\n <!-- Sidebar -->\n <div id=\"sidebar\">\n <button class=\"sidebar-item active\" data-view=\"dashboard\" data-tooltip=\"Dashboard\">\n 🏠\n </button>\n <button class=\"sidebar-item\" data-view=\"workorders\" data-tooltip=\"Work Orders\">\n 📋\n </button>\n <button class=\"sidebar-item warning-icon\" data-view=\"report\" data-tooltip=\"Report Issue\">\n ⛔\n </button>\n <button class=\"sidebar-item\" data-view=\"machine\" data-tooltip=\"Machine Status\">\n 📊\n </button>\n <button class=\"sidebar-item\" data-view=\"notifications\" data-tooltip=\"Notifications\">\n 🔔\n <span class=\"notification-count\" id=\"notif-count\">3</span>\n </button>\n <button class=\"sidebar-item\" data-view=\"help\" data-tooltip=\"Help\">\n ❓\n </button>\n <button class=\"sidebar-item\" data-view=\"settings\" data-tooltip=\"Settings\">\n ⚙\n </button>\n </div>\n\n <!-- Main Content -->\n <div class=\"main-layout\">\n\n <!-- VIEW 1: Dashboard (Main HMI) -->\n <div id=\"view-dashboard\" class=\"view active\">\n <div class=\"dashboard-grid\">\n <!-- TOP: KPI Buttons -->\n <div class=\"top-kpis\">\n <button class=\"kpi\">\n <div class=\"kpi-label\">OEE</div>\n <div class=\"kpi-value\" id=\"oee_value\">--%</div>\n </button>\n <button class=\"kpi\">\n <div class=\"kpi-label\">Availability</div>\n <div class=\"kpi-value\" id=\"availability_value\">--%</div>\n </button>\n <button class=\"kpi\">\n <div class=\"kpi-label\">Performance</div>\n <div class=\"kpi-value\" id=\"performance_value\">--%</div>\n </button>\n <button class=\"kpi\">\n <div class=\"kpi-label\">Quality</div>\n <div class=\"kpi-value\" id=\"quality_value\">--%</div>\n </button>\n </div>\n\n <!-- MIDDLE: Current Work Order -->\n <div class=\"workorder\">\n <div class=\"wo-header\">Current Work Order</div>\n <div class=\"wo-grid\">\n <div class=\"wo-item\">\n <div class=\"wo-label\">Work Order</div>\n <div class=\"wo-value\" id=\"workorder_number\">—</div>\n </div>\n <div class=\"wo-item\">\n <div class=\"wo-label\">SKU</div>\n <div class=\"wo-value\" id=\"workorder_sku\">—</div>\n </div>\n <div class=\"wo-item\">\n <div class=\"wo-label\">Cycle Time</div>\n <div class=\"wo-value\" id=\"cycle_time\">—</div>\n </div>\n <div class=\"wo-progress\">\n <div class=\"progress-track\">\n <div class=\"progress-fill\" id=\"progress_bar\"></div>\n <div class=\"progress-text\" id=\"progress_value\">0%</div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- BOTTOM: Counters + Status -->\n <div class=\"bottom\">\n <div class=\"counter good\">\n <div class=\"counter-label\">Good Parts</div>\n <div class=\"counter-value\" id=\"good_parts\">0</div>\n </div>\n <div class=\"counter scrap\">\n <div class=\"counter-label\">Scrap Parts</div>\n <div class=\"counter-value\" id=\"scrap_parts\">0</div>\n </div>\n <div class=\"status\">\n <div class=\"status-item\">\n <span class=\"led\" id=\"status_online\"></span>\n <span class=\"status-label\">Online</span>\n <span class=\"status-value\" id=\"status_online_text\">—</span>\n </div>\n <div class=\"status-item\">\n <span class=\"led\" id=\"status_running\"></span>\n <span class=\"status-label\">Running</span>\n <span class=\"status-value\" id=\"status_running_text\">—</span>\n </div>\n </div>\n </div>\n\n <!-- FOOTER -->\n <div class=\"footer\">\n <span id=\"footer_datetime\">—</span>\n </div>\n </div>\n </div>\n\n <!-- VIEW 2: Work Orders -->\n <div id=\"view-workorders\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">📋</span>\n Work Orders\n </div>\n <button class=\"btn btn-primary\" onclick=\"app.createWorkOrder()\">\n ➕ New Work Order\n </button>\n </div>\n <div class=\"card\" style=\"overflow-y: auto;\">\n <table class=\"table\">\n <thead>\n <tr>\n <th>ID</th>\n <th>SKU</th>\n <th>Description</th>\n <th>Target Qty</th>\n <th>Completed</th>\n <th>Progress</th>\n <th>Status</th>\n <th>Due Date</th>\n </tr>\n </thead>\n <tbody id=\"workorder-table-body\">\n <tr>\n <td>WO-2024-001</td>\n <td>SKU-ABC123</td>\n <td>Plastic Housing - Black</td>\n <td>2000</td>\n <td id=\"wo-completed\">1200</td>\n <td>\n <div class=\"progress-track\" style=\"height: 20px; width: 100px;\">\n <div class=\"progress-fill\" id=\"wo-progress\" style=\"width: 60%;\"></div>\n </div>\n </td>\n <td><span class=\"badge badge-info\">In Progress</span></td>\n <td>2024-10-15</td>\n </tr>\n <tr>\n <td>WO-2024-002</td>\n <td>SKU-DEF456</td>\n <td>Cover Panel - White</td>\n <td>1500</td>\n <td>0</td>\n <td>\n <div class=\"progress-track\" style=\"height: 20px; width: 100px;\">\n <div class=\"progress-fill\" style=\"width: 0%;\"></div>\n </div>\n </td>\n <td><span class=\"badge badge-warning\">Pending</span></td>\n <td>2024-10-20</td>\n </tr>\n <tr>\n <td>WO-2024-003</td>\n <td>SKU-GHI789</td>\n <td>Mounting Bracket - Grey</td>\n <td>800</td>\n <td>800</td>\n <td>\n <div class=\"progress-track\" style=\"height: 20px; width: 100px;\">\n <div class=\"progress-fill\" style=\"width: 100%;\"></div>\n </div>\n </td>\n <td><span class=\"badge badge-success\">Completed</span></td>\n <td>2024-09-28</td>\n </tr>\n </tbody>\n </table>\n </div>\n </div>\n </div>\n\n <!-- VIEW 3: Report Issue -->\n <div id=\"view-report\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">⚠️</span>\n Report Issue\n </div>\n </div>\n <div class=\"card\" style=\"overflow-y: auto;\">\n <form id=\"issue-form\" onsubmit=\"app.submitIssue(event)\">\n <div class=\"form-group\">\n <label class=\"form-label\">Issue Category</label>\n <select class=\"form-select\" name=\"category\" required>\n <option value=\"\">Select category...</option>\n <option value=\"mechanical\">Mechanical Failure</option>\n <option value=\"electrical\">Electrical Issue</option>\n <option value=\"quality\">Quality Problem</option>\n <option value=\"software\">Software/HMI Issue</option>\n <option value=\"safety\">Safety Concern</option>\n <option value=\"other\">Other</option>\n </select>\n </div>\n\n <div class=\"form-group\">\n <label class=\"form-label\">Severity</label>\n <select class=\"form-select\" name=\"severity\" required>\n <option value=\"\">Select severity...</option>\n <option value=\"critical\">🔴 Critical - Machine Stopped</option>\n <option value=\"high\">🟠 High - Production Affected</option>\n <option value=\"medium\">🟡 Medium - Workaround Available</option>\n <option value=\"low\">🟢 Low - Minor Issue</option>\n </select>\n </div>\n\n <div class=\"form-group\">\n <label class=\"form-label\">Machine/Area</label>\n <input type=\"text\" class=\"form-input\" name=\"machine\" value=\"Maquina 1\" required>\n </div>\n\n <div class=\"form-group\">\n <label class=\"form-label\">Issue Description</label>\n <textarea class=\"form-textarea\" name=\"description\" placeholder=\"Describe the issue in detail...\" required></textarea>\n </div>\n\n <div class=\"form-group\">\n <label class=\"form-label\">Reported By</label>\n <input type=\"text\" class=\"form-input\" name=\"reporter\" value=\"Operator\" required>\n </div>\n\n <div style=\"display: flex; gap: 12px;\">\n <button type=\"submit\" class=\"btn btn-danger\">🚨 Submit Issue Report</button>\n <button type=\"reset\" class=\"btn btn-secondary\">Clear Form</button>\n </div>\n </form>\n\n <div class=\"card-header\" style=\"margin-top: 32px;\">Recent Reports</div>\n <div id=\"recent-reports\">\n <p style=\"color: var(--muted); padding: 16px;\">No recent reports</p>\n </div>\n </div>\n </div>\n </div>\n\n <!-- VIEW 4: Machine Status (Charts) -->\n <div id=\"view-machine\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">📊</span>\n Machine Status & Analytics\n </div>\n </div>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); overflow-y: auto;\">\n <div class=\"card\">\n <div class=\"card-header\">OEE Trend (Last Hour)</div>\n <div class=\"chart-container\">\n <canvas id=\"oee-chart\"></canvas>\n </div>\n </div>\n <div class=\"card\">\n <div class=\"card-header\">Production Rate</div>\n <div class=\"chart-container\">\n <canvas id=\"production-chart\"></canvas>\n </div>\n </div>\n <div class=\"card\">\n <div class=\"card-header\">Quality Metrics</div>\n <div class=\"chart-container\">\n <canvas id=\"quality-chart\"></canvas>\n </div>\n </div>\n <div class=\"card\">\n <div class=\"card-header\">Machine State Timeline</div>\n <div class=\"chart-container\">\n <canvas id=\"state-chart\"></canvas>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- VIEW 5: Notifications -->\n <div id=\"view-notifications\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">🔔</span>\n Notifications\n </div>\n <button class=\"btn btn-secondary\" onclick=\"app.clearNotifications()\">\n Clear All\n </button>\n </div>\n <div class=\"card\" style=\"overflow-y: auto;\">\n <div id=\"notifications-list\">\n <!-- Sample notifications -->\n <div class=\"status-item\" style=\"margin-bottom: 12px;\">\n <span class=\"led online\"></span>\n <div>\n <div class=\"status-label\">Machine Started</div>\n <div class=\"status-value\">2 minutes ago</div>\n </div>\n <span class=\"badge badge-success\">Info</span>\n </div>\n <div class=\"status-item\" style=\"margin-bottom: 12px;\">\n <span class=\"led running\"></span>\n <div>\n <div class=\"status-label\">Work Order Progress: 75%</div>\n <div class=\"status-value\">5 minutes ago</div>\n </div>\n <span class=\"badge badge-info\">Update</span>\n </div>\n <div class=\"status-item\" style=\"margin-bottom: 12px;\">\n <span class=\"led\" style=\"background: var(--warning);\"></span>\n <div>\n <div class=\"status-label\">Quality Alert: Scrap rate increasing</div>\n <div class=\"status-value\">15 minutes ago</div>\n </div>\n <span class=\"badge badge-warning\">Warning</span>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n <!-- VIEW 6: Help -->\n <div id=\"view-help\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">❓</span>\n Help & Documentation\n </div>\n </div>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); overflow-y: auto;\">\n <div class=\"card\">\n <div class=\"card-header\">Quick Start Guide</div>\n <p style=\"color: var(--muted); line-height: 1.6;\">\n <strong>Dashboard View:</strong> Main HMI showing real-time machine data, OEE metrics,\n and production counters.<br><br>\n <strong>Navigation:</strong> Use the sidebar on the left to switch between different\n views.<br><br>\n <strong>Real-time Updates:</strong> All data updates automatically via MQTT connection.\n </p>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">Troubleshooting</div>\n <p style=\"color: var(--muted); line-height: 1.6;\">\n <strong>No Data Showing:</strong><br>\n • Check MQTT broker connection<br>\n • Verify Node-RED is running<br>\n • Check browser console (F12)<br><br>\n <strong>Slow Updates:</strong><br>\n • Check network connection<br>\n • Restart local MQTT broker\n </p>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">Keyboard Shortcuts</div>\n <table class=\"table\">\n <tr>\n <td><strong>F11</strong></td>\n <td>Toggle Fullscreen</td>\n </tr>\n <tr>\n <td><strong>F12</strong></td>\n <td>Developer Console</td>\n </tr>\n <tr>\n <td><strong>Ctrl+R</strong></td>\n <td>Refresh Dashboard</td>\n </tr>\n </table>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">System Information</div>\n <p style=\"color: var(--muted); line-height: 1.6;\">\n <strong>Version:</strong> 1.0.0<br>\n <strong>MQTT Broker:</strong> localhost:9001<br>\n <strong>Update Rate:</strong> 1 second<br>\n <strong>Browser:</strong> <span id=\"browser-info\"></span>\n </p>\n </div>\n </div>\n </div>\n </div>\n\n <!-- VIEW 7: Settings -->\n <div id=\"view-settings\" class=\"view\">\n <div class=\"view-grid\">\n <div class=\"page-header\">\n <div class=\"page-title\">\n <span class=\"page-title-icon\">⚙️</span>\n Settings\n </div>\n </div>\n <div style=\"display: grid; grid-template-columns: 1fr 1fr; gap: var(--gap); overflow-y: auto;\">\n <div class=\"card\">\n <div class=\"card-header\">Display Settings</div>\n <div class=\"form-group\">\n <label class=\"form-label\">Screen Brightness</label>\n <input type=\"range\" class=\"form-input\" min=\"50\" max=\"100\" value=\"100\"\n onchange=\"app.setBrightness(this.value)\">\n </div>\n <div class=\"form-group\">\n <label class=\"form-label\">Theme</label>\n <select class=\"form-select\">\n <option value=\"dark\" selected>Dark (Current)</option>\n <option value=\"light\">Light</option>\n <option value=\"auto\">Auto (System)</option>\n </select>\n </div>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">Connection Settings</div>\n <div class=\"form-group\">\n <label class=\"form-label\">MQTT Broker</label>\n <input type=\"text\" class=\"form-input\" value=\"ws://localhost:9001\" readonly>\n </div>\n <div class=\"form-group\">\n <label class=\"form-label\">Connection Status</label>\n <div class=\"status-item\">\n <span class=\"led online\" id=\"settings-mqtt-status\"></span>\n <span class=\"status-label\" id=\"settings-mqtt-text\">Connected</span>\n </div>\n </div>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">Data Settings</div>\n <div class=\"form-group\">\n <label class=\"form-label\">Update Interval</label>\n <select class=\"form-select\">\n <option value=\"1000\" selected>1 second (Current)</option>\n <option value=\"2000\">2 seconds</option>\n <option value=\"5000\">5 seconds</option>\n </select>\n </div>\n <button class=\"btn btn-secondary\" onclick=\"app.exportData()\">\n 📥 Export Historical Data\n </button>\n </div>\n\n <div class=\"card\">\n <div class=\"card-header\">System Actions</div>\n <div style=\"display: flex; flex-direction: column; gap: 12px;\">\n <button class=\"btn btn-secondary\" onclick=\"location.reload()\">\n 🔄 Reload Dashboard\n </button>\n <button class=\"btn btn-secondary\" onclick=\"app.clearCache()\">\n 🗑️ Clear Cache\n </button>\n <button class=\"btn btn-danger\" onclick=\"app.resetSettings()\">\n ⚠️ Reset to Defaults\n </button>\n </div>\n </div>\n </div>\n </div>\n </div>\n\n </div>\n </div>\n\n <script>\n // Application State\n const app = {\n mqttClient: null,\n currentView: 'dashboard',\n charts: {},\n data: {\n oee: [],\n production: [],\n quality: [],\n notifications: []\n },\n\n // Initialize application\n init() {\n this.setupNavigation();\n this.connectMQTT();\n this.startClock();\n this.initCharts();\n this.loadBrowserInfo();\n },\n\n // Setup sidebar navigation\n setupNavigation() {\n const sidebarItems = document.querySelectorAll('.sidebar-item');\n \n sidebarItems.forEach(item => {\n item.addEventListener('click', () => {\n const viewName = item.getAttribute('data-view');\n if (viewName) {\n this.switchView(viewName);\n }\n });\n });\n },\n\n // Switch between views\n switchView(viewName) {\n // Hide all views\n document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));\n \n // Show selected view\n document.getElementById(`view-${viewName}`).classList.add('active');\n \n // Update sidebar\n document.querySelectorAll('.sidebar-item').forEach(item => {\n item.classList.remove('active');\n if (item.getAttribute('data-view') === viewName) {\n item.classList.add('active');\n }\n });\n \n this.currentView = viewName;\n \n // Update charts if switching to machine view\n if (viewName === 'machine') {\n setTimeout(() => this.updateCharts(), 100);\n }\n },\n\n // Connect to MQTT\n connectMQTT() {\n this.mqttClient = mqtt.connect('ws://localhost:9001', {\n clientId: 'dashboard_' + Math.random().toString(16).substr(2, 8),\n clean: true,\n reconnectPeriod: 5000,\n connectTimeout: 30 * 1000\n });\n \n this.mqttClient.on('connect', () => {\n console.log('Connected to MQTT broker');\n \n // Subscribe to all topics\n const topics = [\n 'pimsa/maquina1/status',\n 'pimsa/maquina1/oee',\n 'pimsa/maquina1/production',\n 'pimsa/erp/workorder/current'\n ];\n\n topics.forEach(topic => {\n this.mqttClient.subscribe(topic, (err) => {\n if (!err) {\n console.log('Subscribed to ' + topic);\n }\n });\n });\n });\n \n this.mqttClient.on('message', (topic, message) => {\n try {\n const data = JSON.parse(message.toString());\n \n if (topic === 'pimsa/maquina1/status') {\n this.updateStatus(data);\n } else if (topic === 'pimsa/maquina1/oee') {\n this.updateOEE(data);\n } else if (topic === 'pimsa/maquina1/production') {\n this.updateProduction(data);\n } else if (topic === 'pimsa/erp/workorder/current') {\n this.updateWorkOrder(data);\n }\n } catch (error) {\n console.error('Error parsing MQTT message:', error);\n }\n });\n \n this.mqttClient.on('error', (error) => {\n console.error('MQTT error:', error);\n });\n },\n\n // Update functions\n updateStatus(data) {\n if (data.isOnline !== undefined) {\n const led = document.getElementById('status_online');\n led.className = 'led' + (data.isOnline ? ' online' : '');\n document.getElementById('status_online_text').textContent = \n data.isOnline ? 'Connected' : 'Disconnected';\n }\n \n if (data.isRunning !== undefined) {\n const led = document.getElementById('status_running');\n led.className = 'led' + (data.isRunning ? ' running' : '');\n document.getElementById('status_running_text').textContent = \n data.isRunning ? 'Active' : 'Stopped';\n }\n },\n\n updateOEE(data) {\n if (data.oee !== undefined) {\n document.getElementById('oee_value').textContent = Math.round(data.oee) + '%';\n this.data.oee.push({time: Date.now(), value: data.oee});\n if (this.data.oee.length > 60) this.data.oee.shift();\n }\n if (data.availability !== undefined) {\n document.getElementById('availability_value').textContent = Math.round(data.availability) + '%';\n }\n if (data.performance !== undefined) {\n document.getElementById('performance_value').textContent = Math.round(data.performance) + '%';\n }\n if (data.quality !== undefined) {\n document.getElementById('quality_value').textContent = Math.round(data.quality) + '%';\n this.data.quality.push({time: Date.now(), value: data.quality});\n if (this.data.quality.length > 60) this.data.quality.shift();\n }\n },\n\n updateProduction(data) {\n if (data.goodParts !== undefined) {\n document.getElementById('good_parts').textContent = data.goodParts.toLocaleString();\n document.getElementById('wo-completed').textContent = data.goodParts;\n this.data.production.push({time: Date.now(), value: data.goodParts});\n if (this.data.production.length > 60) this.data.production.shift();\n }\n if (data.scrapParts !== undefined) {\n document.getElementById('scrap_parts').textContent = data.scrapParts;\n }\n if (data.progress !== undefined) {\n const bar = document.getElementById('progress_bar');\n const text = document.getElementById('progress_value');\n const woBar = document.getElementById('wo-progress');\n const progress = Math.round(data.progress);\n bar.style.width = progress + '%';\n text.textContent = progress + '%';\n if (woBar) woBar.style.width = progress + '%';\n }\n if (data.cycleTime !== undefined) {\n document.getElementById('cycle_time').textContent = data.cycleTime;\n }\n },\n\n updateWorkOrder(data) {\n if (data.workOrder !== undefined) {\n document.getElementById('workorder_number').textContent = data.workOrder;\n }\n if (data.sku !== undefined) {\n document.getElementById('workorder_sku').textContent = data.sku;\n }\n },\n\n // Clock\n startClock() {\n setInterval(() => {\n const now = new Date();\n const datetime = now.toISOString().replace('T', ' ').substring(0, 19);\n document.getElementById('footer_datetime').textContent = datetime;\n }, 1000);\n },\n\n // Initialize Charts\n initCharts() {\n const commonOptions = {\n responsive: true,\n maintainAspectRatio: false,\n plugins: {\n legend: { labels: { color: '#eaf1fb' } }\n },\n scales: {\n y: { ticks: { color: '#96a5b7' }, grid: { color: 'rgba(255,255,255,0.1)' } },\n x: { ticks: { color: '#96a5b7' }, grid: { color: 'rgba(255,255,255,0.1)' } }\n }\n };\n\n // OEE Chart\n const oeeCtx = document.getElementById('oee-chart');\n if (oeeCtx) {\n this.charts.oee = new Chart(oeeCtx, {\n type: 'line',\n data: {\n labels: Array(20).fill(''),\n datasets: [{\n label: 'OEE %',\n data: Array(20).fill(0),\n borderColor: '#2ea3ff',\n backgroundColor: 'rgba(46, 163, 255, 0.1)',\n tension: 0.4\n }]\n },\n options: commonOptions\n });\n }\n\n // Production Chart\n const prodCtx = document.getElementById('production-chart');\n if (prodCtx) {\n this.charts.production = new Chart(prodCtx, {\n type: 'bar',\n data: {\n labels: Array(20).fill(''),\n datasets: [{\n label: 'Parts Produced',\n data: Array(20).fill(0),\n backgroundColor: '#2ecc71'\n }]\n },\n options: commonOptions\n });\n }\n\n // Quality Chart\n const qualityCtx = document.getElementById('quality-chart');\n if (qualityCtx) {\n this.charts.quality = new Chart(qualityCtx, {\n type: 'doughnut',\n data: {\n labels: ['Good Parts', 'Scrap Parts'],\n datasets: [{\n data: [95, 5],\n backgroundColor: ['#2ecc71', '#ff4d4f']\n }]\n },\n options: {\n responsive: true,\n maintainAspectRatio: false,\n plugins: {\n legend: { labels: { color: '#eaf1fb' } }\n }\n }\n });\n }\n\n // State Chart\n const stateCtx = document.getElementById('state-chart');\n if (stateCtx) {\n this.charts.state = new Chart(stateCtx, {\n type: 'line',\n data: {\n labels: Array(20).fill(''),\n datasets: [{\n label: 'Running State',\n data: Array(20).fill(0),\n borderColor: '#2ea3ff',\n stepped: true\n }]\n },\n options: commonOptions\n });\n }\n },\n\n // Update charts with latest data\n updateCharts() {\n if (this.charts.oee && this.data.oee.length > 0) {\n this.charts.oee.data.datasets[0].data = this.data.oee.map(d => d.value);\n this.charts.oee.update();\n }\n \n if (this.charts.production && this.data.production.length > 0) {\n this.charts.production.data.datasets[0].data = this.data.production.map(d => d.value);\n this.charts.production.update();\n }\n },\n\n // UI Actions\n submitIssue(event) {\n event.preventDefault();\n const formData = new FormData(event.target);\n const issue = {\n category: formData.get('category'),\n severity: formData.get('severity'),\n machine: formData.get('machine'),\n description: formData.get('description'),\n reporter: formData.get('reporter'),\n timestamp: new Date().toISOString()\n };\n \n console.log('Issue reported:', issue);\n \n // Store in localStorage\n const issues = JSON.parse(localStorage.getItem('issues') || '[]');\n issues.unshift(issue);\n localStorage.setItem('issues', JSON.stringify(issues));\n \n alert('✅ Issue reported successfully!');\n event.target.reset();\n this.loadRecentReports();\n },\n\n loadRecentReports() {\n const issues = JSON.parse(localStorage.getItem('issues') || '[]');\n const container = document.getElementById('recent-reports');\n \n if (issues.length === 0) {\n container.innerHTML = '<p style=\"color: var(--muted); padding: 16px;\">No recent reports</p>';\n return;\n }\n \n container.innerHTML = issues.slice(0, 5).map(issue => `\n <div class=\"status-item\" style=\"margin-bottom: 12px;\">\n <span class=\"badge badge-${issue.severity === 'critical' ? 'danger' : 'warning'}\">${issue.severity}</span>\n <div style=\"flex: 1;\">\n <div class=\"status-label\">${issue.category}: ${issue.description.substring(0, 60)}...</div>\n <div class=\"status-value\">${new Date(issue.timestamp).toLocaleString()}</div>\n </div>\n </div>\n `).join('');\n },\n\n createWorkOrder() {\n alert('Create Work Order functionality coming soon!');\n },\n\n clearNotifications() {\n document.getElementById('notifications-list').innerHTML = \n '<p style=\"color: var(--muted); padding: 16px;\">No notifications</p>';\n document.getElementById('notif-count').textContent = '0';\n },\n\n setBrightness(value) {\n document.body.style.opacity = value / 100;\n },\n\n exportData() {\n const data = {\n oee: this.data.oee,\n production: this.data.production,\n quality: this.data.quality,\n timestamp: new Date().toISOString()\n };\n \n const blob = new Blob([JSON.stringify(data, null, 2)], {type: 'application/json'});\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = `hmi-data-${Date.now()}.json`;\n a.click();\n },\n\n clearCache() {\n localStorage.clear();\n alert('Cache cleared!');\n },\n\n resetSettings() {\n if (confirm('Reset all settings to default?')) {\n localStorage.clear();\n location.reload();\n }\n },\n\n loadBrowserInfo() {\n document.getElementById('browser-info').textContent = navigator.userAgent.split(' ').pop();\n }\n };\n\n // Start application when DOM is ready\n document.addEventListener('DOMContentLoaded', () => {\n app.init();\n });\n </script>\n</body>\n\n</html>",
|
||
"storeOutMessages": true,
|
||
"fwdInMessages": true,
|
||
"resendOnRefresh": true,
|
||
"templateScope": "local",
|
||
"className": "",
|
||
"x": 700,
|
||
"y": 320,
|
||
"wires": [
|
||
[]
|
||
]
|
||
}
|
||
] |