"use client"; import { useState } from "react"; import { trpc } from "~/lib/trpc/client.js"; const WEBHOOK_EVENTS = [ "allocation.created", "allocation.updated", "allocation.deleted", "project.created", "project.status_changed", "vacation.approved", "estimate.submitted", "estimate.approved", ] as const; const EVENT_LABELS: Record = { "allocation.created": "Allocation Created", "allocation.updated": "Allocation Updated", "allocation.deleted": "Allocation Deleted", "project.created": "Project Created", "project.status_changed": "Project Status Changed", "vacation.approved": "Vacation Approved", "estimate.submitted": "Estimate Submitted", "estimate.approved": "Estimate Approved", }; const INPUT_CLASS = "app-input"; const LABEL_CLASS = "app-label"; const PRIMARY_BUTTON = "rounded-xl bg-brand-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-brand-700 disabled:opacity-50"; const SECONDARY_BUTTON = "rounded-xl border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50 disabled:opacity-50 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-800"; const DANGER_BUTTON = "rounded-xl bg-red-600 px-4 py-2 text-sm font-semibold text-white transition hover:bg-red-700 disabled:opacity-50"; interface WebhookFormData { name: string; url: string; secret: string; events: string[]; isActive: boolean; } const emptyForm: WebhookFormData = { name: "", url: "", secret: "", events: [], isActive: true, }; function maskUrl(url: string): string { try { const u = new URL(url); const host = u.hostname; // Show scheme + host, mask the rest if (u.pathname.length > 1) { return `${u.protocol}//${host}/****`; } return `${u.protocol}//${host}`; } catch { return "****"; } } export function WebhooksClient() { const utils = trpc.useUtils(); const { data: webhooks, isLoading } = trpc.webhook.list.useQuery(); const createMut = trpc.webhook.create.useMutation({ onSuccess: () => { void utils.webhook.list.invalidate(); setModalOpen(false); }, }); const updateMut = trpc.webhook.update.useMutation({ onSuccess: () => { void utils.webhook.list.invalidate(); setModalOpen(false); }, }); const deleteMut = trpc.webhook.delete.useMutation({ onSuccess: () => void utils.webhook.list.invalidate(), }); const testMut = trpc.webhook.test.useMutation(); const [modalOpen, setModalOpen] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState(emptyForm); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [testResult, setTestResult] = useState<{ id: string; success: boolean; statusCode: number; statusText: string; } | null>(null); function openCreateModal() { setEditingId(null); setForm(emptyForm); setModalOpen(true); } function openEditModal(wh: { id: string; name: string; url: string; secret: string | null; events: string[]; isActive: boolean; }) { setEditingId(wh.id); setForm({ name: wh.name, url: wh.url, secret: wh.secret ?? "", events: wh.events, isActive: wh.isActive, }); setModalOpen(true); } function toggleEvent(event: string) { setForm((prev) => ({ ...prev, events: prev.events.includes(event) ? prev.events.filter((e) => e !== event) : [...prev.events, event], })); } function handleSubmit() { if (editingId) { updateMut.mutate({ id: editingId, data: { name: form.name, url: form.url, ...(form.secret ? { secret: form.secret } : { secret: null }), events: form.events, isActive: form.isActive, }, }); } else { createMut.mutate({ name: form.name, url: form.url, ...(form.secret ? { secret: form.secret } : {}), events: form.events, isActive: form.isActive, }); } } function handleTest(id: string) { setTestResult(null); testMut.mutate( { id }, { onSuccess: (result) => { setTestResult({ id, ...result }); }, }, ); } function handleToggleActive(id: string, currentActive: boolean) { updateMut.mutate({ id, data: { isActive: !currentActive } }); } const isSaving = createMut.isPending || updateMut.isPending; return (

Webhooks

Configure outbound webhooks to notify external services about events in CapaKraken.

{/* Webhook List */} {isLoading ? (
Loading...
) : !webhooks?.length ? (
No webhooks configured yet.
) : (
{webhooks.map((wh) => (
{/* Active indicator */}
{/* Info */}
{wh.name} {wh.url.includes("hooks.slack.com") && ( Slack )}
{maskUrl(wh.url)}
{wh.events.map((ev) => ( {EVENT_LABELS[ev] ?? ev} ))}
{/* Test result */} {testResult && testResult.id === wh.id && (
Test: {testResult.statusCode} {testResult.statusText}
)}
{/* Actions */}
{deleteConfirmId === wh.id ? (
) : ( )}
))}
)} {/* Modal */} {modalOpen && (

{editingId ? "Edit Webhook" : "Create Webhook"}

{/* Name */}
setForm((prev) => ({ ...prev, name: e.target.value }))} placeholder="e.g. Slack Notifications" />
{/* URL */}
setForm((prev) => ({ ...prev, url: e.target.value }))} placeholder="https://hooks.slack.com/services/..." />
{/* Secret */}
setForm((prev) => ({ ...prev, secret: e.target.value }))} placeholder="HMAC signing secret" autoComplete="new-password" />

If set, requests include an X-Webhook-Signature header (HMAC-SHA256).

{/* Events */}
{WEBHOOK_EVENTS.map((ev) => ( ))}
{/* Active toggle */} {/* Error display */} {(createMut.error || updateMut.error) && (

{createMut.error?.message ?? updateMut.error?.message}

)} {/* Actions */}
)}
); }