fix: OpsPipeline dashboard — use time-filtered doc stages, add time labels

Document Stages now shows time-filtered counts from /health endpoint
(consistent with Parsing/Extraction/Trends sections) with all-time
totals shown as a subtle subtitle from the SSE stream. Added "(last Xh)"
labels to all sections so the time window context is always visible.
This commit is contained in:
Celes Renata
2026-07-10 22:14:56 +00:00
parent ed474c521a
commit d018c75182
+26 -7
View File
@@ -63,15 +63,18 @@ export function OpsPipelinePage() {
const aggregation = (data?.aggregation ?? {}) as Record<string, unknown>; const aggregation = (data?.aggregation ?? {}) as Record<string, unknown>;
const pipelineEnabled = (data?.pipeline_enabled ?? true) as boolean; const pipelineEnabled = (data?.pipeline_enabled ?? true) as boolean;
// Prefer live stream data for queue depths and doc stages, fall back to initial fetch // Queue depths come from live stream (always current)
// Document stages: show time-filtered from /health endpoint so numbers are consistent
// with Parsing/Extraction/Trends sections. Stream provides live queue data only.
const queueDepths = stream?.queue_depths const queueDepths = stream?.queue_depths
?? (data?.queue_depths as Record<string, number> | undefined) ?? (data?.queue_depths as Record<string, number> | undefined)
?? {}; ?? {};
const docStages = stream?.document_stages const docStages = Object.fromEntries(
?? Object.fromEntries(
((data?.document_stages as Array<{ status: string; doc_count: number }>) ?? []) ((data?.document_stages as Array<{ status: string; doc_count: number }>) ?? [])
.map((s) => [s.status, s.doc_count]), .map((s) => [s.status, s.doc_count]),
); );
// All-time totals from stream for reference
const allTimeStages = stream?.document_stages ?? {};
const failedCount = docStages['extraction_failed'] ?? 0; const failedCount = docStages['extraction_failed'] ?? 0;
@@ -150,9 +153,13 @@ export function OpsPipelinePage() {
{/* Document Stage Counts */} {/* Document Stage Counts */}
<Card> <Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Document Stages</h2> <h2 className="mb-3 text-sm font-medium text-gray-400">
Document Stages
<span className="ml-2 text-xs text-gray-600">(last {hours}h)</span>
</h2>
<div className="grid grid-cols-2 gap-3 sm:grid-cols-4"> <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
{Object.entries(docStages).map(([status, count]) => { {Object.entries(docStages).map(([status, count]) => {
const allTime = allTimeStages[status];
const color = status === 'extracted' ? 'text-green-400' const color = status === 'extracted' ? 'text-green-400'
: status === 'parsed' ? 'text-yellow-400' : status === 'parsed' ? 'text-yellow-400'
: status === 'extraction_failed' ? 'text-red-400' : status === 'extraction_failed' ? 'text-red-400'
@@ -162,6 +169,9 @@ export function OpsPipelinePage() {
<div key={status} className="rounded-lg border border-surface-700 bg-surface-950 p-3 text-center"> <div key={status} className="rounded-lg border border-surface-700 bg-surface-950 p-3 text-center">
<div className={`text-2xl font-bold tabular-nums ${color}`}>{count}</div> <div className={`text-2xl font-bold tabular-nums ${color}`}>{count}</div>
<div className="text-xs capitalize text-gray-500">{status.replace('_', ' ')}</div> <div className="text-xs capitalize text-gray-500">{status.replace('_', ' ')}</div>
{allTime != null && allTime !== count && (
<div className="mt-0.5 text-[10px] text-gray-600">{allTime} total</div>
)}
</div> </div>
); );
})} })}
@@ -170,7 +180,10 @@ export function OpsPipelinePage() {
{/* Parsing Quality */} {/* Parsing Quality */}
<Card> <Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Parsing Quality</h2> <h2 className="mb-3 text-sm font-medium text-gray-400">
Parsing Quality
<span className="ml-2 text-xs text-gray-600">(last {hours}h)</span>
</h2>
<dl className="grid grid-cols-2 gap-3 text-sm sm:grid-cols-5"> <dl className="grid grid-cols-2 gap-3 text-sm sm:grid-cols-5">
<Stat label="Total Parsed" value={parsing.total_parsed} /> <Stat label="Total Parsed" value={parsing.total_parsed} />
<Stat label="High Confidence" value={parsing.high_confidence} color="text-green-400" /> <Stat label="High Confidence" value={parsing.high_confidence} color="text-green-400" />
@@ -182,7 +195,10 @@ export function OpsPipelinePage() {
{/* Extraction Stats */} {/* Extraction Stats */}
<Card> <Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Extraction Validation</h2> <h2 className="mb-3 text-sm font-medium text-gray-400">
Extraction Validation
<span className="ml-2 text-xs text-gray-600">(last {hours}h)</span>
</h2>
<dl className="grid grid-cols-2 gap-3 text-sm sm:grid-cols-5"> <dl className="grid grid-cols-2 gap-3 text-sm sm:grid-cols-5">
<Stat label="Total" value={extraction.total_extractions} /> <Stat label="Total" value={extraction.total_extractions} />
<Stat label="Valid" value={extraction.valid} color="text-green-400" /> <Stat label="Valid" value={extraction.valid} color="text-green-400" />
@@ -194,7 +210,10 @@ export function OpsPipelinePage() {
{/* Aggregation */} {/* Aggregation */}
<Card> <Card>
<h2 className="mb-3 text-sm font-medium text-gray-400">Trend Generation</h2> <h2 className="mb-3 text-sm font-medium text-gray-400">
Trend Generation
<span className="ml-2 text-xs text-gray-600">(last {hours}h)</span>
</h2>
<dl className="grid grid-cols-2 gap-3 text-sm sm:grid-cols-4"> <dl className="grid grid-cols-2 gap-3 text-sm sm:grid-cols-4">
<Stat label="Trends Generated" value={aggregation.trends_generated} /> <Stat label="Trends Generated" value={aggregation.trends_generated} />
<Stat label="Symbols Covered" value={aggregation.symbols_covered} /> <Stat label="Symbols Covered" value={aggregation.symbols_covered} />