420 lines
24 KiB
HTML
420 lines
24 KiB
HTML
<div class="plugin-page-root" style="padding:16px">
|
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
|
|
<h2 style="margin:0">🛡️ Sentinel 集群监控</h2>
|
|
<div style="display:flex;gap:4px;align-items:center">
|
|
<button class="btn btn-sm btn-filled" id="tab-mon" onclick="switchTab('mon')">仪表盘</button>
|
|
<button class="btn btn-sm btn-outlined" id="tab-cfg" onclick="switchTab('cfg')">节点管理</button>
|
|
<span style="color:var(--border);margin:0 4px">|</span>
|
|
<button class="btn btn-sm btn-outlined" id="btn-layout" onclick="toggleLayout()" title="切换卡片布局">☷ 横版</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ── 仪表盘 ── -->
|
|
<div id="view-mon">
|
|
<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>
|
|
</div>
|
|
|
|
<!-- ── 节点管理 ── -->
|
|
<div id="view-cfg" style="display:none">
|
|
<div style="margin-bottom:12px">
|
|
<button class="btn btn-filled" onclick="showAdd()">+ 添加节点</button>
|
|
</div>
|
|
<div id="node-list"></div>
|
|
</div>
|
|
|
|
<!-- ── 编辑对话框 ── -->
|
|
<div id="edit-dialog" style="display:none;position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);z-index:1000;align-items:center;justify-content:center">
|
|
<div class="card" style="min-width:440px;max-width:520px;max-height:90vh;overflow-y:auto">
|
|
<h3 id="edit-title" style="margin:0 0 16px 0">添加节点</h3>
|
|
<input type="hidden" id="edit-id">
|
|
<div class="form-row"><label>名称 *</label><input id="edit-name" placeholder="例如: OnePlus 15"></div>
|
|
<div class="form-row"><label>URL *</label><input id="edit-url" placeholder="http://192.168.1.100:4200"></div>
|
|
<div class="form-row"><label>API Key</label><input id="edit-key" placeholder="sk-..."></div>
|
|
<div class="form-row"><label>API 路径前缀</label><input id="edit-base" placeholder="/SenSu" value="/SenSu"></div>
|
|
<div class="form-row"><label>备注</label><textarea id="edit-notes" rows="2" placeholder="设备位置、用途等..."></textarea></div>
|
|
<div style="display:flex;gap:8px;justify-content:flex-end;margin-top:16px">
|
|
<button class="btn btn-outlined" onclick="testConn()">测试连接</button>
|
|
<button class="btn btn-outlined" onclick="hideEdit()">取消</button>
|
|
<button class="btn btn-filled" onclick="saveNode()">保存</button>
|
|
</div>
|
|
<div id="test-result" style="margin-top:8px;font-size:.85rem"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.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.online { border-left:3px solid var(--success, #4caf50) }
|
|
.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:2px }
|
|
.node-sys { font-size:.75rem; color:var(--text-dim); margin-bottom:4px }
|
|
.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 }
|
|
/* 横版布局 — 左中右三列 */
|
|
#sentinel-grid.layout-row { display:flex;flex-direction:column;gap:12px }
|
|
#sentinel-grid.layout-row .node-card { display:grid;grid-template-columns:220px 1fr 180px;gap:12px 16px;align-items:start }
|
|
#sentinel-grid.layout-row .col-left { min-width:0 }
|
|
#sentinel-grid.layout-row .col-mid { min-width:0 }
|
|
#sentinel-grid.layout-row .col-right { display:flex;flex-direction:column;gap:8px }
|
|
#sentinel-grid.layout-row .col-chart { display:flex;flex-direction:column;gap:4px }
|
|
#sentinel-grid.layout-row .col-chart canvas { border-radius:4px;background:var(--md-sys-color-surface-container-lowest) }
|
|
#sentinel-grid.layout-row .col-info { text-align:right }
|
|
#sentinel-grid.layout-row .node-time { grid-column:1/-1;margin-top:8px }
|
|
#sentinel-grid.layout-row .metric-bar { display:none }
|
|
.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; transition:color .4s }
|
|
.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 .6s cubic-bezier(.4,0,.2,1) }
|
|
.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 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:focus, .form-row textarea:focus { outline:1px solid var(--primary) }
|
|
.node-item { display:flex;align-items:center;padding:10px 12px;margin-bottom:6px;background:var(--md-sys-color-surface-container);border-radius:var(--shape-sm);gap:12px }
|
|
.node-item .info { flex:1;min-width:0 }
|
|
.node-item .name { font-weight:600 }
|
|
.node-item .url { font-size:.75rem;color:var(--text-dim);word-break:break-all }
|
|
.node-item .notes { font-size:.75rem;color:var(--text-dim);font-style:italic }
|
|
</style>
|
|
|
|
<script>
|
|
// 确保 chart.js 已加载 (SPA 导航时可能未加载)
|
|
|
|
var pollTimer = null;
|
|
var _chartInstances = {};
|
|
var editingId = null;
|
|
var _layoutMode = 'grid'; // 'grid' | 'row'
|
|
|
|
function toggleLayout() {
|
|
_layoutMode = _layoutMode === 'grid' ? 'row' : 'grid';
|
|
var btn = document.getElementById("btn-layout");
|
|
var grid = document.getElementById("sentinel-grid");
|
|
// 销毁旧图表
|
|
try { Object.keys(_chartInstances).forEach(function(k){ if(_chartInstances[k]) _chartInstances[k].forEach(function(c){ if(c.destroy)c.destroy(); }); }); } catch(e) {}
|
|
_chartInstances = {};
|
|
_cardCache = {};
|
|
_firstRender = true;
|
|
grid.innerHTML = '<div class="card" style="text-align:center;padding:24px;color:var(--text-dim)">加载中...</div>';
|
|
if (_layoutMode === 'row') {
|
|
grid.className = 'layout-row';
|
|
grid.style.cssText = 'display:flex;flex-direction:column;gap:12px';
|
|
btn.textContent = '⊞ 网格';
|
|
btn.className = 'btn btn-sm btn-filled';
|
|
} else {
|
|
grid.className = '';
|
|
grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px';
|
|
btn.textContent = '☷ 横版';
|
|
btn.className = 'btn btn-sm btn-outlined';
|
|
}
|
|
pollNodes();
|
|
}
|
|
|
|
function switchTab(t) {
|
|
document.getElementById("view-mon").style.display = t === "mon" ? "block" : "none";
|
|
document.getElementById("view-cfg").style.display = t === "cfg" ? "block" : "none";
|
|
document.getElementById("tab-mon").className = "btn btn-sm " + (t === "mon" ? "btn-filled" : "btn-outlined");
|
|
document.getElementById("tab-cfg").className = "btn btn-sm " + (t === "cfg" ? "btn-filled" : "btn-outlined");
|
|
if (t === "cfg") loadNodes();
|
|
}
|
|
|
|
// ═══ 仪表盘 ═══
|
|
|
|
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 = 1000;
|
|
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);
|
|
}
|
|
|
|
var _firstRender = true;
|
|
|
|
function renderNodes(nodes) {
|
|
var grid = document.getElementById("sentinel-grid");
|
|
if (!nodes.length) {
|
|
grid.innerHTML = '<div class="card" style="text-align:center;padding:24px;color:var(--text-dim)">暂无节点 — 切换到节点管理添加</div>';
|
|
try { Object.keys(_chartInstances).forEach(function(k){ if(_chartInstances[k]) _chartInstances[k].forEach(function(c){ if(c.destroy)c.destroy(); }); }); } catch(e) {}
|
|
_chartInstances = {};
|
|
_cardCache = {};
|
|
_firstRender = true;
|
|
return;
|
|
}
|
|
if (_firstRender) { grid.innerHTML = ''; _firstRender = false; }
|
|
var isRow = _layoutMode === 'row';
|
|
|
|
// 清理删除的节点
|
|
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 cpuVal = cpu.percent || 0;
|
|
var mem = sys.memory || {}; var memVal = mem.percent || 0;
|
|
var net = sys.net_speed || {};
|
|
var id = n.id || n.name;
|
|
|
|
var card = _cardCache[id];
|
|
if (!card) {
|
|
card = document.createElement('div');
|
|
card.className = 'node-card ' + (n.online ? 'online' : 'offline');
|
|
card.setAttribute('data-id', id);
|
|
var plat = n.platform || {};
|
|
var sysInfo = (plat.system||'') + (plat.release ? ' ' + plat.release : '') + (plat.machine ? ' | ' + plat.machine : '');
|
|
if (isRow) {
|
|
card.innerHTML =
|
|
'<div class="col-left">'+
|
|
'<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+
|
|
(sysInfo.trim() ? '<div class="node-sys">'+esc(sysInfo)+'</div>' : '')+
|
|
'<div class="node-url">'+esc(n.url||'')+'</div>'+
|
|
(n.notes ? '<div class="node-notes">📝 '+esc(n.notes)+'</div>' : '')+
|
|
'</div>'+
|
|
'<div class="col-mid"><div class="metrics-body"></div></div>'+
|
|
'<div class="col-right">'+
|
|
'<div class="col-chart">'+
|
|
'<canvas id="'+id+'-cpu" width="160" height="50"></canvas>'+
|
|
'<canvas id="'+id+'-mem" width="160" height="50"></canvas>'+
|
|
'<canvas id="'+id+'-net" width="160" height="50"></canvas>'+
|
|
'</div>'+
|
|
'<div class="col-info"></div>'+
|
|
'</div>'+
|
|
'<div class="node-time"></div>';
|
|
} else {
|
|
card.innerHTML =
|
|
'<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+
|
|
(sysInfo.trim() ? '<div class="node-sys">'+esc(sysInfo)+'</div>' : '')+
|
|
'<div class="node-url">'+esc(n.url||'')+'</div>'+
|
|
(n.notes ? '<div class="node-notes">📝 '+esc(n.notes)+'</div>' : '')+
|
|
'<div class="metrics-body"></div>'+
|
|
'<div class="node-time"></div>';
|
|
}
|
|
grid.appendChild(card);
|
|
_cardCache[id] = card;
|
|
if (isRow && typeof MiniChart !== 'undefined' && !_chartInstances[id]) {
|
|
try {
|
|
_chartInstances[id] = [
|
|
new MiniChart(id+'-cpu', '#7aa2f7'),
|
|
new MiniChart(id+'-mem', '#9ece6a'),
|
|
new MiniChart(id+'-net', '#e0af68'),
|
|
];
|
|
} catch(e) {}
|
|
}
|
|
}
|
|
|
|
// 更新通用信息
|
|
card.className = 'node-card ' + (n.online ? 'online' : 'offline');
|
|
var nameEl = card.querySelector('.node-name');
|
|
if (nameEl) nameEl.innerHTML = (n.online?'🟢':'🔴')+' '+esc(n.name);
|
|
var urlEl = card.querySelector('.node-url');
|
|
if (urlEl) urlEl.textContent = n.url||'';
|
|
var sysEl = card.querySelector('.node-sys');
|
|
var plat2 = n.platform || {};
|
|
var si2 = (plat2.system||'') + (plat2.release ? ' ' + plat2.release : '') + (plat2.machine ? ' | ' + plat2.machine : '');
|
|
if (si2.trim()) {
|
|
if (!sysEl) { sysEl = document.createElement('div'); sysEl.className = 'node-sys'; card.insertBefore(sysEl, card.querySelector('.metrics-body')||card.querySelector('.card-mid')); }
|
|
sysEl.textContent = si2;
|
|
} else if (sysEl) { sysEl.remove(); }
|
|
|
|
var body = card.querySelector('.metrics-body');
|
|
if (!body) return;
|
|
|
|
var oldCpu = parseFloat(body.getAttribute('data-cpu')||(sys.cpu||{}).percent||0);
|
|
var oldMem = parseFloat(body.getAttribute('data-mem')||(sys.memory||{}).percent||0);
|
|
body.setAttribute('data-cpu', (sys.cpu||{}).percent||0);
|
|
body.setAttribute('data-mem', (sys.memory||{}).percent||0);
|
|
|
|
if (isRow) {
|
|
// 初始化/更新中间栏的数值
|
|
var isNewRow = !body.querySelector('.cpu-val');
|
|
if (isNewRow) {
|
|
body.innerHTML =
|
|
'<div class="metric-row">'+
|
|
'<div class="metric"><div class="metric-label">CPU</div><div class="metric-value cpu-val" style="font-size:1.3rem">0%</div></div>'+
|
|
'<div class="metric"><div class="metric-label">MEM</div><div class="metric-value mem-val" style="font-size:1.3rem">0%</div></div>'+
|
|
'<div class="metric"><div class="mem-row-detail" style="font-size:.7rem;color:var(--text-dim)"></div></div>'+
|
|
'</div>'+
|
|
'<div class="metric-row">'+
|
|
'<div class="metric"><div class="metric-label">NET ↓</div><div class="metric-value net-down" style="font-size:1rem"></div></div>'+
|
|
'<div class="metric"><div class="metric-label">NET ↑</div><div class="metric-value net-up" style="font-size:1rem"></div></div>'+
|
|
'</div>';
|
|
}
|
|
body.querySelector('.net-down').textContent = fmtSpeed(net.rx_bytes_sec||0);
|
|
body.querySelector('.net-up').textContent = fmtSpeed(net.tx_bytes_sec||0);
|
|
var mrd = body.querySelector('.mem-row-detail');
|
|
if (mrd) mrd.textContent = (mem.used_gb||0).toFixed(1)+'/'+(mem.total_gb||0).toFixed(1)+' GB';
|
|
animateValue(body.querySelector('.cpu-val'), oldCpu, cpuVal, 1, '%');
|
|
animateValue(body.querySelector('.mem-val'), oldMem, memVal, 1, '%');
|
|
// 图表更新
|
|
var charts = _chartInstances[id];
|
|
if (charts && charts[0]) {
|
|
try { charts[0].update(cpuVal); charts[1].update(memVal); charts[2].update(net.rx_bytes_sec||0); } catch(e) {}
|
|
}
|
|
var infoR = card.querySelector('.col-info');
|
|
if (infoR) infoR.innerHTML =
|
|
'<div style="font-size:1.1rem;font-weight:600">CPU '+(cpuVal).toFixed(1)+'%</div>'+
|
|
'<div style="font-size:1.1rem;font-weight:600;margin-top:4px">MEM '+(memVal).toFixed(1)+'%</div>'+
|
|
'<div style="font-size:.7rem;color:var(--text-dim);margin-top:4px">'+(mem.used_gb||0).toFixed(1)+'/'+(mem.total_gb||0).toFixed(1)+' GB</div>'+
|
|
'<div style="font-size:.75rem;color:var(--text-dim);margin-top:8px">↓ '+fmtSpeed(net.rx_bytes_sec||0)+'</div>'+
|
|
'<div style="font-size:.75rem;color:var(--text-dim)">↑ '+fmtSpeed(net.tx_bytes_sec||0)+'</div>';
|
|
card.querySelector('.node-time').innerHTML = '<div style="font-size:.7rem;color:var(--text-dim);margin-top:8px">'+(n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—')+'</div>';
|
|
} else {
|
|
// 网格模式
|
|
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);
|
|
var isNew = !body.querySelector('.cpu-val');
|
|
if (isNew) {
|
|
body.innerHTML =
|
|
'<div class="metric-row">'+
|
|
'<div class="metric"><div class="metric-label">CPU</div><div class="metric-value cpu-val">0%</div><div class="metric-bar"><div class="metric-bar-fill" style="width:0%"></div></div></div>'+
|
|
'<div class="metric"><div style="display:flex;justify-content:space-between;align-items:baseline"><div class="metric-label">MEM</div><div class="mem-detail-val" style="font-size:.65rem;color:var(--text-dim)"></div></div><div class="metric-value mem-val">0%</div><div class="metric-bar"><div class="metric-bar-fill" style="width:0%"></div></div></div>'+
|
|
'</div>'+
|
|
'<div class="metric-row">'+
|
|
'<div class="metric"><div class="metric-label">NET ↓</div><div class="metric-value net-down" style="font-size:.9rem"></div></div>'+
|
|
'<div class="metric"><div class="metric-label">NET ↑</div><div class="metric-value net-up" style="font-size:.9rem"></div></div>'+
|
|
'</div>';
|
|
}
|
|
body.querySelector('.net-down').textContent = fmtSpeed(net.rx_bytes_sec||0);
|
|
body.querySelector('.net-up').textContent = fmtSpeed(net.tx_bytes_sec||0);
|
|
var md = body.querySelector('.mem-detail-val');
|
|
if (md) md.textContent = (mem.used_gb||0).toFixed(1) + '/' + (mem.total_gb||0).toFixed(1) + ' GB';
|
|
var barCpu = body.querySelectorAll('.metric-bar-fill')[0];
|
|
var barMem = body.querySelectorAll('.metric-bar-fill')[1];
|
|
if (barCpu && barMem) {
|
|
var cpuBarCls = bar(cpuVal), memBarCls = bar(memVal);
|
|
barCpu.style.transition = 'none'; barMem.style.transition = 'none';
|
|
barCpu.style.width = oldCpu+'%'; barMem.style.width = oldMem+'%';
|
|
barCpu.className = 'metric-bar-fill '+cpuBarCls; barMem.className = 'metric-bar-fill '+memBarCls;
|
|
barCpu.offsetWidth; barMem.offsetWidth;
|
|
barCpu.style.transition = ''; barMem.style.transition = '';
|
|
barCpu.style.width = cpuVal+'%'; barMem.style.width = memVal+'%';
|
|
}
|
|
animateValue(body.querySelector('.cpu-val'), oldCpu, cpuVal, 1, '%');
|
|
animateValue(body.querySelector('.mem-val'), oldMem, memVal, 1, '%');
|
|
card.querySelector('.node-time').innerHTML = '<div style="font-size:.7rem;color:var(--text-dim);margin-top:8px">'+(n.last_seen ? new Date(n.last_seen*1000).toLocaleTimeString() : '—')+'</div>';
|
|
}
|
|
});
|
|
}
|
|
|
|
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,'>').replace(/"/g,'"'); }
|
|
|
|
function pollNodes() {
|
|
fetch("/sentinel/api/nodes").then(function(r){return r.json()}).then(function(d){
|
|
renderNodes(d.nodes || []);
|
|
}).catch(function(e){
|
|
var grid = document.getElementById("sentinel-grid");
|
|
if (grid && _firstRender) {
|
|
grid.innerHTML = '<div class="card" style="text-align:center;padding:24px;color:var(--error)">连接失败 — 检查节点配置</div>';
|
|
}
|
|
});
|
|
}
|
|
|
|
pollNodes();
|
|
pollTimer = setInterval(pollNodes, 2000);
|
|
|
|
// ═══ 节点管理 ═══
|
|
function showAdd(){
|
|
editingId = null;
|
|
document.getElementById("edit-title").textContent = "添加节点";
|
|
document.getElementById("edit-id").value = "";
|
|
document.getElementById("edit-name").value = "";
|
|
document.getElementById("edit-url").value = "http://";
|
|
document.getElementById("edit-key").value = "";
|
|
document.getElementById("edit-base").value = "/SenSu";
|
|
document.getElementById("edit-notes").value = "";
|
|
document.getElementById("test-result").innerHTML = "";
|
|
document.getElementById("edit-dialog").style.display = "flex";
|
|
}
|
|
|
|
function showEdit(n){
|
|
editingId = n.id;
|
|
document.getElementById("edit-title").textContent = "编辑: " + esc(n.name);
|
|
document.getElementById("edit-id").value = n.id||"";
|
|
document.getElementById("edit-name").value = n.name||"";
|
|
document.getElementById("edit-url").value = n.url||"";
|
|
document.getElementById("edit-key").value = n.api_key||"";
|
|
document.getElementById("edit-base").value = n.base_path||"/SenSu";
|
|
document.getElementById("edit-notes").value = n.notes||"";
|
|
document.getElementById("test-result").innerHTML = "";
|
|
document.getElementById("edit-dialog").style.display = "flex";
|
|
}
|
|
|
|
function hideEdit(){ document.getElementById("edit-dialog").style.display = "none"; }
|
|
|
|
async function saveNode(){
|
|
var data = {
|
|
id: document.getElementById("edit-id").value,
|
|
name: document.getElementById("edit-name").value.trim(),
|
|
url: document.getElementById("edit-url").value.trim(),
|
|
api_key: document.getElementById("edit-key").value.trim(),
|
|
base_path: document.getElementById("edit-base").value.trim() || "/SenSu",
|
|
notes: document.getElementById("edit-notes").value.trim()
|
|
};
|
|
if(!data.name||!data.url){ alert("名称和 URL 不能为空"); return; }
|
|
var resp = await fetch("/sentinel/api/nodes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(data)});
|
|
var r = await resp.json();
|
|
if(r.ok){ hideEdit(); loadNodes(); }
|
|
else alert("保存失败: "+(r.error||""));
|
|
}
|
|
|
|
async function deleteNode(id,name){
|
|
if(!confirm("确认删除节点 '"+name+"'?")) return;
|
|
await fetch("/sentinel/api/nodes/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:id})});
|
|
loadNodes();
|
|
}
|
|
|
|
async function testConn(){
|
|
var url = document.getElementById("edit-url").value.trim();
|
|
var key = document.getElementById("edit-key").value.trim();
|
|
var el = document.getElementById("test-result");
|
|
el.innerHTML = '<span style="color:var(--text-dim)">⏳ 测试中...</span>';
|
|
try {
|
|
var resp = await fetch("/sentinel/api/nodes/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:url,api_key:key})});
|
|
var r = await resp.json();
|
|
if(r.ok) el.innerHTML = '<span style="color:var(--success)">✅ 连接成功 — CPU:'+(r.system.cpu.percent||0)+'% MEM:'+(r.system.memory.percent||0)+'%</span>';
|
|
else el.innerHTML = '<span style="color:var(--error)">❌ '+esc(r.error||'失败')+'</span>';
|
|
}catch(e){ el.innerHTML = '<span style="color:var(--error)">❌ '+esc(String(e))+'</span>'; }
|
|
}
|
|
|
|
async function loadNodes(){
|
|
var resp = await fetch("/sentinel/api/nodes");
|
|
var data = await resp.json();
|
|
var nodes = data.nodes||[];
|
|
var list = document.getElementById("node-list");
|
|
if(!nodes.length){ list.innerHTML = '<div style="text-align:center;padding:24px;color:var(--text-dim)">暂无节点 — 点击上方添加</div>'; return; }
|
|
list.innerHTML = nodes.map(function(n){
|
|
var status = n.online ? '<span style="color:var(--success)">🟢</span>' : '<span style="color:var(--error)">🔴</span>';
|
|
return '<div class="node-item">'+
|
|
'<div>'+status+'</div>'+
|
|
'<div class="info"><div class="name">'+esc(n.name)+'</div><div class="url">'+esc(n.url)+'</div>'+
|
|
(n.notes?'<div class="notes">📝 '+esc(n.notes)+'</div>':'')+
|
|
'</div>'+
|
|
'<button class="btn btn-sm btn-outlined" onclick=\'showEdit('+JSON.stringify(n)+')\'>✏</button>'+
|
|
'<button class="btn btn-sm btn-outlined" style="color:var(--error)" onclick="deleteNode(\''+esc(n.id)+'\',\''+esc(n.name)+'\')">✕</button>'+
|
|
'</div>';
|
|
}).join("");
|
|
}
|
|
</script>
|
|
</div>
|