feat: live docker/database widgets over WebSocket (M2)

Adds a custom server.ts (http server + Next request handler + a
noServer:true WebSocket server on /ws) so the app can push live data
without polling. A CollectorScheduler runs one interval-based job per
docker/database widget instance, backed by dockerode against
/var/run/docker.sock, and reconciles jobs when config.yml changes.

Config hot-reload is now fully wired: chokidar watches config.yml/.env,
re-validates on change, and broadcasts config:update (or a
non-destructive config:error, keeping the last-good config) to every
connected browser. The client subscribes to per-widget topics over a
single shared WebSocket with exponential-backoff reconnect and
last-result caching for instant resubscribe.

Verified end-to-end against real throwaway containers (nginx, redis):
live CPU/mem/uptime/health streamed over the socket, interval changes
picked up without a server restart, and a broken config.yml correctly
falls back to the last-valid config instead of crashing the app.
This commit is contained in:
2026-08-17 14:01:53 +02:00
parent 80c2362ac2
commit 9890f964c5
17 changed files with 693 additions and 7 deletions
+26
View File
@@ -0,0 +1,26 @@
import type { DockerContainerResult } from "@/lib/types/widget-result";
interface Props {
status?: DockerContainerResult["status"];
health?: DockerContainerResult["health"];
}
function resolveColorClass(status?: Props["status"], health?: Props["health"]): string {
if (!status) return "bg-fg-muted";
if (health === "unhealthy") return "bg-status-down";
if (health === "starting") return "bg-status-degraded";
if (status === "running") return "bg-status-up";
if (status === "restarting") return "bg-status-degraded";
return "bg-status-down";
}
export function StatusDot({ status, health }: Props) {
const label = status ? `${status}${health && health !== "none" ? ` (${health})` : ""}` : "unknown";
return (
<span
className={`h-2.5 w-2.5 shrink-0 rounded-full ${resolveColorClass(status, health)}`}
title={label}
/>
);
}