feat: WS实时推送、网速双线图、按钮MD3修复、页面加载器修复 (v0.6.0)

后端:
- 新增 /api/system/ws WebSocket端点 替代HTTP轮询 (每2s推送系统+框架数据)
- 修复 uptime始终为0 (sm.start_time时间戳)
- 网络采集新增TX上行+实时网速(delta法)
- 进程内存采集 (/proc/self/status VmRSS)
- 抑制aiohttp内部WS帧日志防刷爆日志文件

前端:
- 仪表盘: HTTP轮询→WS连接+指数退避自动重连
- 实时日志: 修复onclose重连bug+批量渲染30fps+500行上限防卡死
- 网速卡片: DualLineChart双线图(下行实线/上行虚线)
- Y轴零点偏移-5% 防止零网速贴底
- 所有按钮修复MD3组合类(btn+btn-tonal+btn-sm)
- 页面加载器: 改动态script元素执行 修复onclick全局作用域问题
- emoji图标→Material Design SVG图标
- CSS去残留</style>+新增.nav-item svg约束
- 登录页输入框+标签左对齐+按钮MD3样式
- 多个版本号显示修复(去双重v前缀)
- chart.js/app.js/HTML页面统一加版本号防浏览器缓存
- .gitignore新增docs/目录

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
qinglong
2026-06-11 16:23:31 +08:00
parent fd0a97fec5
commit 4a989bf50f
24 changed files with 759 additions and 212 deletions
+161 -84
View File
@@ -1,105 +1,182 @@
window.DashboardModule = {
ws: null,
reconnectTimer: null,
reconnectDelay: 1000,
charts: {},
init: () => {
// 1. 初始化图表实例
window.DashboardModule.charts = {
mem: new MiniChart('chart-mem', '#9ece6a'),
cpu: new MiniChart('chart-cpu', '#7aa2f7'),
net: new MiniChart('chart-net', '#e0af68'),
init: function() {
var self = this;
// 初始化图表
self.charts = {
mem: new MiniChart('chart-mem', '#9ece6a'),
cpu: new MiniChart('chart-cpu', '#7aa2f7'),
net: new MiniChart('chart-net', '#e0af68'),
speed: new DualLineChart('chart-speed', '#bb9af7', '#c0a8f0'),
proc_mem: new MiniChart('chart-proc-mem', '#f7768e')
};
// 2. 获取并填充右侧固定信息 (只获取一次即可,除非重启)
// 静态信息 (HTTP 一次)
fetchSystemStaticInfo();
// 3. 启动实时数据轮询
fetchDash();
window._dashInterval = setInterval(fetchDash, 2000); // 2秒刷新
// WebSocket 实时推送
self._connect();
},
destroy: () => {
clearInterval(window._dashInterval);
window.DashboardModule.charts = {};
_connect: function() {
var self = this;
if (self.ws) {
self.ws.onclose = null;
self.ws.close();
self.ws = null;
}
var base = window.location.pathname.split("/").slice(0, 2).join("/");
var ws = new WebSocket("ws://" + location.host + base + "/api/system/ws");
self.ws = ws;
ws.onopen = function() {
self.reconnectDelay = 1000;
};
ws.onmessage = function(e) {
try {
var d = JSON.parse(e.data);
if (d.type === "sys") {
self._updateUI(d.system, d.framework);
}
} catch(ex) {}
};
ws.onclose = function() {
self.ws = null;
self.reconnectTimer = setTimeout(function() {
self.reconnectDelay = Math.min(self.reconnectDelay * 1.5, 15000);
self._connect();
}, self.reconnectDelay);
};
ws.onerror = function() { ws.close(); };
},
_updateUI: function(sys, fw) {
// ── 框架卡片 ──
if (fw) {
var el;
el = document.getElementById('d-uptime'); if (el) el.textContent = formatUptime(fw.uptime || 0);
el = document.getElementById('d-plugins'); if (el) el.textContent = fw.plugins || 0;
el = document.getElementById('d-ver-badge'); if (el) el.textContent = fw.version || '?';
}
if (!sys) return;
var self = this;
// ── 内存 ──
if (sys.memory) {
var m = sys.memory.percent || 0;
var el = document.getElementById('d-mem'); if (el) el.textContent = m + '%';
self.charts.mem.update(m);
}
// ── CPU ──
if (sys.cpu) {
var cpuVal = sys.cpu.percent;
if (cpuVal === null || cpuVal === undefined) {
var load = (sys.cpu.load_avg && sys.cpu.load_avg[0]) ? sys.cpu.load_avg[0] : 0;
cpuVal = Math.min(100, (load / (sys.cpu.cores || 1)) * 100);
}
var el = document.getElementById('d-cpu'); if (el) el.textContent = Math.round(cpuVal) + '%';
self.charts.cpu.update(cpuVal);
if (sys.cpu.load_avg) {
el = document.getElementById('info-load'); if (el) el.textContent = sys.cpu.load_avg[2].toFixed(2);
}
}
// ── 网络累计流量 (RX + TX) ──
if (sys.network) {
var el;
el = document.getElementById('d-net-rx'); if (el) el.textContent = formatTraffic(sys.network.rx_mb);
el = document.getElementById('d-net-tx'); if (el) el.textContent = formatTraffic(sys.network.tx_mb);
// Chart shows RX trend
self.charts.net.update(sys.network.rx_mb || 0);
}
// ── 实时网速 ──
if (sys.net_speed) {
var el;
el = document.getElementById('d-speed-rx');
if (el) el.textContent = formatSpeed(sys.net_speed.rx_bytes_sec);
el = document.getElementById('d-speed-tx');
if (el) el.textContent = formatSpeed(sys.net_speed.tx_bytes_sec);
// Chart: RX solid line, TX dashed line
self.charts.speed.update(sys.net_speed.rx_bytes_sec || 0, sys.net_speed.tx_bytes_sec || 0);
}
// ── 进程内存 ──
if (sys.process && sys.process.memory_mb !== undefined) {
var pm = sys.process.memory_mb || 0;
var el = document.getElementById('d-proc-mem'); if (el) el.textContent = pm + ' MB';
self.charts.proc_mem.update(pm);
}
if (sys.process && sys.process.pid !== undefined) {
var el = document.getElementById('info-pid'); if (el) el.textContent = sys.process.pid;
}
},
destroy: function() {
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
if (this.ws) { this.ws.onclose = null; this.ws.close(); this.ws = null; }
this.charts = {};
}
};
/* ── 静态信息 ── */
async function fetchSystemStaticInfo() {
try {
const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json());
// 硬件信息
if(sys.platform) {
document.getElementById('info-os').textContent = sys.platform.system || '--';
document.getElementById('info-arch').textContent = sys.platform.machine || '--';
document.getElementById('info-env').textContent = sys.platform.env || 'Standard';
var sys = await fetch('./api/system', {credentials:'include'}).then(function(r){return r.json();});
var el;
if (sys.platform) {
el = document.getElementById('info-os'); if (el) el.textContent = sys.platform.system || '--';
el = document.getElementById('info-arch'); if (el) el.textContent = sys.platform.machine || '--';
el = document.getElementById('info-env'); if (el) el.textContent = sys.platform.env || 'Standard';
}
if(sys.cpu) {
const c = sys.cpu.cores || 0;
document.getElementById('info-cores').textContent = `${c} / ${c}`; // Android下通常逻辑核=物理核
if (sys.cpu) {
var c = sys.cpu.cores || 0;
el = document.getElementById('info-cores'); if (el) el.textContent = c + ' / ' + c;
}
if(sys.memory) {
document.getElementById('info-mem-total').textContent = sys.memory.total_gb + ' GB';
if (sys.memory) {
el = document.getElementById('info-mem-total'); if (el) el.textContent = sys.memory.total_gb + ' GB';
}
// 框架信息 (部分需结合 API)
const host = window.location.hostname + (window.location.port ? ':'+window.location.port : '');
document.getElementById('info-addr').textContent = host;
// 框架信息 (部分来自 API)
try {
var fw = await fetch('./api/framework', {credentials:'include'}).then(function(r){return r.json();});
el = document.getElementById('info-fw-ver'); if (el) el.textContent = fw.version || '?';
} catch(e) {}
var host = window.location.hostname + (window.location.port ? ':' + window.location.port : '');
el = document.getElementById('info-addr'); if (el) el.textContent = host;
} catch(e) {}
}
async function fetchDash() {
try {
const fw = await fetch('./api/framework', {credentials:'include'}).then(r => r.json());
const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json());
// --- 左侧动态数据更新 ---
if(fw) {
document.getElementById('d-uptime').textContent = formatUptime(fw.uptime || 0);
document.getElementById('d-plugins').textContent = fw.plugins || 0;
if(document.getElementById('d-ver-badge')) document.getElementById('d-ver-badge').textContent = 'v' + (fw.version||'?');
}
if(sys) {
// 内存
const m = sys.memory?.percent || 0;
document.getElementById('d-mem').textContent = m + '%';
window.DashboardModule.charts.mem.update(m);
// CPU (兼容 Android null 情况)
let cpuVal = sys.cpu?.percent;
if (cpuVal === null || cpuVal === undefined) {
const load = sys.cpu?.load_avg?.[0] || 0;
const cores = sys.cpu?.cores || 1;
cpuVal = Math.min(100, (load / cores) * 100);
}
document.getElementById('d-cpu').textContent = Math.round(cpuVal) + '%';
window.DashboardModule.charts.cpu.update(cpuVal);
// 网络 (RX 总量)
const netRx = sys.network?.rx || 0;
document.getElementById('d-net').textContent = netRx + ' MB';
window.DashboardModule.charts.net.update(netRx); // 图表显示总流量趋势
// 进程内存
const pm = sys.process?.memory_mb || 0;
document.getElementById('d-proc-mem').textContent = pm + ' MB';
window.DashboardModule.charts.proc_mem.update(pm);
// --- 右侧动态数据更新 ---
if(sys.process) {
document.getElementById('info-pid').textContent = sys.process.pid || '--';
}
if(sys.cpu?.load_avg) {
document.getElementById('info-load').textContent = sys.cpu.load_avg[2].toFixed(2);
}
}
} catch(e) { console.warn("Dashboard fetch error", e); }
}
// 辅助:秒数转时间格式
/* ── 格式化 ── */
function formatUptime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
var s = Math.floor(seconds % 60);
if (h > 0) return h + 'h ' + m + 'm';
if (m > 0) return m + 'm ' + s + 's';
return s + 's';
}
function formatTraffic(mb) {
if (mb === undefined || mb === null) return '--';
if (mb >= 1024) return (mb / 1024).toFixed(1) + ' GB';
if (mb >= 1) return mb.toFixed(1) + ' MB';
return (mb * 1024).toFixed(0) + ' KB';
}
function formatSpeed(bytesPerSec) {
if (bytesPerSec === undefined || bytesPerSec === null || bytesPerSec < 0) return '0 B/s';
if (bytesPerSec >= 1048576) return (bytesPerSec / 1048576).toFixed(1) + ' MB/s';
if (bytesPerSec >= 1024) return (bytesPerSec / 1024).toFixed(0) + ' KB/s';
return bytesPerSec.toFixed(0) + ' B/s';
}