Adds the remaining monitor types: a systeminformation-backed system widget (single-flight cached so multiple widgets don't force concurrent samples), an http widget with per-widget consecutive- failure tracking, and a traefik widget that reads router/entrypoint/ middleware status from Traefik's own API. Also adds opt-in label-based docker auto-discovery (discovery.docker in config.yml): containers carrying traefik.enable=true are turned into docker widgets in a synthetic "Discovered" group, re-scanned on a timer and whenever config.yml changes, with manual widgets always taking precedence over a discovered one for the same container. Fixes a real bug surfaced while testing discovery: Next's App Router bundles app/** through its own compiler pass, separate from server.ts (run directly via tsx), so configStore/effectiveConfigStore were silently instantiated twice - one instance watched and updated by server.ts, another frozen instance read by SSR. Live config edits were never reflected on page load without a full process restart. Both stores now key their singleton off globalThis, which both module graphs share within the same process. Verified against a real Traefik v3 container (API-driven router list, ping-based http check) and a labeled nginx container (auto-discovery), including that a config.yml edit now shows up in a fresh page load without restarting the server, in both dev and the production build.
53 lines
1.3 KiB
TypeScript
53 lines
1.3 KiB
TypeScript
import si from "systeminformation";
|
|
import type { SystemResult } from "@/lib/types/system-result";
|
|
|
|
const CACHE_MS = 1_000;
|
|
|
|
let lastSample: { data: SystemResult; ts: number } | null = null;
|
|
let inFlight: Promise<SystemResult> | null = null;
|
|
|
|
function pickDisk(disks: si.Systeminformation.FsSizeData[]): si.Systeminformation.FsSizeData | undefined {
|
|
return disks.find((disk) => disk.mount === "/") ?? disks.sort((a, b) => b.size - a.size)[0];
|
|
}
|
|
|
|
async function sample(): Promise<SystemResult> {
|
|
const [load, mem, disks, net] = await Promise.all([
|
|
si.currentLoad(),
|
|
si.mem(),
|
|
si.fsSize(),
|
|
si.networkStats(),
|
|
]);
|
|
|
|
const disk = pickDisk(disks);
|
|
const primaryNet = net[0];
|
|
|
|
return {
|
|
cpuPercent: load.currentLoad,
|
|
memUsedBytes: mem.active,
|
|
memTotalBytes: mem.total,
|
|
diskUsedBytes: disk?.used ?? 0,
|
|
diskTotalBytes: disk?.size ?? 0,
|
|
netRxBytesPerSec: primaryNet?.rx_sec ?? 0,
|
|
netTxBytesPerSec: primaryNet?.tx_sec ?? 0,
|
|
};
|
|
}
|
|
|
|
export async function collectSystem(): Promise<SystemResult> {
|
|
const now = Date.now();
|
|
if (lastSample && now - lastSample.ts < CACHE_MS) {
|
|
return lastSample.data;
|
|
}
|
|
if (inFlight) return inFlight;
|
|
|
|
inFlight = sample()
|
|
.then((data) => {
|
|
lastSample = { data, ts: Date.now() };
|
|
return data;
|
|
})
|
|
.finally(() => {
|
|
inFlight = null;
|
|
});
|
|
|
|
return inFlight;
|
|
}
|