@@ -15,6 +15,15 @@ interface Repository {
1515 lastIndexedAt ?: string ;
1616}
1717
18+ interface IndexQueueStatus {
19+ inFlight : number ;
20+ queued : number ;
21+ maxConcurrent : number ;
22+ maxDepth : number ;
23+ capacityUsed : number ;
24+ capacityTotal : number ;
25+ }
26+
1827export default function Repositories ( ) {
1928 const [ repos , setRepos ] = useState < Repository [ ] > ( [ ] ) ;
2029 const [ loading , setLoading ] = useState ( true ) ;
@@ -30,15 +39,21 @@ export default function Repositories() {
3039 const [ showWorkspaceDialog , setShowWorkspaceDialog ] = useState ( false ) ;
3140 const [ newWorkspacePath , setNewWorkspacePath ] = useState ( '' ) ;
3241 const [ changingWorkspace , setChangingWorkspace ] = useState ( false ) ;
42+ const [ indexQueue , setIndexQueue ] = useState < IndexQueueStatus | null > ( null ) ;
3343 const { toasts, push : toast , dismiss : dismissToast } = useToast ( ) ;
3444
3545 useEffect ( ( ) => {
3646 loadRepositories ( ) ;
47+ loadIndexQueue ( ) ;
3748 fetch ( '/api/health' )
3849 . then ( ( r ) => r . json ( ) )
3950 . then ( ( d ) => setWorkspaceRoot ( d . workspaceRoot || '' ) )
4051 . catch ( ( err ) => console . warn ( 'Failed to load workspace info:' , err ) ) ;
4152 loadConfig ( ) ;
53+
54+ // Poll queue status every 2 seconds for real-time updates
55+ const interval = setInterval ( loadIndexQueue , 2000 ) ;
56+ return ( ) => clearInterval ( interval ) ;
4257 } , [ ] ) ;
4358
4459 const loadConfig = async ( ) => {
@@ -64,6 +79,16 @@ export default function Repositories() {
6479 }
6580 } ;
6681
82+ const loadIndexQueue = async ( ) => {
83+ try {
84+ const response = await fetch ( '/api/metrics' ) ;
85+ const data = await response . json ( ) ;
86+ setIndexQueue ( data . indexQueue || null ) ;
87+ } catch ( error ) {
88+ console . error ( 'Failed to load index queue status:' , error ) ;
89+ }
90+ } ;
91+
6792 const handleReindex = async ( repoId : string ) => {
6893 setBusyAction ( `reindex-${ repoId } ` ) ;
6994 try {
@@ -507,6 +532,161 @@ export default function Repositories() {
507532 </ div >
508533 ) }
509534
535+ { /* Indexing Queue Status Card */ }
536+ { indexQueue && (
537+ < div className = "md:col-span-12 bg-surface-container-low p-6 rounded-xl" >
538+ < div className = "flex items-center justify-between mb-4" >
539+ < div className = "flex items-center gap-3" >
540+ < div className = "w-10 h-10 bg-tertiary-container flex items-center justify-center rounded-xl" >
541+ < span className = "material-symbols-outlined text-on-tertiary-container" >
542+ { indexQueue . inFlight > 0 ? 'progress_activity' : 'task_alt' }
543+ </ span >
544+ </ div >
545+ < div >
546+ < h4 className = "text-[0.875rem] font-bold text-on-surface" > Indexing Queue</ h4 >
547+ < p className = "text-[0.75rem] text-on-surface-variant" >
548+ { indexQueue . inFlight === 0 && indexQueue . queued === 0
549+ ? 'No repositories currently indexing'
550+ : `${ indexQueue . inFlight } indexing · ${ indexQueue . queued } queued` }
551+ </ p >
552+ </ div >
553+ </ div >
554+ < div className = "flex items-center gap-2" >
555+ { indexQueue . inFlight > 0 && (
556+ < span className = "px-3 py-1.5 bg-green-500/10 text-green-600 text-xs font-semibold rounded-lg flex items-center gap-1.5" >
557+ < span className = "w-2 h-2 rounded-full bg-green-500 animate-pulse" > </ span >
558+ Active
559+ </ span >
560+ ) }
561+ < span className = "text-[0.75rem] text-on-surface-variant" >
562+ Capacity: { indexQueue . capacityUsed } /{ indexQueue . capacityTotal }
563+ </ span >
564+ </ div >
565+ </ div >
566+
567+ { /* Progress Bar */ }
568+ < div className = "mb-4" >
569+ < div className = "flex justify-between text-xs text-on-surface-variant mb-2" >
570+ < span > Queue Usage</ span >
571+ < span > { Math . round ( ( indexQueue . capacityUsed / indexQueue . capacityTotal ) * 100 ) } %</ span >
572+ </ div >
573+ < div className = "w-full bg-surface-container h-2.5 rounded-full overflow-hidden" >
574+ < div
575+ className = "h-full rounded-full transition-all duration-500"
576+ style = { {
577+ width : `${ Math . min ( 100 , ( indexQueue . capacityUsed / indexQueue . capacityTotal ) * 100 ) } %` ,
578+ background : indexQueue . capacityUsed >= indexQueue . capacityTotal
579+ ? 'linear-gradient(90deg, var(--error), var(--error-dim))'
580+ : indexQueue . capacityUsed > indexQueue . capacityTotal * 0.7
581+ ? 'linear-gradient(90deg, #f59e0b, #d97706)'
582+ : 'linear-gradient(90deg, var(--tertiary), var(--tertiary-dim))' ,
583+ } }
584+ />
585+ </ div >
586+ </ div >
587+
588+ { /* Stats Grid */ }
589+ < div className = "grid grid-cols-2 md:grid-cols-4 gap-4" >
590+ < div className = "bg-surface-container-highest p-4 rounded-lg" >
591+ < div className = "flex items-center gap-2 mb-2" >
592+ < span className = "material-symbols-outlined text-[16px] text-green-600" >
593+ { indexQueue . inFlight > 0 ? 'sync' : 'check_circle' }
594+ </ span >
595+ < span className = "text-[0.6875rem] font-semibold uppercase tracking-wider text-outline" >
596+ Active
597+ </ span >
598+ </ div >
599+ < span className = { `text-2xl font-bold ${ indexQueue . inFlight > 0 ? 'text-green-600' : 'text-on-surface' } ` } >
600+ { indexQueue . inFlight }
601+ </ span >
602+ < p className = "text-[0.6875rem] text-on-surface-variant mt-1" >
603+ Max: { indexQueue . maxConcurrent }
604+ </ p >
605+ </ div >
606+
607+ < div className = "bg-surface-container-highest p-4 rounded-lg" >
608+ < div className = "flex items-center gap-2 mb-2" >
609+ < span className = "material-symbols-outlined text-[16px] text-orange-500" >
610+ schedule
611+ </ span >
612+ < span className = "text-[0.6875rem] font-semibold uppercase tracking-wider text-outline" >
613+ Queued
614+ </ span >
615+ </ div >
616+ < span className = { `text-2xl font-bold ${ indexQueue . queued > 0 ? 'text-orange-500' : 'text-on-surface' } ` } >
617+ { indexQueue . queued }
618+ </ span >
619+ < p className = "text-[0.6875rem] text-on-surface-variant mt-1" >
620+ Max: { indexQueue . maxDepth }
621+ </ p >
622+ </ div >
623+
624+ < div className = "bg-surface-container-highest p-4 rounded-lg" >
625+ < div className = "flex items-center gap-2 mb-2" >
626+ < span className = "material-symbols-outlined text-[16px] text-tertiary" >
627+ inventory_2
628+ </ span >
629+ < span className = "text-[0.6875rem] font-semibold uppercase tracking-wider text-outline" >
630+ Available
631+ </ span >
632+ </ div >
633+ < span className = "text-2xl font-bold text-on-surface" >
634+ { indexQueue . capacityTotal - indexQueue . capacityUsed }
635+ </ span >
636+ < p className = "text-[0.6875rem] text-on-surface-variant mt-1" >
637+ Slots free
638+ </ p >
639+ </ div >
640+
641+ < div className = "bg-surface-container-highest p-4 rounded-lg" >
642+ < div className = "flex items-center gap-2 mb-2" >
643+ < span className = "material-symbols-outlined text-[16px] text-on-surface-variant" >
644+ info
645+ </ span >
646+ < span className = "text-[0.6875rem] font-semibold uppercase tracking-wider text-outline" >
647+ Status
648+ </ span >
649+ </ div >
650+ < span className = { `text-lg font-bold ${
651+ indexQueue . capacityUsed >= indexQueue . capacityTotal
652+ ? 'text-error'
653+ : indexQueue . capacityUsed > 0
654+ ? 'text-green-600'
655+ : 'text-on-surface'
656+ } `} >
657+ { indexQueue . capacityUsed >= indexQueue . capacityTotal
658+ ? 'Full'
659+ : indexQueue . capacityUsed > 0
660+ ? 'Processing'
661+ : 'Idle' }
662+ </ span >
663+ { indexQueue . capacityUsed >= indexQueue . capacityTotal && (
664+ < p className = "text-[0.6875rem] text-error mt-1" >
665+ New requests will be rejected
666+ </ p >
667+ ) }
668+ </ div >
669+ </ div >
670+
671+ { /* Info Banner */ }
672+ { indexQueue . capacityUsed >= indexQueue . capacityTotal && (
673+ < div className = "mt-4 bg-error/10 border border-error/20 rounded-lg p-3 flex gap-3" >
674+ < span className = "material-symbols-outlined text-error text-[18px] shrink-0" >
675+ warning
676+ </ span >
677+ < div className = "text-xs" >
678+ < p className = "text-error font-semibold mb-1" > Queue at capacity</ p >
679+ < p className = "text-on-surface-variant leading-relaxed" >
680+ The indexing queue is full. New repository additions will return HTTP 429
681+ until current jobs complete. Wait for active indexing to finish or increase
682+ INDEX_QUEUE_MAX_DEPTH environment variable.
683+ </ p >
684+ </ div >
685+ </ div >
686+ ) }
687+ </ div >
688+ ) }
689+
510690 { /* Standard Repository Cards */ }
511691 { repos . slice ( 1 ) . map ( ( repo ) => (
512692 < div
0 commit comments