fix: move system health panel into insights
This commit is contained in:
BIN
docs/pr-media/1688/chat-no-health-bar.png
Normal file
BIN
docs/pr-media/1688/chat-no-health-bar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 132 KiB |
BIN
docs/pr-media/1688/insights-system-health.png
Normal file
BIN
docs/pr-media/1688/insights-system-health.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
@@ -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>
|
||||
|
||||
@@ -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('')}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
10
static/ui.js
10
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);
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user