SystemActions.tsx
1 import { useCallback, useEffect, useState } from "react"; 2 import { api } from "@/lib/api"; 3 import type { ActionStatusResponse } from "@/lib/api"; 4 import { Toast } from "@/components/Toast"; 5 import { useI18n } from "@/i18n"; 6 import { 7 SystemActionsContext, 8 type SystemAction, 9 } from "./system-actions-context"; 10 11 const ACTION_NAMES: Record<SystemAction, string> = { 12 restart: "gateway-restart", 13 update: "hermes-update", 14 }; 15 16 export function SystemActionsProvider({ 17 children, 18 }: { 19 children: React.ReactNode; 20 }) { 21 const [pendingAction, setPendingAction] = useState<SystemAction | null>(null); 22 const [activeAction, setActiveAction] = useState<SystemAction | null>(null); 23 const [actionStatus, setActionStatus] = useState<ActionStatusResponse | null>( 24 null, 25 ); 26 const [toast, setToast] = useState<ToastState | null>(null); 27 const { t } = useI18n(); 28 29 useEffect(() => { 30 if (!toast) return; 31 const timer = setTimeout(() => setToast(null), 4000); 32 return () => clearTimeout(timer); 33 }, [toast]); 34 35 useEffect(() => { 36 if (!activeAction) return; 37 const name = ACTION_NAMES[activeAction]; 38 let cancelled = false; 39 40 const poll = async () => { 41 try { 42 const resp = await api.getActionStatus(name); 43 if (cancelled) return; 44 setActionStatus(resp); 45 if (!resp.running) { 46 const ok = resp.exit_code === 0; 47 setToast({ 48 type: ok ? "success" : "error", 49 message: ok 50 ? t.status.actionFinished 51 : `${t.status.actionFailed} (exit ${resp.exit_code ?? "?"})`, 52 }); 53 return; 54 } 55 } catch { 56 // transient fetch error; keep polling 57 } 58 if (!cancelled) setTimeout(poll, 1500); 59 }; 60 61 poll(); 62 return () => { 63 cancelled = true; 64 }; 65 }, [activeAction, t.status.actionFinished, t.status.actionFailed]); 66 67 const runAction = useCallback( 68 async (action: SystemAction) => { 69 setPendingAction(action); 70 setActionStatus(null); 71 try { 72 if (action === "restart") { 73 await api.restartGateway(); 74 } else { 75 await api.updateHermes(); 76 } 77 setActiveAction(action); 78 } catch (err) { 79 const detail = err instanceof Error ? err.message : String(err); 80 setToast({ 81 type: "error", 82 message: `${t.status.actionFailed}: ${detail}`, 83 }); 84 } finally { 85 setPendingAction(null); 86 } 87 }, 88 [t.status.actionFailed], 89 ); 90 91 const dismissLog = useCallback(() => { 92 setActiveAction(null); 93 setActionStatus(null); 94 }, []); 95 96 const isRunning = activeAction !== null && actionStatus?.running !== false; 97 const isBusy = pendingAction !== null || isRunning; 98 99 return ( 100 <SystemActionsContext.Provider 101 value={{ 102 actionStatus, 103 activeAction, 104 dismissLog, 105 isBusy, 106 isRunning, 107 pendingAction, 108 runAction, 109 }} 110 > 111 {children} 112 <Toast toast={toast} /> 113 </SystemActionsContext.Provider> 114 ); 115 } 116 117 interface ToastState { 118 message: string; 119 type: "success" | "error"; 120 }