diff --git a/web/css/app.css b/web/css/app.css
index b39fda0..6d3a688 100644
--- a/web/css/app.css
+++ b/web/css/app.css
@@ -73,7 +73,7 @@ table.data tbody tr:hover{background:rgba(255,255,255,.04)}
/* buckets CU/CT/CM (simple version) */
.buckets{display:flex;align-items:flex-end;gap:6px;min-width:110px}
-.bucket{position:relative;width:26px;height:42px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);border-radius:8px;padding:4px 4px 16px;box-sizing:border-box;display:flex;justify-content:flex-end}
+.bucket{position:relative;width:24px;height:28px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);border-radius:7px;padding:3px 3px 14px;box-sizing:border-box;display:flex;justify-content:flex-end}
.bucket span{display:block;width:100%;background:var(--accent);border-radius:4px 4px 6px 6px;height:var(--h);align-self:flex-end;transition:height .8s cubic-bezier(.4,0,.2,1),background .3s}
.bucket[data-lv=warn] span{background:var(--warn)}
.bucket[data-lv=bad] span{background:var(--danger)}
@@ -119,6 +119,21 @@ table.data thead th:last-child, table.data tbody td:last-child { text-align:cent
}
.cards .card{border:1px solid var(--border);border-radius:12px;padding:.75rem .85rem;background:linear-gradient(145deg,var(--bg),var(--bg-alt));display:flex;flex-direction:column;gap:.45rem;position:relative;}
.cards .card.offline{opacity:.6;}
+.cards .card.high-load{border-color:rgba(239,68,68,.55);box-shadow:0 0 0 1px rgba(239,68,68,.4),0 4px 16px -4px rgba(239,68,68,.3);}
+table.data tbody tr.high-load{background:rgba(239,68,68,.10);}
+table.data tbody tr.high-load:hover{background:rgba(239,68,68,.18);}
+
+/* 详情进度条 */
+.row-bars{display:flex;gap:.6rem;align-items:stretch}
+.bar-wrap{flex:1;display:flex;flex-direction:column;gap:.25rem;min-width:0}
+.bar-label{display:flex;justify-content:space-between;font-size:11px;opacity:.85;font-family:ui-monospace,monospace;letter-spacing:.5px}
+.bar{position:relative;height:14px;border-radius:8px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);overflow:hidden}
+.bar span{position:absolute;inset:0;--p:0;background:linear-gradient(90deg,var(--accent),var(--accent-glow));width:calc(var(--p)*100%);transition:width .9s cubic-bezier(.4,0,.2,1),background .4s}
+.bar[data-warn] span{background:linear-gradient(90deg,var(--warn),#fbbf24)}
+.bar[data-bad] span{background:linear-gradient(90deg,var(--danger),#f87171)}
+.bar.io span{background:linear-gradient(90deg,#0284c7,#38bdf8)}
+.bar.io[data-warn] span{background:linear-gradient(90deg,var(--warn),#fbbf24)}
+.bar.io[data-bad] span{background:linear-gradient(90deg,var(--danger),#f87171)}
.cards .card-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;}
.cards .card-title{font-weight:600;font-size:.95rem;}
.cards .tag{font-size:.65rem;padding:.15rem .4rem;border-radius:4px;background:var(--border);letter-spacing:.5px;}
diff --git a/web/js/app.js b/web/js/app.js
index 47e740a..0e672e7 100644
--- a/web/js/app.js
+++ b/web/js/app.js
@@ -84,7 +84,8 @@ function renderServers(){
const pingBuckets = `
${bucket(p1)}${bucket(p2)}${bucket(p3)}
`;
const key = s.name || s.location || 'node';
const rowCursor = online? 'pointer':'default';
- html += `
+ const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 );
+ html += `
${statusPill} |
${s.name||'-'} |
${s.type||'-'} |
@@ -133,7 +134,8 @@ function renderServersCards(){
function bucket(p){ const v=Math.max(0,Math.min(100,p)); const level = v>=20?'bad':(v>=10?'warn':'ok'); return `
`; }
const buckets = `${bucket(p1)}${bucket(p2)}${bucket(p3)}
`;
const key = s.name || s.location || 'node';
- html += `\n
\n \n
\n
负载${s.load_1==-1?'–':s.load_1?.toFixed(2)}
\n
在线${s.uptime||'-'}
\n
月流量${monthIn}/${monthOut}
\n
网络${netNow}
\n
总流量${netTotal}
\n
CPU${s.cpu||0}%
\n
内存${memPct.toFixed(0)}%
\n
硬盘${hddPct.toFixed(0)}%
\n
\n ${buckets}\n
\n
${online?'点击卡片可查看详情':'离线,不可查看详情'}
\n
\n
`;
+ const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 );
+ html += `\n
\n \n
\n
负载${s.load_1==-1?'–':s.load_1?.toFixed(2)}
\n
在线${s.uptime||'-'}
\n
月流量${monthIn}/${monthOut}
\n
网络${netNow}
\n
总流量${netTotal}
\n
CPU${s.cpu||0}%
\n
内存${memPct.toFixed(0)}%
\n
硬盘${hddPct.toFixed(0)}%
\n
\n ${buckets}\n
\n
${online?'点击卡片可查看详情':'离线,不可查看详情'}
\n
\n
`;
});
wrap.innerHTML = html || '无数据
';
wrap.querySelectorAll('.card').forEach(card=>{
@@ -269,8 +271,11 @@ function openDetail(i){
const modal = document.getElementById('detailModal');
document.getElementById('detailTitle').textContent = s.name + ' 详情';
const offline = !(s.online4||s.online6);
- const memLine = `内存: ${s.memory_total? bytes(s.memory_used*1024)+' / '+bytes(s.memory_total*1024):'- / -'} | 虚存: ${s.swap_total? bytes(s.swap_used*1024)+' / '+bytes(s.swap_total*1024):'- / -'}`;
- const hddLine = `硬盘: ${s.hdd_total? bytes(s.hdd_used*1024*1024)+' / '+bytes(s.hdd_total*1024*1024):'- / -'} | 读/写: ${(typeof s.io_read==='number')? bytes(s.io_read):'-'} / ${(typeof s.io_write==='number')? bytes(s.io_write):'-'}`;
+ const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0;
+ const swapPct = s.swap_total? (s.swap_used/s.swap_total*100):0;
+ const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0;
+ const ioRead = (typeof s.io_read==='number')? s.io_read:0;
+ const ioWrite = (typeof s.io_write==='number')? s.io_write:0;
const procLine = `${num(s.tcp_count)} / ${num(s.udp_count)} / ${num(s.process_count)} / ${num(s.thread_count)}`;
// 保留延迟数据用于图表,但不再展示当前延迟文字行
const latText = offline ? '离线' : `CU/CT/CM: ${num(s.time_10010)}ms (${(s.ping_10010||0).toFixed(0)}%) / ${num(s.time_189)}ms (${(s.ping_189||0).toFixed(0)}%) / ${num(s.time_10086)}ms (${(s.ping_10086||0).toFixed(0)}%)`;
@@ -296,8 +301,15 @@ function openDetail(i){
`;
}
+ function barHTML(label,valPct,text,role,io){
+ const lvl = valPct>=90?'bad':(valPct>=80?'warn':'');
+ const ioCls = io? ' io':'';
+ return ``;
+ }
+ function ioBar(label,bytesVal,role){ const peak=150*1000*1000; const pct=Math.min(100,(bytesVal/peak)*100); const lvl = bytesVal>100*1000*1000?'bad': bytesVal>50*1000*1000?'warn':''; return barHTML(label,pct, (bytes(bytesVal)+'/s'), role, true).replace('位置${s.location||'-'}
+ TCP/UDP/进/线${procLine}
@@ -307,9 +319,15 @@ function openDetail(i){
(~${(S.loadHist[key]?S.loadHist[key].l1.length:0)} 条)
- 内存|虚存${offline?'- / - | 虚存: - / -':memLine}
- 硬盘|读写${offline?'- / - | 读/写: - / -':hddLine}
- TCP/UDP/进/线${procLine}
+
+ ${offline?'
':barHTML('内存', memPct, memPct.toFixed(1)+'%','mem')}
+ ${offline?'
':barHTML('虚存', swapPct, s.swap_total? swapPct.toFixed(1)+'%':'-','swap')}
+
+
+ ${offline?'
':barHTML('硬盘', hddPct, hddPct.toFixed(1)+'%','disk')}
+ ${offline?'
':ioBar('读速', ioRead,'io-read')}
+ ${offline?'
':ioBar('写速', ioWrite,'io-write')}
+
${latencyBlock}
`;
modal.style.display='flex';
@@ -318,12 +336,14 @@ function openDetail(i){
drawLatencyChart(key);
drawLoadChart(key);
S._openDetailKey = key; // 记录当前弹窗对应节点
+ startDetailAutoUpdate();
} else {
S._openDetailKey = null;
+ stopDetailAutoUpdate();
}
}
function escCloseOnce(e){ if(e.key==='Escape'){ closeDetail(); } }
-function closeDetail(){ const m=document.getElementById('detailModal'); m.style.display='none'; document.removeEventListener('keydown', escCloseOnce); }
+function closeDetail(){ const m=document.getElementById('detailModal'); m.style.display='none'; document.removeEventListener('keydown', escCloseOnce); stopDetailAutoUpdate(); }
document.getElementById('detailClose').addEventListener('click', closeDetail);
document.getElementById('detailModal').addEventListener('click', e=>{ if(e.target.id==='detailModal') closeDetail(); });
@@ -360,7 +380,7 @@ function drawLatencyChart(key){
// 在每次 render 后若弹窗打开则重绘最新图
const _oldRender = render;
-render = function(){ _oldRender(); if(S._openDetailKey){ drawLatencyChart(S._openDetailKey); } };
+render = function(){ _oldRender(); if(S._openDetailKey){ drawLatencyChart(S._openDetailKey); drawLoadChart(S._openDetailKey); } };
window.addEventListener('resize', ()=>{
if(S._openDetailKey){ drawLatencyChart(S._openDetailKey); drawLoadChart(S._openDetailKey); }
renderServersCards();
@@ -430,3 +450,25 @@ function drawLoadChart(key){
}
//# sourceMappingURL=app.js.map
+
+// ====== 详情动态刷新 ======
+function findServerByKey(key){ return S.servers.find(x=> (x.name||x.location||'node')===key); }
+function updateDetailMetrics(key){
+ const s = findServerByKey(key); if(!s) return; if(!(s.online4||s.online6)) return; // 离线不更新
+ const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0;
+ const swapPct = s.swap_total? (s.swap_used/s.swap_total*100):0;
+ const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0;
+ const ioRead = (typeof s.io_read==='number')? s.io_read:0;
+ const ioWrite = (typeof s.io_write==='number')? s.io_write:0;
+ const procLine = `${num(s.tcp_count)} / ${num(s.udp_count)} / ${num(s.process_count)} / ${num(s.thread_count)}`;
+ const procEl = document.getElementById('detail-proc'); if(procEl) procEl.textContent = procLine;
+ function upd(role,pct,text){ const wrap=document.querySelector(`#detailContent .bar-wrap[data-role="${role}"]`); if(!wrap) return; const valSpan=wrap.querySelector('.bar-label span:last-child'); if(valSpan) valSpan.textContent=text; const bar=wrap.querySelector('.bar span'); if(bar) bar.style.setProperty('--p',(pct/100).toFixed(3)); const box=wrap.querySelector('.bar'); if(box){ box.removeAttribute('data-warn'); box.removeAttribute('data-bad'); if(pct>=90) box.setAttribute('data-bad',''); else if(pct>=80) box.setAttribute('data-warn',''); } }
+ upd('mem', memPct, memPct.toFixed(1)+'%');
+ if(document.querySelector('.bar-wrap[data-role="swap"]')) upd('swap', swapPct, s.swap_total? swapPct.toFixed(1)+'%':'-');
+ upd('disk', hddPct, hddPct.toFixed(1)+'%');
+ function updIO(role,val){ const wrap=document.querySelector(`#detailContent .bar-wrap[data-role="${role}"]`); if(!wrap) return; const peak=150*1000*1000; const pct=Math.min(100,(val/peak)*100); const bar=wrap.querySelector('.bar span'); if(bar) bar.style.setProperty('--p',(pct/100).toFixed(3)); const lbl=wrap.querySelector('.bar-label span:last-child'); if(lbl) lbl.textContent= bytes(val)+'/s'; const box=wrap.querySelector('.bar'); if(box){ box.removeAttribute('data-warn'); box.removeAttribute('data-bad'); if(val>100*1000*1000) box.setAttribute('data-bad',''); else if(val>50*1000*1000) box.setAttribute('data-warn',''); } }
+ updIO('io-read', ioRead);
+ updIO('io-write', ioWrite);
+}
+function startDetailAutoUpdate(){ stopDetailAutoUpdate(); S._detailTimer = setInterval(()=>{ if(S._openDetailKey) updateDetailMetrics(S._openDetailKey); }, 1000); }
+function stopDetailAutoUpdate(){ if(S._detailTimer){ clearInterval(S._detailTimer); S._detailTimer=null; } }