/** * DLQ Dashboard Page (S-3.6) * * Shows dead-letter queue depth, entries with error metadata, * and allows manual inspection and replay of failed events. */ import React, { useState, useCallback, useEffect } from 'react'; import { useOrg } from './OrgContext'; import { getDlqHealth, listDlqEntries, replayDlqEntry, replayDlqBatch, type DlqEntry, type DlqHealth, } from './api'; // ═══════════════════════════════════════════ // DLQ Dashboard Component // ═══════════════════════════════════════════ export function DlqDashboard() { const { currentOrg: org } = useOrg(); const [health, setHealth] = useState(null); const [entries, setEntries] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [selected, setSelected] = useState>(new Set()); const [replaying, setReplaying] = useState(false); const [expandedId, setExpandedId] = useState(null); const load = useCallback(async () => { if (!org) return; setLoading(true); setError(null); try { const [h, e] = await Promise.all([ getDlqHealth(org.id), listDlqEntries(org.id, 100), ]); setHealth(h); setEntries(e); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load DLQ data'); } finally { setLoading(false); } }, [org]); useEffect(() => { load(); }, [load]); const handleReplaySingle = useCallback(async (streamId: string) => { if (!org) return; setReplaying(true); try { await replayDlqEntry(org.id, streamId); await load(); } catch (err) { setError(err instanceof Error ? err.message : 'Replay failed'); } finally { setReplaying(false); } }, [org, load]); const handleReplaySelected = useCallback(async () => { if (!org || selected.size === 0) return; setReplaying(true); try { const result = await replayDlqBatch(org.id, Array.from(selected)); setSelected(new Set()); if (result.failed > 0) { setError(`Replayed ${result.replayed}, failed ${result.failed}: ${result.errors.join(', ')}`); } await load(); } catch (err) { setError(err instanceof Error ? err.message : 'Batch replay failed'); } finally { setReplaying(false); } }, [org, selected, load]); const toggleSelect = (streamId: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(streamId)) next.delete(streamId); else next.add(streamId); return next; }); }; const toggleSelectAll = () => { if (selected.size === entries.length) { setSelected(new Set()); } else { setSelected(new Set(entries.map((e) => e.streamId))); } }; if (!org) return
Select an organization
; return (

Dead Letter Queue

{/* Health Banner */} {health && (
{health.dlqDepth === 0 ? '✅ DLQ Empty' : `⚠️ ${health.dlqDepth} events in DLQ`} {health.dlqWarning && (

{health.dlqWarning}

)}
{selected.size > 0 && ( )}
)} {error && (
{error}
)} {/* Entry Table */} {entries.length === 0 && !loading ? (
No events in the dead letter queue. All clear! 🎉
) : (
{entries.map((entry) => ( setExpandedId(expandedId === entry.streamId ? null : entry.streamId)} > {expandedId === entry.streamId && ( )} ))}
0} onChange={toggleSelectAll} /> Event ID Type Reason DLQ Time Actions
e.stopPropagation()}> toggleSelect(entry.streamId)} /> {entry.event.id} {entry.event.type} {entry.dlqReason} {entry.dlqTimestamp ? new Date(entry.dlqTimestamp).toLocaleString() : '—'} e.stopPropagation()}>
                          {JSON.stringify(entry.event, null, 2)}
                        
Stream ID: {entry.streamId} | Original: {entry.originalStreamId}
)}
); }