import { useEffect, useRef, useState } from "react"; import { Activity, AlertTriangle, CheckCircle2, Clock, Cpu, Database, Download, Loader2, Radio, RotateCw, Wifi, WifiOff, Wrench, X, } from "lucide-react"; import { Cell, Grid } from "@nous-research/ui"; import { api } from "@/lib/api"; import type { ActionStatusResponse, PlatformStatus, SessionInfo, StatusResponse, } from "@/lib/api"; import { cn, timeAgo, isoTimeAgo } from "@/lib/utils"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Toast } from "@/components/Toast"; import { useI18n } from "@/i18n"; const ACTION_NAMES: Record<"restart" | "update", string> = { restart: "gateway-restart", update: "hermes-update", }; export default function StatusPage() { const [status, setStatus] = useState(null); const [sessions, setSessions] = useState([]); const [pendingAction, setPendingAction] = useState< "restart" | "update" | null >(null); const [activeAction, setActiveAction] = useState<"restart" | "update" | null>( null, ); const [actionStatus, setActionStatus] = useState( null, ); const [toast, setToast] = useState(null); const logScrollRef = useRef(null); const { t } = useI18n(); useEffect(() => { const load = () => { api .getStatus() .then(setStatus) .catch(() => {}); api .getSessions(50) .then((resp) => setSessions(resp.sessions)) .catch(() => {}); }; load(); const interval = setInterval(load, 5000); return () => clearInterval(interval); }, []); useEffect(() => { if (!toast) return; const timer = setTimeout(() => setToast(null), 4000); return () => clearTimeout(timer); }, [toast]); useEffect(() => { if (!activeAction) return; const name = ACTION_NAMES[activeAction]; let cancelled = false; const poll = async () => { try { const resp = await api.getActionStatus(name); if (cancelled) return; setActionStatus(resp); if (!resp.running) { const ok = resp.exit_code === 0; setToast({ type: ok ? "success" : "error", message: ok ? t.status.actionFinished : `${t.status.actionFailed} (exit ${resp.exit_code ?? "?"})`, }); return; } } catch { // transient fetch error; keep polling } if (!cancelled) setTimeout(poll, 1500); }; poll(); return () => { cancelled = true; }; }, [activeAction, t.status.actionFinished, t.status.actionFailed]); useEffect(() => { const el = logScrollRef.current; if (el) el.scrollTop = el.scrollHeight; }, [actionStatus?.lines]); const runAction = async (action: "restart" | "update") => { setPendingAction(action); setActionStatus(null); try { if (action === "restart") { await api.restartGateway(); } else { await api.updateHermes(); } setActiveAction(action); } catch (err) { const detail = err instanceof Error ? err.message : String(err); setToast({ type: "error", message: `${t.status.actionFailed}: ${detail}`, }); } finally { setPendingAction(null); } }; const dismissLog = () => { setActiveAction(null); setActionStatus(null); }; if (!status) { return (
); } const PLATFORM_STATE_BADGE: Record< string, { variant: "success" | "warning" | "destructive"; label: string } > = { connected: { variant: "success", label: t.status.connected }, disconnected: { variant: "warning", label: t.status.disconnected }, fatal: { variant: "destructive", label: t.status.error }, }; const GATEWAY_STATE_DISPLAY: Record< string, { badge: "success" | "warning" | "destructive" | "outline"; label: string } > = { running: { badge: "success", label: t.status.running }, starting: { badge: "warning", label: t.status.starting }, startup_failed: { badge: "destructive", label: t.status.failed }, stopped: { badge: "outline", label: t.status.stopped }, }; function gatewayValue(): string { if (status!.gateway_running && status!.gateway_health_url) return status!.gateway_health_url; if (status!.gateway_running && status!.gateway_pid) return `${t.status.pid} ${status!.gateway_pid}`; if (status!.gateway_running) return t.status.runningRemote; if (status!.gateway_state === "startup_failed") return t.status.startFailed; return t.status.notRunning; } function gatewayBadge() { const info = status!.gateway_state ? GATEWAY_STATE_DISPLAY[status!.gateway_state] : null; if (info) return info; return status!.gateway_running ? { badge: "success" as const, label: t.status.running } : { badge: "outline" as const, label: t.common.off }; } const gwBadge = gatewayBadge(); const items = [ { icon: Cpu, label: t.status.agent, value: `v${status.version}`, badgeText: t.common.live, badgeVariant: "success" as const, }, { icon: Radio, label: t.status.gateway, value: gatewayValue(), badgeText: gwBadge.label, badgeVariant: gwBadge.badge, }, { icon: Activity, label: t.status.activeSessions, value: status.active_sessions > 0 ? `${status.active_sessions} ${t.status.running.toLowerCase()}` : t.status.noneRunning, badgeText: status.active_sessions > 0 ? t.common.live : t.common.off, badgeVariant: (status.active_sessions > 0 ? "success" : "outline") as | "success" | "outline", }, ]; const platforms = Object.entries(status.gateway_platforms ?? {}); const activeSessions = sessions.filter((s) => s.is_active); const recentSessions = sessions.filter((s) => !s.is_active).slice(0, 5); // Collect alerts that need attention const alerts: { message: string; detail?: string }[] = []; if (status.gateway_state === "startup_failed") { alerts.push({ message: t.status.gatewayFailedToStart, detail: status.gateway_exit_reason ?? undefined, }); } const failedPlatforms = platforms.filter( ([, info]) => info.state === "fatal" || info.state === "disconnected", ); for (const [name, info] of failedPlatforms) { const stateLabel = info.state === "fatal" ? t.status.platformError : t.status.platformDisconnected; alerts.push({ message: `${name.charAt(0).toUpperCase() + name.slice(1)} ${stateLabel}`, detail: info.error_message ?? undefined, }); } return (
{alerts.length > 0 && (
{alerts.map((alert, i) => (

{alert.message}

{alert.detail && (

{alert.detail}

)}
))}
)} {items.map(({ icon: Icon, label, value, badgeText, badgeVariant }) => (
{label}
{value}
{badgeText && ( {badgeVariant === "success" && ( )} {badgeText} )}
))}
{t.status.actions}
{activeAction && (
{actionStatus?.running ? ( ) : actionStatus?.exit_code === 0 ? ( ) : actionStatus !== null ? ( ) : ( )} {activeAction === "restart" ? t.status.restartGateway : t.status.updateHermes} {actionStatus?.running ? t.status.running : actionStatus?.exit_code === 0 ? t.status.actionFinished : actionStatus ? `${t.status.actionFailed} (${actionStatus.exit_code ?? "?"})` : t.common.loading}
            {actionStatus?.lines && actionStatus.lines.length > 0
              ? actionStatus.lines.join("\n")
              : t.status.waitingForOutput}
          
)} {platforms.length > 0 && ( )} {activeSessions.length > 0 && (
{t.status.activeSessions}
{activeSessions.map((s) => (
{s.title ?? t.common.untitled} {t.common.live}
{(s.model ?? t.common.unknown).split("/").pop()} {" "} · {s.message_count} {t.common.msgs} ·{" "} {timeAgo(s.last_active)}
))}
)} {recentSessions.length > 0 && (
{t.status.recentSessions}
{recentSessions.map((s) => (
{s.title ?? t.common.untitled} {(s.model ?? t.common.unknown).split("/").pop()} {" "} · {s.message_count} {t.common.msgs} ·{" "} {timeAgo(s.last_active)} {s.preview && ( {s.preview} )}
{s.source ?? "local"}
))}
)}
); } function PlatformsCard({ platforms, platformStateBadge }: PlatformsCardProps) { const { t } = useI18n(); return (
{t.status.connectedPlatforms}
{platforms.map(([name, info]) => { const display = platformStateBadge[info.state] ?? { variant: "outline" as const, label: info.state, }; const IconComponent = info.state === "connected" ? Wifi : info.state === "fatal" ? AlertTriangle : WifiOff; return (
{name} {info.error_message && ( {info.error_message} )} {info.updated_at && ( {t.status.lastUpdate}: {isoTimeAgo(info.updated_at)} )}
{display.variant === "success" && ( )} {display.label}
); })}
); } interface ToastState { message: string; type: "success" | "error"; } interface PlatformsCardProps { platforms: [string, PlatformStatus][]; platformStateBadge: Record< string, { variant: "success" | "warning" | "destructive"; label: string } >; }