diff --git a/config/permissions/pending_requests.json b/config/permissions/pending_requests.json index 1ec6494..63b536a 100644 --- a/config/permissions/pending_requests.json +++ b/config/permissions/pending_requests.json @@ -218,5 +218,27 @@ "framework.command.execute" ], "timestamp": 27154.605505473 + }, + "8408596b": { + "plugin_name": "example_plugin", + "permissions": [ + "plugin.example.read", + "plugin.example.write", + "plugin.example.execute", + "framework.event.subscribe", + "framework.command.execute" + ], + "timestamp": 27252.869951113 + }, + "09adcbfe": { + "plugin_name": "sentinel", + "permissions": [ + "plugin.sentinel.read", + "plugin.sentinel.write", + "plugin.network.access", + "framework.event.subscribe", + "framework.command.execute" + ], + "timestamp": 27252.872485748 } } \ No newline at end of file diff --git a/config/plugins/commands.yaml b/config/plugins/commands.yaml index 87bdced..36a9695 100644 --- a/config/plugins/commands.yaml +++ b/config/plugins/commands.yaml @@ -101,7 +101,7 @@ commands: permissions: - framework.command.test source: internal -last_updated: 27154.562528494 +last_updated: 27252.853477519 plugin_commands: example_plugin: echo: *id001 diff --git a/config/services/network_routes.yaml b/config/services/network_routes.yaml index 72b57a8..795e421 100644 --- a/config/services/network_routes.yaml +++ b/config/services/network_routes.yaml @@ -1,5 +1,5 @@ http_port: 4200 -last_updated: 27154.60330813 +last_updated: 27252.870587988 plugin_routes: example_plugin: - methods: diff --git a/plugins/sentinel/config.yaml b/plugins/sentinel/config.yaml index d18c756..b7b4ec8 100644 --- a/plugins/sentinel/config.yaml +++ b/plugins/sentinel/config.yaml @@ -2,11 +2,11 @@ author: SenSu Team description: 🛡️ 多节点 SenSu 集群性能监控 — 实时仪表盘 + 节点管理 name: Sentinel nodes: -- api_key: sk-e77716bb9be5ab0d42a3154d7bacee7e555bfba1b828eb09 +- api_key: sk-035259bb700b17ae51cfd50a717b27281b1a5347b2aa4723 enabled: true id: node-3733a070 name: 本地容器 - notes: 骁龙8E Gen5 + notes: LocalBian url: http://127.0.0.1:4200 settings: auto_start: true diff --git a/plugins/sentinel/dashboard.html b/plugins/sentinel/dashboard.html index 89f65a1..dee1004 100644 --- a/plugins/sentinel/dashboard.html +++ b/plugins/sentinel/dashboard.html @@ -48,7 +48,8 @@ .node-card.flash { animation:card-flash .6s ease-out } @keyframes card-flash { 0%{background:var(--md-sys-color-surface-container-highest)} 100%{background:var(--md-sys-color-surface-container)} } .node-name { font-weight:600; font-size:1rem; margin-bottom:4px } - .node-url { font-size:.75rem; color:var(--text-dim); margin-bottom:12px; word-break:break-all } + .node-url { font-size:.75rem; color:var(--text-dim); margin-bottom:8px; word-break:break-all } + .node-notes { font-size:.75rem; color:var(--text-dim); font-style:italic; margin-bottom:8px } .metric-row { display:flex; gap:12px; margin-bottom:8px } .metric { flex:1 } .metric-label { font-size:.7rem; color:var(--text-dim); text-transform:uppercase } @@ -81,51 +82,109 @@ } // ═══ 仪表盘 ═══ - var _prevValues = {}; + var _cardCache = {}; + + function animateValue(el, oldVal, newVal, decimals, suffix) { + if (oldVal === newVal) { el.textContent = newVal.toFixed(decimals) + suffix; return; } + var start = performance.now(); + var duration = 350; + function step(ts) { + var progress = Math.min((ts - start) / duration, 1); + // ease-out + var eased = 1 - Math.pow(1 - progress, 3); + var current = oldVal + (newVal - oldVal) * eased; + el.textContent = current.toFixed(decimals) + suffix; + if (progress < 1) requestAnimationFrame(step); + } + requestAnimationFrame(step); + } function renderNodes(nodes) { - var now = Date.now(); var grid = document.getElementById("sentinel-grid"); if (!nodes.length) { grid.innerHTML = '
暂无节点 — 切换到节点管理添加
'; + _cardCache = {}; return; } - grid.innerHTML = nodes.map(function(n){ + // 清理已删除的节点 + var activeIds = {}; + nodes.forEach(function(n){ activeIds[n.id||n.name] = true; }); + Object.keys(_cardCache).forEach(function(id){ + if (!activeIds[id]) { var el = _cardCache[id]; if(el.parentNode) el.parentNode.removeChild(el); delete _cardCache[id]; } + }); + + nodes.forEach(function(n){ var sys = n.system || {}; var cpu = sys.cpu || {}; var mem = sys.memory || {}; var net = sys.net_speed || {}; - var cls = n.online ? 'online' : 'offline'; - var cpuBar = bar(cpu.percent||0); - var memBar = bar(mem.percent||0); var id = n.id || n.name; - var prev = _prevValues[id] || {}; - var cpuTrend = prev.cpu !== undefined ? (cpu.percent > prev.cpu ? 'up' : (cpu.percent < prev.cpu ? 'down' : 'steady')) : 'steady'; - var memTrend = prev.mem !== undefined ? (mem.percent > prev.mem ? 'up' : (mem.percent < prev.mem ? 'down' : 'steady')) : 'steady'; - _prevValues[id] = {cpu: cpu.percent||0, mem: mem.percent||0, ts: now}; - var flash = prev.ts && (now - prev.ts > 1500) ? ' flash' : ''; - return '
'+ - '
'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'
'+ - '
'+esc(n.url)+'
'+ - (n.notes ? '
📝 '+esc(n.notes)+'
' : '')+ - (n.online ? + var cpuVal = cpu.percent || 0; + var memVal = mem.percent || 0; + var cpuBarCls = bar(cpuVal); + var memBarCls = bar(memVal); + + var card = _cardCache[id]; + if (!card) { + // 首次创建卡片 + card = document.createElement('div'); + card.className = 'node-card ' + (n.online ? 'online' : 'offline'); + card.setAttribute('data-id', id); + card.innerHTML = + '
'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'
'+ + '
'+esc(n.url||'')+'
'+ + (n.notes ? '
📝 '+esc(n.notes)+'
' : '')+ + '
'+ + '
'; + grid.appendChild(card); + _cardCache[id] = card; + } + + // 更新状态样式 + card.className = 'node-card ' + (n.online ? 'online' : 'offline'); + card.querySelector('.node-name').innerHTML = (n.online?'🟢':'🔴')+' '+esc(n.name); + card.querySelector('.node-url').textContent = n.url||''; + var notesEl = card.querySelector('.node-notes'); + if (n.notes) { + if (!notesEl) { + notesEl = document.createElement('div'); notesEl.className = 'node-notes'; + card.insertBefore(notesEl, card.querySelector('.metrics-body')); + } + notesEl.innerHTML = '📝 '+esc(n.notes); + } else if (notesEl) { notesEl.remove(); } + + var body = card.querySelector('.metrics-body'); + if (n.online) { + var oldCpu = parseFloat((body.getAttribute('data-cpu')||cpuVal)); + var oldMem = parseFloat((body.getAttribute('data-mem')||memVal)); + body.setAttribute('data-cpu', cpuVal); + body.setAttribute('data-mem', memVal); + body.innerHTML = '
'+ - '
CPU
'+(cpu.percent||0).toFixed(1)+'%
'+ - '
MEM
'+(mem.percent||0).toFixed(1)+'%
'+ + '
CPU
'+oldCpu.toFixed(1)+'%
'+ + '
MEM
'+oldMem.toFixed(1)+'%
'+ '
'+ '
'+ '
NET ↓
'+fmtSpeed(net.rx_bytes_sec||0)+'
'+ '
NET ↑
'+fmtSpeed(net.tx_bytes_sec||0)+'
'+ '
'+ - '
内存 '+(mem.used_gb||0).toFixed(1)+'/'+(mem.total_gb||0).toFixed(1)+' GB
' - : '
'+(n.error||'离线')+'
')+ - '
'+(n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—')+'
'+ - '
'; - }).join(""); - // 清除 flash 动画 - setTimeout(function(){ - document.querySelectorAll('.node-card.flash').forEach(function(el){ el.classList.remove('flash'); }); - }, 700); + '
内存 '+(mem.used_gb||0).toFixed(1)+'/'+(mem.total_gb||0).toFixed(1)+' GB
'; + // 动画计数 + var cpuEl = body.querySelector('.cpu-val'); + var memEl = body.querySelector('.mem-val'); + animateValue(cpuEl, oldCpu, cpuVal, 1, '%'); + animateValue(memEl, oldMem, memVal, 1, '%'); + // 进度条平滑过渡 (CSS transition 自动处理) + requestAnimationFrame(function(){ + var fills = body.querySelectorAll('.metric-bar-fill'); + if (fills[0]) { fills[0].style.width = cpuVal+'%'; fills[0].className = 'metric-bar-fill '+cpuBarCls; } + if (fills[1]) { fills[1].style.width = memVal+'%'; fills[1].className = 'metric-bar-fill '+memBarCls; } + }); + } else { + body.innerHTML = '
'+(n.error||'离线')+'
'; + } + card.querySelector('.node-time').innerHTML = '
'+(n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—')+'
'; + }); } function bar(v) { return v < 60 ? 'bar-green' : (v < 85 ? 'bar-yellow' : 'bar-red'); }