diff --git a/app/api/supervisor/groups/[name]/signal/route.ts b/app/api/supervisor/groups/[name]/signal/route.ts
new file mode 100644
index 0000000..f208aec
--- /dev/null
+++ b/app/api/supervisor/groups/[name]/signal/route.ts
@@ -0,0 +1,37 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { createSupervisorClient } from '@/lib/supervisor/client';
+
+interface RouteParams {
+ params: Promise<{ name: string }>;
+}
+
+// POST - Send signal to all processes in a group
+export async function POST(request: NextRequest, { params }: RouteParams) {
+ try {
+ const { name } = await params;
+ const body = await request.json();
+ const { signal } = body;
+
+ if (!signal) {
+ return NextResponse.json(
+ { error: 'Signal is required' },
+ { status: 400 }
+ );
+ }
+
+ const client = createSupervisorClient();
+ const results = await client.signalProcessGroup(name, signal);
+
+ return NextResponse.json({
+ success: true,
+ message: `Signal ${signal} sent to group ${name}`,
+ results,
+ });
+ } catch (error: any) {
+ console.error('Supervisor signal process group error:', error);
+ return NextResponse.json(
+ { error: error.message || 'Failed to send signal to process group' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/supervisor/processes/[name]/signal/route.ts b/app/api/supervisor/processes/[name]/signal/route.ts
new file mode 100644
index 0000000..5e41c15
--- /dev/null
+++ b/app/api/supervisor/processes/[name]/signal/route.ts
@@ -0,0 +1,36 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { createSupervisorClient } from '@/lib/supervisor/client';
+
+interface RouteParams {
+ params: Promise<{ name: string }>;
+}
+
+// POST - Send signal to a process
+export async function POST(request: NextRequest, { params }: RouteParams) {
+ try {
+ const { name } = await params;
+ const body = await request.json();
+ const { signal } = body;
+
+ if (!signal) {
+ return NextResponse.json(
+ { error: 'Signal is required' },
+ { status: 400 }
+ );
+ }
+
+ const client = createSupervisorClient();
+ const result = await client.signalProcess(name, signal);
+
+ return NextResponse.json({
+ success: result,
+ message: `Signal ${signal} sent to ${name}`,
+ });
+ } catch (error: any) {
+ console.error('Supervisor signal process error:', error);
+ return NextResponse.json(
+ { error: error.message || 'Failed to send signal to process' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/app/api/supervisor/processes/signal-all/route.ts b/app/api/supervisor/processes/signal-all/route.ts
new file mode 100644
index 0000000..914b32a
--- /dev/null
+++ b/app/api/supervisor/processes/signal-all/route.ts
@@ -0,0 +1,32 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { createSupervisorClient } from '@/lib/supervisor/client';
+
+// POST - Send signal to all processes
+export async function POST(request: NextRequest) {
+ try {
+ const body = await request.json();
+ const { signal } = body;
+
+ if (!signal) {
+ return NextResponse.json(
+ { error: 'Signal is required' },
+ { status: 400 }
+ );
+ }
+
+ const client = createSupervisorClient();
+ const results = await client.signalAllProcesses(signal);
+
+ return NextResponse.json({
+ success: true,
+ message: `Signal ${signal} sent to all processes`,
+ results,
+ });
+ } catch (error: any) {
+ console.error('Supervisor signal all processes error:', error);
+ return NextResponse.json(
+ { error: error.message || 'Failed to send signal to all processes' },
+ { status: 500 }
+ );
+ }
+}
diff --git a/components/process/ProcessCard.tsx b/components/process/ProcessCard.tsx
index 81e24e9..25793f3 100644
--- a/components/process/ProcessCard.tsx
+++ b/components/process/ProcessCard.tsx
@@ -1,11 +1,13 @@
'use client';
-import { Play, Square, RotateCw, Activity } from 'lucide-react';
+import { useState } from 'react';
+import { Play, Square, RotateCw, Activity, Zap } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { ProcessInfo, ProcessStateCode, getProcessStateClass, formatUptime, canStartProcess, canStopProcess } from '@/lib/supervisor/types';
import { useStartProcess, useStopProcess, useRestartProcess } from '@/lib/hooks/useSupervisor';
+import { SignalSender } from './SignalSender';
import { cn } from '@/lib/utils/cn';
interface ProcessCardProps {
@@ -15,6 +17,7 @@ interface ProcessCardProps {
}
export function ProcessCard({ process, isSelected = false, onSelectionChange }: ProcessCardProps) {
+ const [showSignalModal, setShowSignalModal] = useState(false);
const startMutation = useStartProcess();
const stopMutation = useStopProcess();
const restartMutation = useRestartProcess();
@@ -131,6 +134,15 @@ export function ProcessCard({ process, isSelected = false, onSelectionChange }:
>
+
{/* Description */}
@@ -140,6 +152,11 @@ export function ProcessCard({ process, isSelected = false, onSelectionChange }:
)}
+
+ {/* Signal Modal */}
+ {showSignalModal && (
+ setShowSignalModal(false)} />
+ )}
);
}
diff --git a/components/process/SignalSender.tsx b/components/process/SignalSender.tsx
new file mode 100644
index 0000000..d94293b
--- /dev/null
+++ b/components/process/SignalSender.tsx
@@ -0,0 +1,154 @@
+'use client';
+
+import { useState } from 'react';
+import { useSignalProcess } from '@/lib/hooks/useSupervisor';
+import { Button } from '@/components/ui/button';
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
+import { X, AlertTriangle } from 'lucide-react';
+import { cn } from '@/lib/utils/cn';
+
+interface SignalSenderProps {
+ processName: string;
+ onClose: () => void;
+}
+
+const COMMON_SIGNALS = [
+ { value: 'HUP', label: 'HUP (1)', description: 'Hangup - reload configuration', dangerous: false },
+ { value: 'INT', label: 'INT (2)', description: 'Interrupt - graceful shutdown', dangerous: false },
+ { value: 'QUIT', label: 'QUIT (3)', description: 'Quit', dangerous: false },
+ { value: 'TERM', label: 'TERM (15)', description: 'Terminate - graceful shutdown', dangerous: true },
+ { value: 'KILL', label: 'KILL (9)', description: 'Kill - immediate termination', dangerous: true },
+ { value: 'USR1', label: 'USR1 (10)', description: 'User-defined signal 1', dangerous: false },
+ { value: 'USR2', label: 'USR2 (12)', description: 'User-defined signal 2', dangerous: false },
+];
+
+export function SignalSender({ processName, onClose }: SignalSenderProps) {
+ const [selectedSignal, setSelectedSignal] = useState('');
+ const [customSignal, setCustomSignal] = useState('');
+ const [showConfirm, setShowConfirm] = useState(false);
+
+ const signalMutation = useSignalProcess();
+
+ const handleSendSignal = () => {
+ const signal = selectedSignal || customSignal;
+ if (!signal) return;
+
+ const isDangerous = ['TERM', 'KILL', 'QUIT'].includes(signal.toUpperCase());
+
+ if (isDangerous && !showConfirm) {
+ setShowConfirm(true);
+ return;
+ }
+
+ signalMutation.mutate(
+ { name: processName, signal },
+ {
+ onSuccess: () => {
+ onClose();
+ },
+ }
+ );
+ };
+
+ const signal = selectedSignal || customSignal;
+ const isDangerous = signal && ['TERM', 'KILL', 'QUIT'].includes(signal.toUpperCase());
+
+ return (
+
+
+
+
+
+ Send Signal
+
+ Send Unix signal to {processName}
+
+
+
+
+
+
+
+ {/* Common Signals */}
+
+
+
+ {COMMON_SIGNALS.map((sig) => (
+
+ ))}
+
+
+
+ {/* Custom Signal */}
+
+
+ {
+ setCustomSignal(e.target.value.toUpperCase());
+ setSelectedSignal('');
+ setShowConfirm(false);
+ }}
+ className="w-full h-10 rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
+ />
+
+
+ {/* Confirmation Warning */}
+ {isDangerous && showConfirm && (
+
+
+
+
+
Warning: Dangerous Signal
+
+ Signal {signal} may terminate or kill the process.
+ Are you sure you want to continue?
+
+
+
+
+ )}
+
+ {/* Actions */}
+
+
+
+
+
+
+
+ );
+}
diff --git a/lib/hooks/useSupervisor.ts b/lib/hooks/useSupervisor.ts
index 9c162d7..9d074c3 100644
--- a/lib/hooks/useSupervisor.ts
+++ b/lib/hooks/useSupervisor.ts
@@ -558,3 +558,90 @@ export function useRemoveProcessGroup() {
},
});
}
+
+// 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();
+
+ return useMutation({
+ mutationFn: ({ name, signal }: { name: string; signal: string }) => signalProcess(name, signal),
+ onSuccess: (data, variables) => {
+ toast.success(data.message);
+ queryClient.invalidateQueries({ queryKey: supervisorKeys.process(variables.name) });
+ queryClient.invalidateQueries({ queryKey: supervisorKeys.processes() });
+ },
+ onError: (error: Error) => {
+ 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}`);
+ },
+ });
+}