@@ -35,9 +35,11 @@ const SCHEDULER_MODAL_ACTIVE_POLL_MS = 1000;
3535let schedulerStatusPollTimer = null ;
3636let schedulerStatusRequestInFlight = false ;
3737let schedulerStatusFirstRequestedAt = null ;
38+ let cachedSchedulerStatusData = null ;
3839let schedulerModalPollTimer = null ;
3940let schedulerModalRequestInFlight = false ;
4041let schedulerModalInstance = null ;
42+ let cachedSchedulerDetailsData = null ;
4143let cachedQueueModeConfig = null ;
4244
4345function listenOnceWithTimeout ( eventName , timeoutMs , handler , onTimeout ) {
@@ -125,6 +127,42 @@ function schedulerErrorText(data) {
125127 return String ( error ) . trim ( ) ;
126128}
127129
130+ function schedulerSnapshotUpdatedUnix ( data ) {
131+ const progressUpdatedUnix = Number ( data ?. progress ?. updated_unix ) ;
132+ if ( Number . isFinite ( progressUpdatedUnix ) && progressUpdatedUnix > 0 ) {
133+ return progressUpdatedUnix ;
134+ }
135+ const statusUpdatedUnix = Number ( data ?. updated_unix ) ;
136+ if ( Number . isFinite ( statusUpdatedUnix ) && statusUpdatedUnix > 0 ) {
137+ return statusUpdatedUnix ;
138+ }
139+ return null ;
140+ }
141+
142+ function schedulerUpdatedText ( data ) {
143+ const updatedUnix = schedulerSnapshotUpdatedUnix ( data ) ;
144+ if ( ! updatedUnix ) {
145+ return "Update time unavailable" ;
146+ }
147+ return schedulerRelativeTime ( updatedUnix ) ;
148+ }
149+
150+ function schedulerTransportWarningText ( message , data ) {
151+ const updatedText = schedulerUpdatedText ( data ) ;
152+ if ( updatedText === "Update time unavailable" ) {
153+ return `${ message } Retrying in the background.` ;
154+ }
155+ return `${ message } Showing cached scheduler data. ${ updatedText } .` ;
156+ }
157+
158+ function schedulerStaleStatusLabel ( data ) {
159+ const updatedText = schedulerUpdatedText ( data ) ;
160+ if ( updatedText === "Update time unavailable" ) {
161+ return "Scheduler status is stale" ;
162+ }
163+ return `Scheduler status is stale. ${ updatedText } .` ;
164+ }
165+
128166function schedulerLooksRecovered ( data ) {
129167 const progress = data ?. progress || null ;
130168 if ( ! data ?. available || ! progress || progress . active ) {
@@ -170,6 +208,17 @@ function schedulerStateDescriptor(data) {
170208 icon : "fa-list-check" ,
171209 } ;
172210 }
211+ if ( data ?. stale ) {
212+ return {
213+ tone : "warning" ,
214+ badgeClass : "text-bg-warning" ,
215+ label : "Stale" ,
216+ title : "Scheduler status is stale" ,
217+ subtitle : "Showing the last scheduler snapshot older than five minutes" ,
218+ ringTone : "tone-warning" ,
219+ icon : "fa-clock" ,
220+ } ;
221+ }
173222 if ( active ) {
174223 return {
175224 tone : "info" ,
@@ -250,7 +299,7 @@ function schedulerActivityItems(output, error, data) {
250299 . reverse ( ) ;
251300}
252301
253- function renderSchedulerDetails ( data ) {
302+ function renderSchedulerDetails ( data , options = { } ) {
254303 const progress = data ?. progress || null ;
255304 const descriptor = schedulerStateDescriptor ( data ) ;
256305 const liveError = schedulerHasLiveError ( data ) ? schedulerErrorText ( data ) : "" ;
@@ -262,22 +311,30 @@ function renderSchedulerDetails(data) {
262311 const stepText = Number . isFinite ( stepIndex ) && Number . isFinite ( stepCount ) && stepCount > 0
263312 ? `Step ${ stepIndex } of ${ stepCount } `
264313 : "No active step reported" ;
265- const updatedText = progress ?. updated_unix
266- ? schedulerRelativeTime ( progress . updated_unix )
267- : "Update time unavailable" ;
268- const availabilityText = data ?. setup_required ? "Setup Required" : ( data ?. available ? "Healthy" : "Unavailable" ) ;
314+ const updatedText = schedulerUpdatedText ( data ) ;
315+ const availabilityText = data ?. setup_required
316+ ? "Setup Required"
317+ : data ?. stale
318+ ? "Stale"
319+ : ( data ?. available ? "Healthy" : "Unavailable" ) ;
269320 const recentResult = schedulerSetupMessage ( data ) || summarizeSchedulerOutput ( data ?. output , historicalError || liveError ) ;
270321 const activity = schedulerActivityItems ( data ?. output , historicalError || liveError , data ) ;
271- const progressMeta = data ?. setup_required
272- ? "Complete runtime setup to enable scheduler work"
273- : progress ?. active
274- ? `${ percent } % complete`
275- : descriptor . label === "Idle"
276- ? "Last run complete"
277- : "Waiting for activity" ;
322+ let progressMeta = "Waiting for activity" ;
323+ if ( options . transportWarning ) {
324+ progressMeta = "Showing cached scheduler details while the UI reconnects" ;
325+ } else if ( data ?. setup_required ) {
326+ progressMeta = "Complete runtime setup to enable scheduler work" ;
327+ } else if ( progress ?. active ) {
328+ progressMeta = `${ percent } % complete` ;
329+ } else if ( descriptor . label === "Idle" ) {
330+ progressMeta = "Last run complete" ;
331+ }
278332 const setupAlert = data ?. setup_required
279333 ? `<div class="alert alert-warning mt-3 mb-0" role="alert"><i class="fa fa-list-check me-2"></i>${ escapeHtml ( schedulerSetupMessage ( data ) || "Choose a topology source in Complete Setup before expecting scheduler activity." ) } </div>`
280334 : "" ;
335+ const transportWarningMarkup = options . transportWarning
336+ ? `<div class="alert alert-warning mt-3 mb-0" role="alert"><i class="fa fa-wifi me-2"></i>${ escapeHtml ( options . transportWarning ) } </div>`
337+ : "" ;
281338 const alertMarkup = liveError
282339 ? `<div class="alert alert-danger mt-3 mb-0" role="alert"><i class="fa fa-triangle-exclamation me-2"></i>${ escapeHtml ( liveError ) } </div>`
283340 : "" ;
@@ -304,6 +361,7 @@ function renderSchedulerDetails(data) {
304361 </div>
305362 </div>
306363 ${ setupAlert }
364+ ${ transportWarningMarkup }
307365 ${ alertMarkup }
308366 <div class="lqos-scheduler-progress-card">
309367 <div class="lqos-scheduler-progress-topline">
@@ -360,7 +418,7 @@ function updateSchedulerModalBody(contentHtml) {
360418 }
361419}
362420
363- function renderSchedulerStatus ( container , state , progress ) {
421+ function renderSchedulerStatus ( container , state , progress , labelOverride = null ) {
364422 if ( ! container ) return ;
365423
366424 let color = "text-secondary" ;
@@ -388,11 +446,18 @@ function renderSchedulerStatus(container, state, progress) {
388446 color = "text-danger" ;
389447 label = "Scheduler has an internal error" ;
390448 indicator = schedulerRingMarkup ( 100 , "tone-danger" , "fa-triangle-exclamation" ) ;
449+ } else if ( state === "stale" ) {
450+ color = "text-warning" ;
451+ label = "Scheduler status is stale" ;
452+ indicator = schedulerRingMarkup ( 100 , "tone-warning" , "fa-clock" ) ;
391453 } else if ( state === "setup" ) {
392454 color = "text-warning" ;
393455 label = "Scheduler needs topology setup" ;
394456 indicator = schedulerRingMarkup ( 100 , "tone-warning" , "fa-list-check" ) ;
395457 }
458+ if ( labelOverride ) {
459+ label = labelOverride ;
460+ }
396461
397462 container . innerHTML = `
398463 <button class="nav-link btn btn-link text-start w-100 p-0 border-0 ${ color } lqos-scheduler-link" type="button" id="schedulerStatusLink" aria-label="${ escapeAttr ( label ) } " title="${ escapeAttr ( label ) } ">
@@ -432,16 +497,27 @@ function loadSchedulerStatus(force = false) {
432497 listenOnceWithTimeout ( "SchedulerStatus" , SCHEDULER_STATUS_TIMEOUT_MS , ( msg ) => {
433498 schedulerStatusRequestInFlight = false ;
434499 if ( ! msg || ! msg . data ) {
435- renderSchedulerStatus ( container , "unavailable" , null ) ;
500+ if ( cachedSchedulerStatusData ) {
501+ renderSchedulerStatus (
502+ container ,
503+ "stale" ,
504+ cachedSchedulerStatusData . progress || null ,
505+ schedulerTransportWarningText ( "Scheduler status response was empty." , cachedSchedulerStatusData )
506+ ) ;
507+ } else {
508+ renderSchedulerStatus ( container , "unavailable" , null ) ;
509+ }
436510 scheduleNextSchedulerStatusPoll ( ) ;
437511 return ;
438512 }
439513 const data = msg . data ;
514+ cachedSchedulerStatusData = data ;
440515 const hasError = schedulerHasLiveError ( data ) ;
441516 const isHealthy = ! ! data . available && ! hasError ;
442517 const progress = data . progress || null ;
443518 const progressActive = ! ! ( progress && progress . active ) ;
444519 const setupRequired = ! ! data . setup_required ;
520+ const stale = ! ! data . stale ;
445521 const elapsed = schedulerStatusFirstRequestedAt === null ? 0 : ( Date . now ( ) - schedulerStatusFirstRequestedAt ) ;
446522
447523 if ( hasError ) {
@@ -456,6 +532,12 @@ function loadSchedulerStatus(force = false) {
456532 return ;
457533 }
458534
535+ if ( stale ) {
536+ renderSchedulerStatus ( container , "stale" , progress , schedulerStaleStatusLabel ( data ) ) ;
537+ scheduleNextSchedulerStatusPoll ( ) ;
538+ return ;
539+ }
540+
459541 if ( progressActive ) {
460542 renderSchedulerStatus ( container , "progress" , progress ) ;
461543 scheduleNextSchedulerStatusPoll ( ) ;
@@ -475,7 +557,14 @@ function loadSchedulerStatus(force = false) {
475557 } , ( ) => {
476558 schedulerStatusRequestInFlight = false ;
477559 const elapsed = schedulerStatusFirstRequestedAt === null ? 0 : ( Date . now ( ) - schedulerStatusFirstRequestedAt ) ;
478- if ( elapsed < SCHEDULER_STATUS_STARTING_GRACE_MS ) {
560+ if ( cachedSchedulerStatusData ) {
561+ renderSchedulerStatus (
562+ container ,
563+ "stale" ,
564+ cachedSchedulerStatusData . progress || null ,
565+ schedulerTransportWarningText ( "Scheduler status refresh timed out." , cachedSchedulerStatusData )
566+ ) ;
567+ } else if ( elapsed < SCHEDULER_STATUS_STARTING_GRACE_MS ) {
479568 renderSchedulerStatus ( container , "loading" , null ) ;
480569 } else {
481570 renderSchedulerStatus ( container , "unavailable" , null ) ;
@@ -534,13 +623,6 @@ function renderSchedulerModalLoading() {
534623 </div>` ) ;
535624}
536625
537- function renderSchedulerModalError ( message ) {
538- updateSchedulerModalBody ( `
539- <div class="alert alert-danger mb-0" role="alert">
540- <i class="fa fa-triangle-exclamation me-2"></i>${ escapeHtml ( message ) }
541- </div>` ) ;
542- }
543-
544626function ensureSchedulerModalLifecycle ( ) {
545627 const modalEl = document . getElementById ( 'schedulerModal' ) ;
546628 if ( ! modalEl || modalEl . dataset . schedulerLiveBound === "1" ) {
@@ -567,10 +649,24 @@ function refreshSchedulerModalDetails(showLoading = false) {
567649 return ;
568650 }
569651 if ( ! msg || ! msg . data ) {
570- renderSchedulerModalError ( "Failed to load scheduler details" ) ;
652+ if ( cachedSchedulerDetailsData ) {
653+ updateSchedulerModalBody ( renderSchedulerDetails ( cachedSchedulerDetailsData , {
654+ transportWarning : schedulerTransportWarningText (
655+ "Scheduler details response was empty." ,
656+ cachedSchedulerDetailsData
657+ ) ,
658+ } ) ) ;
659+ } else {
660+ updateSchedulerModalBody ( `
661+ <div class="alert alert-warning mb-0" role="alert">
662+ <i class="fa fa-wifi me-2"></i>${ escapeHtml ( "Failed to load scheduler details. Retrying in the background." ) }
663+ </div>` ) ;
664+ }
571665 scheduleNextSchedulerModalPoll ( SCHEDULER_MODAL_IDLE_POLL_MS ) ;
572666 return ;
573667 }
668+ cachedSchedulerDetailsData = msg . data ;
669+ cachedSchedulerStatusData = msg . data ;
574670 updateSchedulerModalBody ( renderSchedulerDetails ( msg . data ) ) ;
575671 scheduleNextSchedulerModalPoll ( schedulerModalPollDelay ( msg . data ) ) ;
576672 } , ( ) => {
@@ -579,7 +675,19 @@ function refreshSchedulerModalDetails(showLoading = false) {
579675 stopSchedulerModalPolling ( ) ;
580676 return ;
581677 }
582- renderSchedulerModalError ( "Timed out while loading scheduler details" ) ;
678+ if ( cachedSchedulerDetailsData ) {
679+ updateSchedulerModalBody ( renderSchedulerDetails ( cachedSchedulerDetailsData , {
680+ transportWarning : schedulerTransportWarningText (
681+ "Scheduler details refresh timed out." ,
682+ cachedSchedulerDetailsData
683+ ) ,
684+ } ) ) ;
685+ } else {
686+ updateSchedulerModalBody ( `
687+ <div class="alert alert-warning mb-0" role="alert">
688+ <i class="fa fa-wifi me-2"></i>${ escapeHtml ( "Timed out while loading scheduler details. Retrying in the background." ) }
689+ </div>` ) ;
690+ }
583691 scheduleNextSchedulerModalPoll ( SCHEDULER_MODAL_IDLE_POLL_MS ) ;
584692 } ) ;
585693 wsClient . send ( { SchedulerDetails : { } } ) ;
0 commit comments