feat: system monitoring + push agent (merge plan M3)

systems + system_stats tables; agents push JSON every N seconds
to POST /api/systems/ingest with a per-system bearer token. The
server stores each sample, keeps a `latest` snapshot for cheap
list/widget reads, prunes stats after 7d, and marks systems down
when silent > max(3x interval, 30s) — firing system.up/down
webhook events on transitions.

cmd/dash-agent is a standalone Linux collector: /proc for
cpu/mem/net/load/uptime, /sys hwmon temps, statfs disk, and
container liveness via /var/run/docker.sock when present.
No imports from the server module — the sample shape is
duplicated deliberately so the agent stays a single static
binary.

Frontend: /systems card grid with usage bars, /systems/:id with
recharts cpu/mem/net/load, temps + containers tables, and the
agent command; `system` board widget; nav + palette + WidgetDialog
picker wired.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Tomas DvorakandDevin committed 2026-09-21 19:02:58 +02:00
1 parent 2f3e8a1879
commit 74c987c18a
16 files changed
+1541 -10

No files matched your search

@@ -34,6 +34,7 @@ export function WidgetDialog({
const [values, setValues] = useState<Record<string, string>>({});
const [monitors, setMonitors] = useState<{ id?: string; name?: string }[]>([]);
const [domains, setDomains] = useState<{ id?: string; name?: string }[]>([]);
const [systems, setSystems] = useState<{ id?: string; name?: string }[]>([]);
const [busy, setBusy] = useState(false);
const [err, setErr] = useState("");
@@ -47,6 +48,7 @@ export function WidgetDialog({
.catch(() => setTypes([]));
void api.GET("/api/monitors").then(({ data }) => setMonitors(data ?? []));
void api.GET("/api/domains").then(({ data }) => setDomains(data ?? []));
void api.GET("/api/systems").then(({ data }) => setSystems(data ?? []));
const cfg = item?.config ?? null;
setType(cfg && typeof cfg.type === "string" ? cfg.type : "");
@@ -146,14 +148,14 @@ export function WidgetDialog({
<Label htmlFor={`wdg-${f.key}`}>
{f.label}{f.required ? "" : " (optional)"}
</Label>
{f.key === "monitorId" || f.key === "domainId" ? (
{f.key === "monitorId" || f.key === "domainId" || f.key === "systemId" ? (
<Select
value={values[f.key] ?? ""}
onValueChange={(v) => setValues((s) => ({ ...s, [f.key]: v }))}
>
<SelectTrigger><SelectValue placeholder="Pick one…" /></SelectTrigger>
<SelectContent>
{(f.key === "monitorId" ? monitors : domains).map((m) => (
{(f.key === "monitorId" ? monitors : f.key === "domainId" ? domains : systems).map((m) => (
<SelectItem key={m.id} value={m.id ?? ""}>{m.name}</SelectItem>
))}
</SelectContent>