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:
+128
-21
@@ -1,25 +1,132 @@
|
||||
window.LogsModule = {
|
||||
ws: null,
|
||||
init: () => {
|
||||
const box = document.getElementById("log-box");
|
||||
const base = window.location.pathname.split("/").slice(0,2).join("/");
|
||||
const connect = () => {
|
||||
window.LogsModule.ws = new WebSocket("ws://"+location.host+base+"/api/logs/ws");
|
||||
window.LogsModule.ws.onopen = () => box.innerHTML += "<div style=\"color:var(--success)\">Connected</div>";
|
||||
window.LogsModule.ws.onmessage = e => {
|
||||
try {
|
||||
const d = JSON.parse(e.data);
|
||||
if(d.type === "log") {
|
||||
const cls = d.level === "ERROR" ? "log-ERROR" : d.level === "WARNING" ? "log-WARNING" : "log-INFO";
|
||||
const t = d.timestamp ? new Date(d.timestamp*1000).toLocaleTimeString() : "--";
|
||||
box.innerHTML += "<div class=\"log-entry\"><span style=\"color:#555;margin-right:5px\">"+t+"</span><span class=\""+cls+"\">["+d.level+"]</span> "+d.message+"</div>";
|
||||
box.scrollTop = box.scrollHeight;
|
||||
}
|
||||
}catch(e){}
|
||||
};
|
||||
window.LogsModule.ws.onclose = setTimeout(connect, 3000);
|
||||
};
|
||||
connect();
|
||||
reconnectTimer: null,
|
||||
reconnectDelay: 1000,
|
||||
maxLines: 500,
|
||||
lineCount: 0,
|
||||
batchBuffer: [],
|
||||
batchTimer: null,
|
||||
|
||||
init: function() {
|
||||
var self = this;
|
||||
self.box = document.getElementById("log-box");
|
||||
if (!self.box) return;
|
||||
self.box.innerHTML = "";
|
||||
self.lineCount = 0;
|
||||
self._connect();
|
||||
},
|
||||
destroy: () => window.LogsModule.ws?.close()
|
||||
|
||||
_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/logs/ws");
|
||||
self.ws = ws;
|
||||
|
||||
ws.onopen = function() {
|
||||
self.reconnectDelay = 1000;
|
||||
self._appendHTML('<div style="color:var(--md-sys-color-primary)">🟢 Connected</div>');
|
||||
};
|
||||
|
||||
ws.onmessage = function(e) {
|
||||
try {
|
||||
var d = JSON.parse(e.data);
|
||||
if (d.type === "log") {
|
||||
self._bufferLog(d);
|
||||
}
|
||||
} catch(ex) {}
|
||||
};
|
||||
|
||||
ws.onclose = function() {
|
||||
self.ws = null;
|
||||
self._appendHTML('<div style="color:var(--error)">🔴 Disconnected — reconnecting...</div>');
|
||||
self.reconnectTimer = setTimeout(function() {
|
||||
self.reconnectDelay = Math.min(self.reconnectDelay * 1.5, 15000);
|
||||
self._connect();
|
||||
}, self.reconnectDelay);
|
||||
};
|
||||
|
||||
ws.onerror = function() {
|
||||
ws.close();
|
||||
};
|
||||
},
|
||||
|
||||
/* Buffer log entries then flush at ~30fps to avoid DOM thrashing */
|
||||
_bufferLog: function(d) {
|
||||
var self = this;
|
||||
self.batchBuffer.push(d);
|
||||
if (!self.batchTimer) {
|
||||
self.batchTimer = setTimeout(function() {
|
||||
self._flush();
|
||||
self.batchTimer = null;
|
||||
}, 33); // ~30fps flush
|
||||
}
|
||||
},
|
||||
|
||||
_flush: function() {
|
||||
var self = this;
|
||||
var batch = self.batchBuffer;
|
||||
self.batchBuffer = [];
|
||||
if (!batch.length || !self.box) return;
|
||||
|
||||
var html = '';
|
||||
for (var i = 0; i < batch.length; i++) {
|
||||
var d = batch[i];
|
||||
var cls = d.level === "ERROR" ? "log-ERROR" : d.level === "WARNING" ? "log-WARNING" : "log-INFO";
|
||||
var t = d.timestamp ? new Date(d.timestamp * 1000).toLocaleTimeString() : "--";
|
||||
html += '<div class="log-entry">' +
|
||||
'<span style="color:#555;margin-right:5px">' + t + '</span>' +
|
||||
'<span class="' + cls + '">[' + d.level + ']</span> ' +
|
||||
self._escapeHtml(d.message || '') + '</div>';
|
||||
}
|
||||
|
||||
self._appendHTML(html);
|
||||
},
|
||||
|
||||
_appendHTML: function(html) {
|
||||
var self = this;
|
||||
if (!self.box) return;
|
||||
|
||||
// Estimate line count from <div> tags
|
||||
var newLines = (html.match(/<div/g) || []).length;
|
||||
self.lineCount += newLines;
|
||||
|
||||
// Trim old lines if over cap
|
||||
while (self.lineCount > self.maxLines && self.box.firstChild) {
|
||||
// Count lines in the first child
|
||||
var removed = 1;
|
||||
if (self.box.firstChild.nodeType === 1) {
|
||||
var inner = self.box.firstChild.innerHTML || '';
|
||||
removed = Math.max(1, (inner.match(/<div/g) || []).length);
|
||||
}
|
||||
self.box.removeChild(self.box.firstChild);
|
||||
self.lineCount = Math.max(0, self.lineCount - removed);
|
||||
}
|
||||
|
||||
// Efficient append: insertAdjacentHTML instead of innerHTML +=
|
||||
self.box.insertAdjacentHTML("beforeend", html);
|
||||
self.box.scrollTop = self.box.scrollHeight;
|
||||
},
|
||||
|
||||
_escapeHtml: function(text) {
|
||||
return String(text)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
},
|
||||
|
||||
destroy: function() {
|
||||
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
|
||||
if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; }
|
||||
if (this.ws) { this.ws.onclose = null; this.ws.close(); this.ws = null; }
|
||||
this.batchBuffer = [];
|
||||
this.box = null;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user