[
  {
    "id": "063f54dba22818e8",
    "type": "tab",
    "label": "Dashboard",
    "disabled": false,
    "info": "",
    "env": []
  },
  {
    "id": "d40e1c4938f31319",
    "type": "inject",
    "z": "063f54dba22818e8",
    "name": "5초 시스템",
    "repeat": "5",
    "once": true,
    "onceDelay": "2",
    "x": 130,
    "y": 260,
    "wires": [
      [
        "5b2612f9157a4313"
      ]
    ]
  },
  {
    "id": "5b2612f9157a4313",
    "type": "function",
    "z": "063f54dba22818e8",
    "name": "메트릭→UI",
    "func": "var m=global.get(\"metrics_cache\");if(!m)return null;return [{payload:parseFloat(m.cpu)||0},{payload:parseFloat(m.memory)||0},{payload:parseFloat(m.disk)||0},{payload:m.temperature||\"\"},{payload:\"Tags:\"+m.tag_count+\" | OPC:\"+m.opc_count+\" | MPS:\"+m.mps}];",
    "outputs": 5,
    "x": 370,
    "y": 260,
    "wires": [
      [
        "00a9ff55589947e6"
      ],
      [
        "3108d96cea4a6e88"
      ],
      [
        "9d75bbf4db9b5c4d"
      ],
      [
        "84893137b115e8f2"
      ],
      [
        "cea619bd887d8af7"
      ]
    ]
  },
  {
    "id": "00a9ff55589947e6",
    "type": "ui_gauge",
    "z": "063f54dba22818e8",
    "name": "CPU",
    "group": "543ed5643d3782f9",
    "order": 1,
    "width": 2,
    "height": 2,
    "gtype": "gage",
    "title": "CPU",
    "label": "%",
    "format": "{{value}}",
    "min": 0,
    "max": 100,
    "colors": [
      "#00b500",
      "#e6e600",
      "#ca3838"
    ],
    "seg1": 50,
    "seg2": 80,
    "x": 590,
    "y": 180,
    "wires": []
  },
  {
    "id": "3108d96cea4a6e88",
    "type": "ui_gauge",
    "z": "063f54dba22818e8",
    "name": "메모리",
    "group": "543ed5643d3782f9",
    "order": 2,
    "width": 2,
    "height": 2,
    "gtype": "gage",
    "title": "메모리",
    "label": "%",
    "format": "{{value}}",
    "min": 0,
    "max": 100,
    "colors": [
      "#00b500",
      "#e6e600",
      "#ca3838"
    ],
    "seg1": 60,
    "seg2": 85,
    "x": 590,
    "y": 220,
    "wires": []
  },
  {
    "id": "9d75bbf4db9b5c4d",
    "type": "ui_gauge",
    "z": "063f54dba22818e8",
    "name": "디스크",
    "group": "543ed5643d3782f9",
    "order": 3,
    "width": 2,
    "height": 2,
    "gtype": "gage",
    "title": "디스크",
    "label": "%",
    "format": "{{value}}",
    "min": 0,
    "max": 100,
    "colors": [
      "#00b500",
      "#e6e600",
      "#ca3838"
    ],
    "seg1": 70,
    "seg2": 90,
    "x": 590,
    "y": 260,
    "wires": []
  },
  {
    "id": "84893137b115e8f2",
    "type": "ui_text",
    "z": "063f54dba22818e8",
    "group": "54d2e65152d90f3c",
    "order": 1,
    "width": 3,
    "height": 1,
    "name": "온도",
    "format": "🌡️ {{msg.payload}}",
    "layout": "row-spread",
    "className": "",
    "style": false,
    "font": "",
    "fontSize": 16,
    "color": "#717171",
    "x": 590,
    "y": 300,
    "wires": []
  },
  {
    "id": "cea619bd887d8af7",
    "type": "ui_text",
    "z": "063f54dba22818e8",
    "group": "54d2e65152d90f3c",
    "order": 2,
    "width": 6,
    "height": 1,
    "name": "정보",
    "format": "📊 {{msg.payload}}",
    "layout": "row-spread",
    "className": "",
    "style": false,
    "font": "",
    "fontSize": 14,
    "color": "#717171",
    "x": 590,
    "y": 340,
    "wires": []
  },
  {
    "id": "af20f269db5617dd",
    "type": "inject",
    "z": "063f54dba22818e8",
    "name": "5초 태그",
    "repeat": "5",
    "once": true,
    "onceDelay": "3",
    "x": 120,
    "y": 440,
    "wires": [
      [
        "21bb5015cd9e1028"
      ]
    ]
  },
  {
    "id": "21bb5015cd9e1028",
    "type": "function",
    "z": "063f54dba22818e8",
    "name": "태그→UI",
    "func": "var d=global.get(\"tags_cache\")||[];if(!d.length)return null;return {payload:d};",
    "outputs": 1,
    "x": 380,
    "y": 440,
    "wires": [
      [
        "1d8b962138dec5e1"
      ]
    ]
  },
  {
    "id": "1d8b962138dec5e1",
    "type": "ui_template",
    "z": "063f54dba22818e8",
    "group": "a68ba43d50233268",
    "name": "태그 테이블",
    "order": 1,
    "width": "12",
    "height": "8",
    "format": "<style>\n  .tag-table { width:100%; border-collapse:collapse; font-size:13px; }\n  .tag-table th { background:#0094CE; color:#fff; padding:7px 10px; text-align:left; }\n  .tag-table td { padding:6px 10px; border-bottom:1px solid #eee; }\n  .tag-table tr:nth-child(even) td { background:#f5f5f5; }\n  .val-ok { color:#2e7d32; font-weight:bold; }\n  .val-err { color:#c62828; }\n</style>\n<div style=\"overflow:auto; max-height:380px;\">\n  <table class=\"tag-table\">\n    <thead><tr>\n      <th>태그 ID</th><th>값</th><th>시간</th><th>상태</th>\n    </tr></thead>\n    <tbody>\n      <tr ng-repeat=\"t in msg.payload track by t.id\">\n        <td>{{t.id}}</td>\n        <td>{{t.value}}</td>\n        <td>{{t.time | limitTo:19 | limitTo:-8}}</td>\n        <td ng-class=\"t.status === 'SUCCESS' ? 'val-ok' : 'val-err'\">{{t.status}}</td>\n      </tr>\n    </tbody>\n  </table>\n</div>",
    "storeOutMessages": false,
    "resendOnRefresh": true,
    "templateScope": "local",
    "className": "",
    "x": 610,
    "y": 440,
    "wires": [
      []
    ]
  },
  {
    "id": "n_pp_monitor_tmpl",
    "type": "ui_template",
    "z": "063f54dba22818e8",
    "group": "grp_tag_monitor",
    "name": "태그 모니터 UI",
    "order": 1,
    "width": "12",
    "height": "20",
    "format": "<style>\n    #pp-monitor {\n  position: fixed; top: 64px; left: 0; width: 100vw; height: calc(100vh - 64px); z-index: 50;\n  display: flex; flex-direction: column; overflow: hidden;\n  background: var(--bg); color: var(--text);\n  font-family: 'Segoe UI', sans-serif;\n  transition: background .3s, color .3s;\n      --bg:       #f5f6fa; --bg2: #ffffff; --bg3: #eef0f6;\n      --border:   #dde0ea; --text: #1a1d2e; --text2: #5a5f7a; --text3: #9095b0;\n      --accent:   #0094ce; --ok: #2e7d32; --err: #c62828; --changed: #0077aa;\n      --chip-opc-bg:#e8f5e9; --chip-opc-b:#a5d6a7; --chip-opc-t:#2e7d32;\n      --chip-tag-bg:#e3f2fd; --chip-tag-b:#90caf9; --chip-tag-t:#1565c0;\n      --shadow: 0 2px 8px rgba(0,0,0,.08);\n    }\n    #pp-monitor.dark,\nbody.nr-dark-theme #pp-monitor {\n      --bg:       #0f1117; --bg2: #1a1d27; --bg3: #14161f;\n      --border:   #2a2d3a; --text: #e0e0e0; --text2: #999; --text3: #555;\n      --accent:   #61dafb; --ok: #4caf50; --err: #f44336; --changed: #61dafb;\n      --chip-opc-bg:#1e2a20; --chip-opc-b:#4caf5055; --chip-opc-t:#4caf50;\n      --chip-tag-bg:#1e2535; --chip-tag-b:#61dafb55; --chip-tag-t:#61dafb;\n      --shadow: 0 2px 8px rgba(0,0,0,.4);\n    }\n#pp-monitor * { box-sizing: border-box; margin: 0; padding: 0; }\n    \n\n    #pp-monitor .pp-header { display:flex; align-items:center; justify-content:space-between;\n             padding:12px 20px; background:var(--bg2); border-bottom:1px solid var(--border); flex-shrink:0; }\n    header h1 { font-size:17px; font-weight:600; color:var(--accent); }\n#pp-monitor .pp-header-right { display:flex; align-items:center; gap:10px; }\n    #pp-monitor .pp-status { display:flex; align-items:center; gap:6px; font-size:12px; padding:3px 10px;\n                    border-radius:20px; background:var(--bg3); color:var(--text2); }\n#pp-monitor .pp-dot { width:8px; height:8px; border-radius:50%; background:var(--text3); transition:background .3s; }\n#pp-monitor .pp-dot.connected    { background:var(--ok); box-shadow:0 0 5px var(--ok); }\n#pp-monitor .pp-dot.disconnected { background:var(--err); }\n#pp-monitor .pp-dot.connecting   { background:#ff9800; animation:pp-pulse 1s infinite; }\n    @keyframes pp-pulse { 0%,100%{opacity:1}50%{opacity:.3} }\n#pp-monitor .pp-theme-btn { background:none; border:1px solid var(--border); border-radius:6px;\n                 padding:3px 8px; cursor:pointer; font-size:14px; color:var(--text2); transition:border-color .2s; }\n#pp-monitor .pp-theme-btn:hover { border-color:var(--accent); }\n\n#pp-monitor .pp-filterbar { display:flex; align-items:flex-start; gap:8px; flex-wrap:wrap;\n                 padding:8px 20px; background:var(--bg3); border-bottom:1px solid var(--border); flex-shrink:0; }\n#pp-monitor .pp-filter-group { position:relative; }\n#pp-monitor .pp-filter-btn { display:flex; align-items:center; gap:5px; padding:5px 10px; border-radius:6px;\n                  border:1px solid var(--border); background:var(--bg2); color:var(--text2);\n                  font-size:12px; cursor:pointer; white-space:nowrap; user-select:none; }\n#pp-monitor .pp-filter-btn:hover { border-color:var(--accent); }\n#pp-monitor .pp-filter-btn.active { border-color:var(--accent); color:var(--accent); }\n#pp-monitor .pp-badge { background:var(--accent); color:var(--bg2); border-radius:10px; padding:0 5px; font-size:10px; font-weight:700; }\n#pp-monitor .pp-dropdown { display:none; position:absolute; top:calc(100% + 3px); left:0;\n                background:var(--bg2); border:1px solid var(--border); border-radius:8px;\n                min-width:210px; max-width:310px; max-height:310px; overflow-y:auto;\n                z-index:100; box-shadow:var(--shadow); }\n#pp-monitor .pp-dropdown.open { display:block; }\n#pp-monitor .pp-dd-search { width:100%; padding:7px 10px; background:var(--bg3); border:none;\n                 border-bottom:1px solid var(--border); color:var(--text); font-size:12px; outline:none;\n                 position:sticky; top:0; }\n#pp-monitor .pp-dd-actions { display:flex; border-bottom:1px solid var(--border); position:sticky; top:33px; background:var(--bg2); }\n#pp-monitor .pp-dd-actions button { flex:1; padding:4px; background:none; border:none; color:var(--text3); font-size:11px; cursor:pointer; }\n#pp-monitor .pp-dd-actions button:hover { color:var(--accent); }\n#pp-monitor .pp-dd-item { display:flex; align-items:center; gap:8px; padding:6px 12px; cursor:pointer; font-size:12px; }\n#pp-monitor .pp-dd-item:hover { background:var(--bg3); }\n#pp-monitor .pp-dd-item input[type=checkbox] { accent-color:var(--accent); cursor:pointer; flex-shrink:0; }\n#pp-monitor .pp-dd-item label { cursor:pointer; flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:var(--text); }\n#pp-monitor .pp-dd-item .pp-cnt { font-size:10px; color:var(--text3); flex-shrink:0; }\n#pp-monitor .pp-dd-hint { padding:5px 12px; font-size:11px; color:var(--accent); border-bottom:1px solid var(--border); background:var(--bg3); }\n#pp-monitor .pp-dd-empty { padding:10px; font-size:11px; color:var(--text3); text-align:center; }\n#pp-monitor .pp-search-input { padding:5px 10px; border-radius:6px; border:1px solid var(--border);\n                    background:var(--bg2); color:var(--text); font-size:12px; width:180px; }\n#pp-monitor .pp-search-input:focus { outline:none; border-color:var(--accent); }\n#pp-monitor .pp-toolbar-right { margin-left:auto; display:flex; align-items:center; gap:12px; }\n#pp-monitor .pp-chk-label { font-size:12px; color:var(--text2); cursor:pointer; white-space:nowrap; }\n#pp-monitor .pp-chk-label input { accent-color:var(--accent); }\n#pp-monitor .pp-count { font-size:12px; color:var(--text3); }\n#pp-monitor .pp-update { font-size:11px; color:var(--text3); }\n\n#pp-monitor .pp-chips { display:flex; flex-wrap:wrap; gap:5px; padding:0 20px 6px; flex-shrink:0; }\n#pp-monitor .pp-chips:empty { display:none; }\n#pp-monitor .pp-chip { display:flex; align-items:center; gap:3px; padding:2px 7px; border-radius:12px; font-size:11px; border:1px solid; }\n#pp-monitor .pp-chip.opc { background:var(--chip-opc-bg); border-color:var(--chip-opc-b); color:var(--chip-opc-t); }\n#pp-monitor .pp-chip.tag { background:var(--chip-tag-bg); border-color:var(--chip-tag-b); color:var(--chip-tag-t); }\n#pp-monitor .pp-chip .rm { cursor:pointer; opacity:.6; margin-left:2px; }\n#pp-monitor .pp-chip .rm:hover { opacity:1; color:var(--err); }\n\n#pp-monitor .pp-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(180px,1fr));\n            gap:9px; padding:14px 20px; overflow-y:auto; flex:1; align-content:start; }\n#pp-monitor .pp-card { background:var(--bg2); border:1px solid var(--border); border-radius:8px;\n            padding:10px 12px; transition:border-color .5s,background .5s; box-shadow:var(--shadow); }\n#pp-monitor .pp-card.updated { border-color:var(--accent); background:var(--bg3); }\n#pp-monitor .pp-card .pp-opc-id { font-size:10px; color:var(--accent); margin-bottom:1px; }\n#pp-monitor .pp-card .pp-tag-id { font-size:11px; color:var(--text3); margin-bottom:3px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n#pp-monitor .pp-card .pp-val  { font-size:20px; font-weight:700; color:var(--text); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n#pp-monitor .pp-card .pp-val.changed { color:var(--changed); }\n#pp-monitor .pp-card .pp-meta   { font-size:10px; color:var(--text3); margin-top:4px; }\n#pp-monitor .pp-ok  { color:var(--ok); }\n#pp-monitor .pp-err { color:var(--err); }\n#pp-monitor .pp-empty { grid-column:1/-1; text-align:center; color:var(--text3); font-size:13px; padding:50px 0; }\n  </style>\n\n<div id=\"pp-monitor\">\n  <!-- Header -->\n  <div class=\"pp-header\">\n    <span class=\"pp-title\">🌿 실시간 태그 모니터</span>\n    <div class=\"pp-header-right\">\n      <div class=\"pp-status\">\n        <div class=\"pp-dot connecting\" id=\"pp-dot\"></div>\n        <span id=\"pp-status-text\">연결 중...</span>\n      </div>\n      <button class=\"pp-theme-btn\" id=\"pp-theme-btn\" onclick=\"ppToggleTheme()\" title=\"라이트/다크 전환\">🌙</button>\n    </div>\n  </div>\n\n  <!-- Filter Bar -->\n  <div class=\"pp-filterbar\">\n    <div class=\"pp-filter-group\">\n      <div class=\"pp-filter-btn\" id=\"pp-btn-opc\" onclick=\"ppToggleDd('pp-dd-opc')\">\n        OPC ID <span class=\"pp-badge\" id=\"pp-badge-opc\" style=\"display:none\">0</span> ▾\n      </div>\n      <div class=\"pp-dropdown\" id=\"pp-dd-opc\">\n        <input class=\"pp-dd-search\" type=\"text\" placeholder=\"OPC 검색...\" oninput=\"ppRenderOpc(this.value)\" />\n        <div class=\"pp-dd-actions\">\n          <button onclick=\"ppSelAllOpc()\">전체선택</button>\n          <button onclick=\"ppClrOpc()\">전체해제</button>\n        </div>\n        <div id=\"pp-opc-list\"></div>\n      </div>\n    </div>\n\n    <div class=\"pp-filter-group\">\n      <div class=\"pp-filter-btn\" id=\"pp-btn-tag\" onclick=\"ppToggleDd('pp-dd-tag')\">\n        Tag ID <span class=\"pp-badge\" id=\"pp-badge-tag\" style=\"display:none\">0</span> ▾\n      </div>\n      <div class=\"pp-dropdown\" id=\"pp-dd-tag\">\n        <input class=\"pp-dd-search\" type=\"text\" placeholder=\"태그 검색...\" oninput=\"ppRenderTag(this.value)\" />\n        <div class=\"pp-dd-actions\">\n          <button onclick=\"ppSelAllTag()\">전체선택</button>\n          <button onclick=\"ppClrTag()\">전체해제</button>\n        </div>\n        <div id=\"pp-tag-hint\"></div>\n        <div id=\"pp-tag-list\"></div>\n      </div>\n    </div>\n\n    <input class=\"pp-search-input\" type=\"text\" id=\"pp-search\" placeholder=\"태그 텍스트 검색...\" oninput=\"ppApply()\" />\n\n    <div class=\"pp-toolbar-right\">\n      <label class=\"pp-chk-label\"><input id=\"pp-chk-changed\" type=\"checkbox\" onchange=\"ppApply()\" /> 변경만</label>\n      <span class=\"pp-count\" id=\"pp-count\">태그: 0</span>\n      <span class=\"pp-update\" id=\"pp-update\"></span>\n    </div>\n  </div>\n\n  <!-- Chips -->\n  <div class=\"pp-chips\" id=\"pp-chips\"></div>\n\n  <!-- Grid -->\n  <div class=\"pp-grid\" id=\"pp-grid\"><div class=\"pp-empty\">WebSocket 연결 대기 중...</div></div>\n</div>\n\n<script>\n(function() {\nvar WS_URL = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.hostname + ':1880/ws/edge';\n  var RECONNECT_DELAY = 3000;\n  var tags = {}, changedSet = new Set(), ws;\n  var selOpc = new Set(), selTag = new Set();\n\n  // Theme\n  function ppToggleTheme() {\n    var dark = !document.getElementById('pp-monitor').classList.contains('dark');\n    document.getElementById('pp-monitor').classList.toggle('dark', dark);\n    document.getElementById('pp-theme-btn').textContent = dark ? '☀️' : '🌙';\n  }\n\n  // WebSocket\n  function ppConnect() {\n    ppSetStatus('pp-dot connecting', '연결 중...');\n    ws = new WebSocket(WS_URL);\n    ws.onopen  = () => ppSetStatus('pp-dot connected', '연결됨');\n    ws.onclose = () => { ppSetStatus('pp-dot disconnected', '연결 끊김'); setTimeout(ppConnect, RECONNECT_DELAY); };\n    ws.onerror = () => ws.close();\n    ws.onmessage = ({ data }) => {\n      try {\n        var arr = JSON.parse(data); if (!Array.isArray(arr)) return;\n        changedSet.clear();\n        arr.forEach(t => { var id=t.id||t.tag; if(!id) return;\n          if(id in tags && String(tags[id].value)!==String(t.value)) changedSet.add(id);\n          tags[id] = {...t, id}; });\n        document.getElementById('pp-update').textContent = '수신: '+new Date().toLocaleTimeString('ko-KR');\n        ppRenderOpc(document.querySelector('#pp-dd-opc .pp-dd-search').value);\n        ppRenderTag(document.querySelector('#pp-dd-tag .pp-dd-search').value);\n        ppApply();\n      } catch(e) { console.error(e); }\n    };\n  }\n  function ppSetStatus(cls,txt){\n    document.getElementById('pp-dot').className=cls;\n    document.getElementById('pp-status-text').textContent=txt;\n  }\n\n  // OPC\n  function ppGetOpcCounts(){ var c={}; Object.values(tags).forEach(t=>{var o=t.opc_id||'(없음)';c[o]=(c[o]||0)+1;}); return c; }\n  function ppRenderOpc(s=''){\n    var counts=ppGetOpcCounts(); var q=s.toLowerCase();\n    var all=Object.keys(counts).sort().filter(v=>!q||v.toLowerCase().includes(q));\n    var el=document.getElementById('pp-opc-list');\n    if(!all.length){el.innerHTML='<div class=\"pp-dd-empty\">항목 없음</div>';return;}\n    el.innerHTML=all.map(v=>`<div class=\"pp-dd-item\"><input type=\"checkbox\" id=\"oc${v.replace(/\\W/g,'_')}\" ${selOpc.has(v)?'checked':''}\n      onchange=\"ppOpcChg('${esc(v)}',this.checked)\"><label for=\"oc${v.replace(/\\W/g,'_')}\" title=\"${v}\">${v}</label>\n      <span class=\"pp-cnt\">${counts[v]}</span></div>`).join('');\n  }\n  function ppOpcChg(v,chk){\n    if(chk) selOpc.add(v); else { selOpc.delete(v); var a=ppGetTagsForOpc(); selTag.forEach(t=>{if(!a.has(t))selTag.delete(t);}); }\n    ppBadge('opc',selOpc); ppRenderTag(''); ppRenderOpc(''); ppChips(); ppApply();\n  }\n  function ppSelAllOpc(){ Object.keys(ppGetOpcCounts()).forEach(v=>selOpc.add(v)); ppRenderOpc(''); ppRenderTag(''); ppBadge('opc',selOpc); ppChips(); ppApply(); }\n  function ppClrOpc()  { selOpc.clear(); ppRenderOpc(''); ppRenderTag(''); ppBadge('opc',selOpc); ppChips(); ppApply(); }\n\n  // Tag (cascaded)\n  function ppGetTagsForOpc(){ var s=new Set(); Object.values(tags).forEach(t=>{var o=t.opc_id||'(없음)';if(selOpc.size===0||selOpc.has(o))s.add(t.id);}); return s; }\n  function ppRenderTag(s=''){\n    var q=s.toLowerCase(); var avail=[...ppGetTagsForOpc()].sort().filter(v=>!q||v.toLowerCase().includes(q));\n    var hint=document.getElementById('pp-tag-hint'); var el=document.getElementById('pp-tag-list');\n    hint.innerHTML=selOpc.size?`<div class=\"pp-dd-hint\">▸ ${[...selOpc].join(', ')} (${ppGetTagsForOpc().size}개)</div>`:'';\n    if(!avail.length){el.innerHTML='<div class=\"pp-dd-empty\">항목 없음</div>';return;}\n    el.innerHTML=avail.map(v=>`<div class=\"pp-dd-item\"><input type=\"checkbox\" id=\"tc${v.replace(/\\W/g,'_')}\" ${selTag.has(v)?'checked':''}\n      onchange=\"ppTagChg('${esc(v)}',this.checked)\"><label for=\"tc${v.replace(/\\W/g,'_')}\" title=\"${v}\">${v}</label></div>`).join('');\n  }\n  function ppTagChg(v,chk){ if(chk) selTag.add(v); else selTag.delete(v); ppBadge('tag',selTag); ppChips(); ppApply(); }\n  function ppSelAllTag(){ ppGetTagsForOpc().forEach(v=>selTag.add(v)); ppRenderTag(''); ppBadge('tag',selTag); ppChips(); ppApply(); }\n  function ppClrTag()  { selTag.clear(); ppRenderTag(''); ppBadge('tag',selTag); ppChips(); ppApply(); }\n\n  function ppBadge(k,s){ var b=document.getElementById('pp-badge-'+k); b.style.display=s.size?'':'none'; b.textContent=s.size;\n    document.getElementById('pp-btn-'+k).classList.toggle('active',s.size>0); }\n  function ppChips(){\n    var p=[];\n    selOpc.forEach(v=>p.push(`<div class=\"pp-chip opc\">OPC: ${v} <span class=\"rm\" onclick=\"ppOpcChg('${esc(v)}',false);ppRenderOpc('')\">✕</span></div>`));\n    selTag.forEach(v=>p.push(`<div class=\"pp-chip tag\">Tag: ${v} <span class=\"rm\" onclick=\"ppTagChg('${esc(v)}',false);ppRenderTag('')\">✕</span></div>`));\n    document.getElementById('pp-chips').innerHTML=p.join('');\n  }\n  function esc(s){ return s.replace(/\\\\/g,'\\\\\\\\').replace(/'/g,\"\\\\'\"); }\n\n  function ppApply(){\n    var search=document.getElementById('pp-search').value.toLowerCase();\n    var onlyChg=document.getElementById('pp-chk-changed').checked;\n    var list=Object.values(tags);\n    if(selOpc.size) list=list.filter(t=>selOpc.has(t.opc_id||'(없음)'));\n    if(selTag.size) list=list.filter(t=>selTag.has(t.id));\n    if(search) list=list.filter(t=>t.id.toLowerCase().includes(search));\n    if(onlyChg) list=list.filter(t=>changedSet.has(t.id));\n    list.sort((a,b)=>(a.opc_id||'').localeCompare(b.opc_id||'')||a.id.localeCompare(b.id));\n    document.getElementById('pp-count').textContent='태그: '+list.length+'/'+Object.keys(tags).length;\n    var grid=document.getElementById('pp-grid');\n    if(!list.length){ grid.innerHTML='<div class=\"pp-empty\">'+(Object.keys(tags).length?'필터 결과 없음':'WebSocket 연결 대기 중...')+'</div>'; return; }\n    grid.innerHTML=list.map(t=>{\n      var chg=changedSet.has(t.id);\n      var ok=(t.status==='Good'||t.status==='0'||t.status===0);\n      var val=(t.value===null||t.value===undefined)?'—':t.value;\n      return `<div class=\"pp-card${chg?' updated':''}\" title=\"${t.id}\">\n        ${t.opc_id?`<div class=\"pp-opc-id\">${t.opc_id}</div>`:''}\n        <div class=\"pp-tag-id\">${t.id}</div>\n        <div class=\"pp-val${chg?' changed':''}\">${val}</div>\n        <div class=\"pp-meta\"><span class=\"${ok?'pp-ok':'pp-err'}\">${t.status??''}</span>${t.time?' · '+t.time.slice(-8):''}</div>\n      </div>`;\n    }).join('');\n  }\n\n  function ppToggleDd(id){\n    document.querySelectorAll('.pp-dropdown').forEach(d=>{ if(d.id!==id) d.classList.remove('open'); });\n    document.getElementById(id).classList.toggle('open');\n  }\n  document.addEventListener('click',e=>{ if(!e.target.closest('.pp-filter-group')) document.querySelectorAll('.pp-dropdown').forEach(d=>d.classList.remove('open')); });\n\n  ppConnect();\n  // Expose functions to global scope for onclick attributes\n  window.ppToggleTheme = ppToggleTheme;\n  window.ppToggleDd    = ppToggleDd;\n  window.ppRenderOpc   = ppRenderOpc;\n  window.ppOpcChg      = ppOpcChg;\n  window.ppSelAllOpc   = ppSelAllOpc;\n  window.ppClrOpc      = ppClrOpc;\n  window.ppRenderTag   = ppRenderTag;\n  window.ppTagChg      = ppTagChg;\n  window.ppSelAllTag   = ppSelAllTag;\n  window.ppClrTag      = ppClrTag;\n  window.ppApply       = ppApply;\n  window.ppChips       = ppChips;\n\n  // Sync with Node-RED dashboard dark theme\n  var ppDarkObs = new MutationObserver(function() {\n    var sysDark = document.body.classList.contains('nr-dark-theme');\n    document.getElementById('pp-monitor').classList.toggle('dark', sysDark);\n    document.getElementById('pp-theme-btn').textContent = sysDark ? '☀️' : '🌙';\n  });\n  ppDarkObs.observe(document.body, { attributes: true, attributeFilter: ['class'] });\n})();\n</script>",
    "storeOutMessages": false,
    "resendOnRefresh": true,
    "templateScope": "local",
    "className": "",
    "x": 120,
    "y": 120,
    "wires": [
      []
    ]
  },
  {
    "id": "543ed5643d3782f9",
    "type": "ui_group",
    "name": "리소스",
    "tab": "19b7f41773b6f5d3",
    "order": 1,
    "disp": true,
    "width": "6",
    "collapse": false
  },
  {
    "id": "54d2e65152d90f3c",
    "type": "ui_group",
    "name": "정보",
    "tab": "19b7f41773b6f5d3",
    "order": 2,
    "disp": true,
    "width": "6",
    "collapse": false
  },
  {
    "id": "a68ba43d50233268",
    "type": "ui_group",
    "name": "태그 현재값",
    "tab": "06645bd4e34ae6fb",
    "order": 1,
    "disp": true,
    "width": "12",
    "collapse": false
  },
  {
    "id": "grp_tag_monitor",
    "type": "ui_group",
    "name": "실시간 태그",
    "tab": "tab_tag_monitor",
    "order": 1,
    "disp": false,
    "width": "12",
    "collapse": false
  },
  {
    "id": "19b7f41773b6f5d3",
    "type": "ui_tab",
    "name": "시스템 모니터",
    "icon": "dashboard",
    "disabled": false,
    "hidden": false
  },
  {
    "id": "06645bd4e34ae6fb",
    "type": "ui_tab",
    "name": "OPC-UA 태그",
    "icon": "label",
    "disabled": false,
    "hidden": false
  },
  {
    "id": "tab_tag_monitor",
    "type": "ui_tab",
    "name": "태그 모니터",
    "icon": "dashboard",
    "order": 3,
    "disabled": false,
    "hidden": false
  },
  {
    "id": "2cc7293e4ff5ea15",
    "type": "ui_base",
    "theme": {
      "name": "theme-light",
      "lightTheme": {
        "default": "#0094CE",
        "baseColor": "#0094CE",
        "baseFont": "-apple-system",
        "edited": true
      },
      "dark": false
    },
    "site": {
      "name": "Edge 모니터링",
      "hideToolbar": "false",
      "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
      }
    }
  }
]
