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
${s.name||'-'} ${s.location||'-'}
\n ${pill}\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
${s.name||'-'} ${s.location||'-'}
\n ${pill}\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 `
${label}${text}
`; + } + 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; } }