@@ -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 >
0 commit comments