diff --git a/docs/pr-media/1688/chat-no-health-bar.png b/docs/pr-media/1688/chat-no-health-bar.png new file mode 100644 index 00000000..f79ee650 Binary files /dev/null and b/docs/pr-media/1688/chat-no-health-bar.png differ diff --git a/docs/pr-media/1688/insights-system-health.png b/docs/pr-media/1688/insights-system-health.png new file mode 100644 index 00000000..c9788835 Binary files /dev/null and b/docs/pr-media/1688/insights-system-health.png differ diff --git a/static/index.html b/static/index.html index 5a09626b..0903badd 100644 --- a/static/index.html +++ b/static/index.html @@ -79,27 +79,6 @@ -
-
- - VPS health - Loading… -
-
-
-
CPU—
-
-
-
-
RAM—
-
-
-
-
Disk—
-
-
-
-
`; box.innerHTML = ` + ${_renderSystemHealthPanel()} ${_renderLlmWikiStatus(wikiStatus)}
${overviewCards.map(c => `
${c.icon}
${c.value}
${esc(c.label)}
`).join('')} diff --git a/static/style.css b/static/style.css index b1a66c3c..d5e8fe61 100644 --- a/static/style.css +++ b/static/style.css @@ -293,18 +293,20 @@ .layout{display:flex;width:100%;flex:1 1 auto;min-height:0;} .app-titlebar{display:flex;align-items:center;justify-content:center;height:38px;flex-shrink:0;background:var(--sidebar);border-bottom:1px solid var(--border);padding:0 12px;padding-top:var(--app-titlebar-safe-top);padding-left:max(12px,env(safe-area-inset-left,0));padding-right:max(12px,env(safe-area-inset-right,0));box-sizing:content-box;font-size:12px;color:var(--muted);user-select:none;-webkit-app-region:drag;position:relative;z-index:20;} .app-titlebar-inner{display:flex;align-items:center;gap:8px;min-width:0;max-width:100%;justify-content:center;} - .system-health-panel{display:flex;align-items:center;gap:14px;min-height:38px;padding:6px 14px;padding-left:max(14px,env(safe-area-inset-left,0));padding-right:max(14px,env(safe-area-inset-right,0));border-bottom:1px solid var(--border);background:color-mix(in srgb,var(--sidebar) 92%,var(--bg));color:var(--muted);box-sizing:border-box;flex-shrink:0;} + .system-health-panel.insights-card{display:flex;flex-direction:column;gap:12px;color:var(--muted);} .system-health-panel.unavailable{display:none;} - .system-health-title{display:flex;align-items:center;gap:7px;font-size:11px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;white-space:nowrap;color:var(--muted);} + .system-health-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;} + .system-health-sub{font-size:11px;color:var(--muted);margin-top:-4px;} .system-health-dot{width:7px;height:7px;border-radius:999px;background:var(--accent);box-shadow:0 0 0 3px var(--accent-bg);opacity:.88;} .system-health-panel.loading .system-health-dot{background:var(--muted);box-shadow:none;opacity:.55;} - .system-health-status{font-size:10px;font-weight:600;letter-spacing:0;text-transform:none;color:var(--muted);opacity:.8;} - .system-health-metrics{display:grid;grid-template-columns:repeat(3,minmax(90px,1fr));gap:10px;flex:1;min-width:0;} - .system-health-metric{min-width:0;display:flex;flex-direction:column;gap:3px;} + .system-health-status{display:inline-flex;align-items:center;gap:7px;border-radius:999px;padding:3px 8px;font-size:11px;font-weight:700;border:1px solid var(--border);color:var(--muted);background:var(--surface);white-space:nowrap;} + .system-health-metrics{display:grid;grid-template-columns:repeat(3,minmax(120px,1fr));gap:10px;min-width:0;} + .system-health-metric{min-width:0;display:flex;flex-direction:column;gap:5px;padding:10px 11px;border:1px solid var(--border);border-radius:8px;background:var(--surface);} .system-health-label{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:11px;line-height:1;color:var(--muted);} .system-health-value{font-variant-numeric:tabular-nums;color:var(--text);font-weight:650;} .system-health-bar{height:5px;overflow:hidden;border-radius:999px;background:color-mix(in srgb,var(--border) 70%,transparent);border:1px solid color-mix(in srgb,var(--border) 75%,transparent);} .system-health-bar-fill{height:100%;width:0%;border-radius:inherit;background:linear-gradient(90deg,var(--accent),var(--accent-hover));transition:width .25s ease;} + .system-health-foot{font-size:11px;color:var(--muted);line-height:1.45;opacity:.82;} .app-titlebar-icon{display:inline-flex;align-items:center;color:var(--accent);} .app-titlebar-title{font-size:12px;font-weight:600;color:var(--text);letter-spacing:-.01em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:60vw;} .app-titlebar-sub{font-size:10px;color:var(--muted);background:var(--hover-bg);padding:2px 7px;border-radius:4px;font-family:'SF Mono',ui-monospace,monospace;white-space:nowrap;flex-shrink:0;} @@ -1292,9 +1294,9 @@ .app-titlebar{justify-content:space-between;} .app-titlebar-hamburger,.app-titlebar-spacer{display:flex;} .app-titlebar-inner{flex:1 1 auto;} - .system-health-panel{align-items:stretch;gap:6px;min-height:0;padding:6px 10px;flex-direction:column;} - .system-health-title{font-size:10px;justify-content:space-between;} - .system-health-metrics{grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;} + .system-health-panel.insights-card{gap:10px;padding:12px;} + .system-health-head{align-items:flex-start;} + .system-health-metrics{grid-template-columns:1fr;gap:8px;} .system-health-label{font-size:10px;gap:4px;} .system-health-bar{height:4px;} /* Overlay backdrop */ diff --git a/static/ui.js b/static/ui.js index dd7e3d45..6711bb30 100644 --- a/static/ui.js +++ b/static/ui.js @@ -3155,6 +3155,7 @@ function renderSystemHealth(payload){ } async function pollSystemHealth(){ if(document.visibilityState !== 'visible') return; + if(!_systemHealthPanelIsVisible()) return; try{ const payload=await api('/api/system/health'); renderSystemHealth(payload); @@ -3162,8 +3163,13 @@ async function pollSystemHealth(){ setSystemHealthUnavailable('Unavailable'); } } +function _systemHealthPanelIsVisible(){ + return document.visibilityState === 'visible' && + !!document.querySelector('main.main.showing-insights') && + !!$('systemHealthPanel'); +} function startSystemHealthMonitor(){ - if(document.visibilityState !== 'visible') return; + if(!_systemHealthPanelIsVisible()) return; if(_systemHealthTimer) return; void pollSystemHealth(); _systemHealthTimer=setInterval(pollSystemHealth,SYSTEM_HEALTH_INTERVAL_MS); @@ -3172,7 +3178,7 @@ function stopSystemHealthMonitor(){ if(_systemHealthTimer){clearInterval(_systemHealthTimer);_systemHealthTimer=null;} } function _syncSystemHealthMonitorVisibility(){ - if(document.visibilityState === 'visible') startSystemHealthMonitor(); + if(_systemHealthPanelIsVisible()) startSystemHealthMonitor(); else stopSystemHealthMonitor(); } document.addEventListener('visibilitychange',_syncSystemHealthMonitorVisibility); diff --git a/tests/test_issue693_system_health_panel.py b/tests/test_issue693_system_health_panel.py index d35e52bb..1161dc0a 100644 --- a/tests/test_issue693_system_health_panel.py +++ b/tests/test_issue693_system_health_panel.py @@ -10,6 +10,7 @@ from urllib.parse import urlparse REPO_ROOT = pathlib.Path(__file__).parent.parent UI_JS = (REPO_ROOT / "static" / "ui.js").read_text(encoding="utf-8") +PANELS_JS = (REPO_ROOT / "static" / "panels.js").read_text(encoding="utf-8") INDEX_HTML = (REPO_ROOT / "static" / "index.html").read_text(encoding="utf-8") STYLE_CSS = (REPO_ROOT / "static" / "style.css").read_text(encoding="utf-8") ROUTES_PY = (REPO_ROOT / "api" / "routes.py").read_text(encoding="utf-8") @@ -137,27 +138,36 @@ def test_system_health_route_returns_only_sanitized_payload(monkeypatch): assert set(payload) == {"status", "available", "checked_at", "cpu", "memory", "disk", "errors"} -def test_system_health_panel_markup_and_styles_exist(): - assert 'id="systemHealthPanel"' in INDEX_HTML - assert 'aria-label="Host resource health"' in INDEX_HTML - assert 'data-system-health-metric="cpu"' in INDEX_HTML - assert 'data-system-health-metric="memory"' in INDEX_HTML - assert 'data-system-health-metric="disk"' in INDEX_HTML - assert ".system-health-panel" in STYLE_CSS +def test_system_health_panel_markup_and_styles_live_under_insights_not_top_chrome(): + top_shell = INDEX_HTML[: INDEX_HTML.index('
')] + assert 'id="systemHealthPanel"' not in top_shell + assert 'aria-label="Host resource health"' not in top_shell + assert 'function _renderSystemHealthPanel()' in PANELS_JS + assert 'id="systemHealthPanel"' in PANELS_JS + assert 'aria-label="Host resource health"' in PANELS_JS + assert 'System health' in PANELS_JS + assert 'Current VPS resource usage' in PANELS_JS + assert PANELS_JS.index('_renderSystemHealthPanel()') < PANELS_JS.index('_renderLlmWikiStatus(wikiStatus)') + assert 'data-system-health-metric="cpu"' in PANELS_JS + assert 'data-system-health-metric="memory"' in PANELS_JS + assert 'data-system-health-metric="disk"' in PANELS_JS + assert ".system-health-panel.insights-card" in STYLE_CSS assert ".system-health-bar-fill" in STYLE_CSS assert ".system-health-panel.unavailable" in STYLE_CSS - assert "@media(max-width:640px)" in STYLE_CSS and ".system-health-panel" in STYLE_CSS + assert "@media(max-width:640px)" in STYLE_CSS and ".system-health-panel.insights-card" in STYLE_CSS def test_system_health_frontend_polls_visible_and_renders_progress_labels(): assert "const SYSTEM_HEALTH_INTERVAL_MS=5000" in UI_JS assert "api('/api/system/health')" in UI_JS assert "document.visibilityState !== 'visible'" in UI_JS + assert "document.querySelector('main.main.showing-insights')" in UI_JS assert "document.addEventListener('visibilitychange',_syncSystemHealthMonitorVisibility)" in UI_JS + assert "typeof _syncSystemHealthMonitorVisibility === 'function'" in PANELS_JS assert "function renderSystemHealth(payload)" in UI_JS assert "setSystemHealthUnavailable" in UI_JS - assert "data-system-health-metric" in INDEX_HTML - assert "CPU" in INDEX_HTML and "RAM" in INDEX_HTML and "Disk" in INDEX_HTML + assert "data-system-health-metric" in PANELS_JS + assert "CPU" in PANELS_JS and "RAM" in PANELS_JS and "Disk" in PANELS_JS assert "aria-valuenow" in UI_JS assert "style.width=`${percent}%`" in UI_JS