(() => { const root = document.querySelector('#content'); const summaryEl = document.querySelector('#summaryText'); const lastUpdatedEl = document.querySelector('#lastUpdated'); const fmtBytes = n => { if (!Number.isFinite(n) || n <= 0) return '0 B'; const u = ['B', 'KB', 'MB', 'GB', 'TB']; let i = 0, v = n; while (v >= 1024 && i < u.length - 1) { v /= 1024; i++; } return `${v.toFixed(i === 0 ? 0 : 1)} ${u[i]}`; }; const fmtRate = n => `${fmtBytes(n)}/s`; const fmtUp = n => { let s = Math.max(0, Math.floor(n || 0)); if (s <= 0) return '--'; const d = Math.floor(s / 86400); s %= 86400; const h = Math.floor(s / 3600); s %= 3600; const m = Math.floor(s / 60); if (d > 0) return `${d}天 ${h}时`; if (h > 0) return `${h}小时 ${m}分`; return `${m} 分钟`; }; const esc = s => String(s ?? '-').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c])); const getFlag = (cc, country) => { if (!cc || typeof cc !== 'string' || cc.length !== 2) return ''; const code = cc.toUpperCase(); const flag = String.fromCodePoint(...[...code].map(c => 0x1F1E6 - 65 + c.charCodeAt(0))); const title = country ? `${country} (${code})` : code; return `${flag}`; }; const getStatusLevel = pct => { const val = Number(pct || 0); if (val >= 85) return 'danger'; if (val >= 65) return 'warn'; return 'normal'; }; let mode = localStorage.getItem('vpc_probe_view') || 'card'; const setMode = m => { mode = m; localStorage.setItem('vpc_probe_view', m); render(window.__data || []); }; document.querySelector('#cardBtn').onclick = () => setMode('card'); document.querySelector('#compactBtn').onclick = () => setMode('compact'); const renderCard = d => { const p = d.profile || {}; const isOnline = d.status === 'online'; const cpuPct = p.cpu_usage_percent != null ? Number(p.cpu_usage_percent) : 0; const memPct = p.mem_percent != null ? Number(p.mem_percent) : 0; const diskPct = p.disk_percent != null ? Number(p.disk_percent) : 0; const loadStr = [p.load_1m, p.load_5m, p.load_15m].map(x => Number(x || 0).toFixed(2)).join(', '); return `
${getFlag(d.country_code, d.country)} ${esc(d.name)} ${d.is_local ? '本机' : ''}
${p.cpu_cores || 1} 核 ${esc(d.os || 'Linux')} ${fmtUp(p.uptime_seconds)}
${isOnline ? '在线' : '离线'}
CPU 负载 ${p.cpu_usage_percent != null ? `${cpuPct}%` : '--'}
内存 (空闲 ${esc(p.mem_available || '--')}) ${p.mem_percent != null ? `${memPct}%` : '--'}
磁盘 (空闲 ${esc(p.disk_free || '--')}) ${p.disk_percent != null ? `${diskPct}%` : '--'}
实时网络 (↓ / ↑) ↓ ${fmtRate(p.net_rx_rate)} / ↑ ${fmtRate(p.net_tx_rate)}
总流量 (↓ / ↑) ↓ ${fmtBytes(p.net_rx_total)} / ↑ ${fmtBytes(p.net_tx_total)}
系统平均负载 (1/5/15m) ${loadStr}
IO PSI / Docker ${p.io_psi != null ? Number(p.io_psi).toFixed(2) : '0.00'} / ${p.docker ? '有' : '无'}
`; }; const renderTable = list => { const rows = list.map(d => { const p = d.profile || {}; const isOnline = d.status === 'online'; const cpuPct = p.cpu_usage_percent != null ? Number(p.cpu_usage_percent) : 0; const memPct = p.mem_percent != null ? Number(p.mem_percent) : 0; const diskPct = p.disk_percent != null ? Number(p.disk_percent) : 0; const loadStr = [p.load_1m, p.load_5m, p.load_15m].map(x => Number(x || 0).toFixed(2)).join(' / '); return `
${getFlag(d.country_code, d.country)} ${esc(d.name)} ${d.is_local ? '本机' : ''} ${p.cpu_cores || 1}核 | ${esc(d.os || 'Linux')}
${isOnline ? '在线' : '离线'} ${fmtUp(p.uptime_seconds)} ${loadStr}
${p.cpu_usage_percent != null ? `${cpuPct}%` : '--'}
${p.mem_percent != null ? `${memPct}%` : '--'}
↓ ${fmtRate(p.net_rx_rate)}
↑ ${fmtRate(p.net_tx_rate)} ↓ ${fmtBytes(p.net_rx_total)}
↑ ${fmtBytes(p.net_tx_total)}
${p.disk_percent != null ? `${diskPct}%` : '--'}
`; }).join(''); return `
${rows}
节点名称 状态 运行时间 负载 (1/5/15m) CPU 内存 实时网速 累计流量 磁盘
`; }; function render(data) { document.querySelector('#cardBtn').classList.toggle('active', mode === 'card'); document.querySelector('#compactBtn').classList.toggle('active', mode === 'compact'); const total = data.length; const onlineCount = data.filter(x => x.status === 'online').length; if (summaryEl) { summaryEl.textContent = `${onlineCount} / ${total} 节点在线`; } if (!data || data.length === 0) { root.innerHTML = '
暂无节点数据
'; return; } if (mode === 'card') { root.className = 'card-view'; root.innerHTML = `
${data.map(renderCard).join('')}
`; } else { root.className = 'table-view'; root.innerHTML = renderTable(data); } } async function load() { try { const r = await fetch('/probe/summary', { cache: 'no-store' }); if (!r.ok) throw Error(); let rawData = await r.json(); if (Array.isArray(rawData)) { rawData.sort((a, b) => { if (a.is_local && !b.is_local) return -1; if (!a.is_local && b.is_local) return 1; return String(a.name || "").localeCompare(String(b.name || "")); }); } window.__data = rawData; render(window.__data); if (lastUpdatedEl) { const now = new Date(); const timeStr = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`; lastUpdatedEl.textContent = `更新于 ${timeStr}`; } } catch (e) { root.innerHTML = '
探针数据暂时不可用,正在重试...
'; } } load(); setInterval(load, 5000); })();