feat(outbox): UI fuer Event-Outbox — Modul 9/16 des UI-Backlogs

This commit is contained in:
Agent Zero
2026-09-13 23:13:58 +02:00
parent 00bfcafb9c
commit 31154b9dc6
7 changed files with 863 additions and 3 deletions
+130
View File
@@ -0,0 +1,130 @@
/**
* Outbox API client — transactional outbox monitoring & management.
*
* Backend: /api/v1/outbox (stats, failed, replay, replay-all,
* consumer-registry, recover-stuck, cleanup-published).
* All endpoints require the admin role (require_admin, enforced server-side).
*/
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { apiGet, apiPost } from '@/api/client';
export interface OutboxStats {
/** Count per status: pending, processing, published, failed, no_handlers. */
counts: Record<string, number>;
total: number;
oldest_pending_age_seconds: number | null;
}
export interface FailedOutboxEvent {
id: string;
tenant_id: string;
event_name: string;
error_message: string | null;
failed_at: string | null;
attempts: number;
created_at: string | null;
status: string;
}
export interface FailedEventsResponse {
events: FailedOutboxEvent[];
limit: number;
offset: number;
count: number;
}
export interface ConsumerRegistryResponse {
registry: Record<string, string[]>;
}
export interface ReplaySingleResponse {
replayed: boolean;
event_id: string;
}
export interface ReplayAllResponse {
replayed_count: number;
tenant_id: string;
}
export interface RecoverStuckResponse {
recovered_count: number;
timeout_seconds: number;
}
export interface CleanupPublishedResponse {
deleted_count: number;
retention_days: number;
}
// ─── Query hooks ─────────────────────────────────────────────
export function useOutboxStats(enabled = true) {
return useQuery<OutboxStats>({
queryKey: ['outbox', 'stats'],
queryFn: () => apiGet<OutboxStats>('/outbox/stats'),
enabled,
});
}
export function useOutboxFailedEvents(limit: number, offset: number, enabled = true) {
return useQuery<FailedEventsResponse>({
queryKey: ['outbox', 'failed', limit, offset],
queryFn: () =>
apiGet<FailedEventsResponse>(`/outbox/failed?limit=${limit}&offset=${offset}`),
enabled,
});
}
export function useOutboxConsumerRegistry(enabled = true) {
return useQuery<ConsumerRegistryResponse>({
queryKey: ['outbox', 'consumer-registry'],
queryFn: () => apiGet<ConsumerRegistryResponse>('/outbox/consumer-registry'),
enabled,
});
}
// ─── Mutation hooks ──────────────────────────────────────────
export function useReplayFailedEvent() {
const qc = useQueryClient();
return useMutation<ReplaySingleResponse, Error, string>({
mutationFn: (eventId) => apiPost<ReplaySingleResponse>(`/outbox/replay/${eventId}`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['outbox'] });
},
});
}
export function useReplayAllFailedEvents() {
const qc = useQueryClient();
return useMutation<ReplayAllResponse, Error, void>({
mutationFn: () => apiPost<ReplayAllResponse>('/outbox/replay-all'),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['outbox'] });
},
});
}
export function useRecoverStuckEvents() {
const qc = useQueryClient();
return useMutation<RecoverStuckResponse, Error, number>({
mutationFn: (timeoutSeconds) =>
apiPost<RecoverStuckResponse>(`/outbox/recover-stuck?timeout_seconds=${timeoutSeconds}`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['outbox'] });
},
});
}
export function useCleanupPublishedEvents() {
const qc = useQueryClient();
return useMutation<CleanupPublishedResponse, Error, number>({
mutationFn: (retentionDays) =>
apiPost<CleanupPublishedResponse>(`/outbox/cleanup-published?retention_days=${retentionDays}`),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['outbox'] });
},
});
}