@@ -279,10 +279,22 @@ <h2 class="text-2xl font-semibold text-[#E5E7EB] mb-1 flex items-center">
279279 <!-- Backends Section -->
280280 < div class ="mt-8 ">
281281 < div class ="mb-6 ">
282- < h2 class ="text-2xl font-semibold text-[#E5E7EB] mb-1 flex items-center ">
283- < i class ="fas fa-cogs mr-2 text-[#8B5CF6] text-sm "> </ i >
284- Installed Backends
285- </ h2 >
282+ < div class ="flex items-center justify-between mb-1 ">
283+ < h2 class ="text-2xl font-semibold text-[#E5E7EB] flex items-center ">
284+ < i class ="fas fa-cogs mr-2 text-[#8B5CF6] text-sm "> </ i >
285+ Installed Backends
286+ </ h2 >
287+ {{ if gt (len .InstalledBackends) 0 }}
288+ < button
289+ @click ="reinstallAllBackends() "
290+ :disabled ="reinstallingAll "
291+ class ="inline-flex items-center bg-[#38BDF8] hover:bg-[#38BDF8]/80 disabled:opacity-50 disabled:cursor-not-allowed text-white py-1.5 px-3 rounded text-xs font-medium transition-colors "
292+ title ="Reinstall all backends ">
293+ < i class ="fas fa-arrow-rotate-right mr-1.5 text-[10px] " :class ="reinstallingAll ? 'fa-spin' : '' "> </ i >
294+ < span x-text ="reinstallingAll ? 'Reinstalling...' : 'Reinstall All' "> </ span >
295+ </ button >
296+ {{ end }}
297+ </ div >
286298 < p class ="text-sm text-[#94A3B8] mb-4 ">
287299 < span class ="text-[#8B5CF6] font-medium "> {{len .InstalledBackends}}</ span > backend{{if gt (len .InstalledBackends) 1}}s{{end}} ready to use
288300 </ p >
@@ -324,7 +336,7 @@ <h2 class="text-2xl font-bold text-[#E5E7EB] mb-2">No backends installed yet</h2
324336 </ thead >
325337 < tbody >
326338 {{ range .InstalledBackends }}
327- < tr class ="hover:bg-[#1E293B]/50 border-b border-[#1E293B] transition-colors ">
339+ < tr class ="hover:bg-[#1E293B]/50 border-b border-[#1E293B] transition-colors "data-backend-name =" {{.Name}} " data-is-system =" {{.IsSystem}} " >
328340 <!-- Name Column -->
329341 < td class ="p-2 ">
330342 < div class ="flex items-center gap-2 ">
@@ -378,6 +390,13 @@ <h2 class="text-2xl font-bold text-[#E5E7EB] mb-2">No backends installed yet</h2
378390 < td class ="p-2 ">
379391 < div class ="flex items-center justify-end gap-1 ">
380392 {{ if not .IsSystem }}
393+ < button
394+ @click ="reinstallBackend('{{.Name}}') "
395+ :disabled ="reinstallingBackends['{{.Name}}'] "
396+ class ="text-[#38BDF8]/60 hover:text-[#38BDF8] hover:bg-[#38BDF8]/10 disabled:opacity-50 disabled:cursor-not-allowed rounded p-1 transition-colors "
397+ title ="Reinstall {{.Name}} ">
398+ < i class ="fas fa-arrow-rotate-right text-xs " :class ="reinstallingBackends['{{.Name}}'] ? 'fa-spin' : '' "> </ i >
399+ </ button >
381400 < button
382401 @click ="deleteBackend('{{.Name}}') "
383402 class ="text-red-400/60 hover:text-red-400 hover:bg-red-500/10 rounded p-1 transition-colors "
@@ -406,9 +425,13 @@ <h2 class="text-2xl font-bold text-[#E5E7EB] mb-2">No backends installed yet</h2
406425function indexDashboard ( ) {
407426 return {
408427 notifications : [ ] ,
428+ reinstallingBackends : { } ,
429+ reinstallingAll : false ,
430+ backendJobs : { } ,
409431
410432 init ( ) {
411- // Initialize component
433+ // Poll for job progress every 600ms
434+ setInterval ( ( ) => this . pollJobs ( ) , 600 ) ;
412435 } ,
413436
414437 addNotification ( message , type = 'success' ) {
@@ -422,6 +445,137 @@ <h2 class="text-2xl font-bold text-[#E5E7EB] mb-2">No backends installed yet</h2
422445 this . notifications = this . notifications . filter ( n => n . id !== id ) ;
423446 } ,
424447
448+ async reinstallBackend ( backendName ) {
449+ if ( this . reinstallingBackends [ backendName ] ) {
450+ return ; // Already reinstalling
451+ }
452+ 453+ try {
454+ this . reinstallingBackends [ backendName ] = true ;
455+ const response = await fetch ( `/api/backends/install/${ encodeURIComponent ( backendName ) } ` , {
456+ method : 'POST'
457+ } ) ;
458+ 459+ const data = await response . json ( ) ;
460+ 461+ if ( response . ok && data . jobID ) {
462+ this . backendJobs [ backendName ] = data . jobID ;
463+ this . addNotification ( `Reinstalling backend "${ backendName } "...` , 'success' ) ;
464+ } else {
465+ this . reinstallingBackends [ backendName ] = false ;
466+ this . addNotification ( `Failed to start reinstall: ${ data . error || 'Unknown error' } ` , 'error' ) ;
467+ }
468+ } catch ( error ) {
469+ console . error ( 'Error reinstalling backend:' , error ) ;
470+ this . reinstallingBackends [ backendName ] = false ;
471+ this . addNotification ( `Failed to reinstall backend: ${ error . message } ` , 'error' ) ;
472+ }
473+ } ,
474+ 475+ async reinstallAllBackends ( ) {
476+ if ( this . reinstallingAll ) {
477+ return ; // Already reinstalling
478+ }
479+ 480+ if ( ! confirm ( 'Are you sure you want to reinstall all backends? This may take some time.' ) ) {
481+ return ;
482+ }
483+ 484+ this . reinstallingAll = true ;
485+ 486+ // Get all non-system backends from the page using data attributes
487+ const backendRows = document . querySelectorAll ( 'tr[data-backend-name]' ) ;
488+ const backendsToReinstall = [ ] ;
489+ 490+ backendRows . forEach ( row => {
491+ const backendName = row . getAttribute ( 'data-backend-name' ) ;
492+ const isSystem = row . getAttribute ( 'data-is-system' ) === 'true' ;
493+ if ( backendName && ! isSystem && ! this . reinstallingBackends [ backendName ] ) {
494+ backendsToReinstall . push ( backendName ) ;
495+ }
496+ } ) ;
497+ 498+ if ( backendsToReinstall . length === 0 ) {
499+ this . reinstallingAll = false ;
500+ this . addNotification ( 'No backends available to reinstall' , 'error' ) ;
501+ return ;
502+ }
503+ 504+ this . addNotification ( `Starting reinstall of ${ backendsToReinstall . length } backend(s)...` , 'success' ) ;
505+ 506+ // Reinstall all backends sequentially to avoid overwhelming the system
507+ for ( const backendName of backendsToReinstall ) {
508+ await this . reinstallBackend ( backendName ) ;
509+ // Small delay between installations
510+ await new Promise ( resolve => setTimeout ( resolve , 500 ) ) ;
511+ }
512+ 513+ // Don't set reinstallingAll to false here - let pollJobs handle it when all jobs complete
514+ // This allows the UI to show the batch operation is in progress
515+ } ,
516+ 517+ async pollJobs ( ) {
518+ for ( const [ backendName , jobID ] of Object . entries ( this . backendJobs ) ) {
519+ try {
520+ const response = await fetch ( `/api/backends/job/${ jobID } ` ) ;
521+ const jobData = await response . json ( ) ;
522+ 523+ if ( jobData . completed ) {
524+ delete this . backendJobs [ backendName ] ;
525+ this . reinstallingBackends [ backendName ] = false ;
526+ this . addNotification ( `Backend "${ backendName } " reinstalled successfully!` , 'success' ) ;
527+ 528+ // Only reload if not in batch mode and no other jobs are running
529+ if ( ! this . reinstallingAll && Object . keys ( this . backendJobs ) . length === 0 ) {
530+ setTimeout ( ( ) => {
531+ window . location . reload ( ) ;
532+ } , 1500 ) ;
533+ }
534+ }
535+ 536+ if ( jobData . error || ( jobData . message && jobData . message . startsWith ( 'error:' ) ) ) {
537+ delete this . backendJobs [ backendName ] ;
538+ this . reinstallingBackends [ backendName ] = false ;
539+ let errorMessage = 'Unknown error' ;
540+ if ( typeof jobData . error === 'string' ) {
541+ errorMessage = jobData . error ;
542+ } else if ( jobData . error && typeof jobData . error === 'object' ) {
543+ const errorKeys = Object . keys ( jobData . error ) ;
544+ if ( errorKeys . length > 0 ) {
545+ errorMessage = jobData . error . message || jobData . error . error || jobData . error . Error || JSON . stringify ( jobData . error ) ;
546+ } else {
547+ errorMessage = jobData . message || 'Unknown error' ;
548+ }
549+ } else if ( jobData . message ) {
550+ errorMessage = jobData . message ;
551+ }
552+ if ( errorMessage . startsWith ( 'error: ' ) ) {
553+ errorMessage = errorMessage . substring ( 7 ) ;
554+ }
555+ this . addNotification ( `Error reinstalling backend "${ backendName } ": ${ errorMessage } ` , 'error' ) ;
556+ 557+ // If batch mode and all jobs are done (completed or errored), reload
558+ if ( this . reinstallingAll && Object . keys ( this . backendJobs ) . length === 0 ) {
559+ this . reinstallingAll = false ;
560+ setTimeout ( ( ) => {
561+ window . location . reload ( ) ;
562+ } , 2000 ) ;
563+ }
564+ }
565+ } catch ( error ) {
566+ console . error ( 'Error polling job:' , error ) ;
567+ }
568+ }
569+ 570+ // If batch mode completed and no jobs left, reload
571+ if ( this . reinstallingAll && Object . keys ( this . backendJobs ) . length === 0 ) {
572+ this . reinstallingAll = false ;
573+ setTimeout ( ( ) => {
574+ window . location . reload ( ) ;
575+ } , 2000 ) ;
576+ }
577+ } ,
578+ 425579 async deleteBackend ( backendName ) {
426580 if ( ! confirm ( `Are you sure you want to delete the backend "${ backendName } "?` ) ) {
427581 return ;
0 commit comments