feat(webhooks): admin replay for failed/dead-letter deliveries
Outbound webhook deliveries already retry with backoff, dead-letter after maxAttempts, and notify super admins. This adds operator-level replay: a per-row button on the deliveries log spawns a fresh pending delivery + queues a new BullMQ job. The original failed row stays intact so the response body remains for audit; the replay payload carries retried_from/retried_at markers so receivers can deduplicate. Inbound idempotency was already handled via the documentEvents signatureHash unique index. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { RotateCcw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
@@ -39,6 +41,22 @@ export function WebhookDeliveryLog({ webhookId }: Props) {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [retrying, setRetrying] = useState<string | null>(null);
|
||||
|
||||
async function retry(deliveryId: string) {
|
||||
setRetrying(deliveryId);
|
||||
try {
|
||||
await apiFetch(`/api/v1/admin/webhooks/${webhookId}/deliveries/${deliveryId}/redeliver`, {
|
||||
method: 'POST',
|
||||
});
|
||||
toast.success('Replay queued');
|
||||
await load(page);
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : 'Replay failed');
|
||||
} finally {
|
||||
setRetrying(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function load(p: number) {
|
||||
setLoading(true);
|
||||
@@ -80,6 +98,7 @@ export function WebhookDeliveryLog({ webhookId }: Props) {
|
||||
<TableHead>HTTP</TableHead>
|
||||
<TableHead>Attempt</TableHead>
|
||||
<TableHead>Time</TableHead>
|
||||
<TableHead className="w-16 text-right">Replay</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -96,6 +115,22 @@ export function WebhookDeliveryLog({ webhookId }: Props) {
|
||||
? new Date(d.deliveredAt).toLocaleString()
|
||||
: new Date(d.createdAt).toLocaleString()}
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
{(d.status === 'failed' || d.status === 'dead_letter') && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={retrying === d.id}
|
||||
onClick={() => retry(d.id)}
|
||||
title="Replay this delivery"
|
||||
aria-label="Replay this delivery"
|
||||
>
|
||||
<RotateCcw
|
||||
className={`h-3.5 w-3.5 ${retrying === d.id ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
Reference in New Issue
Block a user