@@ -99,10 +99,50 @@ function Section({
9999}
100100
101101/** Zona de administración (patrón AdminBar del skill): tarjeta horizontal con
102- * borde de acento, título "Administración" y secciones desplegables a la derecha. */
102+ * borde de acento, título "Administración", secciones desplegables (Usuarios)
103+ * y widgets inline. El comprobador de actualizaciones vive AQUÍ, no en Acerca de. */
103104function AdminZone ( ) {
104105 const { t } = useTranslation ( ) ;
105106 const [ open , setOpen ] = useState ( false ) ;
107+ const [ checking , setChecking ] = useState ( false ) ;
108+ const [ updateStatus , setUpdateStatus ] = useState < 'idle' | 'uptodate' | 'available' | 'error' > ( 'idle' ) ;
109+ const [ latestVersion , setLatestVersion ] = useState ( '' ) ;
110+
111+ const checkUpdates = async ( ) => {
112+ setChecking ( true ) ;
113+ setUpdateStatus ( 'idle' ) ;
114+ try {
115+ const repo = REPO_URL . match ( / g i t h u b \. c o m \/ ( [ ^ / ] + \/ [ ^ / . ] + ) / ) ?. [ 1 ] ;
116+ if ( ! repo ) throw new Error ( 'no repo' ) ;
117+ const res = await fetch ( `https://api.github.com/repos/${ repo } /releases/latest` , {
118+ headers : { Accept : 'application/vnd.github+json' } ,
119+ } ) ;
120+ let version = '' ;
121+ if ( res . ok ) {
122+ const data = await res . json ( ) ;
123+ version = data . tag_name || '' ;
124+ } else if ( res . status === 404 ) {
125+ const tagRes = await fetch ( `https://api.github.com/repos/${ repo } /tags?per_page=1` , {
126+ headers : { Accept : 'application/vnd.github+json' } ,
127+ } ) ;
128+ if ( tagRes . ok ) {
129+ const tags = await tagRes . json ( ) ;
130+ version = tags [ 0 ] ?. name || '' ;
131+ }
132+ }
133+ if ( ! version || compareSemver ( version , pkg . version ) <= 0 ) {
134+ setUpdateStatus ( 'uptodate' ) ;
135+ } else {
136+ setLatestVersion ( version ) ;
137+ setUpdateStatus ( 'available' ) ;
138+ }
139+ } catch {
140+ setUpdateStatus ( 'error' ) ;
141+ } finally {
142+ setChecking ( false ) ;
143+ }
144+ } ;
145+
106146 return (
107147 < motion . section
108148 initial = { { opacity : 0 , y : 24 } }
@@ -133,8 +173,44 @@ function AdminZone() {
133173 < ChevronDown size = { 14 } className = { cn ( 'transition-transform' , open && 'rotate-180' ) } />
134174 </ button >
135175 </ div >
176+
177+ { /* Widget inline: Comprobar actualizaciones (en la AdminBar según el skill) */ }
178+ < div className = "mt-3 flex flex-wrap items-center gap-x-4 gap-y-2 border-t border-app pt-3" >
179+ < button
180+ type = "button"
181+ onClick = { checkUpdates }
182+ disabled = { checking }
183+ className = "inline-flex h-8 items-center gap-2 rounded-lg border border-brand/40 bg-brand/10 px-3 text-xs font-semibold text-brand transition-colors hover:bg-brand/20 disabled:opacity-60"
184+ >
185+ { checking && < RefreshCw size = { 13 } className = "animate-spin" /> }
186+ { checking ? t ( 'ajustes.about.checking' ) : t ( 'ajustes.about.checkUpdates' ) }
187+ </ button >
188+ { updateStatus === 'uptodate' && (
189+ < span className = "inline-flex items-center gap-1.5 text-xs text-emerald-500" >
190+ < Check size = { 13 } />
191+ { t ( 'ajustes.about.upToDate' , { version : pkg . version } ) }
192+ </ span >
193+ ) }
194+ { updateStatus === 'available' && (
195+ < span className = "inline-flex items-center gap-2 text-xs text-brand" >
196+ { t ( 'ajustes.about.updateAvailable' , { version : latestVersion } ) }
197+ < a
198+ href = { `${ REPO_URL } /releases` }
199+ target = "_blank"
200+ rel = "noreferrer"
201+ className = "inline-flex h-6 items-center rounded-full border border-brand/40 bg-brand/10 px-2.5 text-[11px] font-medium text-brand hover:bg-brand/20"
202+ >
203+ { t ( 'ajustes.about.viewRelease' ) }
204+ </ a >
205+ </ span >
206+ ) }
207+ { updateStatus === 'error' && (
208+ < span className = "text-xs text-destructive" > { t ( 'ajustes.about.updateError' ) } </ span >
209+ ) }
210+ </ div >
211+
136212 { open && (
137- < motion . div initial = { { opacity : 0 } } animate = { { opacity : 1 } } className = "mt-4 border-t border-app pt-4 " >
213+ < motion . div initial = { { opacity : 0 } } animate = { { opacity : 1 } } className = "mt-3 " >
138214 < UsersSection />
139215 </ motion . div >
140216 ) }
@@ -1479,6 +1555,13 @@ function InstallSection({ state, install }: { state: InstallState; install: () =
14791555
14801556const REPO_URL = 'https://github.com/gnacho/panelsolar' ;
14811557
1558+ function compareSemver ( a : string , b : string ) : number {
1559+ const pa = a . replace ( / ^ v / , '' ) . split ( '.' ) . map ( ( n ) => parseInt ( n , 10 ) || 0 ) ;
1560+ const pb = b . replace ( / ^ v / , '' ) . split ( '.' ) . map ( ( n ) => parseInt ( n , 10 ) || 0 ) ;
1561+ for ( let i = 0 ; i < 3 ; i ++ ) if ( ( pa [ i ] || 0 ) !== ( pb [ i ] || 0 ) ) return ( pa [ i ] || 0 ) - ( pb [ i ] || 0 ) ;
1562+ return 0 ;
1563+ }
1564+
14821565function AboutSection ( ) {
14831566 const { t } = useTranslation ( ) ;
14841567 const [ systemInfo , setSystemInfo ] = useState < {
0 commit comments