/* * SysDeck - Glances Panel (v0.0.47) * Author: Jeremy Anderson (https://dcos.net) * * v0.0.47 — DEFAULT-ON EMBEDDED WEBUI. Per user directive: "the glances * we should default to enabling the built in webui and embedding that * into our module instead it visually looks stunning in comparison to * ours." v0.0.34 added the webui integration but kept it opt-in (the * operator had to click "Start Web UI" each time they opened the panel) * and rendered the legacy SysDeck snapshot cards above the iframe. * v0.0.47 flips the default: * * 1. AUTO-START: when the panel mounts and glances is installed but * the webserver isn't running, the panel calls * bridge.glances.startWeb() automatically. The operator sees a * "Starting Glances web UI …" stub for <1s, then the full Glances * web UI loads in the iframe. No click required. * 2. EMBEDDED-FIRST LAYOUT: the iframe is now the primary view, * sized to fill the viewport (min-height: calc(100vh - 200px)). * The legacy snapshot cards (CPU / Memory / Swap / Network / Disk * / Processes) are moved into a collapsed
at the bottom * of the page so they don't push the iframe below the fold. The * operator can still expand them for a quick numeric read, but * the default view is the Glances web UI. * 3. STOP ON UNMOUNT (best-effort): when the panel is unmounted * (operator navigates away), we don't stop the webserver — it's * cheap to keep running and the operator may re-open the panel * soon. The existing Stop button is still there for explicit * shutdown. (If we wanted to be aggressive we could stop on * pagehide, but that would slow re-entry.) * * v0.0.34 INTEGRATES THE GLANCES BUILT-IN WEB UI as a module. * The user directive: "glances is not integrated yet i just assumed * you would integrate the built in webui as a module." Glances ships * a webserver via `glances -w` (default 127.0.0.1:61208) that serves * the full Glances web UI — every chart, every sensor, every top * process, every history graph. SysDeck starts that webserver as a * background process via bridge.glances.startWeb() and iframes the * running web UI into this panel. No SysDeck-side reimplementation * of the Glances UI. * * Glances is GPL-3.0 licensed by Nicolargo. The bridge helper invokes * it as a separate process via subprocess — the suite (MIT) and * Glances (GPL-3.0) remain independent programs. No Glances code is * bundled. */ // v0.0.47: how long to wait between calling startWeb() and re-checking // web-status. Glances typically takes <1s on a warm start, but we give // it a small grace period before re-rendering so the iframe doesn't // load a half-up webserver. const WEB_START_POLL_MS = 800; export async function mount(panel, { bridge, EventBus }) { panel.innerHTML = renderSkeleton(); let webStatus = await safe(bridge.glances.webStatus(), {}); let snapshot = await safe(bridge.glances.snapshot(), {}); // v0.0.47: AUTO-START. If glances is installed and the webserver // isn't running, start it automatically. The operator gets the // full Glances web UI on first paint instead of an empty iframe // and a "click here" prompt. if (webStatus?.available !== false && !webStatus?.running) { panel.innerHTML = renderStartingCard(webStatus); try { const startResp = await bridge.glances.startWeb(); if (startResp?.started || startResp?.already_running) { // Poll web-status after a short grace period so the // webserver has time to bind the socket. await new Promise((r) => setTimeout(r, WEB_START_POLL_MS)); webStatus = await safe(bridge.glances.webStatus(), webStatus); // Re-fetch the snapshot too — it's now backed by the // running webserver's data. snapshot = await safe(bridge.glances.snapshot(), snapshot); } else if (startResp?.error) { console.warn('glances.startWeb returned error:', startResp.error); } } catch (err) { console.warn('glances.startWeb threw:', err); } } const webRunning = webStatus?.running === true; const webUrl = webStatus?.url || 'http://127.0.0.1:61208'; const glancesAvailable = webStatus?.available !== false; panel.innerHTML = `

System Monitor

Glances — real-time system overview · GPL-3.0 · Nicolargo ${webRunning ? ` · web UI running` : (glancesAvailable ? ` · web UI stopped` : ` · glances not installed`)} ${webRunning && webStatus?.pid ? ` · PID ${webStatus.pid}` : ''}

${renderWebControls(webStatus, webRunning, webUrl)} ${webRunning ? renderWebIframe(webUrl) : ''} ${!webRunning && glancesAvailable ? renderStartPrompt(webUrl) : ''} ${glancesAvailable ? renderLegacySnapshotDetails(snapshot) : ''} `; wireEvents(panel, { bridge, EventBus }); EventBus.emit('glances.loaded', { webRunning, autoStarted: webRunning }); } // ── Skeleton + starting states ────────────────────────────────────── function renderSkeleton() { return `
`; } function renderStartingCard(webStatus) { // v0.0.47: shown while we're auto-starting the webserver. const url = webStatus?.url || 'http://127.0.0.1:61208'; return `

System Monitor

Glances — real-time system overview · GPL-3.0 · Nicolargo · starting web UI…

Starting Glances web UI…

v0.0.47 auto-start: the panel is launching the built-in Glances webserver at ${escapeHtml(url)} via glances -w --bind 127.0.0.1 --port 61208. The full web UI will appear here in a moment — every chart, every sensor, every top process, every history graph, without SysDeck re-implementing any of it.

If this takes more than a few seconds, click ↻ Refresh or check the cockpit bridge log. The first start may be slow if glances needs to warm its import cache.

`; } // ── Web UI controls ───────────────────────────────────────────────── function renderWebControls(webStatus, webRunning, webUrl) { if (webStatus?.available === false) { return `

Glances Web UI

