Add crawl job progress drawer with phase tracking and live logs
- Add phase-aware progress reporting across all crawl phases (splitting, fetching, filtering, processing) with per-step counters - Add TaskProgressDrawer component with phase timeline stepper, detail counters, progress bar with ETA, and live worker log viewer - Add on_step_complete callback to ListingProcessor for granular tracking of details/images/OCR steps - Extend QuerySplitter on_progress callback with structured counter data - Capture celery worker logs via ring buffer handler and inject into task state updates for frontend display - Guard taskResult updates with phase presence check to prevent drawer from blanking during state transitions
This commit is contained in:
parent
4018503723
commit
b4837e1603
6 changed files with 617 additions and 24 deletions
|
|
@ -7,6 +7,7 @@ import { useEffect, useState } from 'react';
|
|||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './ui/tooltip';
|
||||
import { Button } from './ui/button';
|
||||
import { Loader2, CheckCircle2, XCircle, X, Trash2 } from 'lucide-react';
|
||||
import { TaskProgressDrawer } from './TaskProgressDrawer';
|
||||
|
||||
interface TaskIndicatorProps {
|
||||
taskID: string | null;
|
||||
|
|
@ -19,8 +20,10 @@ export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
|
|||
const [processed, setProcessed] = useState<number | null>(null);
|
||||
const [total, setTotal] = useState<number | null>(null);
|
||||
const [taskStatus, setTaskStatus] = useState<TaskStatus | null>(null);
|
||||
const [taskResult, setTaskResult] = useState<TaskResult | null>(null);
|
||||
const [isCancelling, setIsCancelling] = useState(false);
|
||||
const [isClearing, setIsClearing] = useState(false);
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getUser().then(setUser);
|
||||
|
|
@ -29,6 +32,7 @@ export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
|
|||
useEffect(() => {
|
||||
if (!user || !taskID) {
|
||||
setTaskStatus(null);
|
||||
setTaskResult(null);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -37,6 +41,7 @@ export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
|
|||
setProgressPercentage(0);
|
||||
setProcessed(null);
|
||||
setTotal(null);
|
||||
setTaskResult(null);
|
||||
|
||||
const pollTaskStatus = async () => {
|
||||
try {
|
||||
|
|
@ -46,6 +51,20 @@ export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
|
|||
|
||||
if (status === TaskStatus.SUCCESS) {
|
||||
setProgressPercentage(100);
|
||||
// Parse final result for the drawer to show completed state.
|
||||
// Only update taskResult if the new result has phase info;
|
||||
// otherwise keep the last in-progress result which has richer data
|
||||
// than the bare SUCCESS return value.
|
||||
if (data.result) {
|
||||
try {
|
||||
const parsedResult: TaskResult = JSON.parse(data.result);
|
||||
if (parsedResult.phase) {
|
||||
setTaskResult(parsedResult);
|
||||
}
|
||||
} catch {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
}
|
||||
return true; // Stop polling
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +76,18 @@ export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
|
|||
if (data.result) {
|
||||
try {
|
||||
const parsedResult: TaskResult = JSON.parse(data.result);
|
||||
setProgressPercentage(parsedResult.progress * 100);
|
||||
// Only update taskResult if the parsed data has a phase field.
|
||||
// This prevents blanking the drawer when the backend sends a
|
||||
// state update without phase info (e.g. during brief transitions).
|
||||
if (parsedResult.phase) {
|
||||
setTaskResult(parsedResult);
|
||||
}
|
||||
// Only update progress/processed/total when the fields are
|
||||
// actually present — otherwise keep the previous values so
|
||||
// the UI doesn't flash back to 0 during phase transitions.
|
||||
if (parsedResult.progress !== undefined) {
|
||||
setProgressPercentage(parsedResult.progress * 100);
|
||||
}
|
||||
if (parsedResult.processed !== undefined) {
|
||||
setProcessed(parsedResult.processed);
|
||||
}
|
||||
|
|
@ -113,6 +143,7 @@ export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
|
|||
const result = await clearAllTasks(user);
|
||||
if (result.success) {
|
||||
setTaskStatus(null);
|
||||
setTaskResult(null);
|
||||
onTaskCancelled?.();
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -144,18 +175,27 @@ export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
|
|||
if (processed !== null && total !== null && total > 0) {
|
||||
return `${processed} / ${total}`;
|
||||
}
|
||||
if (taskResult?.phase && taskResult.phase !== 'processing') {
|
||||
const phaseLabels: Record<string, string> = {
|
||||
splitting: 'Splitting',
|
||||
splitting_complete: 'Split done',
|
||||
fetching: 'Fetching',
|
||||
filtering: 'Filtering',
|
||||
};
|
||||
return phaseLabels[taskResult.phase] ?? `${Math.round(progressPercentage)}%`;
|
||||
}
|
||||
return `${Math.round(progressPercentage)}%`;
|
||||
};
|
||||
|
||||
const getTooltipContent = () => {
|
||||
if (isInProgress) {
|
||||
if (processed !== null && total !== null && total > 0) {
|
||||
return `Processing: ${processed} / ${total} listings (${Math.round(progressPercentage)}%)`;
|
||||
return `Processing: ${processed} / ${total} listings (${Math.round(progressPercentage)}%) — click for details`;
|
||||
}
|
||||
return `Task running: ${Math.round(progressPercentage)}%`;
|
||||
return `Task running: ${getProgressText()} — click for details`;
|
||||
}
|
||||
if (taskStatus === TaskStatus.SUCCESS) {
|
||||
return 'Task completed successfully';
|
||||
return 'Task completed successfully — click for details';
|
||||
}
|
||||
if (taskStatus === TaskStatus.REVOKED) {
|
||||
return 'Task was cancelled';
|
||||
|
|
@ -168,7 +208,10 @@ export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
|
|||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center gap-2 cursor-default">
|
||||
<div
|
||||
className="flex items-center gap-2 cursor-pointer"
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
>
|
||||
{getStatusIcon()}
|
||||
{isInProgress && (
|
||||
<div className="flex items-center gap-2">
|
||||
|
|
@ -230,6 +273,15 @@ export function TaskIndicator({ taskID, onTaskCancelled }: TaskIndicatorProps) {
|
|||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<TaskProgressDrawer
|
||||
open={drawerOpen}
|
||||
onOpenChange={setDrawerOpen}
|
||||
taskResult={taskResult}
|
||||
taskStatus={taskStatus}
|
||||
taskID={taskID}
|
||||
onCancel={handleCancel}
|
||||
isCancelling={isCancelling}
|
||||
/>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue