diff --git a/package-lock.json b/package-lock.json index 41adee1a2..b804a5a39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "@xterm/xterm": "^6.0.0", "angular-oauth2-oidc": "^20.0.2", "angular-oauth2-oidc-jwks": "^20.0.0", + "cypress": "15.11.0", "date-fns": "^4.1.0", "esbuild": "^0.27.4", "monaco-editor": "^0.55.1", diff --git a/src/app/devices/device-toolbar/device-toolbar.component.spec.ts b/src/app/devices/device-toolbar/device-toolbar.component.spec.ts index 6bf311cdc..ef1b4de34 100644 --- a/src/app/devices/device-toolbar/device-toolbar.component.spec.ts +++ b/src/app/devices/device-toolbar/device-toolbar.component.spec.ts @@ -38,6 +38,7 @@ describe('DeviceToolbarComponent', () => { 'sendDeactivate', 'getPowerState', 'getAMTFeatures', + 'getAMTFeaturesCached', 'getAMTVersion', 'featuresChanges' ]) @@ -54,28 +55,28 @@ describe('DeviceToolbarComponent', () => { ) devicesService.getPowerState.and.returnValue(of({ powerstate: 2 })) - devicesService.getAMTFeatures.and.returnValue( - of({ - userConsent: 'None', - ocr: true, - httpsBootSupported: true, - kvm: true, - sol: true, - ider: true, - redirection: true, - optInState: 1, - kvmAvailable: true, - winREBootSupported: true, - localPBABootSupported: true, - remoteErase: true, - pbaBootFilesPath: [], - winREBootFilesPath: { - instanceID: '', - biosBootString: '', - bootString: '' - } - } as any) - ) + const mockAMTFeatures = { + userConsent: 'None', + ocr: true, + httpsBootSupported: true, + kvm: true, + sol: true, + ider: true, + redirection: true, + optInState: 1, + kvmAvailable: true, + winREBootSupported: true, + localPBABootSupported: true, + remoteErase: true, + pbaBootFilesPath: [], + winREBootFilesPath: { + instanceID: '', + biosBootString: '', + bootString: '' + } + } as any + devicesService.getAMTFeatures.and.returnValue(of(mockAMTFeatures)) + devicesService.getAMTFeaturesCached.and.returnValue(of(mockAMTFeatures)) getDeviceSpy = devicesService.getDevice.and.returnValue(of({ guid: 'guid' } as any)) sendDeactivateSpy = devicesService.sendDeactivate.and.returnValue(of({ status: 'SUCCESS' })) sendDeactivateErrorSpy = devicesService.sendDeactivate.and.returnValue(throwError({ error: 'Error' })) diff --git a/src/app/devices/device-toolbar/device-toolbar.component.ts b/src/app/devices/device-toolbar/device-toolbar.component.ts index 339f90ad8..59fba97f7 100644 --- a/src/app/devices/device-toolbar/device-toolbar.component.ts +++ b/src/app/devices/device-toolbar/device-toolbar.component.ts @@ -177,7 +177,9 @@ export class DeviceToolbarComponent implements OnInit { } private loadAMTFeatures(): void { - this.devicesService.getAMTFeatures(this.deviceId()).subscribe((features) => { + // Use cached features if fresher than 30s — avoids a duplicate AMT round-trip + // when the KVM/SOL component already loaded features moments before the toolbar. + this.devicesService.getAMTFeaturesCached(this.deviceId()).subscribe((features) => { this.amtFeatures.set(features) this.buildPowerOptions() }) diff --git a/src/app/devices/devices.service.ts b/src/app/devices/devices.service.ts index 71708d0ff..fbdd0a555 100644 --- a/src/app/devices/devices.service.ts +++ b/src/app/devices/devices.service.ts @@ -5,7 +5,7 @@ import { HttpClient } from '@angular/common/http' import { EventEmitter, Injectable, inject } from '@angular/core' -import { Observable, Subject, BehaviorSubject } from 'rxjs' +import { Observable, Subject, BehaviorSubject, of } from 'rxjs' import { catchError, map, tap } from 'rxjs/operators' import { environment } from 'src/environments/environment' import { @@ -43,6 +43,9 @@ export class DevicesService { private readonly http = inject(HttpClient) // Reactive AMT features stream, keyed by deviceId private readonly amtFeaturesStreams = new Map>() + // Timestamps for TTL-based features cache — configured via environment.amtFeaturesCacheTtlMs, capped at 3 min + private readonly featuresTimestamps = new Map() + private readonly FEATURES_TTL_MS = Math.min(environment.amtFeaturesCacheTtlMs ?? 30_000, 180_000) private getOrCreateFeaturesStream(deviceId: string): BehaviorSubject { if (!this.amtFeaturesStreams.has(deviceId)) { @@ -265,13 +268,31 @@ export class DevicesService { getAMTFeatures(guid: string): Observable { return this.http.get(`${environment.mpsServer}/api/v1/amt/features/${guid}`).pipe( - tap((features) => this.getOrCreateFeaturesStream(guid).next(features)), + tap((features) => { + this.getOrCreateFeaturesStream(guid).next(features) + this.featuresTimestamps.set(guid, Date.now()) + }), catchError((err) => { throw err }) ) } + /** + * Returns cached AMT features immediately if they are fresher than ttlMs, + * otherwise falls back to a real HTTP call. Eliminates duplicate round-trips + * when the toolbar and KVM component both load features within the same session. + * TTL is read from environment.amtFeaturesCacheTtlMs (default 30 s, max 3 min). + */ + getAMTFeaturesCached(guid: string, ttlMs = this.FEATURES_TTL_MS): Observable { + const cached = this.getOrCreateFeaturesStream(guid).value + const ts = this.featuresTimestamps.get(guid) ?? 0 + if (cached !== null && Date.now() - ts < ttlMs) { + return of(cached) + } + return this.getAMTFeatures(guid) + } + getAlarmOccurrences(guid: string): Observable { return this.http .get(`${environment.mpsServer}/api/v1/amt/alarmOccurrences/${guid}`) @@ -366,8 +387,7 @@ export class DevicesService { } getDevice(guid: string): Observable { - const query = `${environment.mpsServer}/api/v1/devices/${guid}` - return this.http.get(query).pipe( + return this.http.get(`${environment.mpsServer}/api/v1/devices/${guid}`).pipe( catchError((err) => { throw err }) diff --git a/src/environments/environment.enterprise.dev.ts b/src/environments/environment.enterprise.dev.ts index 9f14ff764..e4f6b019e 100644 --- a/src/environments/environment.enterprise.dev.ts +++ b/src/environments/environment.enterprise.dev.ts @@ -10,6 +10,7 @@ export const environment = { mpsServer: 'http://localhost:8181', rpsServer: 'http://localhost:8181', vault: '', + amtFeaturesCacheTtlMs: 30_000, // 30 s default; max 3 min (180_000) auth: { clientId: '##CLIENTID##', issuer: '##ISSUER##', diff --git a/src/environments/environment.enterprise.ts b/src/environments/environment.enterprise.ts index d1b08f534..c111e7cf3 100644 --- a/src/environments/environment.enterprise.ts +++ b/src/environments/environment.enterprise.ts @@ -10,6 +10,7 @@ export const environment = { mpsServer: '##CONSOLE_SERVER_API##', rpsServer: '##CONSOLE_SERVER_API##', vault: '##VAULT_SERVER##', + amtFeaturesCacheTtlMs: 30_000, // 30 s default; max 3 min (180_000) auth: { clientId: '##CLIENTID##', issuer: '##ISSUER##', diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index dc6e255d7..31d0f4fcb 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -10,5 +10,6 @@ export const environment = { mpsServer: '##MPS_SERVER##', rpsServer: '##RPS_SERVER##', vault: '##VAULT_SERVER##', + amtFeaturesCacheTtlMs: 30_000, // 30 s default; max 3 min (180_000) auth: {} } diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 66afbd9a4..640e43292 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -14,6 +14,7 @@ export const environment = { mpsServer: 'http://localhost:3000', rpsServer: 'http://localhost:8081', vault: 'http://localhost/vault', + amtFeaturesCacheTtlMs: 30_000, // 30 s default; max 3 min (180_000) auth: { clientId: '', issuer: '',