619 lines
26 KiB
TypeScript
619 lines
26 KiB
TypeScript
/**
|
|
* Current Processing Card Component
|
|
* Shows real-time automation progress with pause/resume/cancel controls
|
|
*/
|
|
import React, { useEffect, useState } from 'react';
|
|
import { automationService, ProcessingState, AutomationRun, PipelineStage } from '../../services/automationService';
|
|
import {
|
|
fetchKeywords,
|
|
fetchClusters,
|
|
fetchContentIdeas,
|
|
fetchTasks,
|
|
fetchContent,
|
|
fetchContentImages,
|
|
} from '../../services/api';
|
|
import { useToast } from '../ui/toast/ToastContainer';
|
|
import Button from '../ui/button/Button';
|
|
import {
|
|
PlayIcon,
|
|
PauseIcon,
|
|
XMarkIcon,
|
|
ClockIcon,
|
|
BoltIcon
|
|
} from '../../icons';
|
|
|
|
interface CurrentProcessingCardProps {
|
|
runId: string;
|
|
siteId: number;
|
|
currentRun: AutomationRun;
|
|
onUpdate: () => void;
|
|
onClose: () => void;
|
|
pipelineOverview?: PipelineStage[];
|
|
}
|
|
|
|
const CurrentProcessingCard: React.FC<CurrentProcessingCardProps> = ({
|
|
runId,
|
|
siteId,
|
|
currentRun,
|
|
onUpdate,
|
|
onClose,
|
|
pipelineOverview,
|
|
}) => {
|
|
const [processingState, setProcessingState] = useState<ProcessingState | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [isPausing, setIsPausing] = useState(false);
|
|
const [isResuming, setIsResuming] = useState(false);
|
|
const [isCancelling, setIsCancelling] = useState(false);
|
|
const [fetchedCurrently, setFetchedCurrently] = useState<ProcessingState['currently_processing']>([]);
|
|
const [fetchedUpNext, setFetchedUpNext] = useState<ProcessingState['up_next']>([]);
|
|
const [isLocallyPaused, setIsLocallyPaused] = useState(false);
|
|
const [showDebugTable, setShowDebugTable] = useState(false);
|
|
const toast = useToast();
|
|
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
|
|
const fetchState = async () => {
|
|
try {
|
|
const state = await automationService.getCurrentProcessing(siteId, runId);
|
|
console.debug('getCurrentProcessing response for run', runId, state);
|
|
if (!isMounted) return;
|
|
|
|
setProcessingState(state);
|
|
setError(null);
|
|
|
|
// If stage completed (all items processed), trigger page refresh
|
|
if (state && state.processed_items >= state.total_items && state.total_items > 0) {
|
|
onUpdate();
|
|
}
|
|
} catch (err) {
|
|
if (!isMounted) return;
|
|
console.error('Error fetching processing state:', err);
|
|
setError('Failed to load processing state');
|
|
}
|
|
};
|
|
|
|
// Only fetch if status is running or paused and not locally paused
|
|
if (!isLocallyPaused && (currentRun.status === 'running' || currentRun.status === 'paused')) {
|
|
// Initial fetch
|
|
fetchState();
|
|
|
|
// Poll every 3 seconds
|
|
const interval = setInterval(fetchState, 3000);
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
clearInterval(interval);
|
|
};
|
|
}
|
|
|
|
return () => {
|
|
isMounted = false;
|
|
};
|
|
}, [siteId, runId, currentRun.status, currentRun.current_stage, onUpdate, isLocallyPaused]);
|
|
|
|
// Attempt to fetch example items for the current stage when the API does not provide up_next/currently_processing
|
|
useEffect(() => {
|
|
let isMounted = true;
|
|
const stageNumber = currentRun.current_stage;
|
|
const loadStageQueue = async () => {
|
|
try {
|
|
switch (stageNumber) {
|
|
case 1: {
|
|
const res = await fetchKeywords({ page_size: 5, site_id: siteId, status: 'new' });
|
|
if (!isMounted) return;
|
|
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'keyword' }));
|
|
setFetchedUpNext(items);
|
|
setFetchedCurrently(items.slice(0, 1));
|
|
break;
|
|
}
|
|
case 2: {
|
|
const res = await fetchClusters({ page_size: 5, site_id: siteId, status: 'new' });
|
|
if (!isMounted) return;
|
|
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.name || String(r.id), type: 'cluster' }));
|
|
setFetchedUpNext(items);
|
|
setFetchedCurrently(items.slice(0, 1));
|
|
break;
|
|
}
|
|
case 3: {
|
|
const res = await fetchContentIdeas({ page_size: 5, site_id: siteId, status: 'queued' });
|
|
if (!isMounted) return;
|
|
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || String(r.id), type: 'idea' }));
|
|
setFetchedUpNext(items);
|
|
setFetchedCurrently(items.slice(0, 1));
|
|
break;
|
|
}
|
|
case 4: {
|
|
// Tasks -> Content (show queued tasks)
|
|
try {
|
|
const res = await fetchTasks({ page_size: 5, site_id: siteId, status: 'queued' });
|
|
if (!isMounted) return;
|
|
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'task' }));
|
|
setFetchedUpNext(items);
|
|
setFetchedCurrently(items.slice(0, 1));
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
break;
|
|
}
|
|
case 5: {
|
|
// Content -> Image Prompts (show content items awaiting prompts)
|
|
try {
|
|
const res = await fetchContent({ page_size: 5, site_id: siteId, status: 'queued' });
|
|
if (!isMounted) return;
|
|
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.title || r.name || String(r.id), type: 'content' }));
|
|
setFetchedUpNext(items);
|
|
setFetchedCurrently(items.slice(0, 1));
|
|
} catch (e) {
|
|
// ignore
|
|
}
|
|
break;
|
|
}
|
|
case 6: {
|
|
const res = await fetchContentImages({ page_size: 5, site_id: siteId, status: 'pending' });
|
|
if (!isMounted) return;
|
|
const items = (res.results || []).map((r: any) => ({ id: r.id, title: r.filename || String(r.id), type: 'image' }));
|
|
setFetchedUpNext(items);
|
|
setFetchedCurrently(items.slice(0, 1));
|
|
break;
|
|
}
|
|
default:
|
|
// For stages without a clear read API, clear fetched lists
|
|
setFetchedUpNext([]);
|
|
setFetchedCurrently([]);
|
|
}
|
|
} catch (err) {
|
|
console.warn('Failed to fetch stage queue samples:', err);
|
|
}
|
|
};
|
|
|
|
// Only attempt when there's no live up_next data
|
|
if ((!processingState || (processingState && (processingState.up_next || []).length === 0)) && currentRun.status === 'running') {
|
|
loadStageQueue();
|
|
}
|
|
|
|
return () => { isMounted = false; };
|
|
}, [siteId, currentRun.current_stage, currentRun.status]);
|
|
|
|
const handlePause = async () => {
|
|
setIsPausing(true);
|
|
try {
|
|
await automationService.pause(siteId, runId);
|
|
toast?.success('Automation pausing... will complete current item');
|
|
// Optimistically mark paused locally so UI stays paused until backend confirms
|
|
setIsLocallyPaused(true);
|
|
// Trigger update to refresh run status
|
|
setTimeout(onUpdate, 1000);
|
|
} catch (error: any) {
|
|
toast?.error(error?.message || 'Failed to pause automation');
|
|
} finally {
|
|
setIsPausing(false);
|
|
}
|
|
};
|
|
|
|
const handleResume = async () => {
|
|
setIsResuming(true);
|
|
try {
|
|
await automationService.resume(siteId, runId);
|
|
toast?.success('Automation resumed');
|
|
// Clear local paused flag
|
|
setIsLocallyPaused(false);
|
|
// Trigger update to refresh run status
|
|
setTimeout(onUpdate, 1000);
|
|
} catch (error: any) {
|
|
toast?.error(error?.message || 'Failed to resume automation');
|
|
} finally {
|
|
setIsResuming(false);
|
|
}
|
|
};
|
|
|
|
const handleCancel = async () => {
|
|
if (!confirm('Are you sure you want to cancel this automation run? This cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
setIsCancelling(true);
|
|
try {
|
|
await automationService.cancel(siteId, runId);
|
|
toast?.success('Automation cancelling... will complete current item');
|
|
// Trigger update to refresh run status
|
|
setTimeout(onUpdate, 1500);
|
|
} catch (error: any) {
|
|
toast?.error(error?.message || 'Failed to cancel automation');
|
|
} finally {
|
|
setIsCancelling(false);
|
|
}
|
|
};
|
|
|
|
const formatDuration = (startTime: string) => {
|
|
const start = new Date(startTime).getTime();
|
|
const now = Date.now();
|
|
const diffMs = now - start;
|
|
const diffMins = Math.floor(diffMs / 60000);
|
|
const diffHours = Math.floor(diffMins / 60);
|
|
|
|
if (diffHours > 0) {
|
|
return `${diffHours}h ${diffMins % 60}m`;
|
|
}
|
|
return `${diffMins}m`;
|
|
};
|
|
|
|
if (error) {
|
|
return (
|
|
<div className="bg-red-50 dark:bg-red-900/20 border-2 border-red-500 rounded-lg p-4 mb-6">
|
|
<div className="flex items-center justify-between">
|
|
<p className="text-red-700 dark:text-red-300 text-sm">{error}</p>
|
|
<button
|
|
onClick={onClose}
|
|
className="text-red-500 hover:text-red-700 dark:hover:text-red-300"
|
|
>
|
|
<XMarkIcon className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Build a fallback processing state from currentRun and pipelineOverview when API doesn't return live state
|
|
const currentStageIndex = (currentRun.current_stage || 1) - 1;
|
|
const stageOverview = pipelineOverview && pipelineOverview[currentStageIndex] ? pipelineOverview[currentStageIndex] : null;
|
|
const stageResult = (currentRun as any)[`stage_${currentRun.current_stage}_result`];
|
|
|
|
// FIXED: Helper to get processed count using correct key
|
|
const getProcessedFromResult = (result: any, stageNumber: number): number => {
|
|
if (!result) return 0;
|
|
const keyMap: Record<number, string> = {
|
|
1: 'keywords_processed',
|
|
2: 'clusters_processed',
|
|
3: 'ideas_processed',
|
|
4: 'tasks_processed',
|
|
5: 'content_processed',
|
|
6: 'images_processed',
|
|
7: 'ready_for_review'
|
|
};
|
|
return result[keyMap[stageNumber]] ?? 0;
|
|
};
|
|
|
|
const fallbackState: ProcessingState | null = ((): ProcessingState | null => {
|
|
if (!processingState && (stageOverview || stageResult)) {
|
|
// FIXED: Use stage-specific key instead of summing all numeric values
|
|
const processed = getProcessedFromResult(stageResult, currentRun.current_stage);
|
|
const total = (stageOverview?.pending || 0) + processed;
|
|
const percentage = total > 0 ? Math.round((processed / total) * 100) : 0;
|
|
|
|
return {
|
|
stage_number: currentRun.current_stage,
|
|
stage_name: stageOverview?.name || `Stage ${currentRun.current_stage}`,
|
|
stage_type: stageOverview?.type || 'AI',
|
|
total_items: total,
|
|
processed_items: processed,
|
|
percentage,
|
|
currently_processing: [],
|
|
up_next: [],
|
|
remaining_count: Math.max(0, total - processed),
|
|
};
|
|
}
|
|
return null;
|
|
})();
|
|
|
|
const displayState = processingState || fallbackState;
|
|
|
|
// If we don't have a live displayState, keep rendering the card using computed values
|
|
|
|
// Computed processed/total (use processingState when available, otherwise derive from stageResult + overview)
|
|
const computedProcessed = ((): number => {
|
|
if (displayState && typeof displayState.processed_items === 'number') return displayState.processed_items;
|
|
if (stageResult) {
|
|
// FIXED: Use stage-specific key for processed count
|
|
return getProcessedFromResult(stageResult, currentRun.current_stage);
|
|
}
|
|
return 0;
|
|
})();
|
|
|
|
const computedTotal = ((): number => {
|
|
if (displayState && typeof displayState.total_items === 'number' && displayState.total_items > 0) return displayState.total_items;
|
|
const pending = stageOverview?.pending ?? 0;
|
|
return Math.max(pending + computedProcessed, 0);
|
|
})();
|
|
|
|
const percentage = computedTotal > 0 ? Math.round((computedProcessed / computedTotal) * 100) : 0;
|
|
const isPaused = currentRun.status === 'paused';
|
|
|
|
// Choose stage accent color (simple map matching AutomationPage STAGE_CONFIG)
|
|
const stageColors = [
|
|
'from-blue-500 to-blue-600',
|
|
'from-purple-500 to-purple-600',
|
|
'from-indigo-500 to-indigo-600',
|
|
'from-green-500 to-green-600',
|
|
'from-amber-500 to-amber-600',
|
|
'from-pink-500 to-pink-600',
|
|
'from-teal-500 to-teal-600',
|
|
];
|
|
const stageColorClass = stageColors[(currentRun.current_stage || 1) - 1] || 'from-blue-500 to-blue-600';
|
|
|
|
return (
|
|
<div className={`border-2 rounded-lg p-6 mb-6 ${
|
|
isPaused
|
|
? 'bg-yellow-50 dark:bg-yellow-900/20 border-yellow-500'
|
|
: 'bg-blue-50 dark:bg-blue-900/20 border-blue-500'
|
|
}`}>
|
|
{/* Header Row with Main Info and Close */}
|
|
<div className="flex items-start justify-between mb-4">
|
|
{/* Left Side - Main Info (75%) */}
|
|
<div className="flex-1 pr-6">
|
|
<div className="flex items-center gap-3 mb-4">
|
|
<div className={isPaused ? '' : 'animate-pulse'}>
|
|
{isPaused ? (
|
|
<PauseIcon className="w-8 h-8 text-yellow-600 dark:text-yellow-400" />
|
|
) : (
|
|
<BoltIcon className="w-8 h-8 text-blue-600 dark:text-blue-400" />
|
|
)}
|
|
</div>
|
|
<div className="flex-1">
|
|
<h2 className="text-2xl font-bold text-gray-900 dark:text-white">
|
|
{isPaused ? 'Automation Paused' : 'Automation In Progress'}
|
|
</h2>
|
|
|
|
{/* Centered stage row + dynamic action text */}
|
|
{displayState && (
|
|
<div className="mt-2">
|
|
<div className="text-center text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
Stage {currentRun.current_stage}: {displayState.stage_name}
|
|
<span className={`ml-2 px-2 py-0.5 rounded text-xs ${
|
|
isPaused
|
|
? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300'
|
|
: 'bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300'
|
|
}`}>
|
|
{displayState.stage_type}
|
|
</span>
|
|
</div>
|
|
|
|
<div className="text-center text-sm text-gray-600 dark:text-gray-400 mt-1">
|
|
{(() => {
|
|
// Build dynamic action text based on stage type and counts
|
|
const verb = displayState.stage_type === 'AI' ? 'Generating' : 'Processing';
|
|
// target label for the current stage (what is being produced)
|
|
const targetLabelMap: Record<number, string> = {
|
|
1: 'Clusters',
|
|
2: 'Ideas',
|
|
3: 'Tasks',
|
|
4: 'Content',
|
|
5: 'Image Prompts',
|
|
6: 'Images',
|
|
7: 'Review',
|
|
};
|
|
const label = targetLabelMap[displayState.stage_number] || 'Items';
|
|
return `${verb} ${computedProcessed}/${computedTotal} ${label}`;
|
|
})()}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress Info */}
|
|
{displayState && (
|
|
<>
|
|
<div className="mb-4">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<div className="text-3xl font-bold text-blue-600 dark:text-blue-400">
|
|
{percentage}%
|
|
</div>
|
|
<div className="text-sm text-gray-600 dark:text-gray-400">
|
|
{computedProcessed}/{computedTotal} completed
|
|
</div>
|
|
</div>
|
|
{/* Progress Bar */}
|
|
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded-full h-3">
|
|
<div
|
|
className={`h-3 rounded-full transition-all duration-500 ${
|
|
isPaused
|
|
? 'bg-yellow-600 dark:bg-yellow-500'
|
|
: 'bg-blue-600 dark:bg-blue-500'
|
|
}`}
|
|
style={{ width: `${Math.min(percentage, 100)}%` }}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Currently Processing and Up Next */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
{/* Currently Processing */}
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
|
Currently Processing:
|
|
</h3>
|
|
<div className="space-y-1">
|
|
{((displayState.currently_processing && displayState.currently_processing.length > 0) ? displayState.currently_processing : fetchedCurrently).length > 0 ? (
|
|
((displayState.currently_processing && displayState.currently_processing.length > 0) ? displayState.currently_processing : fetchedCurrently).map((item, idx) => (
|
|
<div key={idx} className="flex items-start gap-2 text-sm">
|
|
<span className={isPaused ? 'text-yellow-600 dark:text-yellow-400 mt-1' : 'text-blue-600 dark:text-blue-400 mt-1'}>•</span>
|
|
<span className="text-gray-800 dark:text-gray-200 font-medium line-clamp-2">
|
|
{item.title}
|
|
</span>
|
|
</div>
|
|
))
|
|
) : (
|
|
<div className="text-sm text-gray-500 dark:text-gray-400 italic">
|
|
{isPaused ? 'Paused' : 'No items currently processing'}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Up Next */}
|
|
<div>
|
|
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
|
Up Next:
|
|
</h3>
|
|
<div className="space-y-1">
|
|
{((displayState.up_next && displayState.up_next.length > 0) ? displayState.up_next : fetchedUpNext).length > 0 ? (
|
|
<>
|
|
{((displayState.up_next && displayState.up_next.length > 0) ? displayState.up_next : fetchedUpNext).map((item, idx) => (
|
|
<div key={idx} className="flex items-start gap-2 text-sm">
|
|
<span className="text-gray-400 dark:text-gray-500 mt-1">•</span>
|
|
<span className="text-gray-600 dark:text-gray-400 line-clamp-2">
|
|
{item.title}
|
|
</span>
|
|
</div>
|
|
))}
|
|
{displayState.remaining_count > ((displayState.up_next?.length || 0) + (displayState.currently_processing?.length || 0)) && (
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
|
|
+ {displayState.remaining_count - ((displayState.up_next?.length || 0) + (displayState.currently_processing?.length || 0))} more in queue
|
|
</div>
|
|
)}
|
|
</>
|
|
) : (
|
|
<div className="text-sm text-gray-500 dark:text-gray-400 italic">
|
|
Queue empty
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Control Buttons */}
|
|
<div className="flex items-center gap-3">
|
|
{currentRun.status === 'running' ? (
|
|
<Button
|
|
onClick={handlePause}
|
|
disabled={isPausing}
|
|
variant="secondary"
|
|
size="sm"
|
|
startIcon={<PauseIcon className="w-4 h-4" />}
|
|
>
|
|
{isPausing ? 'Pausing...' : 'Pause'}
|
|
</Button>
|
|
) : currentRun.status === 'paused' ? (
|
|
<Button
|
|
onClick={handleResume}
|
|
disabled={isResuming}
|
|
variant="primary"
|
|
size="sm"
|
|
startIcon={<PlayIcon className="w-4 h-4" />}
|
|
>
|
|
{isResuming ? 'Resuming...' : 'Resume'}
|
|
</Button>
|
|
) : null}
|
|
|
|
<Button
|
|
onClick={handleCancel}
|
|
disabled={isCancelling}
|
|
variant="danger"
|
|
size="sm"
|
|
startIcon={<XMarkIcon className="w-4 h-4" />}
|
|
>
|
|
{isCancelling ? 'Cancelling...' : 'Cancel'}
|
|
</Button>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
{/* Right Side - Metrics and Close (25%) */}
|
|
<div className="w-64 flex-shrink-0">
|
|
{/* Close Button */}
|
|
<div className="flex justify-end mb-4">
|
|
<Button variant="ghost" size="sm" onClick={onClose} startIcon={<XMarkIcon className="w-4 h-4" />}>
|
|
Close
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Metrics Cards */}
|
|
<div className="space-y-3">
|
|
{/* Duration */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center justify-between mb-0">
|
|
<div className="flex items-center gap-2">
|
|
<ClockIcon className="w-4 h-4 text-gray-500" />
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Duration</div>
|
|
</div>
|
|
<div className="text-sm font-bold text-gray-900 dark:text-white">{formatDuration(currentRun.started_at)}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Credits Used */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center justify-between mb-0">
|
|
<div className="flex items-center gap-2">
|
|
<BoltIcon className="w-4 h-4 text-amber-500" />
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Credits Used</div>
|
|
</div>
|
|
<div className="text-sm font-bold text-amber-600 dark:text-amber-400">{currentRun.total_credits_used}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Current Stage */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center justify-between mb-0">
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Stage</div>
|
|
<div className="text-sm font-bold text-gray-900 dark:text-white">{currentRun.current_stage} of 7</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Status */}
|
|
<div className="bg-white dark:bg-gray-800 rounded-lg p-3 border border-gray-200 dark:border-gray-700">
|
|
<div className="flex items-center justify-between mb-0">
|
|
<div className="text-xs text-gray-500 dark:text-gray-400 uppercase font-semibold">Status</div>
|
|
<div className={`text-sm font-semibold ${isPaused ? 'text-yellow-600 dark:text-yellow-400' : 'text-blue-600 dark:text-blue-400'}`}>
|
|
{isPaused ? 'Paused' : 'Running'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/* Debug table toggle + table for stage data */}
|
|
<div className="mt-4">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowDebugTable(!showDebugTable)}
|
|
className="text-xs text-slate-600 hover:underline"
|
|
>
|
|
{showDebugTable ? 'Hide' : 'Show'} debug table
|
|
</button>
|
|
{showDebugTable && (
|
|
<div className="mt-3 bg-white dark:bg-gray-800 p-3 rounded border">
|
|
<div className="text-sm font-semibold mb-2">Stage Data</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-xs">
|
|
<thead>
|
|
<tr className="text-left">
|
|
<th className="pr-4">Stage</th>
|
|
<th className="pr-4">Pending</th>
|
|
<th className="pr-4">Processed</th>
|
|
<th className="pr-4">Total</th>
|
|
<th className="pr-4">Currently (sample)</th>
|
|
<th className="pr-4">Up Next (sample)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{(pipelineOverview || []).map((stage) => {
|
|
const result = (currentRun as any)[`stage_${stage.number}_result`];
|
|
const processed = result ? Object.values(result).reduce((s: number, v: any) => typeof v === 'number' ? s + v : s, 0) : 0;
|
|
const total = Math.max((stage.pending || 0) + processed, 0);
|
|
const currently = currentRun.current_stage === stage.number ? (processingState?.currently_processing?.slice(0,3) || fetchedCurrently) : [];
|
|
const upnext = currentRun.current_stage === stage.number ? (processingState?.up_next?.slice(0,5) || fetchedUpNext) : [];
|
|
return (
|
|
<tr key={stage.number} className="border-t">
|
|
<td className="py-2">{stage.number} — {stage.name}</td>
|
|
<td className="py-2">{stage.pending}</td>
|
|
<td className="py-2">{processed}</td>
|
|
<td className="py-2">{total}</td>
|
|
<td className="py-2">{currently.map(c => c.title).join(', ') || '-'}</td>
|
|
<td className="py-2">{upnext.map(u => u.title).join(', ') || '-'}</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default CurrentProcessingCard;
|