1"use client";23import { useEffect, useState } from "react";4import { api } from "@/lib/api";5import { useLive } from "@/lib/events";6import { fmtBytes, fmtDate, fmtDuration, fmtGB, fmtMs } from "@/lib/format";7import type { SystemInfo } from "@/lib/types";8import { KV, Meter, PageHeader, Pill, Sparkline, StatTile, Tabs } from "@/components/ui";910interface Storage { total_gb: number; used_gb: number; free_gb: number; models_gb: number; logs_gb: number; database_gb: number; cache_gb: number; other_gb: number; model_root: string; min_free_gb: number; models: { id: string; name: string; disk_size_bytes: number; runtime: string; last_used_at: number | null }[] }11interface Procs { workers: { model_id: string; pid: number; port: number; runtime: string; rss_gb: number; cpu_percent: number; status: string; threads: number }[]; server: { pid: number; rss_gb: number; cpu_percent: number; threads: number } }12interface Hist { ts: number; mem_used_gb: number; gpu_percent: number | null; cpu_percent: number; swap_used_gb: number; worker_rss_gb: number }1314export default function SystemPage() {15 const live = useLive();16 const [sys, setSys] = useState<SystemInfo | null>(null);17 const [storage, setStorage] = useState<Storage | null>(null);18 const [procs, setProcs] = useState<Procs | null>(null);19 const [hist, setHist] = useState<Hist[]>([]);20 const [range, setRange] = useState(60);21 const [tab, setTab] = useState<"overview" | "storage" | "processes" | "logs">("overview");22 const [audit, setAudit] = useState<{ created_at: number; actor: string; action: string; target: string | null; detail: string | null }[]>([]);23 const [events, setEvents] = useState<{ created_at: number; model_id: string | null; event: string; detail: string | null }[]>([]);2425 useEffect(() => {26 api.get<SystemInfo>("/api/system").then(setSys).catch(() => {});27 api.get<{ history: Hist[] }>(`/api/system/metrics?minutes=${range}`).then((r) => setHist(r.history)).catch(() => {});28 }, [range, live.version]);29 useEffect(() => {30 if (tab === "storage") api.get<Storage>("/api/system/storage").then(setStorage).catch(() => {});31 if (tab === "processes") api.get<Procs>("/api/system/processes").then(setProcs).catch(() => {});32 if (tab === "logs") { api.get<{ logs: typeof audit }>("/api/logs/audit?limit=100").then((r) => setAudit(r.logs)); api.get<{ events: typeof events }>("/api/logs/events?limit=100").then((r) => setEvents(r.events)); }33 }, [tab, live.version]);3435 const t = live.metrics || sys?.telemetry;36 const hw = sys?.hardware;37 const series = hist.length > 3 ? hist : live.metricsHistory.map((m) => ({ ts: m.ts, mem_used_gb: m.mem_used_gb, gpu_percent: m.gpu_percent, cpu_percent: m.cpu_percent, swap_used_gb: m.swap_used_gb, worker_rss_gb: m.worker_rss_gb || 0 }));3839 return (40 <div>41 <PageHeader title="System" sub={hw ? `${hw.hostname} · ${hw.chip} · ${hw.os} ${hw.os_version} · Python ${hw.python} · LLM API ${sys?.version}` : "…"} />42 <Tabs value={tab} onChange={setTab} tabs={[{ id: "overview", label: "Overview" }, { id: "storage", label: "Storage" }, { id: "processes", label: "Processes" }, { id: "logs", label: "Audit & events" }]} />4344 {tab === "overview" && t && hw && (45 <div className="flex flex-col gap-4 mt-4">46 <div className="grid grid-cols-2 lg:grid-cols-4 gap-3">47 <StatTile label="Unified memory" value={`${t.mem_used_gb.toFixed(1)} / ${t.mem_total_gb.toFixed(0)} GB`} sub={`${t.mem_available_gb.toFixed(1)} GB available · wired ${fmtGB(t.mem_wired_gb)} · compressed ${fmtGB(t.mem_compressed_gb)}`}>48 <Meter value={t.mem_used_gb} max={t.mem_total_gb} tone={t.mem_pressure_level === "normal" ? "accent" : t.mem_pressure_level === "warning" ? "warn" : "bad"} />49 </StatTile>50 <StatTile label="Memory pressure" value={<span className={t.mem_pressure_level === "normal" ? "text-good" : t.mem_pressure_level === "warning" ? "text-warn" : "text-bad"}>{t.mem_pressure_level}</span>} sub={`${t.mem_pressure_percent}% · swap ${fmtGB(t.swap_used_gb)} / ${fmtGB(t.swap_total_gb)}`} />51 <StatTile label="GPU" value={t.gpu_percent != null ? `${t.gpu_percent.toFixed(0)}%` : "n/a"} sub={`${hw.gpu_cores ?? "—"} cores · renderer ${t.gpu_renderer_percent ?? "—"}% · ${fmtGB(t.gpu_memory_gb)} Metal`} />52 <StatTile label="Thermal" value={t.thermal_state} sub={t.thermal_cpu_speed_limit != null ? `CPU speed limit ${t.thermal_cpu_speed_limit}%` : "no throttling recorded"} />53 </div>54 <div className="card p-4">55 <div className="flex items-center justify-between mb-3">56 <div className="font-medium text-sm">History</div>57 <div className="flex gap-1">{[15, 60, 360, 1440].map((m) => <button key={m} onClick={() => setRange(m)} className={`btn btn-sm ${range === m ? "btn-primary" : ""}`}>{m < 60 ? `${m}m` : `${m / 60}h`}</button>)}</div>58 </div>59 <div className="grid md:grid-cols-2 gap-4">60 <div><div className="label mb-1">Memory used (GB)</div><Sparkline data={series.map((h) => h.mem_used_gb)} max={t.mem_total_gb} height={80} unit=" GB" /></div>61 <div><div className="label mb-1">Worker memory (GB)</div><Sparkline data={series.map((h) => h.worker_rss_gb || 0)} max={t.mem_total_gb} height={80} color="var(--series-7)" unit=" GB" /></div>62 <div><div className="label mb-1">GPU utilization (%)</div><Sparkline data={series.map((h) => h.gpu_percent ?? 0)} max={100} height={80} color="var(--series-3)" unit="%" /></div>63 <div><div className="label mb-1">CPU (%)</div><Sparkline data={series.map((h) => h.cpu_percent)} max={100} height={80} color="var(--series-2)" unit="%" /></div>64 </div>65 </div>66 <div className="grid md:grid-cols-2 gap-4">67 <div className="card p-4"><div className="label mb-2">Hardware</div>68 <KV k="Chip" v={hw.chip} /><KV k="Memory" v={`${hw.memory_gb} GB unified`} /><KV k="CPU" v={`${hw.cpu_cores} cores (${hw.performance_cores ?? "?"} performance + ${hw.efficiency_cores ?? "?"} efficiency)`} /><KV k="GPU" v={`${hw.gpu_cores ?? "?"} cores`} /><KV k="Disk" v={`${hw.disk_total_gb} GB`} /><KV k="OS" v={`${hw.os} ${hw.os_version}`} /><KV k="Uptime" v={fmtDuration(t.uptime_seconds)} /><KV k="Load average" v={t.load_avg.join(" / ")} />69 </div>70 <div className="card p-4"><div className="label mb-2">Runtimes & policy</div>71 <KV k="MLX" v={sys?.runtimes.mlx ? `mlx ${sys.runtimes.mlx} · mlx-lm ${sys.runtimes.mlx_lm}` : <Pill tone="bad">missing</Pill>} />72 <KV k="mlx-vlm (vision)" v={sys?.runtimes.mlx_vlm || <Pill tone="warn">not installed</Pill>} />73 <KV k="llama.cpp" v={sys?.runtimes.llama_cpp || <Pill tone="warn">not installed</Pill>} />74 <KV k="Model memory budget" v={`${sys?.policy.max_model_memory_gb} GB (absolute ${sys?.policy.absolute_max_memory_gb} GB)`} />75 <KV k="macOS reserve" v={`${sys?.policy.macos_reserve_gb} GB`} />76 <KV k="Simultaneous models" v={sys?.policy.max_simultaneous_models} />77 <KV k="Disk reserve" v={`${sys?.policy.min_free_disk_gb} GB`} />78 <KV k="Server started" v={fmtDate(sys?.started_at)} />79 </div>80 </div>81 </div>82 )}8384 {tab === "storage" && storage && (85 <div className="flex flex-col gap-4 mt-4">86 <div className="card p-4">87 <div className="flex justify-between text-sm mb-2"><span>{storage.used_gb.toFixed(0)} GB used of {storage.total_gb.toFixed(0)} GB</span><span className="text-ink-3">{storage.free_gb.toFixed(0)} GB free · reserve {storage.min_free_gb} GB</span></div>88 <div className="flex h-3 rounded-full overflow-hidden bg-surface-3 gap-px">89 {[["models_gb", "var(--series-1)"], ["cache_gb", "var(--series-4)"], ["logs_gb", "var(--series-2)"], ["database_gb", "var(--series-3)"], ["other_gb", "var(--color-border-strong)"]].map(([k, c]) => (90 <div key={k} title={k} style={{ width: `${(storage[k as keyof Storage] as number) / storage.total_gb * 100}%`, background: c }} />91 ))}92 </div>93 <div className="flex flex-wrap gap-x-4 gap-y-1 mt-2 text-xs text-ink-2 num">94 <span><i className="inline-block w-2 h-2 rounded-sm mr-1" style={{ background: "var(--series-1)" }} />Models {storage.models_gb.toFixed(1)} GB</span>95 <span><i className="inline-block w-2 h-2 rounded-sm mr-1" style={{ background: "var(--series-4)" }} />HF cache {storage.cache_gb.toFixed(1)} GB</span>96 <span><i className="inline-block w-2 h-2 rounded-sm mr-1" style={{ background: "var(--series-2)" }} />Logs {storage.logs_gb.toFixed(2)} GB</span>97 <span><i className="inline-block w-2 h-2 rounded-sm mr-1" style={{ background: "var(--series-3)" }} />Database {storage.database_gb.toFixed(2)} GB</span>98 <span><i className="inline-block w-2 h-2 rounded-sm mr-1" style={{ background: "var(--color-border-strong)" }} />Other {storage.other_gb.toFixed(0)} GB</span>99 </div>100 <div className="text-xs text-ink-3 mt-2 mono">{storage.model_root}</div>101 </div>102 <div className="card overflow-x-auto">103 <table className="tbl"><thead><tr><th>Model</th><th>Runtime</th><th className="text-right">Size</th><th>Last used</th></tr></thead>104 <tbody>{storage.models.map((m) => <tr key={m.id} className="row-link" onClick={() => (location.href = `/models/${m.id}`)}><td className="font-medium">{m.name}</td><td className="text-ink-2">{m.runtime}</td><td className="text-right num">{fmtBytes(m.disk_size_bytes)}</td><td className="text-ink-2">{m.last_used_at ? fmtDate(m.last_used_at) : "never"}</td></tr>)}</tbody></table>105 </div>106 <div className="text-xs text-ink-3">Cleanup is manual by design: delete a model from its page (confirmation required). Nothing is removed automatically.</div>107 </div>108 )}109110 {tab === "processes" && procs && (111 <div className="card mt-4 overflow-x-auto">112 <table className="tbl"><thead><tr><th>Process</th><th>PID</th><th>Port</th><th>Runtime</th><th className="text-right">Memory</th><th className="text-right">CPU</th><th className="text-right">Threads</th><th>Status</th></tr></thead>113 <tbody>114 <tr><td className="font-medium">llm-api server</td><td className="num">{procs.server.pid}</td><td></td><td>python</td><td className="text-right num">{fmtGB(procs.server.rss_gb, 2)}</td><td className="text-right num">{procs.server.cpu_percent}%</td><td className="text-right num">{procs.server.threads}</td><td><Pill tone="good">running</Pill></td></tr>115 {procs.workers.map((w) => <tr key={w.pid}><td className="font-medium">worker · {w.model_id}</td><td className="num">{w.pid}</td><td className="num">{w.port}</td><td>{w.runtime}</td><td className="text-right num">{fmtGB(w.rss_gb, 2)}</td><td className="text-right num">{w.cpu_percent}%</td><td className="text-right num">{w.threads}</td><td><Pill tone={w.status === "ready" ? "good" : "accent"}>{w.status}</Pill></td></tr>)}116 {!procs.workers.length && <tr><td colSpan={8} className="text-center text-ink-3 py-6">No inference worker running.</td></tr>}117 </tbody></table>118 </div>119 )}120121 {tab === "logs" && (122 <div className="grid lg:grid-cols-2 gap-4 mt-4">123 <div className="card"><div className="px-4 py-2.5 border-b border-border font-medium text-sm">Model events</div>124 <div className="divide-y divide-border text-xs max-h-[60vh] overflow-auto">{events.map((e, i) => <div key={i} className="px-4 py-2 flex gap-3"><span className="text-ink-3 shrink-0 w-36">{fmtDate(e.created_at)}</span><span className="font-medium w-28 shrink-0">{e.event}</span><span className="truncate text-ink-2">{e.model_id}{e.detail ? ` · ${e.detail}` : ""}</span></div>)}</div></div>125 <div className="card"><div className="px-4 py-2.5 border-b border-border font-medium text-sm">Audit log</div>126 <div className="divide-y divide-border text-xs max-h-[60vh] overflow-auto">{audit.map((a, i) => <div key={i} className="px-4 py-2 flex gap-3"><span className="text-ink-3 shrink-0 w-36">{fmtDate(a.created_at)}</span><span className="font-medium w-32 shrink-0">{a.action}</span><span className="truncate text-ink-2">{a.actor} {a.target ? `→ ${a.target}` : ""} {a.detail || ""}</span></div>)}</div></div>127 </div>128 )}129 <div className="hidden">{fmtMs(0)}</div>130 </div>131 );132}133