'use client'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import type { ProcessInfo, SystemInfo, LogTailResult, ConfigInfo } from '@/lib/supervisor/types'; import { createMutationLogger } from '@/lib/utils/client-logger'; // Query Keys export const supervisorKeys = { all: ['supervisor'] as const, system: () => [...supervisorKeys.all, 'system'] as const, processes: () => [...supervisorKeys.all, 'processes'] as const, process: (name: string) => [...supervisorKeys.processes(), name] as const, logs: (name: string, type: 'stdout' | 'stderr') => [...supervisorKeys.process(name), 'logs', type] as const, config: () => [...supervisorKeys.all, 'config'] as const, }; // API Client Functions async function fetchSystemInfo(): Promise { const response = await fetch('/api/supervisor/system'); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to fetch system info'); } return response.json(); } async function fetchProcesses(): Promise { const response = await fetch('/api/supervisor/processes'); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to fetch processes'); } return response.json(); } async function fetchProcessInfo(name: string): Promise { const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}`); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to fetch process info'); } return response.json(); } async function fetchProcessLogs( name: string, type: 'stdout' | 'stderr', offset: number = -4096, length: number = 4096 ): Promise { const response = await fetch( `/api/supervisor/processes/${encodeURIComponent(name)}/logs/${type}?offset=${offset}&length=${length}` ); if (!response.ok) { const error = await response.json(); throw new Error(error.error || `Failed to fetch ${type} logs`); } return response.json(); } async function startProcess(name: string, wait: boolean = true): Promise<{ success: boolean; message: string }> { const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wait }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to start process'); } return response.json(); } async function stopProcess(name: string, wait: boolean = true): Promise<{ success: boolean; message: string }> { const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}/stop`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wait }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to stop process'); } return response.json(); } async function restartProcess(name: string): Promise<{ success: boolean; message: string }> { const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}/restart`, { method: 'POST', }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to restart process'); } return response.json(); } // Custom Hooks export function useSystemInfo() { return useQuery({ queryKey: supervisorKeys.system(), queryFn: fetchSystemInfo, refetchInterval: 5000, // Refetch every 5 seconds }); } export function useProcesses(options?: { refetchInterval?: number }) { return useQuery({ queryKey: supervisorKeys.processes(), queryFn: fetchProcesses, refetchInterval: options?.refetchInterval ?? 3000, // Default 3 seconds }); } export function useProcessInfo(name: string, enabled: boolean = true) { return useQuery({ queryKey: supervisorKeys.process(name), queryFn: () => fetchProcessInfo(name), enabled, refetchInterval: 3000, }); } export function useProcessLogs( name: string, type: 'stdout' | 'stderr', options?: { offset?: number; length?: number; enabled?: boolean; refetchInterval?: number; } ) { return useQuery({ queryKey: [...supervisorKeys.logs(name, type), options?.offset, options?.length], queryFn: () => fetchProcessLogs(name, type, options?.offset, options?.length), enabled: options?.enabled ?? true, refetchInterval: options?.refetchInterval ?? 2000, }); } export function useStartProcess() { const queryClient = useQueryClient(); const logger = createMutationLogger('startProcess'); return useMutation({ mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => { logger.info('Starting process', { name, wait }); return startProcess(name, wait); }, onSuccess: (data, variables) => { logger.info('Process started successfully', { name: variables.name }); toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) }); }, onError: (error: Error, variables) => { logger.error('Failed to start process', error, { name: variables.name }); toast.error(`Failed to start process: ${error.message}`); }, }); } export function useStopProcess() { const queryClient = useQueryClient(); const logger = createMutationLogger('stopProcess'); return useMutation({ mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => { logger.info('Stopping process', { name, wait }); return stopProcess(name, wait); }, onSuccess: (data, variables) => { logger.info('Process stopped successfully', { name: variables.name }); toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) }); }, onError: (error: Error, variables) => { logger.error('Failed to stop process', error, { name: variables.name }); toast.error(`Failed to stop process: ${error.message}`); }, }); } export function useRestartProcess() { const queryClient = useQueryClient(); const logger = createMutationLogger('restartProcess'); return useMutation({ mutationFn: (name: string) => { logger.info('Restarting process', { name }); return restartProcess(name); }, onSuccess: (data, name) => { logger.info('Process restarted successfully', { name }); toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); queryClient.invalidateQueries({ queryKey: supervisorKeys.process(name) }); }, onError: (error: Error, name) => { logger.error('Failed to restart process', error, { name }); toast.error(`Failed to restart process: ${error.message}`); }, }); } // Log Management async function fetchMainLog(offset: number = -4096, length: number = 0): Promise<{ logs: string }> { const response = await fetch(`/api/supervisor/logs?offset=${offset}&length=${length}`); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to fetch main log'); } return response.json(); } async function clearMainLog(): Promise<{ success: boolean; message: string }> { const response = await fetch('/api/supervisor/logs', { method: 'DELETE' }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to clear main log'); } return response.json(); } async function clearProcessLogs(name: string): Promise<{ success: boolean; message: string }> { const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}/logs`, { method: 'DELETE', }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to clear process logs'); } return response.json(); } async function clearAllLogs(): Promise<{ success: boolean; message: string }> { const response = await fetch('/api/supervisor/processes/logs/clear-all', { method: 'POST', }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to clear all logs'); } return response.json(); } export function useMainLog(options?: { offset?: number; length?: number; enabled?: boolean; refetchInterval?: number }) { return useQuery({ queryKey: [...supervisorKeys.all, 'mainLog', options?.offset, options?.length], queryFn: () => fetchMainLog(options?.offset, options?.length), enabled: options?.enabled ?? true, refetchInterval: options?.refetchInterval ?? 2000, }); } export function useClearMainLog() { const queryClient = useQueryClient(); return useMutation({ mutationFn: clearMainLog, onSuccess: (data) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: [...supervisorKeys.all, 'mainLog'] }); }, onError: (error: Error) => { toast.error(`Failed to clear main log: ${error.message}`); }, }); } export function useClearProcessLogs() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (name: string) => clearProcessLogs(name), onSuccess: (data, name) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.process(name) }); }, onError: (error: Error) => { toast.error(`Failed to clear process logs: ${error.message}`); }, }); } export function useClearAllLogs() { const queryClient = useQueryClient(); return useMutation({ mutationFn: clearAllLogs, onSuccess: (data) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.all }); }, onError: (error: Error) => { toast.error(`Failed to clear all logs: ${error.message}`); }, }); } // Process Group Management async function startProcessGroup(name: string, wait: boolean = true): Promise<{ success: boolean; message: string; results: any[] }> { const response = await fetch(`/api/supervisor/groups/${encodeURIComponent(name)}/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wait }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to start process group'); } return response.json(); } async function stopProcessGroup(name: string, wait: boolean = true): Promise<{ success: boolean; message: string; results: any[] }> { const response = await fetch(`/api/supervisor/groups/${encodeURIComponent(name)}/stop`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wait }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to stop process group'); } return response.json(); } async function restartProcessGroup(name: string, wait: boolean = true): Promise<{ success: boolean; message: string; results: any[] }> { const response = await fetch(`/api/supervisor/groups/${encodeURIComponent(name)}/restart`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wait }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to restart process group'); } return response.json(); } export function useStartProcessGroup() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => startProcessGroup(name, wait), onSuccess: (data) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, onError: (error: Error) => { toast.error(`Failed to start process group: ${error.message}`); }, }); } export function useStopProcessGroup() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => stopProcessGroup(name, wait), onSuccess: (data) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, onError: (error: Error) => { toast.error(`Failed to stop process group: ${error.message}`); }, }); } export function useRestartProcessGroup() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ name, wait }: { name: string; wait?: boolean }) => restartProcessGroup(name, wait), onSuccess: (data) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, onError: (error: Error) => { toast.error(`Failed to restart process group: ${error.message}`); }, }); } // Batch Operations (All Processes) async function startAllProcesses(wait: boolean = true): Promise<{ success: boolean; message: string; results: any[] }> { const response = await fetch('/api/supervisor/processes/start-all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wait }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to start all processes'); } return response.json(); } async function stopAllProcesses(wait: boolean = true): Promise<{ success: boolean; message: string; results: any[] }> { const response = await fetch('/api/supervisor/processes/stop-all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wait }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to stop all processes'); } return response.json(); } async function restartAllProcesses(wait: boolean = true): Promise<{ success: boolean; message: string; results: any[] }> { const response = await fetch('/api/supervisor/processes/restart-all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ wait }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to restart all processes'); } return response.json(); } export function useStartAllProcesses() { const queryClient = useQueryClient(); const logger = createMutationLogger('startAllProcesses'); return useMutation({ mutationFn: (wait: boolean = true) => { logger.info('Starting all processes', { wait }); return startAllProcesses(wait); }, onSuccess: (data) => { logger.info('All processes started successfully', { resultCount: data.results?.length, }); toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, onError: (error: Error) => { logger.error('Failed to start all processes', error); toast.error(`Failed to start all processes: ${error.message}`); }, }); } export function useStopAllProcesses() { const queryClient = useQueryClient(); const logger = createMutationLogger('stopAllProcesses'); return useMutation({ mutationFn: (wait: boolean = true) => { logger.info('Stopping all processes', { wait }); return stopAllProcesses(wait); }, onSuccess: (data) => { logger.info('All processes stopped successfully', { resultCount: data.results?.length, }); toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, onError: (error: Error) => { logger.error('Failed to stop all processes', error); toast.error(`Failed to stop all processes: ${error.message}`); }, }); } export function useRestartAllProcesses() { const queryClient = useQueryClient(); const logger = createMutationLogger('restartAllProcesses'); return useMutation({ mutationFn: (wait: boolean = true) => { logger.info('Restarting all processes', { wait }); return restartAllProcesses(wait); }, onSuccess: (data) => { logger.info('All processes restarted successfully', { resultCount: data.results?.length, }); toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, onError: (error: Error) => { logger.error('Failed to restart all processes', error); toast.error(`Failed to restart all processes: ${error.message}`); }, }); } // Configuration Management async function fetchConfig(): Promise { const response = await fetch('/api/supervisor/config'); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to fetch configuration'); } return response.json(); } async function reloadConfig(): Promise<{ success: boolean; message: string; result: any }> { const response = await fetch('/api/supervisor/config/reload', { method: 'POST', }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to reload configuration'); } return response.json(); } async function addProcessGroup(name: string): Promise<{ success: boolean; message: string }> { const response = await fetch('/api/supervisor/config/group', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to add process group'); } return response.json(); } async function removeProcessGroup(name: string): Promise<{ success: boolean; message: string }> { const response = await fetch('/api/supervisor/config/group', { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to remove process group'); } return response.json(); } export function useConfig() { return useQuery({ queryKey: supervisorKeys.config(), queryFn: fetchConfig, refetchInterval: 10000, // Refetch every 10 seconds }); } export function useReloadConfig() { const queryClient = useQueryClient(); const logger = createMutationLogger('reloadConfig'); return useMutation({ mutationFn: () => { logger.info('Reloading supervisor configuration'); return reloadConfig(); }, onSuccess: (data) => { logger.info('Configuration reloaded successfully'); toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.all }); }, onError: (error: Error) => { logger.error('Failed to reload configuration', error); toast.error(`Failed to reload configuration: ${error.message}`); }, }); } export function useAddProcessGroup() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (name: string) => addProcessGroup(name), onSuccess: (data) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.all }); }, onError: (error: Error) => { toast.error(`Failed to add process group: ${error.message}`); }, }); } export function useRemoveProcessGroup() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (name: string) => removeProcessGroup(name), onSuccess: (data) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.all }); }, onError: (error: Error) => { toast.error(`Failed to remove process group: ${error.message}`); }, }); } // Signal Operations async function signalProcess(name: string, signal: string): Promise<{ success: boolean; message: string }> { const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}/signal`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ signal }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to send signal'); } return response.json(); } async function signalProcessGroup(name: string, signal: string): Promise<{ success: boolean; message: string; results: any[] }> { const response = await fetch(`/api/supervisor/groups/${encodeURIComponent(name)}/signal`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ signal }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to send signal to group'); } return response.json(); } async function signalAllProcesses(signal: string): Promise<{ success: boolean; message: string; results: any[] }> { const response = await fetch('/api/supervisor/processes/signal-all', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ signal }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to send signal to all processes'); } return response.json(); } export function useSignalProcess() { const queryClient = useQueryClient(); const logger = createMutationLogger('signalProcess'); return useMutation({ mutationFn: ({ name, signal }: { name: string; signal: string }) => { logger.info('Sending signal to process', { name, signal }); return signalProcess(name, signal); }, onSuccess: (data, variables) => { logger.info('Signal sent successfully', { name: variables.name, signal: variables.signal }); toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) }); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, onError: (error: Error, variables) => { logger.error('Failed to send signal', error, { name: variables.name, signal: variables.signal, }); toast.error(`Failed to send signal: ${error.message}`); }, }); } export function useSignalProcessGroup() { const queryClient = useQueryClient(); return useMutation({ mutationFn: ({ name, signal }: { name: string; signal: string }) => signalProcessGroup(name, signal), onSuccess: (data) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, onError: (error: Error) => { toast.error(`Failed to send signal to group: ${error.message}`); }, }); } export function useSignalAllProcesses() { const queryClient = useQueryClient(); return useMutation({ mutationFn: (signal: string) => signalAllProcesses(signal), onSuccess: (data) => { toast.success(data.message); queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() }); }, onError: (error: Error) => { toast.error(`Failed to send signal to all processes: ${error.message}`); }, }); } // Process Stdin async function sendProcessStdin(name: string, chars: string): Promise<{ success: boolean; message: string }> { const response = await fetch(`/api/supervisor/processes/${encodeURIComponent(name)}/stdin`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chars }), }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Failed to send input'); } return response.json(); } export function useSendProcessStdin() { return useMutation({ mutationFn: ({ name, chars }: { name: string; chars: string }) => sendProcessStdin(name, chars), onSuccess: (data) => { toast.success(data.message); }, onError: (error: Error) => { toast.error(`Failed to send input: ${error.message}`); }, }); }