@@ -7,7 +7,7 @@ import { Component, OnInit, inject, signal, input, DestroyRef } from '@angular/c
77import { catchError , finalize , switchMap } from 'rxjs/operators'
88import { MatSnackBar } from '@angular/material/snack-bar'
99import { Router } from '@angular/router'
10- import { Observable , of , forkJoin } from 'rxjs'
10+ import { Observable , of , forkJoin , EMPTY , timer } from 'rxjs'
1111import { DevicesService } from '../devices.service'
1212import SnackbarDefaults from '../../shared/config/snackBarDefault'
1313import { AMTFeaturesResponse , BootDetails , Device , UserConsentResponse } from '../../../models/models'
@@ -164,7 +164,7 @@ export class DeviceToolbarComponent implements OnInit {
164164 this . device = data
165165 this . devicesService . device . next ( this . device )
166166 this . isPinned . set ( this . device ?. certHash != null && this . device ?. certHash !== '' )
167- this . getPowerState ( )
167+ this . loadPowerState ( )
168168 this . loadAMTFeatures ( )
169169 // react to AMT feature updates emitted by service
170170 this . devicesService
@@ -217,22 +217,78 @@ export class DeviceToolbarComponent implements OnInit {
217217 this . powerOptions . set ( options )
218218 }
219219
220- getPowerState ( ) : void {
221- this . isLoading ( ) . set ( true )
222- // Goes through the cached variant so a simultaneous KVM deep-link fetch and
223- // this toolbar fetch are deduped into one /power/state round-trip.
224- this . devicesService
225- . getPowerStateCached ( this . deviceId ( ) )
226- . pipe ( takeUntilDestroyed ( this . destroyRef ) )
220+ private setOptimisticPowerState ( action : number ) : void {
221+ // Immediately reflect the expected outcome so the UI doesn't show the
222+ // old state while waiting for the device to transition.
223+ switch ( action ) {
224+ case 2 : // power on
225+ this . powerState . set ( 'deviceToolbar.power.on.value' )
226+ break
227+ case 8 : // power off
228+ case 12 : // soft off
229+ this . powerState . set ( 'deviceToolbar.power.off.value' )
230+ break
231+ default : // reset, cycle, etc. — state is transitioning
232+ this . powerState . set ( 'Unknown' )
233+ }
234+ }
235+
236+ private setPowerStateLabel ( powerstate : number ) : void {
237+ switch ( powerstate ) {
238+ case 2 :
239+ this . powerState . set ( 'deviceToolbar.power.on.value' )
240+ break
241+ case 3 :
242+ case 4 :
243+ this . powerState . set ( 'deviceToolbar.power.sleep.value' )
244+ break
245+ default :
246+ this . powerState . set ( 'deviceToolbar.power.off.value' )
247+ }
248+ }
249+
250+ private loadPowerState ( ) : void {
251+ // Use cached to dedupe simultaneous requests (KVM deep-link + toolbar both loading).
252+ this . fetchPowerState ( this . devicesService . getPowerStateCached ( this . deviceId ( ) ) )
253+ }
254+
255+ refreshPowerState ( ) : void {
256+ // Bypass the cache so manual refresh always fetches the latest state from the device.
257+ this . fetchPowerState ( this . devicesService . getPowerState ( this . deviceId ( ) ) , false , ( ) => {
258+ const msg : string = this . translate . instant ( 'kvm.errorRetrievePower.value' )
259+ this . snackBar . open ( msg , undefined , SnackbarDefaults . defaultError )
260+ } )
261+ }
262+
263+ private silentRefreshPowerState ( ) : void {
264+ // Refresh without showing the loading spinner — used after a power action
265+ // where the optimistic state is already shown to the user.
266+ this . fetchPowerState ( this . devicesService . getPowerState ( this . deviceId ( ) ) , true )
267+ }
268+
269+ private fetchPowerState ( source : Observable < any > , silent = false , onError ?: ( ) => void ) : void {
270+ if ( ! silent ) {
271+ this . isLoading ( ) . set ( true )
272+ }
273+
274+ source
275+ . pipe (
276+ catchError ( ( err ) => {
277+ if ( onError ) {
278+ console . error ( err )
279+ onError ( )
280+ }
281+ return EMPTY
282+ } ) ,
283+ finalize ( ( ) => {
284+ if ( ! silent ) {
285+ this . isLoading ( ) . set ( false )
286+ }
287+ } ) ,
288+ takeUntilDestroyed ( this . destroyRef )
289+ )
227290 . subscribe ( ( powerState ) => {
228- this . powerState . set (
229- powerState . powerstate . toString ( ) === '2'
230- ? 'deviceToolbar.power.on.value'
231- : powerState . powerstate . toString ( ) === '3' || powerState . powerstate . toString ( ) === '4'
232- ? 'deviceToolbar.power.sleep.value'
233- : 'deviceToolbar.power.off.value'
234- )
235- this . isLoading ( ) . set ( false )
291+ this . setPowerStateLabel ( powerState . powerstate )
236292 } )
237293 }
238294
@@ -437,6 +493,14 @@ export class DeviceToolbarComponent implements OnInit {
437493 const msg : string = this . translate . instant ( 'devices.powerActionSent.value' )
438494 console . log ( 'Power action sent successfully:' , data )
439495 this . snackBar . open ( msg , undefined , SnackbarDefaults . defaultSuccess )
496+ // Set expected state immediately for instant feedback.
497+ this . setOptimisticPowerState ( action )
498+ // Then silently confirm actual state after device transitions — no loading spinner.
499+ timer ( 5000 )
500+ . pipe ( takeUntilDestroyed ( this . destroyRef ) )
501+ . subscribe ( ( ) => {
502+ this . silentRefreshPowerState ( )
503+ } )
440504 } else {
441505 console . log ( 'Power action failed:' , data )
442506 const msg : string = this . translate . instant ( 'devices.failPowerAction.value' )
0 commit comments