/* * SysDeck - Databases Panel (v0.0.33) * Author: Jeremy Anderson (https://dcos.net) * * DB Control module — unified control for SQL / NoSQL / vector / * time-series / graph / embedded / cloud / AI database engines. * The bridge helper bridge/db.py surfaces 32+ engines with * summary / status / start / stop / restart / connections / query * subcommands. * * The cockpit-way pattern (v0.0.31+): the bridge runs systemctl * directly via subprocess; the JS panel passes { superuser: 'try' } * to cockpit.spawn so the cockpit bridge prompts the operator via * polkit for the org.sysdeck.db.modify action. * * The panel surfaces: * - Summary card: total engines, running count, total data size, * total memory, total connections. * - Engines table: per-engine id, name, family, status, version, * port, size, memory, connections, with Start/Stop/Restart * buttons per row. * - Query runner: SQL-family engines only — input a SQL string * and execute against the selected engine. * - Connections viewer: list active TCP connections to an engine. * * Bridge surface (see shared/bridge.js → bridge.db): * summary() → {engines, totalEngines, runningCount, ...} * status(id) → single-engine detail dict * start(id) → {action, engine, rc, success, output, stderr} * stop(id) → same shape * restart(id) → same shape * connections(id) → {connections, count} * query(id, sql) → {output, engine, query} */ export async function mount(panel, { bridge, EventBus }) { panel.innerHTML = renderSkeleton(); let summary = {}; try { summary = await bridge.db.summary(); } catch (err) { panel.innerHTML = renderError(err); return; } const engines = summary.engines || []; const running = engines.filter((e) => e.status === 'running'); const totalSize = summary.totalSizeMB || 0; const totalMem = summary.totalMemoryMB || 0; const totalConn = summary.totalConnections || 0; // Group engines by family for cleaner rendering. const byFamily = new Map(); for (const e of engines) { if (!byFamily.has(e.family)) byFamily.set(e.family, []); byFamily.get(e.family).push(e); } panel.innerHTML = `

Database Control

${summary.totalEngines || engines.length} engines across ${byFamily.size} families · ${running.length} running · ${(totalSize / 1024).toFixed(1)} GB data · ${(totalMem / 1024).toFixed(1)} GB memory · ${totalConn} connections

Engines

${summary.totalEngines || engines.length}
total (${byFamily.size} families)

Running

${running.length}
${running.length ? 'active services' : 'all stopped'}

Data size

${(totalSize / 1024).toFixed(1)}
GB across all engines
${renderEnginesTable(engines, byFamily)}

Run SQL Query

SQL-family engines only (postgresql, mysql/mariadb, sqlite, cockroachdb, clickhouse, duckdb). The query is run via the engine's CLI client (psql -tAc, mysql -e, etc.).

Engine Detail

Click ▶/■/↻ on a row above to start / stop / restart an engine. Click 🔍 to view the engine's full status (version, port, data dir, config path, log path, connections, memory).

`; wireEvents(panel, { bridge, EventBus }); EventBus.emit('db.loaded', { engineCount: engines.length, running: running.length }); } // ── Render helpers ─────────────────────────────────────────────────── function renderEnginesTable(engines, byFamily) { if (!engines.length) { return `

Engines (0)

No database engines detected on this host.

`; } // Render a table per family for clearer grouping. const cards = []; for (const [family, items] of byFamily) { const running = items.filter((e) => e.status === 'running').length; cards.push(`

${escapeHtml(family)} (${items.length})

${running} running
${items.map((e) => ` `).join('')}
EngineStatusVersionPortSizeMemConnsActions
${escapeHtml(e.name)}
${escapeHtml(e.id)}
${statusBadge(e.status)} ${escapeHtml((e.version || '').slice(0, 30))} ${e.port || '—'} ${formatSize(e.sizeMB)} ${formatSize(e.memoryMB)} ${e.connections || 0}
`); } return cards.join(''); } function statusBadge(status) { const map = { 'running': 'running', 'starting': 'starting', 'stopped': 'stopped', 'error': 'error', 'uninstalled': 'uninstalled', }; return map[status] || `${escapeHtml(status)}`; } // ── Event wiring ──────────────────────────────────────────────────── function wireEvents(panel, { bridge, EventBus }) { const outputCard = panel.querySelector('#db-output-card'); const outputPre = panel.querySelector('#db-output-pre'); const outputTitle = panel.querySelector('#db-output-title'); const showOutput = (title, text, isError = false) => { if (!outputCard || !outputPre) return; outputCard.style.display = 'block'; outputTitle.textContent = title; outputPre.textContent = text; outputPre.style.color = isError ? 'var(--sysdeck-accent-danger)' : 'var(--sysdeck-fg)'; }; panel.querySelector('#btn-db-output-close')?.addEventListener('click', () => { if (outputCard) outputCard.style.display = 'none'; }); const startStopRestart = async (btn, method, label) => { const id = btn.dataset.id; if (!id) return; btn.disabled = true; showOutput(`${label} ${id}`, `${label} engine ${id} ... (cockpit will prompt for auth)`); try { const r = await bridge.db[method](id); const ok = r.success ?? (r.rc === 0); showOutput(`${label} ${id} — ${ok ? 'success' : 'failed'}`, `rc=${r.rc}\nsuccess=${ok}\noutput: ${r.output || '(empty)'}\nstderr: ${r.stderr || '(empty)'}`, !ok); if (ok) setTimeout(() => mount(panel, { bridge, EventBus }), 1200); } catch (err) { showOutput(`${label} ${id} — error`, String(err.message || err), true); } finally { setTimeout(() => { btn.disabled = false; }, 1200); } }; panel.querySelectorAll('.btn-db-start').forEach((btn) => { btn.addEventListener('click', () => startStopRestart(btn, 'start', 'Start')); }); panel.querySelectorAll('.btn-db-stop').forEach((btn) => { btn.addEventListener('click', () => startStopRestart(btn, 'stop', 'Stop')); }); panel.querySelectorAll('.btn-db-restart').forEach((btn) => { btn.addEventListener('click', () => startStopRestart(btn, 'restart', 'Restart')); }); panel.querySelectorAll('.btn-db-status').forEach((btn) => { btn.addEventListener('click', async () => { const id = btn.dataset.id; showOutput(`Status: ${id}`, 'Loading ...'); try { const r = await bridge.db.status(id); if (r.error) { showOutput(`Status: ${id} — error`, r.error, true); return; } showOutput(`Status: ${id}`, JSON.stringify(r, null, 2)); } catch (err) { showOutput(`Status: ${id} — error`, String(err.message || err), true); } }); }); panel.querySelector('#btn-db-query')?.addEventListener('click', async () => { const engineId = panel.querySelector('#db-query-engine')?.value; const sql = panel.querySelector('#db-query-sql')?.value?.trim(); if (!engineId) { showOutput('Query', 'Select an engine first.', true); return; } if (!sql) { showOutput('Query', 'Enter a SQL query first.', true); return; } showOutput(`Query: ${engineId}`, `Running query on ${engineId} ... (cockpit will prompt for auth)`); try { const r = await bridge.db.query(engineId, sql); if (r.error) { showOutput(`Query: ${engineId} — error`, r.error, true); } else { showOutput(`Query: ${engineId}`, `query: ${r.query || sql}\n\noutput:\n${r.output || '(empty)'}`); } } catch (err) { showOutput(`Query: ${engineId} — error`, String(err.message || err), true); } }); panel.querySelector('#btn-db-connections')?.addEventListener('click', async () => { const engineId = panel.querySelector('#db-query-engine')?.value; if (!engineId) { showOutput('Connections', 'Select an engine first.', true); return; } showOutput(`Connections: ${engineId}`, 'Loading ...'); try { const r = await bridge.db.connections(engineId); if (r.error) { showOutput(`Connections: ${engineId} — error`, r.error, true); return; } const lines = [`count: ${r.count}`]; for (const c of (r.connections || [])) lines.push(c); showOutput(`Connections: ${engineId}`, lines.join('\n')); } catch (err) { showOutput(`Connections: ${engineId} — error`, String(err.message || err), true); } }); panel.querySelector('#btn-db-refresh')?.addEventListener('click', () => { mount(panel, { bridge, EventBus }); }); } // ── Utilities ─────────────────────────────────────────────────────── function formatSize(mb) { if (!mb) return '0 MB'; if (mb < 1024) return `${mb} MB`; return `${(mb / 1024).toFixed(1)} GB`; } function escapeHtml(s) { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } function renderSkeleton() { return `
`; } function renderError(err) { return `

Database bridge unavailable

${err.message || err}. Ensure the bridge helper is installed at /usr/lib/sysdeck/bridge/db.py.

`; }