// 简洁现代前端 - 仅使用原生 JS
const S = { updated:0, servers:[], ssl:[], error:false, hist:{}, metricHist:{}, loadHist:{} };// hist latency; metricHist: {key:{cpu:[],mem:[],hdd:[]}}; loadHist: {key:[]}
const els = {
notice: ()=>document.getElementById('notice'),
last: ()=>document.getElementById('lastUpdate'),
serversBody: ()=>document.getElementById('serversBody'),
monitorsBody: ()=>document.getElementById('monitorsBody'),
sslBody: ()=>document.getElementById('sslBody')
};
function bytes(v){ if(v===0) return '0B'; if(!v) return '-'; const k=1000; const u=['B','KB','MB','GB','TB','PB']; const i=Math.floor(Math.log(v)/Math.log(k)); return (v/Math.pow(k,i)).toFixed(i?1:0)+u[i]; }
function pct(v){ return (v||0).toFixed(0)+'%'; }
function clsBy(v){ return v>=90?'danger':v>=80?'warn':'ok'; }
function humanAgo(ts){ if(!ts) return '-'; const s=Math.floor((Date.now()/1000 - ts)); const m=Math.floor(s/60); return m>0? m+' 分钟前':'几秒前'; }
function num(v){ return (typeof v==='number' && !isNaN(v)) ? v : '-'; }
async function fetchData(){
try {
const r = await fetch('json/stats.json?_='+Date.now());
if(!r.ok) throw new Error(r.status);
const j = await r.json();
if(j.reload) location.reload();
S.updated = j.updated; S.servers = j.servers||[]; S.ssl = j.sslcerts||[]; S.error=false;
// 更新延迟历史 (按节点名聚合)
S.servers.forEach(s=>{
const key = s.name || s.location || 'node';
if(!S.hist[key]) S.hist[key] = {cu:[],ct:[],cm:[]};
const H = S.hist[key];
// 使用 time_ 字段 (ms) 若不存在则跳过
if(typeof s.time_10010 === 'number') H.cu.push(s.time_10010);
if(typeof s.time_189 === 'number') H.ct.push(s.time_189);
if(typeof s.time_10086 === 'number') H.cm.push(s.time_10086);
const MAX=120; // 保留约 120*4s ≈ 8 分钟
['cu','ct','cm'].forEach(k=>{ if(H[k].length>MAX) H[k].splice(0,H[k].length-MAX); });
// 指标历史 (仅在线时记录)
if(!S.metricHist[key]) S.metricHist[key] = {cpu:[],mem:[],hdd:[]};
const MH = S.metricHist[key];
if(s.online4||s.online6){
const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0;
const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0;
MH.cpu.push(s.cpu||0);
MH.mem.push(memPct||0);
MH.hdd.push(hddPct||0);
const MAXM=120; ['cpu','mem','hdd'].forEach(k=>{ if(MH[k].length>MAXM) MH[k].splice(0,MH[k].length-MAXM); });
}
// 负载历史 (记录 load_1 / load_5 / load_15)
if(!S.loadHist[key]) S.loadHist[key] = {l1:[],l5:[],l15:[]};
const LH = S.loadHist[key];
const pushLoad = (arr,val)=>{ if(typeof val === 'number' && val >= 0){ arr.push(val); if(arr.length>120) arr.splice(0,arr.length-120); } };
pushLoad(LH.l1, s.load_1);
pushLoad(LH.l5, s.load_5);
pushLoad(LH.l15, s.load_15);
});
render();
}catch(e){ S.error=true; els.notice().textContent = '数据获取失败'; console.error(e); }
}
function render(){
els.notice().style.display='none';
renderServers();
renderServersCards();
renderMonitors();
renderMonitorsCards();
renderSSL();
renderSSLCards();
updateTime();
}
function renderServers(){
const tbody = els.serversBody();
let html='';
S.servers.forEach((s,idx)=>{
const online = s.online4||s.online6;
const proto = online ? (s.online4 && s.online6? '双栈': s.online4? 'IPv4':'IPv6') : '离线';
const statusPill = online ? `${proto}` : `${proto}`;
const cpuCls = clsBy(s.cpu);
const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0; const memCls = clsBy(memPct);
const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0; const hddCls = clsBy(hddPct);
const monthInBytes = (s.network_in - s.last_network_in) || 0;
const monthOutBytes = (s.network_out - s.last_network_out) || 0;
const monthIn = bytes(monthInBytes);
const monthOut = bytes(monthOutBytes);
const HEAVY_THRESHOLD = 500 * 1000 * 1000 * 1000; // 500GB
const heavy = monthInBytes >= HEAVY_THRESHOLD || monthOutBytes >= HEAVY_THRESHOLD;
const trafficCls = heavy ? 'caps-traffic duo heavy' : 'caps-traffic duo normal';
const netNow = bytes(s.network_rx) + ' | ' + bytes(s.network_tx);
const netTotal = bytes(s.network_in)+' | '+bytes(s.network_out);
const p1 = (s.ping_10010||0); const p2 = (s.ping_189||0); const p3 = (s.ping_10086||0);
function bucket(p){ const v = Math.max(0, Math.min(100, p)); const level = v>=20?'bad':(v>=10?'warn':'ok'); return `
`; }
const pingBuckets = `${bucket(p1)}${bucket(p2)}${bucket(p3)}
`;
const key = s.name || s.location || 'node';
const rowCursor = online? 'pointer':'default';
const highLoad = online && ( (s.cpu||0)>=90 || (memPct)>=90 || (hddPct)>=90 );
html += `
${statusPill} |
${monthIn}${monthOut} |
${s.name||'-'} |
${s.type||'-'} |
${s.location||'-'} |
${s.uptime||'-'} |
${s.load_1==-1?'–':Math.max(0,(s.load_1||0)).toFixed(2)} |
${netNow} |
${netTotal} |
${online?gaugeHTML('cpu', s.cpu||0):'-'} |
${online?gaugeHTML('mem', memPct):'-'} |
${online?gaugeHTML('hdd', hddPct):'-'} |
${pingBuckets} |
`;
});
tbody.innerHTML = html || `无数据 |
`;
// 绑定行点击
tbody.querySelectorAll('tr.row-server').forEach(tr=>{
tr.addEventListener('click',()=>{
if(tr.getAttribute('data-online')!=='1') return; // 离线不弹出
const i = parseInt(tr.getAttribute('data-idx'));
openDetail(i);
});
});
// 仪表盘无需 drawSparks
}
// 生成仪表盘 (圆形 conic-gradient)
function gaugeHTML(type,val){
const pct = Math.max(0,Math.min(100,val));
const p = (pct/100).toFixed(3);
const warnAttr = pct>=90? 'data-bad' : (pct>=50? 'data-warn' : '');
return ``;
}
function labelOf(t){ return t==='cpu'?'CPU': t==='mem'?'内存':'硬盘'; }
function renderServersCards(){
const wrap = document.getElementById('serversCards');
if(!wrap) return;
// 仅在窄屏时显示 (和 CSS 一致判断, 可稍放宽避免闪烁)
if(window.innerWidth>700){ wrap.innerHTML=''; return; }
let html='';
S.servers.forEach((s,idx)=>{
const online = s.online4||s.online6;
const proto = online ? (s.online4 && s.online6? '双栈': s.online4? 'IPv4':'IPv6') : '离线';
const pill = `${proto}`;
const memPct = s.memory_total? (s.memory_used/s.memory_total*100):0;
const hddPct = s.hdd_total? (s.hdd_used/s.hdd_total*100):0;
// 月流量(移动端)并应用 500GB 阈值配色逻辑
const monthInBytes = (s.network_in - s.last_network_in) || 0;
const monthOutBytes = (s.network_out - s.last_network_out) || 0;
const monthIn = bytes(monthInBytes);
const monthOut = bytes(monthOutBytes);
const HEAVY_THRESHOLD = 500 * 1000 * 1000 * 1000; // 500GB
const heavy = monthInBytes >= HEAVY_THRESHOLD || monthOutBytes >= HEAVY_THRESHOLD;
const trafficCls = heavy ? 'caps-traffic duo heavy sm' : 'caps-traffic duo normal sm';
const netNow = bytes(s.network_rx)+' | '+bytes(s.network_tx);
const netTotal = bytes(s.network_in)+' | '+bytes(s.network_out);
const p1 = (s.ping_10010||0); const p2=(s.ping_189||0); const p3=(s.ping_10086||0);
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';
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=>{
const idx = parseInt(card.getAttribute('data-idx'));
card.addEventListener('click', e=>{
if(e.target.classList.contains('expand-btn')){ card.classList.toggle('expanded'); e.stopPropagation(); return;}
if(card.getAttribute('data-online')!=='1') return; // 离线不弹
openDetail(idx);
});
});
}
function renderMonitors(){
const tbody = els.monitorsBody();
let html='';
S.servers.forEach(s=>{
html += `
${(s.online4||s.online6)?'在线':'离线'} |
${s.name||'-'} |
${s.location||'-'} |
${s.custom||'-'} |
`;
});
tbody.innerHTML = html || `无数据 |
`;
}
// 服务卡片 (移动端)
function renderMonitorsCards(){
const wrap = document.getElementById('monitorsCards');
if(!wrap) return; if(window.innerWidth>700){ wrap.innerHTML=''; return; }
let html='';
S.servers.forEach(s=>{
const online = (s.online4||s.online6)?'在线':'离线';
const pill = `${online}`;
html += `
监测内容${s.custom||'-'}
协议${online}
`;
});
wrap.innerHTML = html || '无数据
';
}
function renderSSL(){
const tbody = els.sslBody();
let html='';
S.ssl.forEach(c=>{
const cls = c.expire_days<=0? 'err': c.expire_days<=7? 'warn':'ok';
const status = c.expire_days<=0? '已过期': c.expire_days<=7? '将到期':'正常';
const dt = c.expire_ts? new Date(c.expire_ts*1000).toISOString().replace('T',' ').replace(/\.\d+Z/,''):'-';
html += `
${c.name||'-'} |
${(c.domain||'').replace(/^https?:\/\//,'')} |
${c.port||443} |
${c.expire_days??'-'} |
${dt} |
${status} |
`;
});
tbody.innerHTML = html || `无证书数据 |
`;
}
// 证书卡片 (移动端)
function renderSSLCards(){
const wrap = document.getElementById('sslCards');
if(!wrap) return; if(window.innerWidth>700){ wrap.innerHTML=''; return; }
let html='';
S.ssl.forEach(c=>{
const cls = c.expire_days<=0? 'err': c.expire_days<=7? 'warn':'ok';
const status = c.expire_days<=0? '已过期': c.expire_days<=7? '将到期':'正常';
const dt = c.expire_ts? new Date(c.expire_ts*1000).toISOString().replace('T',' ').replace(/\.\d+Z/,''):'-';
html += `
域名${(c.domain||'').replace(/^https?:\/\//,'')}
端口${c.port||443}
剩余(天)${c.expire_days??'-'}
到期${dt.split(' ')[0]||dt}
`;
});
wrap.innerHTML = html || '无证书数据
';
}
function updateTime(){
const el = els.last();
if(S.updated){ el.textContent = '最后更新: '+ humanAgo(S.updated); }
}
function bindTabs(){
document.getElementById('navTabs').addEventListener('click',e=>{
if(e.target.tagName!=='BUTTON') return; const tab=e.target.dataset.tab;
document.querySelectorAll('.nav button').forEach(b=>b.classList.toggle('active',b===e.target));
document.querySelectorAll('.panel').forEach(p=>p.classList.toggle('active', p.id==='panel-'+tab));
});
}
function bindTheme(){
const btn = document.getElementById('themeToggle');
const mql = window.matchMedia('(prefers-color-scheme: light)');
const saved = localStorage.getItem('theme'); // 'light' | 'dark' | null (auto)
const apply = (isLight)=>{ document.body.classList.toggle('light', isLight); document.documentElement.classList.toggle('light', isLight); };
if(!saved){
// 自动跟随系统
apply(mql.matches);
// 监听系统偏好变化(仅在未手动选择时)
mql.addEventListener('change', e=>{ if(!localStorage.getItem('theme')) apply(e.matches); });
} else {
apply(saved==='light');
}
btn.addEventListener('click',()=>{
// 用户手动切换后即固定,不再自动
const toLight = !document.body.classList.contains('light');
apply(toLight);
localStorage.setItem('theme', toLight?'light':'dark');
});
}
bindTabs();
bindTheme();
fetchData();
setInterval(fetchData, 1000);
setInterval(updateTime, 60000);
// 详情弹窗逻辑
function openDetail(i){
const s = S.servers[i]; if(!s) return;
const box = document.getElementById('detailContent');
const modal = document.getElementById('detailModal');
document.getElementById('detailTitle').textContent = s.name + ' 详情';
const offline = !(s.online4||s.online6);
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)}%)`;
const key = s.name || s.location || 'node';
let latencyBlock = '';
if(!offline){
latencyBlock = `
● 联通 (${num(s.time_10010)}ms)
● 电信 (${num(s.time_189)}ms)
● 移动 (${num(s.time_10086)}ms)
(~${S.hist[key]?S.hist[key].cu.length:0} 条)
`;
} else {
latencyBlock = `
`;
}
// 旧进度条函数 barHTML/ioBar 已弃用
// 资源行(移除百分比显示,仅显示 已用 / 总量)
const memLine = s.memory_total? bytes(s.memory_used)+' / '+bytes(s.memory_total):'-';
const swapLine = s.swap_total? bytes(s.swap_used)+' / '+bytes(s.swap_total):'-';
const diskLine = s.hdd_total? bytes(s.hdd_used)+' / '+bytes(s.hdd_total):'-';
const ioReadLine = ioRead? bytes(ioRead)+'/s':'-';
const ioWriteLine = ioWrite? bytes(ioWrite)+'/s':'-';
// 阈值着色 ( >80% / >100MB/s )
const memColor = memPct>80? ' style="color:var(--danger)"':'';
const swapColor = swapPct>80? ' style="color:var(--danger)"':'';
const hddColor = hddPct>80? ' style="color:var(--danger)"':'';
const readColor = ioRead>100*1000*1000? ' style="color:var(--danger)"':'';
const writeColor = ioWrite>100*1000*1000? ' style="color:var(--danger)"':'';
box.innerHTML = `
TCP/UDP/进/线${procLine}
内存 / 虚存${memLine} | ${swapLine}
硬盘 / 读写${diskLine} | 读 ${ioReadLine} / 写 ${ioWriteLine}
● load1
● load5
● load15
(~${(S.loadHist[key]?S.loadHist[key].l1.length:0)} 条)
${latencyBlock}
`;
modal.style.display='flex';
document.addEventListener('keydown', escCloseOnce);
if(!offline){
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); stopDetailAutoUpdate(); }
document.getElementById('detailClose').addEventListener('click', closeDetail);
document.getElementById('detailModal').addEventListener('click', e=>{ if(e.target.id==='detailModal') closeDetail(); });
// 绘制三网延迟折线图 (简易实现)
function drawLatencyChart(key){
const data = S.hist[key];
const canvas = document.getElementById('latChart');
if(!canvas || !data) return;
const ctx = canvas.getContext('2d');
const W = canvas.clientWidth; const H = canvas.height; canvas.width = W; // 适配宽度
ctx.clearRect(0,0,W,H);
const padL=40, padR=10, padT=10, padB=18;
const isLight = document.body.classList.contains('light');
const axisColor = isLight? 'rgba(0,0,0,0.22)' : 'rgba(255,255,255,0.18)';
const gridColor = isLight? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.10)';
const textColor = isLight? 'var(--text-dim)' : 'rgba(226,232,240,0.85)';
const series = [ {arr:data.cu,color:'#3b82f6'}, {arr:data.ct,color:'#10b981'}, {arr:data.cm,color:'#f59e0b'} ];
const allVals = series.flatMap(s=>s.arr);
if(!allVals.length){ ctx.fillStyle='var(--text-dim)'; ctx.font='12px system-ui'; ctx.fillText('暂无数据', W/2-30, H/2); return; }
const max = Math.max(...allVals);
const min = Math.min(...allVals);
const range = Math.max(1, max-min);
const n = Math.max(...series.map(s=>s.arr.length));
const xStep = (W - padL - padR) / Math.max(1,n-1);
// 网格与轴 (增强暗色对比)
ctx.strokeStyle=axisColor; ctx.lineWidth=1.1;
ctx.beginPath(); ctx.moveTo(padL,padT); ctx.lineTo(padL,H-padB); ctx.lineTo(W-padR,H-padB); ctx.stroke();
ctx.fillStyle=textColor; ctx.font='10px system-ui';
const yMarks=4; for(let i=0;i<=yMarks;i++){ const y = padT + (H-padT-padB)*i/yMarks; const val = (max - range*i/yMarks).toFixed(0)+'ms'; ctx.fillText(val,4,y+3); ctx.strokeStyle=gridColor; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke(); }
// 绘制线
series.forEach(s=>{
if(s.arr.length<2) return;
ctx.strokeStyle = s.color; ctx.lineWidth=1.6; ctx.beginPath();
s.arr.forEach((v,i)=>{ const x = padL + xStep*i; const y = padT + (H-padT-padB)*(1-(v-min)/range); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); });
ctx.stroke();
});
}
// 在每次 render 后若弹窗打开则重绘最新图
const _oldRender = render;
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();
renderMonitorsCards();
renderSSLCards();
});
// 绘制小型折线 (sparklines)
function drawSparks(){
const els = document.querySelectorAll('.spark');
els.forEach(div=>{
// 若已有canvas跳过重建
let canvas = div.querySelector('canvas');
if(!canvas){ canvas = document.createElement('canvas'); div.appendChild(canvas); }
const key = div.getAttribute('data-key');
const metric = div.getAttribute('data-metric');
const hist = (S.metricHist[key] && S.metricHist[key][metric])? S.metricHist[key][metric]:[];
const W = 80, H = 26; canvas.width=W; canvas.height=H; const ctx=canvas.getContext('2d');
ctx.clearRect(0,0,W,H);
div.classList.add('spark-ready');
if(hist.length<2){ ctx.fillStyle='var(--text-dim)'; ctx.font='10px system-ui'; ctx.fillText('-', W/2-3, H/2+3); return; }
// 硬盘与 CPU/内存保持一致的折线显示(去掉低波动迷你条特殊样式)
const max = Math.max(...hist); const min = Math.min(...hist); const range = Math.max(1,max-min);
const step = W/(hist.length-1);
// 线颜色
let color = '#3b82f6'; if(metric==='mem') color='#10b981'; else if(metric==='hdd') color='#f59e0b';
ctx.strokeStyle=color; ctx.lineWidth=1.3; ctx.beginPath();
hist.forEach((v,i)=>{ const x=i*step; const y=H - ( (v-min)/range )* (H-4) -2; if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); });
ctx.stroke();
// 当前值点
const last = hist[hist.length-1]; const lx = W-1; const ly = H - ((last-min)/range)*(H-4)-2; ctx.fillStyle=color; ctx.beginPath(); ctx.arc(lx,ly,2,0,Math.PI*2); ctx.fill();
});
}
// 负载折线图 (load1 历史)
function drawLoadChart(key){
const L = S.loadHist[key];
const canvas = document.getElementById('loadChart');
if(!canvas) return; const ctx = canvas.getContext('2d');
if(!L){ ctx.clearRect(0,0,canvas.width,canvas.height); return; }
const l1=L.l1||[], l5=L.l5||[], l15=L.l15||[];
const canvasW = canvas.clientWidth; const H = canvas.height; canvas.width = canvasW; const W=canvasW;
ctx.clearRect(0,0,W,H);
if(l1.length<2){ ctx.fillStyle='var(--text-dim)'; ctx.font='12px system-ui'; ctx.fillText('暂无负载数据', W/2-42, H/2); return; }
const all = [...l1,...l5,...l15];
const padL=38,padR=8,padT=8,padB=16;
const max=Math.max(...all); const min=Math.min(...all); const range=Math.max(0.5,max-min);
const n = Math.max(l1.length,l5.length,l15.length); const xStep=(W-padL-padR)/Math.max(1,n-1);
const isLight = document.body.classList.contains('light');
const axisColor = isLight? 'rgba(0,0,0,0.22)' : 'rgba(255,255,255,0.18)';
const gridColor = isLight? 'rgba(0,0,0,0.08)' : 'rgba(255,255,255,0.10)';
const textColor = isLight? 'var(--text-dim)' : 'rgba(226,232,240,0.85)';
// 轴 & 网格 (增强暗色对比)
ctx.strokeStyle=axisColor; ctx.lineWidth=1.1; ctx.beginPath(); ctx.moveTo(padL,padT); ctx.lineTo(padL,H-padB); ctx.lineTo(W-padR,H-padB); ctx.stroke();
ctx.fillStyle=textColor; ctx.font='10px system-ui';
const yMarks=4; for(let i=0;i<=yMarks;i++){ const y=padT+(H-padT-padB)*i/yMarks; const val=(max - range*i/yMarks).toFixed(2); ctx.fillText(val,4,y+3); ctx.strokeStyle=gridColor; ctx.beginPath(); ctx.moveTo(padL,y); ctx.lineTo(W-padR,y); ctx.stroke(); }
const series=[{arr:l1,color:'#8b5cf6',fill:true},{arr:l5,color:'#10b981'},{arr:l15,color:'#f59e0b'}];
// 面积先画 load1
series.forEach(s=>{
if(s.arr.length<2) return;
ctx.beginPath(); ctx.lineWidth=1.5; ctx.strokeStyle=s.color;
s.arr.forEach((v,i)=>{ const x=padL+xStep*i; const y=padT+(H-padT-padB)*(1-(v-min)/range); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); });
ctx.stroke();
if(s.fill){
const lastX = padL + xStep*(s.arr.length-1);
ctx.lineTo(lastX,H-padB); ctx.lineTo(padL,H-padB); ctx.closePath();
const grd = ctx.createLinearGradient(0,padT,0,H-padB); grd.addColorStop(0,'rgba(139,92,246,0.25)'); grd.addColorStop(1,'rgba(139,92,246,0)');
ctx.fillStyle=grd; ctx.fill();
}
});
}
//# 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 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;
const cuEl=document.getElementById('lat-cu'); if(cuEl) cuEl.textContent = num(s.time_10010)+'ms';
const ctEl=document.getElementById('lat-ct'); if(ctEl) ctEl.textContent = num(s.time_189)+'ms';
const cmEl=document.getElementById('lat-cm'); if(cmEl) cmEl.textContent = num(s.time_10086)+'ms';
// 延迟动态刷新 (若存在)
const cuE1=document.getElementById('lat-cu'); if(cuE1) cuE1.textContent = num(s.time_10010)+'ms';
const ctE1=document.getElementById('lat-ct'); if(ctE1) ctE1.textContent = num(s.time_189)+'ms';
const cmE1=document.getElementById('lat-cm'); if(cmE1) cmE1.textContent = num(s.time_10086)+'ms';
}
function startDetailAutoUpdate(){ stopDetailAutoUpdate(); S._detailTimer = setInterval(()=>{ if(S._openDetailKey) updateDetailMetrics(S._openDetailKey); }, 1000); }
function stopDetailAutoUpdate(){ if(S._detailTimer){ clearInterval(S._detailTimer); S._detailTimer=null; } }