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;