11'use client' ;
22
3+ import { useEffect , useState } from 'react' ;
34import {
45 Dialog ,
56 DialogContent ,
@@ -34,6 +35,9 @@ import { manualAnalysisAdmissionCopy } from './manual-analysis-admission-copy';
3435
3536type Severity = 'critical' | 'high' | 'medium' | 'low' ;
3637
38+ const ACCEPTED_QUEUE_POLL_INTERVAL_MS = 3000 ;
39+ const ACCEPTED_QUEUE_POLL_TIMEOUT_MS = 18_000 ;
40+
3741function isSeverity ( value : string ) : value is Severity {
3842 return [ 'critical' , 'high' , 'medium' , 'low' ] . includes ( value ) ;
3943}
@@ -79,12 +83,16 @@ export function FindingDetailDialog({
7983 const trpc = useTRPC ( ) ;
8084 const queryClient = useQueryClient ( ) ;
8185 const isOrg = ! ! organizationId ;
86+ const [ queuedFindingId , setQueuedFindingId ] = useState < string | null > ( null ) ;
87+ const isAwaitingAnalysisStart = queuedFindingId === finding ?. id ;
8288
83- // Poll for analysis status when running .
89+ // Poll while queued admission is crossing the Worker boundary or analysis is active .
8490 // Two separate queries for org/personal to avoid type-union issues with useQuery.
8591 const pollWhileActive = ( query : { state : { data ?: { status ?: string | null } } } ) => {
8692 const status = query . state . data ?. status ;
87- if ( status === 'pending' || status === 'running' ) return 3000 ;
93+ if ( isAwaitingAnalysisStart || status === 'pending' || status === 'running' ) {
94+ return ACCEPTED_QUEUE_POLL_INTERVAL_MS ;
95+ }
8896 return false as const ;
8997 } ;
9098 const orgAnalysisQuery = useQuery ( {
@@ -104,13 +112,31 @@ export function FindingDetailDialog({
104112 } ) ;
105113 const analysisData = isOrg ? orgAnalysisQuery . data : personalAnalysisQuery . data ;
106114
115+ useEffect ( ( ) => {
116+ if ( ! queuedFindingId ) return ;
117+ const intervalId = window . setInterval ( ( ) => {
118+ void queryClient . invalidateQueries ( ) ;
119+ } , ACCEPTED_QUEUE_POLL_INTERVAL_MS ) ;
120+ const timeoutId = window . setTimeout ( ( ) => {
121+ setQueuedFindingId ( current => ( current === queuedFindingId ? null : current ) ) ;
122+ } , ACCEPTED_QUEUE_POLL_TIMEOUT_MS ) ;
123+ return ( ) => {
124+ window . clearInterval ( intervalId ) ;
125+ window . clearTimeout ( timeoutId ) ;
126+ } ;
127+ } , [ queryClient , queuedFindingId ] ) ;
128+
107129 // Start analysis mutation (organization)
108130 const startOrgAnalysisMutation = useMutation (
109131 trpc . organizations . securityAgent . startAnalysis . mutationOptions ( {
110132 onSuccess : async ( ) => {
111133 toast . success ( manualAnalysisAdmissionCopy . successTitle ) ;
112134 await queryClient . invalidateQueries ( ) ;
113135 } ,
136+ onError : ( error , variables ) => {
137+ toast . error ( manualAnalysisAdmissionCopy . failureTitle , { description : error . message } ) ;
138+ setQueuedFindingId ( current => ( current === variables . findingId ? null : current ) ) ;
139+ } ,
114140 } )
115141 ) ;
116142
@@ -121,6 +147,10 @@ export function FindingDetailDialog({
121147 toast . success ( manualAnalysisAdmissionCopy . successTitle ) ;
122148 await queryClient . invalidateQueries ( ) ;
123149 } ,
150+ onError : ( error , variables ) => {
151+ toast . error ( manualAnalysisAdmissionCopy . failureTitle , { description : error . message } ) ;
152+ setQueuedFindingId ( current => ( current === variables . findingId ? null : current ) ) ;
153+ } ,
124154 } )
125155 ) ;
126156
@@ -135,9 +165,13 @@ export function FindingDetailDialog({
135165 const cliSessionId = analysisData ?. cliSessionId ?? finding . cli_session_id ;
136166
137167 const isAnalyzing =
138- startAnalysisMutation . isPending || analysisStatus === 'pending' || analysisStatus === 'running' ;
168+ isAwaitingAnalysisStart ||
169+ startAnalysisMutation . isPending ||
170+ analysisStatus === 'pending' ||
171+ analysisStatus === 'running' ;
139172
140173 const handleStartAnalysis = ( { retrySandboxOnly } : { retrySandboxOnly ?: boolean } = { } ) => {
174+ setQueuedFindingId ( finding . id ) ;
141175 if ( isOrg ) {
142176 if ( ! organizationId ) return ;
143177 startOrgAnalysisMutation . mutate ( {
@@ -335,12 +369,16 @@ export function FindingDetailDialog({
335369 />
336370 ) }
337371 </ div >
338- ) : analysisStatus === 'running' || analysisStatus === 'pending' ? (
372+ ) : isAwaitingAnalysisStart ||
373+ analysisStatus === 'running' ||
374+ analysisStatus === 'pending' ? (
339375 < div className = "rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3" >
340376 < div className = "flex items-center gap-2" >
341377 < Loader2 className = "h-4 w-4 animate-spin text-yellow-400" />
342378 < p className = "text-sm text-yellow-400" >
343- { analysisStatus === 'pending' ? 'Queued...' : 'Triage in progress...' }
379+ { isAwaitingAnalysisStart || analysisStatus === 'pending'
380+ ? `${ manualAnalysisAdmissionCopy . pendingLabel } ...`
381+ : 'Triage in progress...' }
344382 </ p >
345383 </ div >
346384 </ div >
@@ -463,13 +501,15 @@ export function FindingDetailDialog({
463501 < div className = "space-y-4" >
464502 < MarkdownProse markdown = { analysis . rawMarkdown } className = "text-muted-foreground" />
465503 </ div >
466- ) : analysisStatus === 'running' || analysisStatus === 'pending' ? (
504+ ) : isAwaitingAnalysisStart ||
505+ analysisStatus === 'running' ||
506+ analysisStatus === 'pending' ? (
467507 < div className = "rounded-lg border border-yellow-500/30 bg-yellow-500/10 p-3" >
468508 < div className = "flex items-center gap-2" >
469509 < Loader2 className = "h-4 w-4 animate-spin text-yellow-400" />
470510 < p className = "text-sm text-yellow-400" >
471- { analysisStatus === 'pending'
472- ? 'Queued...'
511+ { isAwaitingAnalysisStart || analysisStatus === 'pending'
512+ ? ` ${ manualAnalysisAdmissionCopy . pendingLabel } ...`
473513 : 'Codebase analysis in progress...' }
474514 </ p >
475515 </ div >
0 commit comments