'use client' import { useState, useEffect, useCallback } from 'react' import { useSmartPoll } from '@/lib/use-smart-poll' import { useMissionControl } from '@/store' interface Webhook { id: number name: string url: string secret: string | null events: string[] enabled: boolean last_fired_at: number | null last_status: number | null total_deliveries: number successful_deliveries: number failed_deliveries: number created_at: number updated_at: number } interface Delivery { id: number webhook_id: number webhook_name: string webhook_url: string event_type: string payload: string status_code: number | null response_body: string | null error: string | null duration_ms: number created_at: number } interface SchedulerTask { id: string name: string enabled: boolean lastRun: number | null nextRun: number | null running: boolean lastResult?: { ok: boolean; message: string; timestamp: number } } const AVAILABLE_EVENTS = [ { value: '*', label: 'All events', description: 'Receive all event types' }, { value: 'agent.error', label: 'Agent error', description: 'Agent enters error state' }, { value: 'agent.status_change', label: 'Agent status change', description: 'Any agent status transition' }, { value: 'security.login_failed', label: 'Login failed', description: 'Failed login attempt' }, { value: 'security.user_created', label: 'User created', description: 'New user account created' }, { value: 'security.user_deleted', label: 'User deleted', description: 'User account deleted' }, { value: 'security.password_change', label: 'Password changed', description: 'User password modified' }, { value: 'notification.mention', label: 'Mention', description: 'Agent was @mentioned' }, { value: 'notification.assignment', label: 'Assignment', description: 'Task assigned to agent' }, { value: 'activity.task_created', label: 'Task created', description: 'New task added' }, { value: 'activity.task_updated', label: 'Task updated', description: 'Task status changed' }, ] export function WebhookPanel() { const { dashboardMode } = useMissionControl() const isLocalMode = dashboardMode === 'local' const [webhooks, setWebhooks] = useState([]) const [webhookAutomations, setWebhookAutomations] = useState([]) const [deliveries, setDeliveries] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [showCreate, setShowCreate] = useState(false) const [selectedWebhook, setSelectedWebhook] = useState(null) const [testingId, setTestingId] = useState(null) const [testResult, setTestResult] = useState(null) const [newSecret, setNewSecret] = useState(null) const [runningAutomationId, setRunningAutomationId] = useState(null) const fetchWebhooks = useCallback(async () => { try { setLoading(true) const res = await fetch('/api/webhooks') if (!res.ok) { const data = await res.json() setError(data.error || 'Failed to fetch webhooks') return } const data = await res.json() setWebhooks(data.webhooks || []) setError('') } catch { setError('Network error') } finally { setLoading(false) } }, []) const fetchDeliveries = useCallback(async () => { if (!selectedWebhook) return try { const res = await fetch(`/api/webhooks/deliveries?webhook_id=${selectedWebhook}&limit=20`) if (res.ok) { const data = await res.json() setDeliveries(data.deliveries || []) } } catch { /* silent */ } }, [selectedWebhook]) const fetchWebhookAutomations = useCallback(async () => { if (!isLocalMode) { setWebhookAutomations([]) return } try { const res = await fetch('/api/scheduler') if (!res.ok) return const data = await res.json() const tasks = Array.isArray(data.tasks) ? data.tasks : [] const webhookTasks = tasks.filter((task: SchedulerTask) => typeof task.id === 'string' && task.id.includes('webhook') ) setWebhookAutomations(webhookTasks) } catch { // Keep UI usable if scheduler endpoint is unavailable. } }, [isLocalMode]) useEffect(() => { fetchWebhooks() }, [fetchWebhooks]) useEffect(() => { fetchDeliveries() }, [fetchDeliveries]) useEffect(() => { fetchWebhookAutomations() }, [fetchWebhookAutomations]) useSmartPoll(fetchWebhooks, 60000, { pauseWhenDisconnected: true }) useSmartPoll(fetchWebhookAutomations, 60000, { pauseWhenDisconnected: true }) async function handleCreate(form: { name: string; url: string; events: string[] }) { try { const res = await fetch('/api/webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...form, generate_secret: true }), }) const data = await res.json() if (!res.ok) { setError(data.error); return } setNewSecret(data.secret) setShowCreate(false) fetchWebhooks() } catch { setError('Failed to create webhook') } } async function handleToggle(id: number, enabled: boolean) { await fetch('/api/webhooks', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id, enabled }), }) fetchWebhooks() } async function handleDelete(id: number) { await fetch(`/api/webhooks?id=${id}`, { method: 'DELETE' }) if (selectedWebhook === id) setSelectedWebhook(null) fetchWebhooks() } async function handleTest(id: number) { setTestingId(id) setTestResult(null) try { const res = await fetch('/api/webhooks/test', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id }), }) const data = await res.json() setTestResult(data) fetchWebhooks() if (selectedWebhook === id) fetchDeliveries() } catch { setTestResult({ error: 'Network error' }) } finally { setTestingId(null) } } async function handleRunAutomation(taskId: string) { setRunningAutomationId(taskId) try { const res = await fetch('/api/scheduler', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ task_id: taskId }), }) const data = await res.json() setTestResult({ success: !!data.ok && res.ok, error: data.error || (!data.ok ? data.message : null), duration_ms: undefined, status_code: res.status, }) await fetchWebhookAutomations() } catch { setTestResult({ success: false, error: 'Failed to run local automation' }) } finally { setRunningAutomationId(null) } } function formatTime(ts: number) { return new Date(ts * 1000).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', }) } return (
{/* Header */}

Webhooks

{webhooks.length} webhook{webhooks.length !== 1 ? 's' : ''} configured

{error && (
{error}
)} {/* Secret reveal (after creation) */} {newSecret && (

Webhook Secret (save now - shown only once)

{newSecret}
)} {/* Test result */} {testResult && (

{testResult.success ? ( Test successful ) : ( Test failed )}

{testResult.status_code &&

Status: {testResult.status_code}

} {testResult.duration_ms &&

Duration: {testResult.duration_ms}ms

} {testResult.error &&

Error: {testResult.error}

}
)} {/* Create form */} {showCreate && ( setShowCreate(false)} /> )} {/* Webhook list */}
{isLocalMode && webhookAutomations.length > 0 && (

Local Webhook Automations

Local scheduler tasks that support webhook delivery and retries

{webhookAutomations.map((task) => (
{task.name} {task.id}
{task.nextRun ? `Next run ${formatTime(task.nextRun / 1000)}` : 'No next run scheduled'} {task.lastResult?.message ? ` ยท ${task.lastResult.message}` : ''}
))}
)} {loading && webhooks.length === 0 ? (
{[...Array(3)].map((_, i) =>
)}
) : webhooks.length === 0 ? (

No webhooks configured

Add a webhook to receive HTTP notifications for events

) : ( webhooks.map((wh) => (
setSelectedWebhook(selectedWebhook === wh.id ? null : wh.id)} >
{wh.name} {wh.last_status !== null && ( = 200 && wh.last_status < 300 ? 'bg-green-500/10 text-green-400' : 'bg-red-500/10 text-red-400' }`}> {wh.last_status} )}

{wh.url}

{wh.events.includes('*') ? 'All events' : `${wh.events.length} event${wh.events.length !== 1 ? 's' : ''}`} {wh.total_deliveries} deliveries {wh.failed_deliveries > 0 && ( {wh.failed_deliveries} failed )} {wh.last_fired_at && ( Last fired {formatTime(wh.last_fired_at)} )}
{/* Delivery log (expanded) */} {selectedWebhook === wh.id && (

Recent Deliveries

{deliveries.length === 0 ? (

No deliveries recorded yet

) : (
{deliveries.map((d) => (
= 200 && d.status_code < 300 ? 'bg-green-500' : 'bg-red-500' }`} /> {d.event_type} = 200 && d.status_code < 300 ? 'text-green-400' : 'text-red-400' }`}> {d.status_code ?? 'ERR'} {d.duration_ms}ms {d.error && ( {d.error} )} {formatTime(d.created_at)}
))}
)}
)}
)) )}
) } function CreateWebhookForm({ onSubmit, onCancel, }: { onSubmit: (form: { name: string; url: string; events: string[] }) => void onCancel: () => void }) { const [name, setName] = useState('') const [url, setUrl] = useState('') const [selectedEvents, setSelectedEvents] = useState(['*']) function toggleEvent(value: string) { if (value === '*') { setSelectedEvents(['*']) return } setSelectedEvents((prev) => { const without = prev.filter((e) => e !== '*' && e !== value) if (prev.includes(value)) return without.length === 0 ? ['*'] : without return [...without, value] }) } return (

New Webhook

setName(e.target.value)} placeholder="e.g. Slack alerts" className="w-full h-8 px-2.5 rounded-md bg-secondary border border-border text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-primary" />
setUrl(e.target.value)} placeholder="https://hooks.slack.com/services/..." className="w-full h-8 px-2.5 rounded-md bg-secondary border border-border text-sm text-foreground font-mono focus:outline-none focus:ring-1 focus:ring-primary" />
{AVAILABLE_EVENTS.map((ev) => ( ))}
) }