diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dcc555..268cb2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [1.23.0] - 2026-10-27 + +### Added +- **Enriquecimiento Contextual de Tooltips en Badges de Plugins (`PluginService`, `groups.component`)**: + - Incorporada caché reactiva de metadatos de clientes (`clientPluginMetadataMap`) en `PluginService`, extrayendo automáticamente atributos contextuales proporcionados por cualquier plugin tanto en la consulta inicial (`statusEndpoint`) como en las actualizaciones en tiempo real recibidas por Mercure. + - Al renderizar los badges (`getClientBadges`), si existen atributos contextuales como el identificador del alumno/usuario (`dni`, `user`) y el tiempo restante (`remaining_minutes`, `remainingMinutes`), se añaden dinámicamente al texto del tooltip (ej. `"Puesto reservado y en uso (DNI: 44212977P · 58 min restantes)"`), preservando el badge visualmente limpio y compacto (`Ocupado`, `A punto de expirar`). + - Limpieza automática y atómica de metadatos cuando el puesto queda libre (`FREE`) o se reinicia (`NONE`), manteniendo sincronizada la consola web en todo momento sin acoplamiento a ningún plugin en concreto. + ## [1.22.0] - 2026-10-27 ### Added diff --git a/ogWebconsole/package.json b/ogWebconsole/package.json index 56cae83..52f528b 100644 --- a/ogWebconsole/package.json +++ b/ogWebconsole/package.json @@ -1,6 +1,6 @@ { "name": "og-webconsole", - "version": "1.22.0", + "version": "1.23.0", "scripts": { "ng": "ng", "start": "ng serve", diff --git a/ogWebconsole/src/app/services/plugin.service.ts b/ogWebconsole/src/app/services/plugin.service.ts index 8d88431..ee54724 100644 --- a/ogWebconsole/src/app/services/plugin.service.ts +++ b/ogWebconsole/src/app/services/plugin.service.ts @@ -113,6 +113,8 @@ export class PluginService { // 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) + private clientPluginMetadataMap = new Map(); private mercureEventSource: EventSource | null = null; constructor() { @@ -149,6 +151,20 @@ export class PluginService { return this.clientPluginStatusMap.get(this.getPartitionStatusKey(pluginName, idOrUuid, disk, partition)); } + public setPluginMetadata(pluginName: string, idOrUuid: string | number, metadata: any): void { + const key = this.getStatusKey(pluginName, idOrUuid); + const existing = this.clientPluginMetadataMap.get(key) || {}; + this.clientPluginMetadataMap.set(key, { ...existing, ...metadata }); + } + + public getPluginMetadata(pluginName: string, idOrUuid: string | number): any { + return this.clientPluginMetadataMap.get(this.getStatusKey(pluginName, idOrUuid)); + } + + public clearPluginMetadata(pluginName: string, idOrUuid: string | number): void { + this.clientPluginMetadataMap.delete(this.getStatusKey(pluginName, idOrUuid)); + } + private initMercureSubscription(): void { const mercureUrl = this.configService.mercureUrl; if (!mercureUrl || this.mercureEventSource) { @@ -170,11 +186,17 @@ export class PluginService { this.clientPluginStatusMap.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)); @@ -190,6 +212,7 @@ export class PluginService { } 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)); @@ -204,14 +227,34 @@ export class PluginService { } } } 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; + if (data.clientId !== undefined && data.clientId !== null) { - this.setPluginStatus(data.plugin, String(data.clientId), data.status); + 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, String(data.clientId), Number(disk), Number(part), data.status); + 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); } @@ -329,15 +372,35 @@ export class PluginService { const disk = row.disk_number ?? row.diskNumber ?? row.disk; const part = row.partition_number ?? row.partitionNumber ?? row.partition; + // Extraer metadatos arbitrarios (dni, remaining_minutes, etc.) + const metadata: any = {}; + for (const [k, v] of Object.entries(row)) { + if (!['status', 'client_id', 'clientId', 'id', 'client_uuid', 'clientUuid', 'uuid', 'disk_number', 'diskNumber', 'disk', 'partition_number', 'partitionNumber', 'partition'].includes(k) && v !== null && v !== undefined) { + metadata[k] = v; + } + } + const hasMeta = Object.keys(metadata).length > 0; + if (status) { if (id !== undefined && id !== null) { - this.setPluginStatus(plugin.name, String(id), status); + const idStr = String(id); + this.setPluginStatus(plugin.name, idStr, status); + if (hasMeta && status !== 'FREE') { + this.setPluginMetadata(plugin.name, idStr, metadata); + } else if (status === 'FREE') { + this.clearPluginMetadata(plugin.name, idStr); + } if (disk !== undefined && disk !== null && part !== undefined && part !== null) { - this.setPartitionPluginStatus(plugin.name, String(id), Number(disk), Number(part), status); + this.setPartitionPluginStatus(plugin.name, idStr, Number(disk), Number(part), status); } } if (uuid) { this.setPluginStatus(plugin.name, uuid, status); + if (hasMeta && status !== 'FREE') { + this.setPluginMetadata(plugin.name, uuid, metadata); + } else if (status === 'FREE') { + this.clearPluginMetadata(plugin.name, uuid); + } if (disk !== undefined && disk !== null && part !== undefined && part !== null) { this.setPartitionPluginStatus(plugin.name, uuid, Number(disk), Number(part), status); } @@ -490,6 +553,26 @@ export class PluginService { } } + // Enriquecer tooltip con metadatos contextuales si están disponibles (p. ej. DNI del alumno, tiempo restante) + const meta = (idKey ? this.getPluginMetadata(plugin.name, idKey) : null) + || (uuidKey ? this.getPluginMetadata(plugin.name, uuidKey) : null); + + if (meta) { + const dni = meta.dni || meta.user || meta.student; + const mins = meta.remaining_minutes !== undefined ? meta.remaining_minutes : meta.remainingMinutes; + const infoParts: string[] = []; + if (dni) { + infoParts.push(`DNI: ${dni}`); + } + if (mins !== undefined && mins !== null && mins !== '') { + infoParts.push(`${mins} min`); + } + if (infoParts.length > 0) { + const detailStr = infoParts.join(' · '); + tooltip = tooltip ? `${tooltip} (${detailStr})` : detailStr; + } + } + badges.push({ id: plugin.name, type: matchingBadge.type,