Commit c3e8efa4 authored by ThinhNC's avatar ThinhNC

Merge develop into staging: resolve false disconnect status on job completion...

Merge develop into staging: resolve false disconnect status on job completion and display status badge in header
parents 084ac012 a4285bee
Pipeline #53753 passed with stage
in 1 minute and 23 seconds
......@@ -38,6 +38,7 @@ import {
Activity,
AlertCircle,
ArrowLeft,
CheckCircle2,
Clock,
Copy,
Download,
......@@ -48,6 +49,7 @@ import {
Layers,
ListOrdered,
Loader2,
Pause,
RefreshCw,
RotateCcw,
Search,
......@@ -114,9 +116,18 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
const wasRunningRef = useRef<boolean>(false);
const hasNotifiedRef = useRef<boolean>(false);
const isJobInitialActive = !initialJob || initialJob.status === "RUNNING" || initialJob.status === "PROCESSING_EXPORT" || initialJob.status === "PENDING";
const isJobInitialActive =
!initialJob ||
initialJob.status === "RUNNING" ||
initialJob.status === "PROCESSING_EXPORT" ||
initialJob.status === "PENDING" ||
initialJob.status === "QUEUED";
const isJobActive = liveProgressJob
? liveProgressJob.status === "RUNNING" || liveProgressJob.status === "PROCESSING_EXPORT" || liveProgressJob.status === "PENDING"
? liveProgressJob.status === "RUNNING" ||
liveProgressJob.status === "PROCESSING_EXPORT" ||
liveProgressJob.status === "PENDING" ||
liveProgressJob.status === "QUEUED"
: isJobInitialActive;
const {
......@@ -135,8 +146,17 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
wasRunningRef.current = true;
}
},
onDone: () => {
refetchJob();
onDone: (doneData) => {
if (doneData?.status) {
setLiveProgressJob((prev) =>
prev ? { ...prev, status: doneData.status as CrawlJob["status"] } : null
);
}
refetchJob().then((res) => {
if (res.data) {
setLiveProgressJob(null);
}
});
if (job) {
notifyJobCompletion({
jobId: job.id,
......@@ -316,13 +336,50 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
{job?.mode || "SCRAPE"}
</Badge>
{/* SSE Live Status Badge with 3-state Exponential Backoff: Green / Yellow / Red */}
<SseStatusBadge
status={sseStatus}
retryCount={sseRetryCount}
nextRetryDelayMs={sseNextRetryDelayMs}
onReconnect={reconnectSse}
/>
{/* Job Status Badge right in header for immediate clarity */}
{job?.status && (
<Badge
variant={
job.status === "COMPLETED"
? "info"
: job.status === "RUNNING" || job.status === "PROCESSING_EXPORT"
? "default"
: job.status === "FAILED"
? "destructive"
: job.status === "CANCELED"
? "warning"
: "secondary"
}
className="text-xs font-bold rounded-full px-3 py-1 leading-none inline-flex items-center gap-1.5"
>
{(job.status === "RUNNING" || job.status === "PROCESSING_EXPORT") && (
<span className="h-2 w-2 animate-pulse rounded-full bg-emerald-500" />
)}
{job.status === "COMPLETED" && (
<CheckCircle2 className="h-3 w-3 text-cyan-600 dark:text-cyan-400" />
)}
{job.status === "FAILED" && (
<AlertCircle className="h-3 w-3 text-rose-500" />
)}
{job.status === "CANCELED" && (
<Pause className="h-3 w-3 text-amber-500" />
)}
{(job.status === "PENDING" || job.status === "QUEUED") && (
<Clock className="h-3 w-3 text-muted-foreground" />
)}
<span>{getStatusText(job.status)}</span>
</Badge>
)}
{/* SSE Live Status Badge with 3-state Exponential Backoff: Green / Yellow / Red (only shown while job is active) */}
{isJobActive && (
<SseStatusBadge
status={sseStatus}
retryCount={sseRetryCount}
nextRetryDelayMs={sseNextRetryDelayMs}
onReconnect={reconnectSse}
/>
)}
{/* Quota Exemption Badge if failed due to connection loss */}
{isConnectionWaived && (
......
......@@ -73,7 +73,12 @@ export function SseStatusBadge({
);
}
// ERROR or closed unexpectedly
// Stream is closed normally or inactive - do not display error badge
if (status === "CLOSED") {
return null;
}
// ERROR or disconnected unexpectedly
return (
<div
className={cn(
......
......@@ -60,6 +60,10 @@ export function useCrawlJob(id: string) {
queryFn: () => crawlJobService.getJobById(id),
enabled: Boolean(id),
staleTime: 4000,
refetchInterval: (q) => {
const job = q.state.data as CrawlJob | undefined;
return job && ACTIVE_STATUSES.includes(job.status) ? 4000 : false;
},
});
}
......
......@@ -84,6 +84,7 @@ export function useEventSource<T = unknown>(
const retryCountRef = useRef(0);
const retryTimerRef = useRef<NodeJS.Timeout | null>(null);
const connectRef = useRef<() => void>(() => {});
const isDoneRef = useRef(false);
// Safe ref callbacks to avoid unnecessary reconnections
const callbacksRef = useRef({ onInitial, onProgress, onDone, onMessage, onError });
......@@ -106,9 +107,12 @@ export function useEventSource<T = unknown>(
cleanUp();
if (!url || !enabled || typeof window === "undefined") {
setStatus("CLOSED");
return;
}
isDoneRef.current = false;
try {
const es = new EventSource(url, { withCredentials });
eventSourceRef.current = es;
......@@ -149,6 +153,7 @@ export function useEventSource<T = unknown>(
// Handle custom SSE event: "done"
es.addEventListener("done", (e: MessageEvent) => {
isDoneRef.current = true;
try {
const parsed = JSON.parse(e.data);
setLastEventName("done");
......@@ -174,6 +179,12 @@ export function useEventSource<T = unknown>(
};
es.onerror = (err) => {
// If stream ended naturally via "done" event, ignore server connection close
if (isDoneRef.current) {
es.close();
return;
}
setError(err);
callbacksRef.current.onError?.(err);
......@@ -225,6 +236,7 @@ export function useEventSource<T = unknown>(
}, [connect, cleanUp]);
const reconnect = useCallback(() => {
isDoneRef.current = false;
retryCountRef.current = 0;
setRetryCount(0);
setNextRetryDelayMs(0);
......@@ -233,6 +245,7 @@ export function useEventSource<T = unknown>(
}, [connect]);
const close = useCallback(() => {
isDoneRef.current = true;
cleanUp();
setStatus("CLOSED");
}, [cleanUp]);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment