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'); }