system-monitor: Workspace/Panel layout with a scope list (bTop / pm2 / dockers)

/system-monitor is now a WorkspaceView (like /music): a left ScopeList panel
selects the scope over the 'monitor:scope' channel, the right MonitorMain panel
renders it. Three scopes:
- bTop  — the existing system snapshot (CPU/mem/disks/top processes)
- pm2 processes — new GET /api/system-monitor/pm2 (pm2 jlist → name/status/cpu/
  mem/restarts/uptime table)
- dockers — new GET /api/system-monitor/docker (docker ps → container cards with
  state/status/image/ports)
Both new endpoints degrade gracefully to an error field. Persisted as
screens/system-monitor; owner-only.

Needs a restart (backend routes + rebundle) + hard-refresh to appear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 10:17:15 +00:00
co-authored by Claude Opus 4.8
parent 621f93b884
commit 059929aa59
11 changed files with 497 additions and 192 deletions
@@ -141,3 +141,61 @@ systemMonitorRouter.get('/stats', async (ctx) => {
timestamp: Date.now(),
});
});
// GET /pm2 — pm2 process list (the "pm2 processes" scope).
systemMonitorRouter.get('/pm2', async (ctx) => {
try {
const { stdout } = await exec('pm2', ['jlist'], { maxBuffer: 8 * 1024 * 1024 });
const list = JSON.parse(stdout) as Array<{
pm_id?: number;
name?: string;
pid?: number;
monit?: { cpu?: number; memory?: number };
pm2_env?: { status?: string; restart_time?: number; pm_uptime?: number };
}>;
const now = Date.now();
return ctx.json({
processes: list.map((p) => {
const env = p.pm2_env ?? {};
return {
id: p.pm_id ?? -1,
name: p.name ?? '?',
status: env.status ?? 'unknown',
pid: p.pid ?? null,
cpuPct: p.monit?.cpu ?? 0,
memBytes: p.monit?.memory ?? 0,
restarts: env.restart_time ?? 0,
uptimeMs: env.status === 'online' && env.pm_uptime ? now - env.pm_uptime : 0,
};
}),
});
} catch (err) {
return ctx.json({ processes: [], error: err instanceof Error ? err.message : String(err) });
}
});
// GET /docker — running docker containers (the "dockers" scope).
systemMonitorRouter.get('/docker', async (ctx) => {
try {
const { stdout } = await exec('docker', ['ps', '--no-trunc', '--format', '{{json .}}'], { maxBuffer: 8 * 1024 * 1024 });
return ctx.json({
containers: stdout
.trim()
.split('\n')
.filter(Boolean)
.map((line) => {
const c = JSON.parse(line) as Record<string, string>;
return {
id: (c.ID ?? '').slice(0, 12),
name: c.Names ?? '',
image: c.Image ?? '',
state: c.State ?? '',
status: c.Status ?? '',
ports: c.Ports ?? '',
};
}),
});
} catch (err) {
return ctx.json({ containers: [], error: err instanceof Error ? err.message : String(err) });
}
});