fix: move system health panel into insights

This commit is contained in:
Michael Lam
2026-05-05 09:48:13 -07:00
committed by test
parent fdeac578da
commit fe9e4645ac
7 changed files with 70 additions and 41 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@@ -79,27 +79,6 @@
</div>
<div class="app-titlebar-spacer" aria-hidden="true"></div>
</header>
<section class="system-health-panel loading" id="systemHealthPanel" aria-label="Host resource health" aria-live="polite">
<div class="system-health-title">
<span class="system-health-dot" aria-hidden="true"></span>
<span>VPS health</span>
<span class="system-health-status" id="systemHealthStatus">Loading…</span>
</div>
<div class="system-health-metrics">
<div class="system-health-metric" data-system-health-metric="cpu">
<div class="system-health-label"><span>CPU</span><span class="system-health-value" data-system-health-value></span></div>
<div class="system-health-bar" role="progressbar" aria-label="CPU usage" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="system-health-bar-fill"></div></div>
</div>
<div class="system-health-metric" data-system-health-metric="memory">
<div class="system-health-label"><span>RAM</span><span class="system-health-value" data-system-health-value></span></div>
<div class="system-health-bar" role="progressbar" aria-label="RAM usage" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="system-health-bar-fill"></div></div>
</div>
<div class="system-health-metric" data-system-health-metric="disk">
<div class="system-health-label"><span>Disk</span><span class="system-health-value" data-system-health-value></span></div>
<div class="system-health-bar" role="progressbar" aria-label="Disk usage" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="system-health-bar-fill"></div></div>
</div>
</div>
</section>
<div class="layout">
<nav class="rail" aria-label="Primary navigation">
<button class="rail-btn nav-tab active" data-panel="chat" onclick="switchPanel('chat')" title="Chat" data-i18n-title="tab_chat" aria-label="Chat"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg></button>

View File

@@ -215,6 +215,7 @@ async function switchPanel(name, opts = {}) {
if (nextPanel === 'insights') await loadInsights();
if (nextPanel === 'logs') await loadLogs();
_syncLogsAutoRefresh();
if (typeof _syncSystemHealthMonitorVisibility === 'function') _syncSystemHealthMonitorVisibility();
if (nextPanel === 'settings') {
switchSettingsSection(_currentSettingsSection);
loadSettingsPanel();
@@ -2118,6 +2119,8 @@ async function loadInsights(animate) {
api('/api/wiki/status').catch(err => ({status:'error', error: err.message || String(err)})),
]);
_renderInsights(data, box, wikiStatus);
if (typeof _syncSystemHealthMonitorVisibility === 'function') _syncSystemHealthMonitorVisibility();
if (typeof pollSystemHealth === 'function') void pollSystemHealth();
} catch(e) {
box.innerHTML = `<div style="color:var(--accent);font-size:12px">${esc(t('error_prefix') + e.message)}</div>`;
} finally {
@@ -2134,6 +2137,34 @@ function _formatLlmWikiTimestamp(value) {
catch (_) { return String(value); }
}
function _renderSystemHealthPanel() {
return `
<section class="insights-card system-health-panel loading" id="systemHealthPanel" aria-label="Host resource health" aria-live="polite">
<div class="system-health-head">
<div>
<div class="insights-card-title">System health</div>
<div class="system-health-sub">Current VPS resource usage</div>
</div>
<span class="system-health-status" id="systemHealthStatus"><span class="system-health-dot" aria-hidden="true"></span>Loading…</span>
</div>
<div class="system-health-metrics">
<div class="system-health-metric" data-system-health-metric="cpu">
<div class="system-health-label"><span>CPU</span><span class="system-health-value" data-system-health-value>—</span></div>
<div class="system-health-bar" role="progressbar" aria-label="CPU usage" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="system-health-bar-fill"></div></div>
</div>
<div class="system-health-metric" data-system-health-metric="memory">
<div class="system-health-label"><span>RAM</span><span class="system-health-value" data-system-health-value>—</span></div>
<div class="system-health-bar" role="progressbar" aria-label="RAM usage" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="system-health-bar-fill"></div></div>
</div>
<div class="system-health-metric" data-system-health-metric="disk">
<div class="system-health-label"><span>Disk</span><span class="system-health-value" data-system-health-value>—</span></div>
<div class="system-health-bar" role="progressbar" aria-label="Disk usage" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0"><div class="system-health-bar-fill"></div></div>
</div>
</div>
<div class="system-health-foot">Live snapshot only; historical resource charts can build on this surface later.</div>
</section>`;
}
function _renderLlmWikiStatus(d) {
const status = d || {status:'error'};
const isReady = status.available && status.status === 'ready';
@@ -2279,6 +2310,7 @@ function _renderInsights(d, box, wikiStatus) {
</div>`;
box.innerHTML = `
${_renderSystemHealthPanel()}
${_renderLlmWikiStatus(wikiStatus)}
<div class="insights-grid">
${overviewCards.map(c => `<div class="insights-stat"><div class="insights-stat-icon">${c.icon}</div><div class="insights-stat-info"><div class="insights-stat-value">${c.value}</div><div class="insights-stat-label">${esc(c.label)}</div></div></div>`).join('')}

View File

@@ -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 */

View File

@@ -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);

View File

@@ -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('<div class="layout">')]
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