feat: retry failed extractions button on pipeline page

- POST /api/ops/pipeline/retry-failed endpoint resets extraction_failed
  docs to parsed, deletes failed intelligence rows, and re-enqueues
  them (batch of 200)
- Scheduler now auto-retries extraction_failed docs every ~10 minutes
  (100 per cycle, 60-min cooldown per doc)
- Pipeline page shows 'Retry Failed (N)' button when extraction_failed
  count > 0, with pending/success/error states
This commit is contained in:
Celes Renata
2026-04-20 08:09:29 +00:00
parent 5289f0f195
commit de35279269
5 changed files with 152 additions and 2 deletions
+8
View File
@@ -525,6 +525,14 @@ export function usePipelineHealth(hours = 24) {
return useGet<Record<string, unknown>>(['pipeline-health', hours], 'query', `/api/ops/pipeline/health?hours=${hours}`);
}
export function useRetryFailedExtractions() {
const qc = useQueryClient();
return useMutation({
mutationFn: () => apiPost<{ retried: number; message: string }>('query', '/api/ops/pipeline/retry-failed', {}),
onSuccess: () => qc.invalidateQueries({ queryKey: ['pipeline-health'] }),
});
}
export function useIngestionSummary(hours = 24) {
return useGet<Record<string, unknown>>(['ingestion-summary', hours], 'query', `/api/ops/ingestion/summary?hours=${hours}`);
}
+20 -1
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { usePipelineHealth } from '../api/hooks';
import { usePipelineHealth, useRetryFailedExtractions } from '../api/hooks';
import { LoadingSpinner, DateRangeSelector, Card } from '../components/ui';
const QUEUE_LABELS: Record<string, string> = {
@@ -53,6 +53,7 @@ export function OpsPipelinePage() {
const [hours, setHours] = useState(24);
const { data, isLoading } = usePipelineHealth(hours);
const stream = usePipelineStream();
const retryMutation = useRetryFailedExtractions();
if (isLoading) return <LoadingSpinner />;
@@ -70,6 +71,8 @@ export function OpsPipelinePage() {
.map((s) => [s.status, s.doc_count]),
);
const failedCount = docStages['extraction_failed'] ?? 0;
// Separate DLQ entries from regular queues
const dlqEntries = Object.entries(queueDepths).filter(([k]) => k.startsWith('dlq:'));
const regularQueues = Object.entries(QUEUE_LABELS);
@@ -79,6 +82,22 @@ export function OpsPipelinePage() {
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold text-gray-100">Pipeline Health</h1>
<div className="flex items-center gap-3">
{failedCount > 0 && (
<button
type="button"
onClick={() => retryMutation.mutate()}
disabled={retryMutation.isPending}
className="rounded-md bg-amber-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-amber-500 disabled:opacity-50"
>
{retryMutation.isPending ? 'Retrying…' : `Retry Failed (${failedCount})`}
</button>
)}
{retryMutation.isSuccess && (
<span className="text-xs text-green-400">{retryMutation.data.message}</span>
)}
{retryMutation.isError && (
<span className="text-xs text-red-400">Retry failed</span>
)}
{stream && (
<span className="flex items-center gap-1.5 text-xs text-green-400">
<span className="inline-block h-2 w-2 rounded-full bg-green-400 animate-pulse" aria-hidden="true" />
+2 -1
View File
@@ -104,7 +104,8 @@ export const handlers = [
return HttpResponse.json({ id: 'lockout-new', ticker: body.ticker, reason: body.reason, lockout_type: body.lockout_type ?? 'manual', expires_at: new Date(Date.now() + ((body.duration_minutes as number) ?? 60) * 60000).toISOString(), created_at: new Date().toISOString() }, { status: 201 });
}),
http.delete('/api/admin/trading/lockouts/:id', () => HttpResponse.json({ status: 'deleted' })),
http.get('/api/ops/pipeline/health', () => HttpResponse.json({ hours: 24, document_stages: [{ status: 'extracted', doc_count: 5 }], parsing: {}, extraction: {}, aggregation: {} })),
http.get('/api/ops/pipeline/health', () => HttpResponse.json({ hours: 24, document_stages: [{ status: 'extracted', doc_count: 5 }], parsing: {}, extraction: {}, aggregation: {}, queue_depths: {} })),
http.post('/api/ops/pipeline/retry-failed', () => HttpResponse.json({ retried: 10, message: 'Re-enqueued 10 documents for extraction' })),
http.get('/api/ops/ingestion/summary', () => HttpResponse.json({ total_runs: 10, completed: 8, failed: 2, total_items_fetched: 50, total_items_new: 12, by_source_type: [] })),
http.get('/api/ops/ingestion/throughput', () => HttpResponse.json([])),
http.get('/api/ops/model/performance', () => HttpResponse.json({ total_extractions: 20, success_rate: 0.9, avg_duration_ms: 1500, retry_rate: 0.05, avg_confidence: 0.8 })),