nodes: store and show host disk free; warn when low (node-26#41)

Checkin now carries disk_free_bytes/disk_total_bytes. Stored on the node doc
(not overwritten by older nodes that don't send them). Node page shows a Disk
row; node cards warn amber under 15% free, red under 5% or 2 GB. A full disk
stopped node-002 recording for 17 days while it showed Online.

Frontend unverified locally (no Node, #207); CI build typechecks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-26 02:24:10 -04:00
co-authored by Claude Opus 5.5
parent 4a93390967
commit 268470b6c0
6 changed files with 86 additions and 1 deletions
+39
View File
@@ -0,0 +1,39 @@
import type { NodeRecord } from "@/lib/types";
// Free disk on the node's host (node-26#41). A full disk stopped node-002
// recording for 17 days while every surface still showed it Online, so low
// space is flagged wherever a node is shown.
export type DiskLevel = "ok" | "low" | "critical";
export interface DiskState {
level: DiskLevel;
freePct: number;
label: string; // e.g. "43.2 GB free of 98 GB (44%)"
}
const LOW_PCT = 15;
const CRITICAL_PCT = 5;
const CRITICAL_BYTES = 2 * 1024 ** 3; // a 2 GB floor regardless of disk size
function gb(bytes: number): string {
const v = bytes / 1024 ** 3;
return `${v >= 10 ? v.toFixed(0) : v.toFixed(1)} GB`;
}
/** null when the node has never reported (older node software). */
export function diskState(node: Pick<NodeRecord, "disk_free_bytes" | "disk_total_bytes">): DiskState | null {
const free = node.disk_free_bytes;
const total = node.disk_total_bytes;
if (free == null || !total) return null;
const freePct = Math.max(0, Math.min(100, (free / total) * 100));
const level: DiskLevel =
freePct < CRITICAL_PCT || free < CRITICAL_BYTES ? "critical" : freePct < LOW_PCT ? "low" : "ok";
return { level, freePct, label: `${gb(free)} free of ${gb(total)} (${Math.round(freePct)}%)` };
}
export const DISK_TEXT_CLASS: Record<DiskLevel, string> = {
ok: "text-ink-2",
low: "text-sev-moderate",
critical: "text-sev-major",
};