${escapeHtml(webStatus.reason || 'Glances is not installed.')}

${webStatus.install ? `

${escapeHtml(webStatus.install)}

` : ''}

Glances is GPL-3.0 licensed by Nicolargo — https://github.com/nicolargo/glances

`; } return `

Glances Web UI

${webRunning ? `Running at ${escapeHtml(webUrl)}${webStatus.pid ? ` (PID ${webStatus.pid})` : ''}. The iframe below loads the full Glances web UI — every chart, every sensor, every top process, every history graph. No SysDeck-side reimplementation. v0.0.47 auto-starts this on panel mount; click Stop to disable.` : `Stopped. v0.0.47 default is auto-start on panel mount — click ▶ Start Web UI to launch it manually, or ↻ Refresh to re-trigger the auto-start. The bridge runs glances -w --bind 127.0.0.1 --port 61208 as a background process via the cockpit superuser channel.`}

`; } function renderWebIframe(webUrl) { // v0.0.47: iframe is now the primary view — sized to fill the // viewport. min-height uses calc(100vh - 200px) so the iframe // extends to just above the page footer, leaving room for the // controls card above and the legacy snapshot
below. return `

Web UI — ${escapeHtml(webUrl)}

↗ Open in new tab
`; } function renderStartPrompt(webUrl) { // Shown when the webserver isn't running and didn't auto-start // (e.g. glances is installed but startWeb returned an error). return `

Web UI not running

The auto-start didn't bring up the Glances webserver. Click ▶ Start Web UI above to try again, or ↻ Refresh to re-run the auto-start sequence. Expected URL: ${escapeHtml(webUrl)}.

`; } // ── Legacy snapshot (collapsed
) ─────────────────────────── function renderLegacySnapshotDetails(snapshot) { // v0.0.47: the snapshot cards are kept (they're a useful numeric // read) but moved into a collapsed
so the iframe is the // primary view. The operator can expand for the SysDeck-rendered // CPU/Memory/Swap/Network/Disk/Processes summary. return `

Legacy SysDeck Snapshot (collapsed — iframe above is the primary view)

${renderSnapshotCards(snapshot)}
`; } function renderSnapshotCards(snapshot) { const cpu = snapshot.cpu || {}; const mem = snapshot.mem || {}; const swap = snapshot.memswap || {}; const network = snapshot.network || {}; const fs = snapshot.fs || {}; const procs = snapshot.processcount || {}; return `

CPU

${(cpu.total || 0).toFixed(1)}%
user: ${(cpu.user || 0).toFixed(1)}% · system: ${(cpu.system || 0).toFixed(1)}% · idle: ${(cpu.idle || 0).toFixed(1)}%

Memory

${(mem.percent || 0).toFixed(1)}%
${fmtMB(mem.used || 0)} / ${fmtMB(mem.total || 0)}

Swap

${(swap.percent || 0).toFixed(1)}%
${fmtMB(swap.used || 0)} / ${fmtMB(swap.total || 0)}

Network Interfaces

${Object.entries(network).map(([iface, n]) => ``).join('') || ''}
InterfaceRx/sTx/sRx ErrTx Err
${escapeHtml(iface)} ${fmtKB(n.rx || 0)}/s ${fmtKB(n.tx || 0)}/s ${n.rx_errors || 0} ${n.tx_errors || 0}
No network data.

Disk I/O

${Object.entries(fs).map(([dev, d]) => ``).join('') || ''}
DeviceRead/sWrite/s
${escapeHtml(dev)} ${fmtKB(d.r_bytes_ps || 0)}/s ${fmtKB(d.w_bytes_ps || 0)}/s
No disk data.

Processes: ${procs.total || 0} total · ${procs.running || 0} running · ${procs.sleeping || 0} sleeping · ${procs.thread || 0} threads

`; } // ── Event wiring ──────────────────────────────────────────────────── function wireEvents(panel, { bridge, EventBus }) { panel.querySelector('#btn-glances-start-web')?.addEventListener('click', async () => { const btn = panel.querySelector('#btn-glances-start-web'); if (btn) { btn.disabled = true; btn.textContent = 'Starting ...'; } try { const r = await bridge.glances.startWeb(); if (r.started || r.already_running) { // Give the webserver a moment to bind before re-mount. await new Promise((res) => setTimeout(res, WEB_START_POLL_MS)); mount(panel, { bridge, EventBus }); } else { if (btn) { btn.disabled = false; btn.textContent = '▶ Start Web UI'; } alert(`Failed to start Glances web UI:\n${r.error || r.reason || 'unknown'}`); } } catch (err) { if (btn) { btn.disabled = false; btn.textContent = '▶ Start Web UI'; } alert(`Start error: ${err.message || err}`); } }); panel.querySelector('#btn-glances-stop-web')?.addEventListener('click', async () => { const btn = panel.querySelector('#btn-glances-stop-web'); if (btn) { btn.disabled = true; } try { await bridge.glances.stopWeb(); setTimeout(() => mount(panel, { bridge, EventBus }), 500); } catch (err) { if (btn) { btn.disabled = false; } alert(`Stop error: ${err.message || err}`); } }); panel.querySelector('#btn-glances-refresh')?.addEventListener('click', () => { mount(panel, { bridge, EventBus }); }); } // ── Utilities ─────────────────────────────────────────────────────── async function safe(p, fallback) { try { const v = await p; return v ?? fallback; } catch { return fallback; } } function fmtMB(kb) { return (kb / 1024).toFixed(1) + ' MB'; } function fmtKB(bytes) { return (bytes / 1024).toFixed(1) + ' KB'; } function escapeHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); }