import { useCallback, useEffect, useRef, useState } from "react"; import { Link } from "react-router-dom"; import { Activity, Brain, Check, Clock, Copy, Cpu, Database, Download, Globe, HardDrive, KeyRound, Link2, Play, Plus, Power, RotateCw, Server, Share2, ShieldCheck, Sparkles, Stethoscope, Terminal, Trash2, X, } from "lucide-react"; import { Badge } from "@nous-research/ui/ui/components/badge"; import { Button } from "@nous-research/ui/ui/components/button"; import { Spinner } from "@nous-research/ui/ui/components/spinner"; import { H2 } from "@nous-research/ui/ui/components/typography/h2"; import { Card, CardContent } from "@nous-research/ui/ui/components/card"; import { Input } from "@nous-research/ui/ui/components/input"; import { Label } from "@nous-research/ui/ui/components/label"; import { Select, SelectOption } from "@nous-research/ui/ui/components/select"; import { Toast } from "@nous-research/ui/ui/components/toast"; import { useToast } from "@nous-research/ui/hooks/use-toast"; import { useConfirmDelete } from "@nous-research/ui/hooks/use-confirm-delete"; import { ConfirmDialog } from "@nous-research/ui/ui/components/confirm-dialog"; import { useModalBehavior } from "@/hooks/useModalBehavior"; import { DeleteConfirmDialog } from "@/components/DeleteConfirmDialog"; import { cn, themedBody } from "@/lib/utils"; import { api } from "@/lib/api"; import type { StatusResponse, MemoryStatus, CredentialPoolProvider, CheckpointsResponse, HooksResponse, HookEntry, SystemStats, UpdateCheckResponse, CuratorStatus, PortalStatus, DebugShareResponse, } from "@/lib/api"; function formatBytes(n: number): string { if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`; return `${(n / (1024 * 1024 * 1024)).toFixed(1)} GB`; } function formatDuration(seconds: number): string { const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); const m = Math.floor((seconds % 3600) / 60); if (d > 0) return `${d}d ${h}h ${m}m`; if (h > 0) return `${h}h ${m}m`; return `${m}m`; } /** * Live action-log viewer for the spawn-based admin actions (doctor, audit, * backup, import, skills update, checkpoints prune, gateway start/stop). * Polls /api/actions//status until the process exits. */ function ActionLogViewer({ action, onClose, }: { action: string; onClose: () => void; }) { const [lines, setLines] = useState([]); const [running, setRunning] = useState(true); const [exitCode, setExitCode] = useState(null); const timer = useRef | null>(null); useEffect(() => { let cancelled = false; const poll = async () => { try { const st = await api.getActionStatus(action, 400); if (cancelled) return; setLines(st.lines); setRunning(st.running); setExitCode(st.exit_code); if (st.running) timer.current = setTimeout(poll, 1200); } catch { if (!cancelled) setRunning(false); } }; poll(); return () => { cancelled = true; if (timer.current) clearTimeout(timer.current); }; }, [action]); return (
{action} {running ? ( running ) : ( {exitCode === 0 ? "done" : `exit ${exitCode}`} )}
          {lines.length ? lines.join("\n") : "Starting…"}
        
); } const HOOK_EVENTS_FALLBACK = [ "pre_tool_call", "post_tool_call", "pre_llm_call", "post_llm_call", "on_session_start", "on_session_end", ]; export default function SystemPage() { const { toast, showToast } = useToast(); const [status, setStatus] = useState(null); const [stats, setStats] = useState(null); const [memory, setMemory] = useState(null); const [pool, setPool] = useState([]); const [checkpoints, setCheckpoints] = useState( null, ); const [hooks, setHooks] = useState(null); const [curator, setCurator] = useState(null); const [portal, setPortal] = useState(null); const [loading, setLoading] = useState(true); const [activeAction, setActiveAction] = useState(null); // Add-credential form. const [credProvider, setCredProvider] = useState("openrouter"); const [credKey, setCredKey] = useState(""); const [credLabel, setCredLabel] = useState(""); const [addingCred, setAddingCred] = useState(false); const [importPath, setImportPath] = useState(""); // Restore-from-backup is destructive (overwrites the live config) and the // spawned `hermes import` runs non-interactively (stdin is /dev/null), so // its CLI "Continue? [y/N]" prompt would auto-abort. The dashboard owns the // consent: confirm here, then call the endpoint with force=true. const [importConfirmOpen, setImportConfirmOpen] = useState(false); // Create-hook modal. const [hookModalOpen, setHookModalOpen] = useState(false); const closeHookModal = useCallback(() => setHookModalOpen(false), []); const hookModalRef = useModalBehavior({ open: hookModalOpen, onClose: closeHookModal, }); const [hookEvent, setHookEvent] = useState("pre_tool_call"); const [hookCommand, setHookCommand] = useState(""); const [hookMatcher, setHookMatcher] = useState(""); const [hookTimeout, setHookTimeout] = useState(""); const [hookApprove, setHookApprove] = useState(true); const [creatingHook, setCreatingHook] = useState(false); // ── Update check ─────────────────────────────────────────────────── const [updateInfo, setUpdateInfo] = useState( null, ); const [checkingUpdate, setCheckingUpdate] = useState(false); const [updateConfirmOpen, setUpdateConfirmOpen] = useState(false); const loadAll = useCallback(() => { Promise.allSettled([ api.getStatus(), api.getSystemStats(), api.getMemory(), api.getCredentialPool(), api.getCheckpoints(), api.getHooks(), api.getCurator(), api.getPortal(), // Cached (non-forced) check so the version row shows update status on // load without a separate effect / a forced network round-trip. api.checkHermesUpdate(false), ]) .then(([s, st, m, p, c, h, cur, prt, upd]) => { if (s.status === "fulfilled") setStatus(s.value); if (st.status === "fulfilled") setStats(st.value); if (m.status === "fulfilled") setMemory(m.value); if (p.status === "fulfilled") setPool(p.value.providers); if (c.status === "fulfilled") setCheckpoints(c.value); if (h.status === "fulfilled") setHooks(h.value); if (cur.status === "fulfilled") setCurator(cur.value); if (prt.status === "fulfilled") setPortal(prt.value); if (upd.status === "fulfilled") setUpdateInfo(upd.value); }) .finally(() => setLoading(false)); }, []); useEffect(() => { loadAll(); }, [loadAll]); // ── Gateway lifecycle ────────────────────────────────────────────── const runGateway = async (verb: "start" | "stop" | "restart") => { try { if (verb === "start") { await api.startGateway(); setActiveAction("gateway-start"); } else if (verb === "stop") { await api.stopGateway(); setActiveAction("gateway-stop"); } else { await api.restartGateway(); setActiveAction("gateway-restart"); } showToast(`Gateway ${verb} started`, "success"); setTimeout(loadAll, 3000); } catch (e) { showToast(`Gateway ${verb} failed: ${e}`, "error"); } }; // ── Curator ──────────────────────────────────────────────────────── const toggleCuratorPaused = async () => { if (!curator) return; try { await api.setCuratorPaused(!curator.paused); showToast(curator.paused ? "Curator resumed" : "Curator paused", "success"); loadAll(); } catch (e) { showToast(`Curator toggle failed: ${e}`, "error"); } }; // ── Memory ───────────────────────────────────────────────────────── // Memory provider selection lives on the /plugins page now (see the // read-only display + link below); the dropdown was intentionally // dropped from this card during the admin-panel refresh. const memoryReset = useConfirmDelete({ onDelete: useCallback( async (target: string) => { try { const res = await api.resetMemory( target as "all" | "memory" | "user", ); showToast(`Reset: ${res.deleted.join(", ") || "nothing"}`, "success"); loadAll(); } catch (e) { showToast(`Reset failed: ${e}`, "error"); throw e; } }, [loadAll, showToast], ), }); // ── Credential pool ──────────────────────────────────────────────── const addCredential = async () => { if (!credProvider.trim() || !credKey.trim()) { showToast("Provider and API key required", "error"); return; } setAddingCred(true); try { await api.addCredentialPoolEntry( credProvider.trim(), credKey.trim(), credLabel.trim() || undefined, ); showToast("Credential added", "success"); setCredKey(""); setCredLabel(""); loadAll(); } catch (e) { showToast(`Failed to add credential: ${e}`, "error"); } finally { setAddingCred(false); } }; const credDelete = useConfirmDelete({ onDelete: useCallback( async (key: string) => { const [provider, idxStr] = key.split("|"); try { await api.removeCredentialPoolEntry(provider, Number(idxStr)); showToast("Credential removed", "success"); loadAll(); } catch (e) { showToast(`Failed to remove: ${e}`, "error"); throw e; } }, [loadAll, showToast], ), }); // ── Operations ───────────────────────────────────────────────────── const runOp = async (fn: () => Promise<{ name: string }>, label: string) => { try { const res = await fn(); setActiveAction(res.name); showToast(`${label} started`, "success"); } catch (e) { showToast(`${label} failed: ${e}`, "error"); } }; // ── Debug share ──────────────────────────────────────────────────── // Unlike the fire-and-forget ops above, `debug share` produces shareable // paste URLs that are the whole point — so we surface them as real, // copyable links rather than a log tail. const [shareRedact, setShareRedact] = useState(true); const [sharing, setSharing] = useState(false); const [shareResult, setShareResult] = useState( null, ); const [copiedLabel, setCopiedLabel] = useState(null); const copyToClipboard = useCallback( async (text: string, label: string) => { try { await navigator.clipboard.writeText(text); setCopiedLabel(label); setTimeout( () => setCopiedLabel((cur) => (cur === label ? null : cur)), 1500, ); } catch { showToast("Couldn't copy to clipboard", "error"); } }, [showToast], ); const runDebugShare = useCallback(async () => { setSharing(true); setShareResult(null); try { const res = await api.runDebugShare({ redact: shareRedact }); setShareResult(res); const n = Object.keys(res.urls).length; showToast( `Uploaded ${n} paste${n === 1 ? "" : "s"}${ res.redacted ? " (redacted)" : "" }`, "success", ); } catch (e) { showToast(`Debug share failed: ${e}`, "error"); } finally { setSharing(false); } }, [shareRedact, showToast]); // ── Update check / apply ─────────────────────────────────────────── const checkForUpdate = useCallback( async (force = false) => { setCheckingUpdate(true); try { const info = await api.checkHermesUpdate(force); setUpdateInfo(info); if (force) { if (info.update_available) { showToast( info.behind && info.behind > 0 ? `Update available — ${info.behind} commit${info.behind === 1 ? "" : "s"} behind` : "Update available", "success", ); } else if (info.behind === 0) { showToast("You're on the latest version", "success"); } else if (info.message) { showToast(info.message, "error"); } } } catch (e) { showToast(`Update check failed: ${e}`, "error"); } finally { setCheckingUpdate(false); } }, [showToast], ); // Auto-check (cached) runs inside loadAll on mount; this is the // user-triggered forced re-check from the "Check for updates" button. const applyUpdate = async () => { setUpdateConfirmOpen(false); try { const resp = await api.updateHermes(); if (!resp.ok && resp.error === "docker_update_unsupported") { showToast( resp.message ?? "Updates don't apply inside Docker — re-pull the image instead.", "error", ); return; } setActiveAction(resp.name ?? "hermes-update"); showToast("Update started", "success"); } catch (e) { showToast(`Update failed: ${e}`, "error"); } }; const checkpointsPrune = useConfirmDelete({ onDelete: useCallback(async () => { try { const res = await api.pruneCheckpoints(); setActiveAction(res.name); showToast("Checkpoint prune started", "success"); } catch (e) { showToast(`Prune failed: ${e}`, "error"); throw e; } }, [showToast]), }); // ── Hooks ────────────────────────────────────────────────────────── const createHook = async () => { if (!hookCommand.trim()) { showToast("Command is required", "error"); return; } setCreatingHook(true); try { await api.createHook({ event: hookEvent, command: hookCommand.trim(), matcher: hookMatcher.trim() || undefined, timeout: hookTimeout.trim() ? Number(hookTimeout) : undefined, approve: hookApprove, }); showToast("Hook created", "success"); setHookCommand(""); setHookMatcher(""); setHookTimeout(""); setHookModalOpen(false); loadAll(); } catch (e) { showToast(`Failed to create hook: ${e}`, "error"); } finally { setCreatingHook(false); } }; const hookDelete = useConfirmDelete({ onDelete: useCallback( async (key: string) => { const sep = key.indexOf("|"); const event = key.slice(0, sep); const command = key.slice(sep + 1); try { await api.deleteHook(event, command); showToast("Hook removed", "success"); loadAll(); } catch (e) { showToast(`Failed to remove hook: ${e}`, "error"); throw e; } }, [loadAll, showToast], ), }); if (loading) { return (
); } const gatewayRunning = status?.gateway_running; const validEvents = hooks?.valid_events?.length ? hooks.valid_events : HOOK_EVENTS_FALLBACK; return (
setUpdateConfirmOpen(false)} onConfirm={() => void applyUpdate()} title="Update Hermes?" description={ updateInfo && updateInfo.behind && updateInfo.behind > 0 ? `This will run 'hermes update' (${updateInfo.update_command}) and pull ${updateInfo.behind} new commit${updateInfo.behind === 1 ? "" : "s"}. The gateway restarts when the update finishes; the current session keeps its prompt cache until then.` : `This will run 'hermes update' (${updateInfo?.update_command ?? "hermes update"}) and restart the gateway when it finishes.` } confirmLabel="Update now" /> {/* Create-hook modal */} {hookModalOpen && (
e.target === e.currentTarget && setHookModalOpen(false)} role="dialog" aria-modal="true" >

New shell hook

setHookCommand(e.target.value)} />
setHookMatcher(e.target.value)} />
setHookTimeout(e.target.value)} />

Shell hooks run arbitrary commands on this host. Only add scripts you trust. Takes effect on the next gateway/session restart.

)} {/* Live action log */} {activeAction && ( setActiveAction(null)} /> )} {/* ── Host / system stats ───────────────────────────────────── */}

Host

OS
{stats?.os} {stats?.os_release}
Arch
{stats?.arch}
Host
{stats?.hostname}
Python
{stats?.python_impl} {stats?.python_version}
Hermes
v{stats?.hermes_version} {updateInfo && (updateInfo.update_available ? ( {updateInfo.behind && updateInfo.behind > 0 ? `${updateInfo.behind} behind` : "update available"} ) : updateInfo.behind === 0 ? ( latest ) : null)}
CPU
{stats?.cpu_count ?? "—"} cores {typeof stats?.cpu_percent === "number" ? ` · ${stats.cpu_percent.toFixed(0)}%` : ""}
{stats?.memory && (
Memory
{formatBytes(stats.memory.used)} / {formatBytes(stats.memory.total)} ({stats.memory.percent}%)
)} {stats?.disk && (
Disk
{formatBytes(stats.disk.used)} / {formatBytes(stats.disk.total)} ({stats.disk.percent}%)
)} {typeof stats?.uptime_seconds === "number" && (
Uptime
{formatDuration(stats.uptime_seconds)}
)} {stats?.load_avg && stats.load_avg.length >= 3 && (
Load avg
{stats.load_avg.map((n) => n.toFixed(2)).join(" / ")}
)}
{stats && !stats.psutil && (

Install the psutil extra for CPU / memory / disk metrics.

)}
{updateInfo?.update_available && updateInfo.can_apply && ( )} {updateInfo && !updateInfo.can_apply && updateInfo.update_available && ( Update with{" "} {updateInfo.update_command} )} {updateInfo?.message && !updateInfo.update_available && ( {updateInfo.message} )}
{/* ── Portal ────────────────────────────────────────────────── */}

Nous Portal

{portal?.logged_in ? "logged in" : "not logged in"} {portal?.provider && ( inference provider: {portal.provider} )} Manage subscription
{portal?.features && portal.features.length > 0 && (
Tool Gateway routing {portal.features.map((f) => (
{f.label} {f.state}
))}
)} {!portal?.logged_in && (

Log in with hermes portal.

)}
{/* ── Curator ───────────────────────────────────────────────── */}

Skill curator

{curator?.paused ? "paused" : curator?.enabled ? "active" : "disabled"} {curator?.interval_hours ? `every ${curator.interval_hours}h` : ""} {curator?.last_run_at ? ` · last run ${new Date(curator.last_run_at).toLocaleString()}` : " · never run"}
{/* ── Gateway ───────────────────────────────────────────────── */}

Gateway

{gatewayRunning ? "running" : "stopped"} {status?.gateway_state ?? "—"} {status?.gateway_pid ? ` · pid ${status.gateway_pid}` : ""}
{/* ── Memory ────────────────────────────────────────────────── */}

Memory

External provider:{" "} {memory?.active || "built-in only"} Change in Plugins → New credentials:{" "} hermes memory setup
Built-in files — MEMORY.md:{" "} {formatBytes(memory?.builtin_files.memory ?? 0)} · USER.md:{" "} {formatBytes(memory?.builtin_files.user ?? 0)}
{/* ── Credential pool ───────────────────────────────────────── */}

Credential pool

setCredProvider(e.target.value)} placeholder="openrouter" />
setCredKey(e.target.value)} placeholder="sk-…" />
setCredLabel(e.target.value)} placeholder="optional" />
{pool.length === 0 && (

No pooled credentials. Add one above to enable key rotation.

)} {pool.map((prov) => (
{prov.provider} {prov.entries.map((entry) => (
{entry.label} {entry.token_preview} {entry.auth_type} {entry.last_status && {entry.last_status}}
))}
))}
{/* ── Operations ────────────────────────────────────────────── */}

Operations

{/* Debug share — uploads a redacted report + logs, returns shareable links. Separated from the buttons above because its output is persistent, copyable URLs, not a fire-and-forget log tail. */}
Share debug report Uploads system info + logs to a public paste service and returns links to send the Hermes team. Pastes auto-delete after 6 hours.
{shareResult && (
uploaded {shareResult.redacted ? ( redacted ) : ( not redacted )} auto-deletes in{" "} {Math.round(shareResult.auto_delete_seconds / 3600)}h
{Object.keys(shareResult.urls).length > 1 && ( )}
{Object.entries(shareResult.urls).map(([label, url]) => (
{label} {url}
))} {shareResult.failures.length > 0 && ( Some logs failed to upload: {shareResult.failures.join("; ")} )}
)}
setImportPath(e.target.value)} placeholder="/path/to/hermes-backup.zip" />
setImportConfirmOpen(false)} onConfirm={() => { setImportConfirmOpen(false); runOp(() => api.runImport(importPath.trim(), true), "Import"); }} />
{/* ── Checkpoints ───────────────────────────────────────────── */}

Checkpoints

{checkpoints?.sessions.length ?? 0} session(s) ·{" "} {formatBytes(checkpoints?.total_bytes ?? 0)}
{/* ── Shell hooks ───────────────────────────────────────────── */}

Shell hooks

{(!hooks || hooks.hooks.length === 0) && ( No shell hooks configured. )} {hooks?.hooks.map((h: HookEntry, i) => ( {h.event} {h.matcher && ( matcher: {h.matcher} )} {h.command} {h.executable === false && ( not executable )} {h.allowed ? "allowed" : "not approved"} ))}
); }