Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
U
upgrade-data-crawler-fe
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ThinhNC
upgrade-data-crawler-fe
Commits
8cad79b1
Commit
8cad79b1
authored
Oct 17, 2026
by
ThinhNC
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: resolve false disconnect status on job completion and display status badge in header
parent
eed6cfa4
Pipeline
#53751
passed with stage
in 2 minutes and 15 seconds
Changes
4
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
91 additions
and
12 deletions
+91
-12
page.tsx
src/app/crawl-jobs/[id]/page.tsx
+68
-11
sse-status-badge.tsx
src/components/common/sse-status-badge.tsx
+6
-1
use-crawl-jobs.ts
src/hooks/use-crawl-jobs.ts
+4
-0
use-event-source.ts
src/hooks/use-event-source.ts
+13
-0
No files found.
src/app/crawl-jobs/[id]/page.tsx
View file @
8cad79b1
...
@@ -38,6 +38,7 @@ import {
...
@@ -38,6 +38,7 @@ import {
Activity
,
Activity
,
AlertCircle
,
AlertCircle
,
ArrowLeft
,
ArrowLeft
,
CheckCircle2
,
Clock
,
Clock
,
Copy
,
Copy
,
Download
,
Download
,
...
@@ -48,6 +49,7 @@ import {
...
@@ -48,6 +49,7 @@ import {
Layers
,
Layers
,
ListOrdered
,
ListOrdered
,
Loader2
,
Loader2
,
Pause
,
RefreshCw
,
RefreshCw
,
RotateCcw
,
RotateCcw
,
Search
,
Search
,
...
@@ -114,9 +116,18 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
...
@@ -114,9 +116,18 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
const
wasRunningRef
=
useRef
<
boolean
>
(
false
);
const
wasRunningRef
=
useRef
<
boolean
>
(
false
);
const
hasNotifiedRef
=
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
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
;
:
isJobInitialActive
;
const
{
const
{
...
@@ -135,8 +146,17 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
...
@@ -135,8 +146,17 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
wasRunningRef
.
current
=
true
;
wasRunningRef
.
current
=
true
;
}
}
},
},
onDone
:
()
=>
{
onDone
:
(
doneData
)
=>
{
refetchJob
();
if
(
doneData
?.
status
)
{
setLiveProgressJob
((
prev
)
=>
prev
?
{
...
prev
,
status
:
doneData
.
status
as
CrawlJob
[
"status"
]
}
:
null
);
}
refetchJob
().
then
((
res
)
=>
{
if
(
res
.
data
)
{
setLiveProgressJob
(
null
);
}
});
if
(
job
)
{
if
(
job
)
{
notifyJobCompletion
({
notifyJobCompletion
({
jobId
:
job
.
id
,
jobId
:
job
.
id
,
...
@@ -316,13 +336,50 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
...
@@ -316,13 +336,50 @@ export default function CrawlJobDetailPage({ params }: PageProps) {
{
job
?.
mode
||
"SCRAPE"
}
{
job
?.
mode
||
"SCRAPE"
}
</
Badge
>
</
Badge
>
{
/* SSE Live Status Badge with 3-state Exponential Backoff: Green / Yellow / Red */
}
{
/* Job Status Badge right in header for immediate clarity */
}
<
SseStatusBadge
{
job
?.
status
&&
(
status=
{
sseStatus
}
<
Badge
retryCount=
{
sseRetryCount
}
variant=
{
nextRetryDelayMs=
{
sseNextRetryDelayMs
}
job
.
status
===
"COMPLETED"
onReconnect=
{
reconnectSse
}
?
"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 */
}
{
/* Quota Exemption Badge if failed due to connection loss */
}
{
isConnectionWaived
&&
(
{
isConnectionWaived
&&
(
...
...
src/components/common/sse-status-badge.tsx
View file @
8cad79b1
...
@@ -73,7 +73,12 @@ export function SseStatusBadge({
...
@@ -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
(
return
(
<
div
<
div
className=
{
cn
(
className=
{
cn
(
...
...
src/hooks/use-crawl-jobs.ts
View file @
8cad79b1
...
@@ -60,6 +60,10 @@ export function useCrawlJob(id: string) {
...
@@ -60,6 +60,10 @@ export function useCrawlJob(id: string) {
queryFn
:
()
=>
crawlJobService
.
getJobById
(
id
),
queryFn
:
()
=>
crawlJobService
.
getJobById
(
id
),
enabled
:
Boolean
(
id
),
enabled
:
Boolean
(
id
),
staleTime
:
4000
,
staleTime
:
4000
,
refetchInterval
:
(
q
)
=>
{
const
job
=
q
.
state
.
data
as
CrawlJob
|
undefined
;
return
job
&&
ACTIVE_STATUSES
.
includes
(
job
.
status
)
?
4000
:
false
;
},
});
});
}
}
...
...
src/hooks/use-event-source.ts
View file @
8cad79b1
...
@@ -84,6 +84,7 @@ export function useEventSource<T = unknown>(
...
@@ -84,6 +84,7 @@ export function useEventSource<T = unknown>(
const
retryCountRef
=
useRef
(
0
);
const
retryCountRef
=
useRef
(
0
);
const
retryTimerRef
=
useRef
<
NodeJS
.
Timeout
|
null
>
(
null
);
const
retryTimerRef
=
useRef
<
NodeJS
.
Timeout
|
null
>
(
null
);
const
connectRef
=
useRef
<
()
=>
void
>
(()
=>
{});
const
connectRef
=
useRef
<
()
=>
void
>
(()
=>
{});
const
isDoneRef
=
useRef
(
false
);
// Safe ref callbacks to avoid unnecessary reconnections
// Safe ref callbacks to avoid unnecessary reconnections
const
callbacksRef
=
useRef
({
onInitial
,
onProgress
,
onDone
,
onMessage
,
onError
});
const
callbacksRef
=
useRef
({
onInitial
,
onProgress
,
onDone
,
onMessage
,
onError
});
...
@@ -106,9 +107,12 @@ export function useEventSource<T = unknown>(
...
@@ -106,9 +107,12 @@ export function useEventSource<T = unknown>(
cleanUp
();
cleanUp
();
if
(
!
url
||
!
enabled
||
typeof
window
===
"undefined"
)
{
if
(
!
url
||
!
enabled
||
typeof
window
===
"undefined"
)
{
setStatus
(
"CLOSED"
);
return
;
return
;
}
}
isDoneRef
.
current
=
false
;
try
{
try
{
const
es
=
new
EventSource
(
url
,
{
withCredentials
});
const
es
=
new
EventSource
(
url
,
{
withCredentials
});
eventSourceRef
.
current
=
es
;
eventSourceRef
.
current
=
es
;
...
@@ -149,6 +153,7 @@ export function useEventSource<T = unknown>(
...
@@ -149,6 +153,7 @@ export function useEventSource<T = unknown>(
// Handle custom SSE event: "done"
// Handle custom SSE event: "done"
es
.
addEventListener
(
"done"
,
(
e
:
MessageEvent
)
=>
{
es
.
addEventListener
(
"done"
,
(
e
:
MessageEvent
)
=>
{
isDoneRef
.
current
=
true
;
try
{
try
{
const
parsed
=
JSON
.
parse
(
e
.
data
);
const
parsed
=
JSON
.
parse
(
e
.
data
);
setLastEventName
(
"done"
);
setLastEventName
(
"done"
);
...
@@ -174,6 +179,12 @@ export function useEventSource<T = unknown>(
...
@@ -174,6 +179,12 @@ export function useEventSource<T = unknown>(
};
};
es
.
onerror
=
(
err
)
=>
{
es
.
onerror
=
(
err
)
=>
{
// If stream ended naturally via "done" event, ignore server connection close
if
(
isDoneRef
.
current
)
{
es
.
close
();
return
;
}
setError
(
err
);
setError
(
err
);
callbacksRef
.
current
.
onError
?.(
err
);
callbacksRef
.
current
.
onError
?.(
err
);
...
@@ -225,6 +236,7 @@ export function useEventSource<T = unknown>(
...
@@ -225,6 +236,7 @@ export function useEventSource<T = unknown>(
},
[
connect
,
cleanUp
]);
},
[
connect
,
cleanUp
]);
const
reconnect
=
useCallback
(()
=>
{
const
reconnect
=
useCallback
(()
=>
{
isDoneRef
.
current
=
false
;
retryCountRef
.
current
=
0
;
retryCountRef
.
current
=
0
;
setRetryCount
(
0
);
setRetryCount
(
0
);
setNextRetryDelayMs
(
0
);
setNextRetryDelayMs
(
0
);
...
@@ -233,6 +245,7 @@ export function useEventSource<T = unknown>(
...
@@ -233,6 +245,7 @@ export function useEventSource<T = unknown>(
},
[
connect
]);
},
[
connect
]);
const
close
=
useCallback
(()
=>
{
const
close
=
useCallback
(()
=>
{
isDoneRef
.
current
=
true
;
cleanUp
();
cleanUp
();
setStatus
(
"CLOSED"
);
setStatus
(
"CLOSED"
);
},
[
cleanUp
]);
},
[
cleanUp
]);
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment