Build a Live Process Dashboard from the Oxmgr Event Socket
The nicest thing about a real-time event bus is that it makes live dashboards trivial. You don’t poll, you don’t diff, you don’t guess how often to refresh. You take one snapshot of the current state, then let events push every change to the browser as it happens.
This post builds a minimal but real dashboard: a Node backend that bridges Oxmgr’s Unix socket to Server-Sent Events, and a browser page that renders a live table.
Architecture
oxmgr daemon ──events.sock──▶ bridge (Node) ──SSE──▶ browser
▲
└── oxmgr ls --json for the initial snapshot Two data sources, one rule of thumb:
- Snapshot comes from
oxmgr ls --json— the full current state when a browser first connects. - Deltas come from the event socket —
process:*,health:*,log:*— applied on top of the snapshot.
The bridge
// bridge.js
import net from 'node:net';
import os from 'node:os';
import path from 'node:path';
import readline from 'node:readline';
import { execFile } from 'node:child_process';
import http from 'node:http';
const SOCK = path.join(
os.homedir(),
process.platform === 'darwin'
? 'Library/Application Support/oxmgr/events.sock'
: '.local/share/oxmgr/events.sock'
);
const clients = new Set();
function snapshot() {
return new Promise((resolve) =>
execFile('oxmgr', ['ls', '--json'], (_e, out) => resolve(out || '[]'))
);
}
// Fan out one event to every connected browser
function broadcast(type, data) {
const frame = `event: ${type}\ndata: ${data}\n\n`;
for (const res of clients) res.write(frame);
}
// Subscribe to Oxmgr once, forward forever
function subscribe() {
const client = net.connect(SOCK, () =>
client.write(JSON.stringify({ subscribe: [] }) + '\n')
);
readline.createInterface({ input: client }).on('line', (line) =>
broadcast('oxmgr', line)
);
client.on('close', () => setTimeout(subscribe, 2000));
client.on('error', () => {});
}
subscribe();
http.createServer(async (req, res) => {
if (req.url === '/events') {
res.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive',
});
res.write(`event: snapshot\ndata: ${await snapshot()}\n\n`);
clients.add(res);
req.on('close', () => clients.delete(res));
} else {
res.writeHead(200, { 'content-type': 'text/html' });
res.end(PAGE);
}
}).listen(4321, () => console.log('dashboard on http://localhost:4321')); The subscribe: [] empty filter means “send me everything.” A browser gets a snapshot event on connect, then a stream of oxmgr events forever.
The page
The frontend keeps a map of processes keyed by name, seeds it from the snapshot, and mutates it on each event:
const PAGE = /* html */ `
<!doctype html><meta charset=utf-8>
<title>oxmgr live</title>
<style>
body { font: 14px/1.5 system-ui; margin: 2rem; }
table { border-collapse: collapse; width: 100%; }
td, th { text-align: left; padding: .4rem .8rem; border-bottom: 1px solid #ddd; }
.online { color: #16a34a; } .stopped, .crashed { color: #dc2626; }
</style>
<h1>Processes</h1>
<table><thead><tr><th>Name<th>Status<th>PID<th>Mem<th>Restarts<th>Health</tr></thead>
<tbody id=rows></tbody></table>
<script>
const state = new Map();
const es = new EventSource('/events');
es.addEventListener('snapshot', (e) => {
for (const p of JSON.parse(e.data)) state.set(p.name, p);
render();
});
es.addEventListener('oxmgr', (e) => {
const ev = JSON.parse(e.data);
const name = ev.process?.name;
if (!name) return;
const p = state.get(name) ?? { name };
if (ev.event?.startsWith('process:')) p.status = ev.event.split(':')[1];
if (ev.event === 'health:healthy') p.health_status = 'healthy';
if (ev.event === 'health:unhealthy') p.health_status = 'unhealthy';
state.set(name, p);
render();
});
function render() {
document.getElementById('rows').innerHTML = [...state.values()].map(p => `
<tr>
<td>${p.name}</td>
<td class="${p.status}">${p.status ?? '—'}</td>
<td>${p.pid ?? '—'}</td>
<td>${p.memory_bytes ? (p.memory_bytes/1e6|0)+'MB' : '—'}</td>
<td>${p.restart_count ?? '—'}</td>
<td>${p.health_status ?? '—'}</td>
</tr>`).join('');
}
</script>`; Open http://localhost:4321, then oxmgr restart api in another terminal — the status flips to restarting and back to online without a page refresh, because the event drove it.
Run the dashboard under Oxmgr
Naturally, supervise the bridge with the thing it’s watching:
[processes.dashboard]
command = "node bridge.js"
restart_on_exit = true
[processes.dashboard.health_check]
endpoint = "http://localhost:4321/"
interval_secs = 30 Where to take it
This is ~120 lines and already useful. From here you can:
- Add memory/CPU sparklines by polling
oxmgr ls --jsonevery few seconds and merging intostate— the events tell you what changed, the snapshot tells you how much. - Stream logs by subscribing to
log:out/log:errand appending to a per-process buffer. - Show crash context — when a
process:crashedevent arrives, itsstderr_tailis the stack trace; render it in a modal (more on that here). - Persist events to build an incident timeline.
Because everything flows through one NDJSON stream, the same bridge pattern works for a TUI, a menu-bar app, or a Grafana panel. The event bus is the contract; the dashboard is just one consumer of it.