feat: add node resource profiling and admin device revocation
This commit is contained in:
@@ -16,3 +16,26 @@ header strong{font-size:18px}header span{margin-left:12px;color:var(--text-muted
|
||||
.theme-svg{display:none;width:18px;height:18px}
|
||||
:root[data-theme="dark"] .svg-sun{display:block;color:#facc15}
|
||||
:root[data-theme="light"] .svg-moon{display:block;color:#475569}
|
||||
|
||||
/* 设备卡片与画像升级 */
|
||||
.device-card{min-width:0;padding:16px;border:1px solid var(--border);border-radius:12px;background:var(--bg-sub);display:flex;flex-direction:column;gap:10px}
|
||||
.device-head{display:flex;align-items:center;justify-content:space-between;gap:8px;min-width:0}
|
||||
.device-title{display:flex;align-items:center;gap:8px;min-width:0;flex-wrap:wrap}
|
||||
.device-name{font-size:15px;color:var(--text-main);overflow-wrap:anywhere;word-break:break-all}
|
||||
.role-badge{font-size:11px;padding:2px 6px;border-radius:5px;font-weight:600;letter-spacing:.5px;border:1px solid var(--border-strong)}
|
||||
.role-control{background:rgba(59,130,246,.15);color:#60a5fa;border-color:rgba(59,130,246,.3)}
|
||||
.role-application{background:rgba(16,185,129,.15);color:#34d399;border-color:rgba(16,185,129,.3)}
|
||||
.role-database{background:rgba(245,158,11,.15);color:#fbbf24;border-color:rgba(245,158,11,.3)}
|
||||
.btn-revoke{padding:3px 8px;font-size:12px;border-radius:6px;background:transparent;border:1px solid var(--border-strong);color:var(--dot-offline);cursor:pointer}
|
||||
.btn-revoke:hover{background:rgba(239,68,68,.15);border-color:var(--dot-offline)}
|
||||
.device-meta{font-size:12px;color:var(--text-muted);display:flex;align-items:center;gap:6px;flex-wrap:wrap}
|
||||
|
||||
/* 资源画像 2列紧凑栅格 */
|
||||
.profile-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;padding:10px;background:var(--bg-card);border:1px solid var(--border);border-radius:8px;font-size:12px}
|
||||
.p-item{display:flex;justify-content:space-between;align-items:center;gap:6px}
|
||||
.p-label{color:var(--text-muted)}
|
||||
.p-val{font-family:ui-monospace,monospace;font-weight:600;color:var(--text-main)}
|
||||
.p-docker{grid-column:1/-1;display:flex;justify-content:flex-end}
|
||||
.badge-docker{font-size:11px;padding:2px 8px;border-radius:4px;font-weight:600}
|
||||
.badge-docker.yes{background:rgba(34,197,94,.15);color:var(--dot-online);border:1px solid rgba(34,197,94,.3)}
|
||||
.badge-docker.no{background:rgba(148,163,184,.15);color:var(--text-muted);border:1px solid var(--border)}
|
||||
|
||||
+54
-2
@@ -9,24 +9,76 @@ const $=s=>document.querySelector(s);
|
||||
localStorage.setItem("vpc_theme",next);
|
||||
};
|
||||
})();
|
||||
|
||||
async function api(path,opt={}){
|
||||
let r=await fetch("/panel/api"+path,opt);
|
||||
if(r.status===401){location.href="/panel/login";throw Error("unauthorized")}
|
||||
if(!r.ok)throw Error(await r.text());
|
||||
return r.json()
|
||||
}
|
||||
|
||||
function esc(v){
|
||||
return String(v??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]))
|
||||
}
|
||||
|
||||
async function removeDevice(name){
|
||||
if(!confirm(`确认注销并踢出节点 [${name}] 吗?\n注销后该节点将被断开并禁止再次连入。`)) return;
|
||||
try{
|
||||
await api("/devices?name="+encodeURIComponent(name), {method: "DELETE"});
|
||||
await loadDevices();
|
||||
}catch(e){
|
||||
alert("注销失败: "+e.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDevices(){
|
||||
let ds=await api("/devices");
|
||||
$("#devices").innerHTML=ds.map(d=>'<div class="device '+(d.status==="online"?"":"offline")+'"><b><i class="dot"></i>'+esc(d.name)+'</b><div class="status">'+esc(d.status)+' · '+(d.is_local?"local":"remote")+' · '+esc(d.arch||"")+'/'+esc(d.os||"")+'</div></div>').join("");
|
||||
$("#device").innerHTML=ds.map(d=>'<option>'+esc(d.name)+'</option>').join("")
|
||||
$("#devices").innerHTML=ds.map(d=>{
|
||||
const p = d.profile || {};
|
||||
const role = p.role || (d.is_local ? "CONTROL" : "APPLICATION");
|
||||
const isOnline = d.status === "online";
|
||||
const cpuStr = p.cpu_cores ? `${p.cpu_cores}C · ${p.cpu_usage_percent ?? 0}%` : '-';
|
||||
const memStr = p.mem_percent !== undefined ? `${p.mem_percent}%` + (p.mem_available ? ` (${p.mem_available})` : '') : '-';
|
||||
const swapStr = p.swap_percent !== undefined ? `${p.swap_percent}%` : '-';
|
||||
const diskStr = p.disk_percent !== undefined ? `${p.disk_percent}%` : '-';
|
||||
const psiStr = p.io_psi !== undefined ? p.io_psi.toFixed(2) : '-';
|
||||
const dockerBadge = p.docker ? '<span class="badge-docker yes">Docker YES</span>' : '<span class="badge-docker no">Docker NO</span>';
|
||||
|
||||
return `
|
||||
<div class="device-card ${isOnline ? "online" : "offline"}">
|
||||
<div class="device-head">
|
||||
<div class="device-title">
|
||||
<span class="dot"></span>
|
||||
<strong class="device-name">${esc(d.name)}</strong>
|
||||
<span class="role-badge role-${role.toLowerCase()}">${esc(role)}</span>
|
||||
</div>
|
||||
${d.is_local ? '' : `<button class="btn-revoke" onclick="removeDevice('${esc(d.name)}')">注销</button>`}
|
||||
</div>
|
||||
<div class="device-meta">
|
||||
<span>${esc(d.status)}</span> ·
|
||||
<span>${d.is_local ? "local" : "remote"}</span> ·
|
||||
<span>${esc(d.arch||"")}/${esc(d.os||"")}</span>
|
||||
</div>
|
||||
<div class="profile-grid">
|
||||
<div class="p-item"><span class="p-label">CPU</span><span class="p-val">${cpuStr}</span></div>
|
||||
<div class="p-item"><span class="p-label">Mem</span><span class="p-val">${memStr}</span></div>
|
||||
<div class="p-item"><span class="p-label">Swap</span><span class="p-val">${swapStr}</span></div>
|
||||
<div class="p-item"><span class="p-label">Disk</span><span class="p-val">${diskStr}</span></div>
|
||||
<div class="p-item"><span class="p-label">IO PSI</span><span class="p-val">${psiStr}</span></div>
|
||||
<div class="p-item p-docker">${dockerBadge}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
|
||||
$("#device").innerHTML=ds.map(d=>'<option>'+esc(d.name)+'</option>').join("");
|
||||
}
|
||||
|
||||
async function loadAudits(){
|
||||
let a=await api("/audits?limit=100");
|
||||
$("#audits").innerHTML=a.map(x=>'<tr><td>'+new Date(x.timestamp*1000).toLocaleString()+'</td><td>'+esc(x.device)+'</td><td>'+esc(x.action)+'</td><td><code>'+esc(x.command)+'</code></td><td>'+x.exit_code+'</td><td>'+x.duration_ms+'ms</td><td>'+esc(x.caller_ip)+'</td></tr>').join("")
|
||||
}
|
||||
|
||||
$("#refresh").onclick=loadDevices;
|
||||
$("#auditRefresh").onclick=loadAudits;
|
||||
$("#run").onclick=async()=>{
|
||||
|
||||
Reference in New Issue
Block a user