SPB Git forge

spb/llm-api

Public
0commits 0branches 0releases
0 Bsize
maindefault branch
—last push
12.4 KB · 133 lines tsx
Raw Blame History
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