feat: 🛡️ Sentinel 集群监控插件 + API Key 系统 + 框架设置页
== API Key 系统 == - services/web_panel/routes/apikeys.py: CRUD + 权限模板 + 过期控制 - panel_auth + _check_plugin_auth 双重验证 (Session → API Key) - 模板: readonly(只读) / monitor(监控) / full(管理) - 持久化到 SenSuDB + 过期自动清理 == WebUI 框架设置页 == - static/web_panel/pages/settings.html: API Key 管理界面 - 创建对话框 (名称/权限/TTL), 列表表格, 一键删除 == 热重载修复 == - PluginService._reload_plugin: 路由器冻结时跳过 (避免破坏已注册路由) == 🛡️ Sentinel 插件 == - 多节点 SenSu 集群性能监控 - 后台 urllib+run_in_executor 轮询 (避免事件循环死锁) - SSE 实时推送仪表盘 - 节点管理页 (增删改+连接测试+备注) - 路径: /sentinel/api/nodes Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
<div class="plugin-page-root" style="padding:16px">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
|
||||
<h2 style="margin:0">🛡️ Sentinel 集群监控</h2>
|
||||
<span id="sentinel-status" style="font-size:.8rem;color:var(--text-dim)">连接中...</span>
|
||||
</div>
|
||||
<div id="sentinel-grid" style="display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px">
|
||||
<div class="card" style="text-align:center;padding:24px;color:var(--text-dim)">加载中...</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.node-card { background:var(--md-sys-color-surface-container); border-radius:var(--shape-sm); padding:16px; transition:box-shadow .2s }
|
||||
.node-card:hover { box-shadow:0 2px 8px rgba(0,0,0,.2) }
|
||||
.node-card.online { border-left:3px solid var(--success, #4caf50) }
|
||||
.node-card.offline { border-left:3px solid var(--error, #f44336); opacity:.7 }
|
||||
.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 }
|
||||
.metric-row { display:flex; gap:12px; margin-bottom:8px }
|
||||
.metric { flex:1 }
|
||||
.metric-label { font-size:.7rem; color:var(--text-dim); text-transform:uppercase }
|
||||
.metric-value { font-size:1.1rem; font-weight:600 }
|
||||
.metric-bar { height:4px; border-radius:2px; margin-top:2px; background:var(--md-sys-color-surface-container-lowest) }
|
||||
.metric-bar-fill { height:100%; border-radius:2px; transition:width .5s }
|
||||
.bar-green { background:#4caf50 } .bar-yellow { background:#ff9800 } .bar-red { background:#f44336 }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
var sseConn = null;
|
||||
var currentNodes = [];
|
||||
|
||||
function renderNodes(nodes) {
|
||||
currentNodes = nodes || [];
|
||||
var grid = document.getElementById("sentinel-grid");
|
||||
document.getElementById("sentinel-status").textContent =
|
||||
"在线 " + nodes.filter(function(n){return n.online}).length + "/" + nodes.length;
|
||||
|
||||
if (!nodes.length) {
|
||||
grid.innerHTML = '<div class="card" style="text-align:center;padding:24px;color:var(--text-dim)">暂无节点 — 在节点管理中添加</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
grid.innerHTML = nodes.map(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);
|
||||
|
||||
return '<div class="node-card '+cls+'">'+
|
||||
'<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+
|
||||
'<div class="node-url">'+esc(n.url)+'</div>'+
|
||||
(n.notes ? '<div style="font-size:.75rem;color:var(--text-dim);margin-bottom:8px;font-style:italic">📝 '+esc(n.notes)+'</div>' : '')+
|
||||
(n.online ?
|
||||
'<div class="metric-row">'+
|
||||
'<div class="metric"><div class="metric-label">CPU</div><div class="metric-value">'+(cpu.percent||0).toFixed(1)+'%</div><div class="metric-bar"><div class="metric-bar-fill '+cpuBar+'" style="width:'+(cpu.percent||0)+'%"></div></div></div>'+
|
||||
'<div class="metric"><div class="metric-label">MEM</div><div class="metric-value">'+(mem.percent||0).toFixed(1)+'%</div><div class="metric-bar"><div class="metric-bar-fill '+memBar+'" style="width:'+(mem.percent||0)+'%"></div></div></div>'+
|
||||
'</div>'+
|
||||
'<div class="metric-row">'+
|
||||
'<div class="metric"><div class="metric-label">NET ↓</div><div class="metric-value" style="font-size:.9rem">'+fmtSpeed(net.rx_bytes_sec||0)+'</div></div>'+
|
||||
'<div class="metric"><div class="metric-label">NET ↑</div><div class="metric-value" style="font-size:.9rem">'+fmtSpeed(net.tx_bytes_sec||0)+'</div></div>'+
|
||||
'</div>'+
|
||||
'<div style="font-size:.7rem;color:var(--text-dim);margin-top:8px">内存 '+(mem.used_gb||0).toFixed(1)+'/'+(mem.total_gb||0).toFixed(1)+' GB</div>'
|
||||
: '<div style="color:var(--error);font-size:.85rem">'+(n.error||'离线')+'</div>')+
|
||||
'<div style="font-size:.7rem;color:var(--text-dim);margin-top:8px">'+(n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—')+'</div>'+
|
||||
'</div>';
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function bar(v) { return v < 60 ? 'bar-green' : (v < 85 ? 'bar-yellow' : 'bar-red'); }
|
||||
function fmtSpeed(bps) { return bps<1024?bps.toFixed(0)+'B/s':(bps<1048576?(bps/1024).toFixed(0)+'K/s':(bps/1048576).toFixed(1)+'M/s'); }
|
||||
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }
|
||||
|
||||
// ── SSE ──
|
||||
function connectSSE() {
|
||||
if (sseConn) { sseConn.close(); sseConn = null; }
|
||||
sseConn = new EventSource("/SenSu/plugin/sentinel/sse");
|
||||
sseConn.addEventListener("nodes_update", function(e){
|
||||
var d = JSON.parse(e.data);
|
||||
if (d.nodes) renderNodes(d.nodes);
|
||||
});
|
||||
sseConn.onerror = function(){ setTimeout(connectSSE, 5000); };
|
||||
}
|
||||
|
||||
// ── 初始加载 ──
|
||||
fetch("/sentinel/api/nodes").then(function(r){return r.json()}).then(function(d){
|
||||
if (d.nodes) renderNodes(d.nodes);
|
||||
}).catch(function(){});
|
||||
connectSSE();
|
||||
</script>
|
||||
</div>
|
||||
Reference in New Issue
Block a user