Skip to content

Commit 30f992f

Browse files
authored
feat(ui): add backend reinstall button (#7305)
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 2709220 commit 30f992f

File tree

1 file changed

+160
-6
lines changed

1 file changed

+160
-6
lines changed

core/http/views/manage.html

Lines changed: 160 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -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
406425
function 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

Comments
 (0)