import { useCallback, useEffect, useState } from "react"; import { api } from "@/lib/api"; import type { ActionStatusResponse } from "@/lib/api"; import { Toast } from "@nous-research/ui/ui/components/toast"; import { useI18n } from "@/i18n"; import { SystemActionsContext, type SystemAction, } from "./system-actions-context"; const ACTION_NAMES: Record = { restart: "gateway-restart", update: "hermes-update", }; export function SystemActionsProvider({ children, }: { children: React.ReactNode; }) { const [pendingAction, setPendingAction] = useState(null); const [activeAction, setActiveAction] = useState(null); const [actionStatus, setActionStatus] = useState( null, ); const [toast, setToast] = useState(null); const { t } = useI18n(); 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]); const runAction = useCallback( async (action: SystemAction) => { setPendingAction(action); setActionStatus(null); try { if (action === "restart") { await api.restartGateway(); setActiveAction(action); } else { const resp = await api.updateHermes(); // In a Docker install the image is immutable, so `hermes update` // can't apply — the endpoint returns 200 with a structured // {ok:false, error:"docker_update_unsupported", message, update_command} // envelope instead of spawning the action (see #34347 / #36263). // Surface that guidance to the user rather than starting the poll, // which would otherwise report a generic "failed (exit 1)". if (!resp.ok && resp.error === "docker_update_unsupported") { const cmd = resp.update_command ? ` ${resp.update_command}` : ""; setToast({ type: "success", message: (resp.message ?? "Updates don't apply inside Docker — re-pull the image instead.") + cmd, }); return; } setActiveAction(action); } } catch (err) { const detail = err instanceof Error ? err.message : String(err); setToast({ type: "error", message: `${t.status.actionFailed}: ${detail}`, }); } finally { setPendingAction(null); } }, [t.status.actionFailed], ); const dismissLog = useCallback(() => { setActiveAction(null); setActionStatus(null); }, []); const isRunning = activeAction !== null && actionStatus?.running !== false; const isBusy = pendingAction !== null || isRunning; return ( {children} ); } interface ToastState { message: string; type: "success" | "error"; }