|
|
@@ -41,19 +41,22 @@
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
<style>
|
|
|
|
.node-card { background:var(--md-sys-color-surface-container); border-radius:var(--shape-sm); padding:16px; transition:box-shadow .2s }
|
|
|
|
.node-card { background:var(--md-sys-color-surface-container); border-radius:var(--shape-sm); padding:16px; transition:box-shadow .3s, background .3s }
|
|
|
|
.node-card:hover { box-shadow:0 2px 8px rgba(0,0,0,.2) }
|
|
|
|
.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.online { border-left:3px solid var(--success, #4caf50) }
|
|
|
|
.node-card.offline { border-left:3px solid var(--error, #f44336); opacity:.7 }
|
|
|
|
.node-card.offline { border-left:3px solid var(--error, #f44336); opacity:.7 }
|
|
|
|
|
|
|
|
.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-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:12px; word-break:break-all }
|
|
|
|
.metric-row { display:flex; gap:12px; margin-bottom:8px }
|
|
|
|
.metric-row { display:flex; gap:12px; margin-bottom:8px }
|
|
|
|
.metric { flex:1 }
|
|
|
|
.metric { flex:1 }
|
|
|
|
.metric-label { font-size:.7rem; color:var(--text-dim); text-transform:uppercase }
|
|
|
|
.metric-label { font-size:.7rem; color:var(--text-dim); text-transform:uppercase }
|
|
|
|
.metric-value { font-size:1.1rem; font-weight:600 }
|
|
|
|
.metric-value { font-size:1.1rem; font-weight:600; transition:color .4s }
|
|
|
|
.metric-bar { height:4px; border-radius:2px; margin-top:2px; background:var(--md-sys-color-surface-container-lowest) }
|
|
|
|
.metric-bar { height:4px; border-radius:2px; margin-top:2px; background:var(--md-sys-color-surface-container-lowest); overflow:hidden }
|
|
|
|
.metric-bar-fill { height:100%; border-radius:2px; transition:width .5s }
|
|
|
|
.metric-bar-fill { height:100%; border-radius:2px; transition:width .6s cubic-bezier(.4,0,.2,1) }
|
|
|
|
.bar-green { background:#4caf50 } .bar-yellow { background:#ff9800 } .bar-red { background:#f44336 }
|
|
|
|
.bar-green { background:linear-gradient(90deg,#66bb6a,#4caf50) } .bar-yellow { background:linear-gradient(90deg,#ffa726,#ff9800) } .bar-red { background:linear-gradient(90deg,#ef5350,#f44336) }
|
|
|
|
|
|
|
|
.metric-value.up { color:#4caf50 } .metric-value.down { color:#f44336 } .metric-value.steady { color:var(--text) }
|
|
|
|
.form-row { margin-bottom:10px }
|
|
|
|
.form-row { margin-bottom:10px }
|
|
|
|
.form-row label { display:block;font-size:.8rem;margin-bottom:3px;color:var(--text-dim) }
|
|
|
|
.form-row label { display:block;font-size:.8rem;margin-bottom:3px;color:var(--text-dim) }
|
|
|
|
.form-row input, .form-row textarea { width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs);font-size:.9rem }
|
|
|
|
.form-row input, .form-row textarea { width:100%;padding:8px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:var(--shape-xs);font-size:.9rem }
|
|
|
@@ -78,7 +81,10 @@
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ═══ 仪表盘 ═══
|
|
|
|
// ═══ 仪表盘 ═══
|
|
|
|
|
|
|
|
var _prevValues = {};
|
|
|
|
|
|
|
|
|
|
|
|
function renderNodes(nodes) {
|
|
|
|
function renderNodes(nodes) {
|
|
|
|
|
|
|
|
var now = Date.now();
|
|
|
|
var grid = document.getElementById("sentinel-grid");
|
|
|
|
var grid = document.getElementById("sentinel-grid");
|
|
|
|
if (!nodes.length) {
|
|
|
|
if (!nodes.length) {
|
|
|
|
grid.innerHTML = '<div class="card" style="text-align:center;padding:24px;color:var(--text-dim)">暂无节点 — 切换到节点管理添加</div>';
|
|
|
|
grid.innerHTML = '<div class="card" style="text-align:center;padding:24px;color:var(--text-dim)">暂无节点 — 切换到节点管理添加</div>';
|
|
|
@@ -92,14 +98,20 @@
|
|
|
|
var cls = n.online ? 'online' : 'offline';
|
|
|
|
var cls = n.online ? 'online' : 'offline';
|
|
|
|
var cpuBar = bar(cpu.percent||0);
|
|
|
|
var cpuBar = bar(cpu.percent||0);
|
|
|
|
var memBar = bar(mem.percent||0);
|
|
|
|
var memBar = bar(mem.percent||0);
|
|
|
|
return '<div class="node-card '+cls+'">'+
|
|
|
|
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 '<div class="node-card '+cls+flash+'" data-id="'+esc(id)+'">'+
|
|
|
|
'<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+
|
|
|
|
'<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+
|
|
|
|
'<div class="node-url">'+esc(n.url)+'</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.notes ? '<div style="font-size:.75rem;color:var(--text-dim);margin-bottom:8px;font-style:italic">📝 '+esc(n.notes)+'</div>' : '')+
|
|
|
|
(n.online ?
|
|
|
|
(n.online ?
|
|
|
|
'<div class="metric-row">'+
|
|
|
|
'<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">CPU</div><div class="metric-value '+cpuTrend+'">'+(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 class="metric"><div class="metric-label">MEM</div><div class="metric-value '+memTrend+'">'+(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>'+
|
|
|
|
'<div class="metric-row">'+
|
|
|
|
'<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.rx_bytes_sec||0)+'</div></div>'+
|
|
|
@@ -110,6 +122,10 @@
|
|
|
|
'<div style="font-size:.7rem;color:var(--text-dim);margin-top:8px">'+(n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—')+'</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>';
|
|
|
|
'</div>';
|
|
|
|
}).join("");
|
|
|
|
}).join("");
|
|
|
|
|
|
|
|
// 清除 flash 动画
|
|
|
|
|
|
|
|
setTimeout(function(){
|
|
|
|
|
|
|
|
document.querySelectorAll('.node-card.flash').forEach(function(el){ el.classList.remove('flash'); });
|
|
|
|
|
|
|
|
}, 700);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function bar(v) { return v < 60 ? 'bar-green' : (v < 85 ? 'bar-yellow' : 'bar-red'); }
|
|
|
|
function bar(v) { return v < 60 ? 'bar-green' : (v < 85 ? 'bar-yellow' : 'bar-red'); }
|
|
|
|