From f40b87ea52e1258495fad8eac51510736524a474 Mon Sep 17 00:00:00 2001 From: qinglong Date: Sun, 14 Jun 2026 16:24:41 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E4=BB=AA=E8=A1=A8=E7=9B=98=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E5=8F=B7=E4=BB=8Eversion.json=E8=AF=BB=E5=8F=96?= =?UTF-8?q?=EF=BC=8C=E4=BF=AE=E5=A4=8D=E7=A1=AC=E7=BC=96=E7=A0=81v0.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/web_panel/routes/status.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/services/web_panel/routes/status.py b/services/web_panel/routes/status.py index a374ea3..16fd2e5 100644 --- a/services/web_panel/routes/status.py +++ b/services/web_panel/routes/status.py @@ -49,14 +49,14 @@ def setup_routes(app, prefix=''): logger.info(f"📡 系统状态WS端点已注册: {prefix}/api/system/ws (已加认证)") def _read_version(): - """Read version from config file (shared helper)""" - ver = "v0.6.0" + """Read version from version.json (shared helper)""" + ver = "v0.0.0" try: - import yaml, os - config_path = os.path.join(os.path.dirname(__file__), "..", "..", "..", "config", "framework", "base_config.yaml") - with open(config_path) as f: - cfg = yaml.safe_load(f) - ver = cfg.get("framework", {}).get("version", ver) + import json, os + vf = os.path.join(os.path.dirname(__file__), "..", "..", "..", "version.json") + with open(vf) as f: + data = json.load(f) + ver = data.get("framework", {}).get("version", ver) except: pass return ver @@ -193,7 +193,7 @@ async def create_backup(req): if sm: try: init = sm.get_service("init") - ver = init.get_config("base").get("framework", {}).get("version", "0.0.0") + ver = _read_version() except: pass name = f"SenSu_backup_{ver}_{_time.strftime('%Y%m%d_%H%M%S')}.zip" dest = _os.path.join(backup_dir, name) From 280cd3e9946385212dca04ebaa7af0c4269fd231 Mon Sep 17 00:00:00 2001 From: qinglong Date: Sun, 14 Jun 2026 16:30:53 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20HiChart=E7=94=A8canvas=E8=87=AA?= =?UTF-8?q?=E8=BA=ABgetBoundingClientRect=E8=8E=B7=E5=8F=96=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=B0=BA=E5=AF=B8=EF=BC=8C=E6=B6=88=E9=99=A4CSS?= =?UTF-8?q?=E6=8B=89=E4=BC=B8=E5=AF=BC=E8=87=B4=E7=9A=84=E7=BA=BF=E6=9D=A1?= =?UTF-8?q?=E5=8F=98=E5=BD=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/sentinel/dashboard.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/sentinel/dashboard.html b/plugins/sentinel/dashboard.html index a1a33e0..47c84e7 100644 --- a/plugins/sentinel/dashboard.html +++ b/plugins/sentinel/dashboard.html @@ -157,12 +157,12 @@ window.addEventListener('resize', function(){ self._resize(); }); } HiChart.prototype._resize = function() { - var rect = this.canvas.parentElement.getBoundingClientRect(); - var w = Math.max(rect.width - 8, 100); - var h = 60; + // 用 canvas 自身的 CSS 渲染尺寸 → 内部分辨率 = CSS尺寸 × DPR (保持高清) + var box = this.canvas.getBoundingClientRect(); + var w = box.width, h = box.height; + if (w <= 0 || h <= 0) { w = 280; h = 60; } // fallback this.canvas.width = w * this._dpr; this.canvas.height = h * this._dpr; - // CSS controls display size via width:100%;height:60px this.ctx.setTransform(this._dpr, 0, 0, this._dpr, 0, 0); this._draw(); }; From 6c36743b899dbc42bb14b45f65b061c46f2b105d Mon Sep 17 00:00:00 2001 From: qinglong Date: Sun, 14 Jun 2026 16:57:04 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20Sentinel=E8=8A=82=E7=82=B9=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E6=B7=BB=E5=8A=A0HTTP=E9=93=BE=E8=B7=AF=E5=BB=B6?= =?UTF-8?q?=E8=BF=9F=E6=98=BE=E7=A4=BA(=E6=A8=AA=E7=89=88=E5=B7=A6?= =?UTF-8?q?=E4=B8=8B=E8=A7=92/=E7=BD=91=E6=A0=BC=E5=8F=B3=E4=B8=8A?= =?UTF-8?q?=E8=A7=92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/sentinel/__init__.py | 14 ++++++++++---- plugins/sentinel/dashboard.html | 17 ++++++++++++++++- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/plugins/sentinel/__init__.py b/plugins/sentinel/__init__.py index 06c860d..0bf043c 100644 --- a/plugins/sentinel/__init__.py +++ b/plugins/sentinel/__init__.py @@ -116,6 +116,7 @@ class Plugin(PluginWebMixin): "system": c.get("system", {}), "platform": c.get("platform", {}), "last_seen": c.get("last_seen", 0), + "latency_ms": c.get("latency_ms", 0), "error": c.get("error", ""), }) return web.json_response({"nodes": result}) @@ -195,12 +196,13 @@ class Plugin(PluginWebMixin): for node in nodes: if not node.get("enabled", True): continue - online, sys_info, err = await self._fetch_system(node) + online, sys_info, err, latency = await self._fetch_system(node) _NODE_CACHE[node.get("id", "")] = { "online": online, "system": sys_info or {}, "platform": (sys_info or {}).get("platform", {}), "last_seen": time.time(), + "latency_ms": latency, "error": err if not online else "", } # 每次轮询都推送 SSE (确保实时性) @@ -217,14 +219,17 @@ class Plugin(PluginWebMixin): def _sync_fetch(): try: + t0 = time.time() headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} req = urllib.request.Request(f"{url}{base}/api/system", headers=headers) with urllib.request.urlopen(req, timeout=timeout) as resp: + elapsed = round((time.time() - t0) * 1000) # ms if resp.status == 200: - return True, _json.loads(resp.read().decode()), "" - return False, None, f"HTTP {resp.status}" + return True, _json.loads(resp.read().decode()), "", elapsed + return False, None, f"HTTP {resp.status}", elapsed except Exception as e: - return False, None, str(e)[:120] + elapsed = round((time.time() - t0) * 1000) + return False, None, str(e)[:120], elapsed return await asyncio.get_event_loop().run_in_executor(None, _sync_fetch) @@ -242,6 +247,7 @@ class Plugin(PluginWebMixin): "system": c.get("system", {}), "platform": c.get("platform", {}), "last_seen": c.get("last_seen", 0), + "latency_ms": c.get("latency_ms", 0), "error": c.get("error", ""), }) return result diff --git a/plugins/sentinel/dashboard.html b/plugins/sentinel/dashboard.html index 47c84e7..3c19db0 100644 --- a/plugins/sentinel/dashboard.html +++ b/plugins/sentinel/dashboard.html @@ -294,6 +294,7 @@ (sysInfo.trim() ? '
'+esc(sysInfo)+'
' : '')+ '
'+esc(n.url||'')+'
'+ (n.notes ? '
📝 '+esc(n.notes)+'
' : '')+ + '
⏱ —
'+ ''+ '
'+ '
CPU 0%
'+ @@ -311,7 +312,10 @@ '
'; } else { card.innerHTML = - '
'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'
'+ + '
'+ + '
'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'
'+ + '⏱ —'+ + '
'+ (sysInfo.trim() ? '
'+esc(sysInfo)+'
' : '')+ '
'+esc(n.url||'')+'
'+ (n.notes ? '
📝 '+esc(n.notes)+'
' : '')+ @@ -342,6 +346,17 @@ sysEl.textContent = si2; } else if (sysEl) { sysEl.remove(); } + // 延迟 (横版: col-left底部 / 网格: 右上角) + var latEl = card.querySelector('.node-latency') || card.querySelector('.latency-val'); + if (latEl && n.latency_ms > 0) { + var ms = n.latency_ms; + latEl.textContent = '⏱ ' + (ms < 1000 ? ms + 'ms' : (ms/1000).toFixed(1)+'s'); + latEl.style.color = ms < 100 ? 'var(--success)' : ms < 500 ? 'var(--warning,#e0af68)' : 'var(--error)'; + } else if (latEl && n.online === false) { + latEl.textContent = '⏱ —'; + latEl.style.color = 'var(--text-dim)'; + } + var body = isRow ? card.querySelector('.col-right-stack') : card.querySelector('.metrics-body'); if (!body) return;