/* * SysDeck - Monitoring Panel (v0.0.43) * Author: Jeremy Anderson (https://dcos.net) * * Shared tabbed module hosting Prometheus + Grafana — the two * observability stack components. Per user directive: "we have 2 * modules left, we can actually have them share a module with tabs * similar to the container/vm module. we should add prometheus, and * graphana webui modules." * * The panel has two tabs: * 1. Prometheus — status card (version, uptime, targets, alerts) + * iframe of the real Prometheus web UI at http://127.0.0.1:9095 * 2. Grafana — status card (version, dashboards, datasources) + * iframe of the real Grafana web UI at http://127.0.0.1:3000 * * Both tabs back their data with REAL bridge helpers (bridge/prometheus.py * and bridge/grafana.py) that call the actual HTTP APIs. No mock data. * The iframes load the real web UIs directly — same pattern as the * v0.0.34 Glances integration. * * v0.0.43 hardening applied to both bridge helpers: * - NoRedirectHandler on all HTTP calls (SSRF defense, CVE-2020-35850) * - 127.0.0.1-only URL check (SSRF defense) * - Env scrubbed on every subprocess (CVE-2024-6126) * - Output sanitized (CVE-2022-36446) * - No sudo — cockpit superuser channel + polkit handles auth * (CVE-2022-0824 lesson — the v0.0.15-era sudo shell-out is gone) * * Bridge surface (see shared/bridge.js → bridge.prometheus + bridge.grafana): * Prometheus: * summary() → overall status + version + targets + alerts count * targets() → scrape target health (up/down/duration) * alerts() → current firing + pending alerts * rules() → alerting + recording rules * config() → full Prometheus config * logSummary() → pushgateway log pipeline throughput * restart() → systemctl restart prometheus.service (superuser) * reload() → SIGHUP config reload (superuser) * Grafana: * summary() → overall status + version + dashboard count * dashboards() → list of provisioned dashboards * datasources() → list of configured datasources * alerts() → Grafana-managed alerts * health() → Grafana health endpoint * org() → current organization info * users() → Grafana user list * plugins() → installed Grafana plugins * search(query) → search dashboards by name * restart() → systemctl restart grafana-server.service (superuser) * reload() → SIGUSR2 provisioning reload (superuser) */ export async function mount(panel, { bridge, EventBus }) { panel.innerHTML = renderSkeleton(); // Load both summaries in parallel. const [promSummary, grafSummary] = await Promise.all([ safe(bridge.prometheus.summary(), { installed: false, status: 'uninstalled' }), safe(bridge.grafana.summary(), { installed: false, status: 'uninstalled' }), ]); panel.innerHTML = `

SysDeck Monitoring

Observability stack — Prometheus (metrics) + Grafana (dashboards) · Prometheus ${promSummary.installed ? 'installed' : 'absent'} · Grafana ${grafSummary.installed ? 'installed' : 'absent'}

${renderPrometheusTab(promSummary)}
${renderGrafanaTab(grafSummary)}
`; wireEvents(panel, { bridge, EventBus }); EventBus.emit('monitoring.loaded', { prometheusInstalled: promSummary.installed, grafanaInstalled: grafSummary.installed, }); } // ── Prometheus tab ────────────────────────────────────────────────── function renderPrometheusTab(summary) { if (!summary.installed) { return renderInstallHint('Prometheus', 'prometheus', 'prometheus.service', [ '# Arch:', 'sudo pacman -S prometheus', '# Debian/Ubuntu:', 'sudo apt install prometheus prometheus-alertmanager', '# Fedora/RHEL:', 'sudo dnf install prometheus alertmanager', '', '# IMPORTANT: Cockpit-ws uses port 9090 by default.', '# Move Prometheus to port 9095 to avoid the conflict.', '# Edit /etc/prometheus/prometheus.yml and add:', '# web.listen_address: "127.0.0.1:9095"', '# Or set in /etc/default/prometheus:', '# ARGS="--web.listen-address=127.0.0.1:9095"', ], '9095'); } const status = summary.status || 'unknown'; const statusBadge = `${escapeHtml(status)}`; const targetsUp = summary.targetsUp || 0; const targetsTotal = summary.targetsTotal || 0; const alertsFiring = summary.alertsFiring || 0; return `

Prometheus Status

status${statusBadge}
version${escapeHtml(summary.version || '?')}
uptime${escapeHtml(summary.uptime || '?')}
targets${targetsUp}/${targetsTotal} up
alerts firing${alertsFiring > 0 ? `${alertsFiring}` : '0'}
API URLhttp://127.0.0.1:9095

Prometheus Web UI

↗ Open in new tab
`; } // ── Grafana tab ───────────────────────────────────────────────────── function renderGrafanaTab(summary) { if (!summary.installed) { return renderInstallHint('Grafana', 'grafana-server', 'grafana-server.service', [ '# Arch (AUR):', 'yay -S grafana', '# Debian/Ubuntu:', 'sudo apt install -y adduser libfontconfig1', 'wget https://dl.grafana.com/oss/release/grafana_latest_amd64.deb', 'sudo dpkg -i grafana_latest_amd64.deb', '# Fedora/RHEL:', 'sudo dnf install grafana', ], '3000'); } const status = summary.status || 'unknown'; const statusBadge = `${escapeHtml(status)}`; const dashboards = summary.dashboardsCount || 0; const datasources = summary.datasourcesCount || 0; return `

Grafana Status

status${statusBadge}
version${escapeHtml(summary.version || '?')}
dashboards${dashboards}
datasources${datasources}
URLhttp://127.0.0.1:3000
default loginadmin / admin (change immediately)

Grafana Web UI

↗ Open in new tab
`; } // ── Shared install hint (shown when service is not installed) ─────── function renderInstallHint(name, binary, service, commands, port) { return `

${escapeHtml(name)} — Not Installed

${escapeHtml(binary)} is not installed. Install it to enable the ${escapeHtml(name)} tab:

${commands.map(escapeHtml).join('\n')}

After install, enable + start the service:

sudo systemctl enable --now ${escapeHtml(service)}

The web UI will be available at http://127.0.0.1:${escapeHtml(port)} and will appear in the iframe below once the service is running.

`; } // ── Event wiring ──────────────────────────────────────────────────── function wireEvents(panel, { bridge, EventBus }) { const output = (msg, isError = false) => { const card = panel.querySelector('#monitoring-output'); const pre = panel.querySelector('#monitoring-output-pre'); if (!card || !pre) return; card.style.display = 'block'; pre.textContent = msg; pre.style.color = isError ? 'var(--sysdeck-accent-danger)' : 'var(--sysdeck-fg)'; }; panel.querySelector('#btn-monitoring-output-close')?.addEventListener('click', () => { const card = panel.querySelector('#monitoring-output'); if (card) card.style.display = 'none'; }); // Tab switching. panel.querySelectorAll('.monitoring-tab').forEach((tab) => { tab.addEventListener('click', () => { panel.querySelectorAll('.monitoring-tab').forEach((t) => t.classList.remove('active')); panel.querySelectorAll('.monitoring-tab-panel').forEach((p) => p.classList.remove('active')); tab.classList.add('active'); const target = tab.dataset.tab; panel.querySelector(`#tab-${target}`)?.classList.add('active'); }); }); // Prometheus controls. panel.querySelector('.btn-prom-refresh')?.addEventListener('click', () => mount(panel, { bridge, EventBus })); panel.querySelector('.btn-prom-reload')?.addEventListener('click', async () => { output('Reloading Prometheus config (SIGHUP) ... (cockpit will prompt for auth)'); try { const r = await bridge.prometheus.reload(); output(r.result === 'ok' ? 'Prometheus config reloaded.' : `Reload FAILED: ${r.stderr || r.message || JSON.stringify(r)}`, r.result !== 'ok'); } catch (err) { output(`Reload error: ${err.message || err}`, true); } }); panel.querySelector('.btn-prom-restart')?.addEventListener('click', async () => { output('Restarting Prometheus ... (cockpit will prompt for auth)'); try { const r = await bridge.prometheus.restart(); output(r.result === 'ok' ? 'Prometheus restarted.' : `Restart FAILED: ${r.stderr || r.message || JSON.stringify(r)}`, r.result !== 'ok'); if (r.result === 'ok') setTimeout(() => mount(panel, { bridge, EventBus }), 2000); } catch (err) { output(`Restart error: ${err.message || err}`, true); } }); // Grafana controls. panel.querySelector('.btn-graf-refresh')?.addEventListener('click', () => mount(panel, { bridge, EventBus })); panel.querySelector('.btn-graf-reload')?.addEventListener('click', async () => { output('Reloading Grafana provisioning (SIGUSR2) ... (cockpit will prompt for auth)'); try { const r = await bridge.grafana.reload(); output(r.result === 'ok' ? 'Grafana provisioning reloaded.' : `Reload FAILED: ${r.stderr || r.message || JSON.stringify(r)}`, r.result !== 'ok'); } catch (err) { output(`Reload error: ${err.message || err}`, true); } }); panel.querySelector('.btn-graf-restart')?.addEventListener('click', async () => { output('Restarting Grafana ... (cockpit will prompt for auth)'); try { const r = await bridge.grafana.restart(); output(r.result === 'ok' ? 'Grafana restarted.' : `Restart FAILED: ${r.stderr || r.message || JSON.stringify(r)}`, r.result !== 'ok'); if (r.result === 'ok') setTimeout(() => mount(panel, { bridge, EventBus }), 2000); } catch (err) { output(`Restart error: ${err.message || err}`, true); } }); } // ── Utilities ─────────────────────────────────────────────────────── async function safe(p, fallback) { try { const v = await p; return v ?? fallback; } catch { return fallback; } } function escapeHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function renderSkeleton() { return `
`; }