diff --git a/docs-es/v2.0/troubleshooting-es.md b/docs-es/v2.0/troubleshooting-es.md
index 14f95604..b2befc84 100644
--- a/docs-es/v2.0/troubleshooting-es.md
+++ b/docs-es/v2.0/troubleshooting-es.md
@@ -117,6 +117,7 @@ Si el scheduler falla inmediatamente después de un reinicio con un mensaje como
### El estado del scheduler en WebUI aparece no saludable
Versiones recientes muestran estado/readiness del scheduler en WebUI.
+Si el modal del scheduler indica que se agotó el tiempo al cargar detalles, las versiones actuales mantienen visible la última instantánea buena del scheduler con su antigüedad en lugar de convertir ese problema de transporte en un error del scheduler. Trate primero esa advertencia como un problema de comunicación entre WebUI y `lqosd`, y confirme la salud real del scheduler en los logs antes de asumir que falló el shaping.
Si aparece caído/desactualizado:
diff --git a/docs/v2.0/troubleshooting.md b/docs/v2.0/troubleshooting.md
index 127b479f..9c341feb 100644
--- a/docs/v2.0/troubleshooting.md
+++ b/docs/v2.0/troubleshooting.md
@@ -184,6 +184,7 @@ If an integration subprocess fails, current builds keep the scheduler alive, pub
Recent builds expose scheduler readiness/state in the WebUI (Node Manager).
If the scheduler is still starting, the sidebar now reports the current startup phase and a coarse progress ring rather than only a spinner.
Current builds also treat scheduler progress, output, and error bus messages as proof that the scheduler is alive, so the sidebar should not stay stuck on `Scheduler available: false` while the scheduler is actively reporting work.
+If the scheduler modal says scheduler details timed out, current builds keep showing the last good scheduler snapshot with its age instead of turning that transport problem into a scheduler error. Treat that warning as a WebUI or `lqosd` communication issue first, then confirm scheduler health in the service logs before assuming shaping failed.
If scheduler status appears down/stale:
1. Verify both services:
diff --git a/src/rust/lqosd/src/node_manager/js_build/src/template.js b/src/rust/lqosd/src/node_manager/js_build/src/template.js
index 233be5f3..af9c347a 100644
--- a/src/rust/lqosd/src/node_manager/js_build/src/template.js
+++ b/src/rust/lqosd/src/node_manager/js_build/src/template.js
@@ -35,9 +35,11 @@ const SCHEDULER_MODAL_ACTIVE_POLL_MS = 1000;
let schedulerStatusPollTimer = null;
let schedulerStatusRequestInFlight = false;
let schedulerStatusFirstRequestedAt = null;
+let cachedSchedulerStatusData = null;
let schedulerModalPollTimer = null;
let schedulerModalRequestInFlight = false;
let schedulerModalInstance = null;
+let cachedSchedulerDetailsData = null;
let cachedQueueModeConfig = null;
function listenOnceWithTimeout(eventName, timeoutMs, handler, onTimeout) {
@@ -125,6 +127,42 @@ function schedulerErrorText(data) {
return String(error).trim();
}
+function schedulerSnapshotUpdatedUnix(data) {
+ const progressUpdatedUnix = Number(data?.progress?.updated_unix);
+ if (Number.isFinite(progressUpdatedUnix) && progressUpdatedUnix > 0) {
+ return progressUpdatedUnix;
+ }
+ const statusUpdatedUnix = Number(data?.updated_unix);
+ if (Number.isFinite(statusUpdatedUnix) && statusUpdatedUnix > 0) {
+ return statusUpdatedUnix;
+ }
+ return null;
+}
+
+function schedulerUpdatedText(data) {
+ const updatedUnix = schedulerSnapshotUpdatedUnix(data);
+ if (!updatedUnix) {
+ return "Update time unavailable";
+ }
+ return schedulerRelativeTime(updatedUnix);
+}
+
+function schedulerTransportWarningText(message, data) {
+ const updatedText = schedulerUpdatedText(data);
+ if (updatedText === "Update time unavailable") {
+ return `${message} Retrying in the background.`;
+ }
+ return `${message} Showing cached scheduler data. ${updatedText}.`;
+}
+
+function schedulerStaleStatusLabel(data) {
+ const updatedText = schedulerUpdatedText(data);
+ if (updatedText === "Update time unavailable") {
+ return "Scheduler status is stale";
+ }
+ return `Scheduler status is stale. ${updatedText}.`;
+}
+
function schedulerLooksRecovered(data) {
const progress = data?.progress || null;
if (!data?.available || !progress || progress.active) {
@@ -170,6 +208,17 @@ function schedulerStateDescriptor(data) {
icon: "fa-list-check",
};
}
+ if (data?.stale) {
+ return {
+ tone: "warning",
+ badgeClass: "text-bg-warning",
+ label: "Stale",
+ title: "Scheduler status is stale",
+ subtitle: "Showing the last scheduler snapshot older than five minutes",
+ ringTone: "tone-warning",
+ icon: "fa-clock",
+ };
+ }
if (active) {
return {
tone: "info",
@@ -250,7 +299,7 @@ function schedulerActivityItems(output, error, data) {
.reverse();
}
-function renderSchedulerDetails(data) {
+function renderSchedulerDetails(data, options = {}) {
const progress = data?.progress || null;
const descriptor = schedulerStateDescriptor(data);
const liveError = schedulerHasLiveError(data) ? schedulerErrorText(data) : "";
@@ -262,22 +311,30 @@ function renderSchedulerDetails(data) {
const stepText = Number.isFinite(stepIndex) && Number.isFinite(stepCount) && stepCount > 0
? `Step ${stepIndex} of ${stepCount}`
: "No active step reported";
- const updatedText = progress?.updated_unix
- ? schedulerRelativeTime(progress.updated_unix)
- : "Update time unavailable";
- const availabilityText = data?.setup_required ? "Setup Required" : (data?.available ? "Healthy" : "Unavailable");
+ const updatedText = schedulerUpdatedText(data);
+ const availabilityText = data?.setup_required
+ ? "Setup Required"
+ : data?.stale
+ ? "Stale"
+ : (data?.available ? "Healthy" : "Unavailable");
const recentResult = schedulerSetupMessage(data) || summarizeSchedulerOutput(data?.output, historicalError || liveError);
const activity = schedulerActivityItems(data?.output, historicalError || liveError, data);
- const progressMeta = data?.setup_required
- ? "Complete runtime setup to enable scheduler work"
- : progress?.active
- ? `${percent}% complete`
- : descriptor.label === "Idle"
- ? "Last run complete"
- : "Waiting for activity";
+ let progressMeta = "Waiting for activity";
+ if (options.transportWarning) {
+ progressMeta = "Showing cached scheduler details while the UI reconnects";
+ } else if (data?.setup_required) {
+ progressMeta = "Complete runtime setup to enable scheduler work";
+ } else if (progress?.active) {
+ progressMeta = `${percent}% complete`;
+ } else if (descriptor.label === "Idle") {
+ progressMeta = "Last run complete";
+ }
const setupAlert = data?.setup_required
? `
${escapeHtml(schedulerSetupMessage(data) || "Choose a topology source in Complete Setup before expecting scheduler activity.")}