Skip to content

Commit 1e083cd

Browse files
committed
feat(ui): add backend version display and upgrade support
- Add upgrade check/trigger API endpoints to config and api module - Backends page: version badge, upgrade indicator, upgrade button - Manage page: version in metadata, context-aware upgrade/reinstall button - Settings page: auto-upgrade backends toggle
1 parent b19e60d commit 1e083cd

5 files changed

Lines changed: 100 additions & 10 deletions

File tree

core/http/react-ui/src/pages/Backends.jsx

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export default function Backends() {
2626
const [expandedRow, setExpandedRow] = useState(null)
2727
const [confirmDialog, setConfirmDialog] = useState(null)
2828
const [allBackends, setAllBackends] = useState([])
29+
const [upgrades, setUpgrades] = useState({})
2930

3031
const fetchBackends = useCallback(async () => {
3132
try {
@@ -52,6 +53,13 @@ export default function Backends() {
5253
if (!loading) fetchBackends()
5354
}, [operations.length])
5455

56+
// Fetch available upgrades
57+
useEffect(() => {
58+
backendsApi.checkUpgrades()
59+
.then(data => setUpgrades(data || {}))
60+
.catch(() => {})
61+
}, [operations.length])
62+
5563
// Client-side filtering by tag
5664
const filteredBackends = filter
5765
? allBackends.filter(b => {
@@ -114,6 +122,15 @@ export default function Backends() {
114122
})
115123
}
116124

125+
const handleUpgrade = async (id) => {
126+
try {
127+
await backendsApi.upgrade(id)
128+
addToast(`Upgrading ${id}...`, 'info')
129+
} catch (err) {
130+
addToast(`Upgrade failed: ${err.message}`, 'error')
131+
}
132+
}
133+
117134
const handleManualInstall = async (e) => {
118135
e.preventDefault()
119136
if (!manualUri.trim()) { addToast('Please enter a URI', 'warning'); return }
@@ -179,6 +196,14 @@ export default function Backends() {
179196
<div style={{ color: 'var(--color-text-muted)' }}>Installed</div>
180197
</a>
181198
</div>
199+
{Object.keys(upgrades).length > 0 && (
200+
<div style={{ textAlign: 'center' }}>
201+
<div style={{ fontSize: '1.25rem', fontWeight: 700, color: 'var(--color-warning)' }}>
202+
{Object.keys(upgrades).length}
203+
</div>
204+
<div style={{ color: 'var(--color-text-muted)' }}>Updates</div>
205+
</div>
206+
)}
182207
</div>
183208
<a className="btn btn-secondary btn-sm" href="https://localai.io/docs/getting-started/manual/" target="_blank" rel="noopener noreferrer">
184209
<i className="fas fa-book" /> Docs
@@ -300,6 +325,11 @@ export default function Backends() {
300325
{/* Name */}
301326
<td>
302327
<span style={{ fontWeight: 500 }}>{b.name || b.id}</span>
328+
{b.version && (
329+
<span className="badge" style={{ fontSize: '0.625rem', marginLeft: 4, background: 'var(--color-bg-tertiary)', color: 'var(--color-text-secondary)' }}>
330+
v{b.version}
331+
</span>
332+
)}
303333
</td>
304334

305335
{/* Description */}
@@ -346,9 +376,17 @@ export default function Backends() {
346376
</span>
347377
</div>
348378
) : b.installed ? (
349-
<span className="badge badge-success">
350-
<i className="fas fa-check" style={{ fontSize: '0.5rem', marginRight: 2 }} /> Installed
351-
</span>
379+
<div style={{ display: 'flex', gap: 4, alignItems: 'center', flexWrap: 'wrap' }}>
380+
<span className="badge badge-success">
381+
<i className="fas fa-check" style={{ fontSize: '0.5rem', marginRight: 2 }} /> Installed
382+
</span>
383+
{upgrades[b.name] && (
384+
<span className="badge" style={{ fontSize: '0.625rem', background: '#fef3cd', color: '#856404' }}>
385+
<i className="fas fa-arrow-up" style={{ fontSize: '0.5rem', marginRight: 2 }} />
386+
{upgrades[b.name].available_version ? `v${upgrades[b.name].available_version}` : 'Update'}
387+
</span>
388+
)}
389+
</div>
352390
) : (
353391
<span className="badge" style={{ background: 'var(--color-bg-tertiary)', color: 'var(--color-text-muted)' }}>
354392
<i className="fas fa-circle" style={{ fontSize: '0.5rem', marginRight: 2 }} /> Not Installed
@@ -361,9 +399,15 @@ export default function Backends() {
361399
<div style={{ display: 'flex', gap: 'var(--spacing-xs)', justifyContent: 'flex-end' }} onClick={e => e.stopPropagation()}>
362400
{b.installed ? (
363401
<>
364-
<button className="btn btn-secondary btn-sm" onClick={() => handleInstall(b.name || b.id)} title="Reinstall" disabled={isProcessing}>
365-
<i className={`fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-rotate'}`} />
366-
</button>
402+
{upgrades[b.name] ? (
403+
<button className="btn btn-primary btn-sm" onClick={() => handleUpgrade(b.name || b.id)} title={`Upgrade to ${upgrades[b.name]?.available_version ? 'v' + upgrades[b.name].available_version : 'latest'}`} disabled={isProcessing}>
404+
<i className={`fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-arrow-up'}`} />
405+
</button>
406+
) : (
407+
<button className="btn btn-secondary btn-sm" onClick={() => handleInstall(b.name || b.id)} title="Reinstall" disabled={isProcessing}>
408+
<i className={`fas ${isProcessing ? 'fa-spinner fa-spin' : 'fa-rotate'}`} />
409+
</button>
410+
)}
367411
<button className="btn btn-danger btn-sm" onClick={() => handleDelete(b.name || b.id)} title="Delete" disabled={isProcessing}>
368412
<i className="fas fa-trash" />
369413
</button>

core/http/react-ui/src/pages/Manage.jsx

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export default function Manage() {
2222
const [backendsLoading, setBackendsLoading] = useState(true)
2323
const [reloading, setReloading] = useState(false)
2424
const [reinstallingBackends, setReinstallingBackends] = useState(new Set())
25+
const [upgrades, setUpgrades] = useState({})
2526
const [confirmDialog, setConfirmDialog] = useState(null)
2627
const [distributedMode, setDistributedMode] = useState(false)
2728
const [togglingModels, setTogglingModels] = useState(new Set())
@@ -62,6 +63,15 @@ export default function Manage() {
6263
nodesApi.list().then(() => setDistributedMode(true)).catch(() => {})
6364
}, [fetchLoadedModels, fetchBackends])
6465

66+
// Fetch available backend upgrades
67+
useEffect(() => {
68+
if (activeTab === 'backends') {
69+
backendsApi.checkUpgrades()
70+
.then(data => setUpgrades(data || {}))
71+
.catch(() => {})
72+
}
73+
}, [activeTab])
74+
6575
const handleStopModel = (modelName) => {
6676
setConfirmDialog({
6777
title: 'Stop Model',
@@ -169,6 +179,22 @@ export default function Manage() {
169179
}
170180
}
171181

182+
const handleUpgradeBackend = async (name) => {
183+
try {
184+
setReinstallingBackends(prev => new Set(prev).add(name))
185+
await backendsApi.upgrade(name)
186+
addToast(`Upgrading ${name}...`, 'info')
187+
} catch (err) {
188+
addToast(`Failed to upgrade: ${err.message}`, 'error')
189+
} finally {
190+
setReinstallingBackends(prev => {
191+
const next = new Set(prev)
192+
next.delete(name)
193+
return next
194+
})
195+
}
196+
}
197+
172198
const handleDeleteBackend = (name) => {
173199
setConfirmDialog({
174200
title: 'Delete Backend',
@@ -471,6 +497,17 @@ export default function Manage() {
471497
For: <span style={{ color: 'var(--color-accent)' }}>{backend.Metadata.meta_backend_for}</span>
472498
</span>
473499
)}
500+
{backend.Metadata?.version && (
501+
<span>
502+
<i className="fas fa-code-branch" style={{ fontSize: '0.5rem', marginRight: 4 }} />
503+
Version: <span style={{ color: 'var(--color-text-primary)' }}>v{backend.Metadata.version}</span>
504+
{upgrades[backend.Name] && (
505+
<span style={{ color: '#856404', marginLeft: 4 }}>
506+
→ v{upgrades[backend.Name].available_version}
507+
</span>
508+
)}
509+
</span>
510+
)}
474511
{backend.Metadata?.installed_at && (
475512
<span>
476513
<i className="fas fa-calendar" style={{ fontSize: '0.5rem', marginRight: 4 }} />
@@ -485,12 +522,12 @@ export default function Manage() {
485522
{!backend.IsSystem ? (
486523
<>
487524
<button
488-
className="btn btn-secondary btn-sm"
489-
onClick={() => handleReinstallBackend(backend.Name)}
525+
className={`btn ${upgrades[backend.Name] ? 'btn-primary' : 'btn-secondary'} btn-sm`}
526+
onClick={() => upgrades[backend.Name] ? handleUpgradeBackend(backend.Name) : handleReinstallBackend(backend.Name)}
490527
disabled={reinstallingBackends.has(backend.Name)}
491-
title="Reinstall"
528+
title={upgrades[backend.Name] ? `Upgrade to v${upgrades[backend.Name]?.available_version || 'latest'}` : 'Reinstall'}
492529
>
493-
<i className={`fas ${reinstallingBackends.has(backend.Name) ? 'fa-spinner fa-spin' : 'fa-rotate'}`} />
530+
<i className={`fas ${reinstallingBackends.has(backend.Name) ? 'fa-spinner fa-spin' : upgrades[backend.Name] ? 'fa-arrow-up' : 'fa-rotate'}`} />
494531
</button>
495532
<button
496533
className="btn btn-danger btn-sm"

core/http/react-ui/src/pages/Settings.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,9 @@ export default function Settings() {
266266
<SettingRow label="Max Active Backends" description="Maximum models to keep loaded simultaneously (0 = unlimited)">
267267
<input className="input" type="number" style={{ width: 120 }} value={settings.max_active_backends ?? ''} onChange={(e) => update('max_active_backends', parseInt(e.target.value) || 0)} placeholder="0" />
268268
</SettingRow>
269+
<SettingRow label="Auto-upgrade Backends" description="Automatically upgrade backends when new versions are detected">
270+
<Toggle checked={settings.auto_upgrade_backends} onChange={(v) => update('auto_upgrade_backends', v)} />
271+
</SettingRow>
269272
</div>
270273
</div>
271274

core/http/react-ui/src/utils/api.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ export const backendsApi = {
120120
installExternal: (body) => postJSON(API_CONFIG.endpoints.installExternalBackend, body),
121121
getJob: (uid) => fetchJSON(API_CONFIG.endpoints.backendJob(uid)),
122122
deleteInstalled: (name) => postJSON(API_CONFIG.endpoints.deleteInstalledBackend(name), {}),
123+
checkUpgrades: () => fetchJSON(API_CONFIG.endpoints.backendsUpgrades),
124+
forceCheckUpgrades: () => postJSON(API_CONFIG.endpoints.backendsUpgradesCheck, {}),
125+
upgrade: (name) => postJSON(API_CONFIG.endpoints.upgradeBackend(name), {}),
123126
}
124127

125128
// Chat API (non-streaming)

core/http/react-ui/src/utils/config.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ export const API_CONFIG = {
2323
installExternalBackend: '/api/backends/install-external',
2424
backendJob: (uid) => `/api/backends/job/${uid}`,
2525
deleteInstalledBackend: (name) => `/api/backends/system/delete/${name}`,
26+
backendsUpgrades: '/api/backends/upgrades',
27+
backendsUpgradesCheck: '/api/backends/upgrades/check',
28+
upgradeBackend: (name) => `/api/backends/upgrade/${name}`,
2629

2730
// Resources
2831
resources: '/api/resources',

0 commit comments

Comments
 (0)