diff --git a/CHANGELOG.md b/CHANGELOG.md index efbbc9d..3a4417e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.24.2] - 2026-10-30 + +### Fixed +- **Actualización Reactiva en Tiempo Real de Tooltips y Badges de Plugins (`PluginService`, `groups.component`, `execute-command.component`)**: + - Solucionado el problema por el cual el tiempo restante en el tooltip de los badges no se actualizaba en vivo al recibir eventos Mercure o tras ejecutar acciones como "Renovar tiempo", requiriendo recargar la página manualmente. + - La recepción de eventos SSE de Mercure en `PluginService` se ejecuta ahora dentro de `NgZone.run()`, emitiendo a través del nuevo flujo observable reactivo `statusUpdates$`. + - Suscripción reactiva en `GroupsComponent` a `pluginService.statusUpdates$`, activando la detección de cambios (`cd.detectChanges()`) de manera inmediata ante cualquier actualización de estado o metadatos de puestos. + - En `groups.component.html` y `client-details.component.html`, la expresión de seguimiento del control de flujo (`@for`) incorpora ahora el tooltip (`track ... + (badge.tooltip || '')`), forzando la actualización instantánea del enlace `[matTooltip]` en el DOM al cambiar los minutos restantes o datos del alumno. + - Tras procesar con éxito un comando de plugin en `ExecuteCommandComponent`, se dispara automáticamente una recarga de estados en segundo plano (`loadActivePluginsStatus()`). + ## [1.24.1] - 2026-10-30 ### Fixed diff --git a/ogWebconsole/package.json b/ogWebconsole/package.json index 2db9697..33cf26f 100644 --- a/ogWebconsole/package.json +++ b/ogWebconsole/package.json @@ -1,6 +1,6 @@ { "name": "og-webconsole", - "version": "1.24.1", + "version": "1.24.2", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts index 6541347..57169bf 100644 --- a/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts +++ b/ogWebconsole/src/app/components/commands/main-commands/execute-command/execute-command.component.ts @@ -626,6 +626,7 @@ export class ExecuteCommandComponent implements OnInit { next: (response) => { const msg = response?.message || 'Comando procesado correctamente'; this.toastService.success(msg, pluginCmd.label); + this.pluginService.loadActivePluginsStatus().subscribe(); }, error: (error) => { const errorMsg = error?.error?.message || error?.error?.['hydra:description'] || 'Error al comunicar con el servidor'; diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index d35f6ac..48a1cc1 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -423,7 +423,7 @@ RemotePC } - @for (badge of getPluginBadges(client); track badge.id + '-' + badge.type + '-' + badge.label) { + @for (badge of getPluginBadges(client); track badge.id + '-' + badge.type + '-' + badge.label + '-' + (badge.tooltip || '')) { {{ badge.icon }} {{ badge.label }} @@ -554,7 +554,7 @@ RemotePC } - @for (badge of getPluginBadges(client); track badge.id + '-' + badge.type + '-' + badge.label) { + @for (badge of getPluginBadges(client); track badge.id + '-' + badge.type + '-' + badge.label + '-' + (badge.tooltip || '')) { {{ badge.icon }} {{ badge.label }} diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 0ccea0a..28981bd 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -270,7 +270,13 @@ export class GroupsComponent implements OnInit, OnDestroy { this.breakpointObserver.observe(['(max-width: 992px)']).subscribe((result) => { this.isSmallScreen = result.matches; - }) + }); + + this.subscriptions.add( + this.pluginService.statusUpdates$.subscribe(() => { + this.cd.detectChanges(); + }) + ); } private updateClientStatus(clientUuid: string, status: string): void { diff --git a/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.html b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.html index 8a51a55..b14bd04 100644 --- a/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.html +++ b/ogWebconsole/src/app/components/groups/shared/client-details/client-details.component.html @@ -92,7 +92,7 @@ Desplegada: {{ image.deployDate | date:'dd/MM/yyyy HH:mm' }} }
- @for (badge of getPartitionBadges(disk.diskNumber, image.partitionNumber); track badge.id + '-' + badge.type + '-' + badge.label) { + @for (badge of getPartitionBadges(disk.diskNumber, image.partitionNumber); track badge.id + '-' + badge.type + '-' + badge.label + '-' + (badge.tooltip || '')) { {{ badge.icon }} {{ badge.label }} diff --git a/ogWebconsole/src/app/services/plugin.service.ts b/ogWebconsole/src/app/services/plugin.service.ts index 68484f6..99bf27e 100644 --- a/ogWebconsole/src/app/services/plugin.service.ts +++ b/ogWebconsole/src/app/services/plugin.service.ts @@ -1,6 +1,6 @@ -import { Injectable, inject } from '@angular/core'; +import { Injectable, NgZone, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, Observable, forkJoin, of } from 'rxjs'; +import { BehaviorSubject, Observable, Subject, forkJoin, of } from 'rxjs'; import { catchError, map, switchMap, tap } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; import { ConfigService } from './config.service'; @@ -115,12 +115,16 @@ export class PluginService { private configService = inject(ConfigService); private translateService = inject(TranslateService); private authService = inject(AuthService); + private ngZone = inject(NgZone); private allPlugins: PluginManifest[] = []; private activePlugins: PluginManifest[] = []; private pluginsSubject = new BehaviorSubject([]); public plugins$ = this.pluginsSubject.asObservable(); + private statusUpdatesSubject = new Subject<{ plugin: string; clientId?: string; status: string; metadata?: any }>(); + public statusUpdates$ = this.statusUpdatesSubject.asObservable(); + // Dynamic state cache: key is `${pluginName}:${clientIdOrUuid}` -> status string private clientPluginStatusMap = new Map(); // Dynamic metadata cache: key is `${pluginName}:${clientIdOrUuid}` -> extra attributes (e.g. dni, remaining_minutes) @@ -184,95 +188,104 @@ export class PluginService { try { this.mercureEventSource = new EventSource(`${mercureUrl}?topic=` + encodeURIComponent('plugins')); this.mercureEventSource.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - if (data && data.plugin && data.status) { - const disk = data.disk ?? data.disk_number ?? data.diskNumber; - const part = data.partition ?? data.partition_number ?? data.partitionNumber; + this.ngZone.run(() => { + try { + const data = JSON.parse(event.data); + if (data && data.plugin && data.status) { + const disk = data.disk ?? data.disk_number ?? data.diskNumber; + const part = data.partition ?? data.partition_number ?? data.partitionNumber; - const clearEntriesForPrefix = (prefix: string) => { - for (const key of Array.from(this.clientPluginStatusMap.keys())) { - if (key.startsWith(prefix)) { - this.clientPluginStatusMap.delete(key); + const clearEntriesForPrefix = (prefix: string) => { + for (const key of Array.from(this.clientPluginStatusMap.keys())) { + if (key.startsWith(prefix)) { + this.clientPluginStatusMap.delete(key); + } } - } - for (const key of Array.from(this.clientPluginMetadataMap.keys())) { - if (key.startsWith(prefix)) { - this.clientPluginMetadataMap.delete(key); + for (const key of Array.from(this.clientPluginMetadataMap.keys())) { + if (key.startsWith(prefix)) { + this.clientPluginMetadataMap.delete(key); + } } - } - }; + }; - if (data.status === 'NONE') { - if (data.clientId !== undefined && data.clientId !== null) { - const idStr = String(data.clientId); - this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, idStr)); - if (disk !== undefined && disk !== null && part !== undefined && part !== null) { - this.clientPluginStatusMap.delete(this.getPartitionStatusKey(data.plugin, idStr, Number(disk), Number(part))); - const curGlobal = this.clientPluginStatusMap.get(this.getStatusKey(data.plugin, idStr)); - if (curGlobal && ['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING'].includes(curGlobal.toUpperCase())) { + if (data.status === 'NONE') { + if (data.clientId !== undefined && data.clientId !== null) { + const idStr = String(data.clientId); + this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, idStr)); + if (disk !== undefined && disk !== null && part !== undefined && part !== null) { + this.clientPluginStatusMap.delete(this.getPartitionStatusKey(data.plugin, idStr, Number(disk), Number(part))); + const curGlobal = this.clientPluginStatusMap.get(this.getStatusKey(data.plugin, idStr)); + if (curGlobal && ['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING'].includes(curGlobal.toUpperCase())) { + this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, idStr)); + } + } else if (disk !== undefined && disk !== null) { + clearEntriesForPrefix(`${data.plugin}:${idStr}:${disk}:`); + } else { this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, idStr)); + clearEntriesForPrefix(`${data.plugin}:${idStr}:`); } - } else if (disk !== undefined && disk !== null) { - clearEntriesForPrefix(`${data.plugin}:${idStr}:${disk}:`); - } else { - this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, idStr)); - clearEntriesForPrefix(`${data.plugin}:${idStr}:`); } - } - if (data.clientUuid) { - const uuidStr = data.clientUuid; - this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, uuidStr)); - if (disk !== undefined && disk !== null && part !== undefined && part !== null) { - this.clientPluginStatusMap.delete(this.getPartitionStatusKey(data.plugin, uuidStr, Number(disk), Number(part))); - const curGlobal = this.clientPluginStatusMap.get(this.getStatusKey(data.plugin, uuidStr)); - if (curGlobal && ['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING'].includes(curGlobal.toUpperCase())) { + if (data.clientUuid) { + const uuidStr = data.clientUuid; + this.clientPluginMetadataMap.delete(this.getStatusKey(data.plugin, uuidStr)); + if (disk !== undefined && disk !== null && part !== undefined && part !== null) { + this.clientPluginStatusMap.delete(this.getPartitionStatusKey(data.plugin, uuidStr, Number(disk), Number(part))); + const curGlobal = this.clientPluginStatusMap.get(this.getStatusKey(data.plugin, uuidStr)); + if (curGlobal && ['REMOVING', 'CHECKING', 'FREEZING', 'UNFREEZING'].includes(curGlobal.toUpperCase())) { + this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, uuidStr)); + } + } else if (disk !== undefined && disk !== null) { + clearEntriesForPrefix(`${data.plugin}:${uuidStr}:${disk}:`); + } else { this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, uuidStr)); + clearEntriesForPrefix(`${data.plugin}:${uuidStr}:`); } - } else if (disk !== undefined && disk !== null) { - clearEntriesForPrefix(`${data.plugin}:${uuidStr}:${disk}:`); - } else { - this.clientPluginStatusMap.delete(this.getStatusKey(data.plugin, uuidStr)); - clearEntriesForPrefix(`${data.plugin}:${uuidStr}:`); } - } - } else { - // Extraer metadatos contextuales (p. ej. dni, remainingMinutes, remaining_minutes) - const metadata: any = {}; - for (const [k, v] of Object.entries(data)) { - if (!['plugin', 'status', 'clientId', 'clientUuid', 'disk', 'disk_number', 'diskNumber', 'partition', 'partition_number', 'partitionNumber', 'action', 'timestamp'].includes(k) && v !== null && v !== undefined) { - metadata[k] = v; + this.statusUpdatesSubject.next({ plugin: data.plugin, status: 'NONE' }); + } else { + // Extraer metadatos contextuales (p. ej. dni, remainingMinutes, remaining_minutes) + const metadata: any = {}; + for (const [k, v] of Object.entries(data)) { + if (!['plugin', 'status', 'clientId', 'clientUuid', 'disk', 'disk_number', 'diskNumber', 'partition', 'partition_number', 'partitionNumber', 'action', 'timestamp'].includes(k) && v !== null && v !== undefined) { + metadata[k] = v; + } } - } - const hasMeta = Object.keys(metadata).length > 0; + const hasMeta = Object.keys(metadata).length > 0; - if (data.clientId !== undefined && data.clientId !== null) { - const idStr = String(data.clientId); - this.setPluginStatus(data.plugin, idStr, data.status); - if (hasMeta && data.status !== 'FREE') { - this.setPluginMetadata(data.plugin, idStr, metadata); - } else if (data.status === 'FREE') { - this.clearPluginMetadata(data.plugin, idStr); + if (data.clientId !== undefined && data.clientId !== null) { + const idStr = String(data.clientId); + this.setPluginStatus(data.plugin, idStr, data.status); + if (hasMeta && data.status !== 'FREE') { + this.setPluginMetadata(data.plugin, idStr, metadata); + } else if (data.status === 'FREE') { + this.clearPluginMetadata(data.plugin, idStr); + } + if (disk !== undefined && disk !== null && part !== undefined && part !== null) { + this.setPartitionPluginStatus(data.plugin, idStr, Number(disk), Number(part), data.status); + } } - if (disk !== undefined && disk !== null && part !== undefined && part !== null) { - this.setPartitionPluginStatus(data.plugin, idStr, Number(disk), Number(part), data.status); - } - } - if (data.clientUuid) { - this.setPluginStatus(data.plugin, data.clientUuid, data.status); - if (hasMeta && data.status !== 'FREE') { - this.setPluginMetadata(data.plugin, data.clientUuid, metadata); - } else if (data.status === 'FREE') { - this.clearPluginMetadata(data.plugin, data.clientUuid); - } - if (disk !== undefined && disk !== null && part !== undefined && part !== null) { - this.setPartitionPluginStatus(data.plugin, data.clientUuid, Number(disk), Number(part), data.status); + if (data.clientUuid) { + this.setPluginStatus(data.plugin, data.clientUuid, data.status); + if (hasMeta && data.status !== 'FREE') { + this.setPluginMetadata(data.plugin, data.clientUuid, metadata); + } else if (data.status === 'FREE') { + this.clearPluginMetadata(data.plugin, data.clientUuid); + } + if (disk !== undefined && disk !== null && part !== undefined && part !== null) { + this.setPartitionPluginStatus(data.plugin, data.clientUuid, Number(disk), Number(part), data.status); + } } + this.statusUpdatesSubject.next({ + plugin: data.plugin, + clientId: data.clientId ? String(data.clientId) : data.clientUuid, + status: data.status, + metadata + }); } } + } catch { } - } catch { - } + }); }; } catch { } @@ -422,7 +435,12 @@ export class PluginService { ); }); - return forkJoin(requests).pipe(map(() => undefined)); + return forkJoin(requests).pipe( + tap(() => { + this.statusUpdatesSubject.next({ plugin: 'all', status: 'REFRESHED' }); + }), + map(() => undefined) + ); } getPlugins(): PluginManifest[] {