'use client'; import { useEffect, useState, useCallback } from 'react'; import { Plus, Trash2, RotateCcw, Send, ChevronDown, ChevronRight, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Progress } from '@/components/ui/progress'; import { ScrollArea } from '@/components/ui/scroll-area'; // ── Types ────────────────────────────────────────────────────────────────── interface Webhook { id: string; name: string; url: string; events: string[]; active: boolean; secret?: string; domain?: string; retry_count: number; last_triggered_at?: string; created: string; updated: string; } interface WebhookDelivery { id: string; webhook_id: string; event_type: string; payload: Record; success: boolean; response_status: number; response_body: string; attempts: number; created: string; } const AVAILABLE_EVENTS = [ '*', 'task.completed', 'habit.completed', 'habit.streak_broken', 'milestone.reached', 'project.status_changed', 'report.generated', 'agent_task.completed', ]; // ── Webhook Delivery History ─────────────────────────────────────────────── function WebhookDeliveryHistory({ webhookId }: { webhookId?: string }) { const [deliveries, setDeliveries] = useState([]); const [loading, setLoading] = useState(true); const [retryingId, setRetryingId] = useState(null); const [expandedId, setExpandedId] = useState(null); const fetchDeliveries = useCallback(async () => { setLoading(true); try { const params = new URLSearchParams({ perPage: '50', sort: '-created' }); if (webhookId) params.set('webhook_id', webhookId); const response = await fetch(`/api/webhook-deliveries?${params}`); if (response.ok) { const data = await response.json(); setDeliveries(data.items || []); } } catch (error) { console.error('Failed to fetch deliveries:', error); } finally { setLoading(false); } }, [webhookId]); useEffect(() => { fetchDeliveries(); }, [fetchDeliveries]); async function handleRetry(deliveryId: string) { setRetryingId(deliveryId); try { const response = await fetch(`/api/webhook-deliveries/${deliveryId}/retry`, { method: 'POST', }); if (response.ok) { toast.success('Retry queued'); fetchDeliveries(); } else { const data = await response.json(); toast.error(data.error?.message || 'Failed to queue retry'); } } catch (error) { toast.error('Failed to queue retry'); } finally { setRetryingId(null); } } if (loading) { return (

Loading delivery history...

); } if (deliveries.length === 0) { return (

No deliveries yet. Send a test event or trigger an event to see delivery history.

); } return (
{deliveries.map((delivery) => (
{delivery.success ? 'Success' : 'Failed'} {delivery.response_status > 0 && ( {delivery.response_status} )}
{delivery.attempts} attempt{delivery.attempts !== 1 ? 's' : ''} {!delivery.success && ( )}
{expandedId === delivery.id && (

{new Date(delivery.created).toLocaleString()}

{delivery.response_body && (
                      {delivery.response_body}
                    
)}
                    {JSON.stringify(delivery.payload, null, 2)}
                  
)}
))}
); } // ── Main Component ───────────────────────────────────────────────────────── export function SettingsWebhooks() { const [webhooks, setWebhooks] = useState([]); const [loading, setLoading] = useState(true); const [newWebhookName, setNewWebhookName] = useState(''); const [newWebhookUrl, setNewWebhookUrl] = useState(''); const [createDialogOpen, setCreateDialogOpen] = useState(false); const [testingId, setTestingId] = useState(null); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [activeTab, setActiveTab] = useState('webhooks'); const fetchWebhooks = useCallback(async () => { try { const response = await fetch('/api/webhooks'); if (response.ok) { const data = await response.json(); setWebhooks(data.items || []); } } catch (error) { console.error('Failed to fetch webhooks:', error); } finally { setLoading(false); } }, []); useEffect(() => { fetchWebhooks(); }, [fetchWebhooks]); async function addWebhook() { if (!newWebhookName.trim() || !newWebhookUrl.trim()) return; try { const response = await fetch('/api/webhooks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: newWebhookName, url: newWebhookUrl, events: ['*'], active: true, domain: 'default', retry_count: 3, }), }); if (response.ok) { toast.success('Webhook created'); setNewWebhookName(''); setNewWebhookUrl(''); setCreateDialogOpen(false); fetchWebhooks(); } else { const data = await response.json(); toast.error(data.error?.message || 'Failed to create webhook'); } } catch (error) { toast.error('Failed to create webhook'); } } async function toggleWebhook(webhook: Webhook) { try { const response = await fetch(`/api/webhooks/${webhook.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ active: !webhook.active }), }); if (response.ok) { toast.success(webhook.active ? 'Webhook disabled' : 'Webhook enabled'); fetchWebhooks(); } else { toast.error('Failed to update webhook'); } } catch (error) { toast.error('Failed to update webhook'); } } async function deleteWebhook(id: string) { try { await fetch(`/api/webhooks/${id}`, { method: 'DELETE' }); toast.success('Webhook deleted'); setDeleteConfirmId(null); fetchWebhooks(); } catch (error) { toast.error('Failed to delete webhook'); } } async function testWebhook(id: string) { setTestingId(id); try { const response = await fetch(`/api/webhooks/${id}/test`, { method: 'POST' }); const data = await response.json(); const statusCode = data.status || 0; const responseBody = data.response || ''; const preview = responseBody.substring(0, 200); if (data.success) { toast.success(`Test delivery succeeded (${statusCode})`, { description: preview || undefined, }); } else { toast.error(`Test delivery failed (${statusCode})`, { description: preview || undefined, }); } } catch (error) { toast.error('Failed to send test event'); } finally { setTestingId(null); } } return (
Webhooks Configure outbound webhooks for event notifications.
Webhooks Delivery History {activeTab === 'webhooks' && ( Create Webhook
setNewWebhookName(e.target.value)} placeholder="e.g., Slack notifications" />
setNewWebhookUrl(e.target.value)} placeholder="https://example.com/webhook" />
)}
{loading ? (

Loading webhooks...

) : webhooks.length === 0 ? (

No webhooks configured. Click "New webhook" to get started.

) : (
{webhooks.map((webhook) => (

{webhook.name}

{webhook.active ? 'Active' : 'Disabled'}

{webhook.url}

{webhook.events.slice(0, 4).map((event) => ( {event} ))} {webhook.events.length > 4 && ( +{webhook.events.length - 4} more )}
toggleWebhook(webhook)} aria-label={`Toggle ${webhook.name}`} />
))}
)}
{/* Delete confirmation dialog */} setDeleteConfirmId(null)}> Delete webhook? This will permanently delete this webhook and all its delivery history. This action cannot be undone. Cancel deleteConfirmId && deleteWebhook(deleteConfirmId)} className="bg-destructive text-destructive-foreground hover:bg-destructive/90" > Delete
); }