diff --git a/app/api/supervisor/logs/route.ts b/app/api/supervisor/logs/route.ts new file mode 100644 index 0000000..90decab --- /dev/null +++ b/app/api/supervisor/logs/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +export const dynamic = 'force-dynamic'; + +// GET main supervisord log +export async function GET(request: NextRequest) { + try { + const searchParams = request.nextUrl.searchParams; + const offset = parseInt(searchParams.get('offset') || '-4096', 10); + const length = parseInt(searchParams.get('length') || '4096', 10); + + const client = createSupervisorClient(); + const logs = await client.readLog(offset, length); + + return NextResponse.json({ logs, offset, length }); + } catch (error: any) { + console.error('Supervisor main log error:', error); + return NextResponse.json( + { error: error.message || 'Failed to fetch main log' }, + { status: 500 } + ); + } +} + +// DELETE - Clear main supervisord log +export async function DELETE() { + try { + const client = createSupervisorClient(); + const result = await client.clearLog(); + return NextResponse.json({ success: result, message: 'Main log cleared' }); + } catch (error: any) { + console.error('Supervisor clear main log error:', error); + return NextResponse.json( + { error: error.message || 'Failed to clear main log' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/processes/[name]/logs/route.ts b/app/api/supervisor/processes/[name]/logs/route.ts new file mode 100644 index 0000000..5b9e518 --- /dev/null +++ b/app/api/supervisor/processes/[name]/logs/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +interface RouteParams { + params: Promise<{ name: string }>; +} + +// DELETE - Clear process logs (both stdout and stderr) +export async function DELETE(request: NextRequest, { params }: RouteParams) { + try { + const { name } = await params; + const client = createSupervisorClient(); + const result = await client.clearProcessLogs(name); + return NextResponse.json({ success: result, message: `Logs cleared for ${name}` }); + } catch (error: any) { + console.error('Supervisor clear process logs error:', error); + return NextResponse.json( + { error: error.message || 'Failed to clear process logs' }, + { status: 500 } + ); + } +} diff --git a/app/api/supervisor/processes/logs/clear-all/route.ts b/app/api/supervisor/processes/logs/clear-all/route.ts new file mode 100644 index 0000000..42ad5ed --- /dev/null +++ b/app/api/supervisor/processes/logs/clear-all/route.ts @@ -0,0 +1,21 @@ +import { NextResponse } from 'next/server'; +import { createSupervisorClient } from '@/lib/supervisor/client'; + +// POST - Clear all process logs +export async function POST() { + try { + const client = createSupervisorClient(); + const results = await client.clearAllProcessLogs(); + return NextResponse.json({ + success: true, + message: 'All process logs cleared', + results + }); + } catch (error: any) { + console.error('Supervisor clear all logs error:', error); + return NextResponse.json( + { error: error.message || 'Failed to clear all logs' }, + { status: 500 } + ); + } +} diff --git a/app/logs/page.tsx b/app/logs/page.tsx index 8b5d218..a6afdd5 100644 --- a/app/logs/page.tsx +++ b/app/logs/page.tsx @@ -1,25 +1,177 @@ 'use client'; +import { useState } from 'react'; +import { useProcesses, useProcessLogs, useMainLog, useClearProcessLogs, useClearMainLog } from '@/lib/hooks/useSupervisor'; +import { LogViewer } from '@/components/logs/LogViewer'; +import { LogControls } from '@/components/logs/LogControls'; +import { LogSearch } from '@/components/logs/LogSearch'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; import { AlertCircle } from 'lucide-react'; -import { Card, CardContent } from '@/components/ui/card'; export default function LogsPage() { - return ( -
-
+ const [selectedProcess, setSelectedProcess] = useState('main'); + const [logType, setLogType] = useState<'stdout' | 'stderr'>('stdout'); + const [searchTerm, setSearchTerm] = useState(''); + const [isPlaying, setIsPlaying] = useState(true); + const [autoScroll, setAutoScroll] = useState(true); + + const { data: processes, isError: processesError } = useProcesses({ + refetchInterval: isPlaying ? 3000 : undefined, + }); + + // Fetch logs based on selection + const { data: processLogs, isLoading: processLogsLoading, refetch: refetchProcessLogs } = useProcessLogs( + selectedProcess as string, + logType, + { + enabled: selectedProcess !== 'main' && isPlaying, + refetchInterval: isPlaying ? 2000 : undefined, + } + ); + + const { data: mainLogData, isLoading: mainLogLoading, refetch: refetchMainLog } = useMainLog({ + enabled: selectedProcess === 'main' && isPlaying, + refetchInterval: isPlaying ? 2000 : undefined, + }); + + const clearProcessLogsMutation = useClearProcessLogs(); + const clearMainLogMutation = useClearMainLog(); + + const currentLogs = selectedProcess === 'main' + ? mainLogData?.logs || '' + : processLogs?.bytes || ''; + + const isLoading = selectedProcess === 'main' ? mainLogLoading : processLogsLoading; + + const handleRefresh = () => { + if (selectedProcess === 'main') { + refetchMainLog(); + } else { + refetchProcessLogs(); + } + }; + + const handleClear = () => { + if (!confirm('Are you sure you want to clear these logs?')) return; + + if (selectedProcess === 'main') { + clearMainLogMutation.mutate(); + } else { + clearProcessLogsMutation.mutate(selectedProcess); + } + }; + + const handleDownload = () => { + const blob = new Blob([currentLogs], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${selectedProcess === 'main' ? 'supervisord' : selectedProcess}-${logType}-${Date.now()}.log`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }; + + if (processesError) { + return ( +

Process Logs

-

Real-time log viewing

+ + + +

Failed to load processes

+

+ Could not connect to Supervisor. Please check your configuration. +

+
+
+
+ ); + } + + return ( +
+
+

Process Logs

+

Real-time log viewing and management

- - - -

Logs View Coming Soon

-

- This feature will display real-time logs from your processes with filtering and search capabilities. -

+ {/* Controls */} + + + Log Controls + + + {/* Process Selector */} +
+
+ + +
+ + {/* Log Type Selector (only for processes, not main) */} + {selectedProcess !== 'main' && ( +
+ +
+ + +
+
+ )} +
+ + {/* Search and Controls */} +
+ + setIsPlaying(!isPlaying)} + autoScroll={autoScroll} + onToggleAutoScroll={() => setAutoScroll(!autoScroll)} + onRefresh={handleRefresh} + onClear={handleClear} + onDownload={handleDownload} + isClearing={clearProcessLogsMutation.isPending || clearMainLogMutation.isPending} + /> +
+ + {/* Log Viewer */} +
+ +
); } diff --git a/lib/hooks/useSupervisor.ts b/lib/hooks/useSupervisor.ts index 0825821..d955963 100644 --- a/lib/hooks/useSupervisor.ts +++ b/lib/hooks/useSupervisor.ts @@ -188,3 +188,99 @@ export function useRestartProcess() { }, }); } + +// Log Management + +async function fetchMainLog(offset: number = -4096, length: number = 4096): 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}`); + }, + }); +}