Skip to content

Commit 0c5b0bd

Browse files
committed
fix(control-ui): retry bundle fetch and improve restart diagnosis (#99092)
When the gateway restarts, the Control UI's JS bundle fails to load. The 'Keep waiting' button only re-armed a 12s timer without retrying the fetch, and the error message blamed browser extensions instead of 'gateway is restarting.' Only a full page reload could fix it. Changes to ui/index.html inline fallback script: - Add gateway reachability probe (fetch location.href) to distinguish 'gateway restarting' from 'module blocked by extension' - Make 'Keep waiting' re-fetch the JS bundle with exponential backoff (5 retries: 2s, 4s, 8s, 16s, 32s) instead of just re-arming the timer - Branch the fallback dialog text based on probe result: - Unreachable gateway: 'Control UI is starting up' with restart guidance - Reachable gateway: original 'Control UI did not start' with extension guidance - Add retry status indicator below the action buttons - Keep 'Try again' (full reload) as a manual escape hatch - Reset probe state on each retry sequence so re-probe works Tests (ui/src/ui/mount-fallback.test.ts): - Original test updated to install fetch mock for probe compatibility - New: re-fetches bundle when 'Keep waiting' is clicked - New: shows 'starting up' message when gateway probe is unreachable - New: shows 'did not start' message when gateway is reachable - New: attempts bundle fetch at least once during retry sequence Fixes #99092
1 parent 0011a18 commit 0c5b0bd

2 files changed

Lines changed: 397 additions & 5 deletions

File tree

ui/index.html

Lines changed: 162 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,13 @@
174174
outline-offset: 3px;
175175
}
176176

177+
.mount-fallback__retry-status {
178+
margin: 12px 0 0;
179+
color: #9fb0bd;
180+
font-size: 0.85rem;
181+
line-height: 1.4;
182+
}
183+
177184
html[data-theme-mode="light"] body.openclaw-mount-fallback-active {
178185
color: #151b21;
179186
background: #f5f7fa;
@@ -194,6 +201,10 @@
194201
html[data-theme-mode="light"] .mount-fallback__panel a {
195202
color: #0369a1;
196203
}
204+
205+
html[data-theme-mode="light"] .mount-fallback__retry-status {
206+
color: #6b7785;
207+
}
197208
</style>
198209
</head>
199210
<body>
@@ -209,12 +220,12 @@
209220
<div class="mount-fallback__panel" tabindex="-1">
210221
<p class="mount-fallback__eyebrow">OpenClaw Control UI</p>
211222
<h1 id="openclaw-mount-fallback-title">Control UI did not start</h1>
212-
<p>
223+
<p id="openclaw-mount-fallback-reason">
213224
The browser loaded the static page, but the app bundle did not register the
214225
<code>openclaw-app</code> web component. A browser extension or early content script may
215226
be blocking module execution.
216227
</p>
217-
<ul>
228+
<ul id="openclaw-mount-fallback-steps">
218229
<li>Try again in a clean browser profile or private window.</li>
219230
<li>Disable extensions that run on all pages, then reload this dashboard.</li>
220231
<li>
@@ -239,6 +250,7 @@ <h1 id="openclaw-mount-fallback-title">Control UI did not start</h1>
239250
Keep waiting
240251
</button>
241252
</div>
253+
<p id="openclaw-mount-retry-status" class="mount-fallback__retry-status" hidden></p>
242254
</div>
243255
</section>
244256
<script>
@@ -251,9 +263,18 @@ <h1 id="openclaw-mount-fallback-title">Control UI did not start</h1>
251263
var panel = fallback.querySelector(".mount-fallback__panel");
252264
var retry = document.getElementById("openclaw-mount-retry");
253265
var wait = document.getElementById("openclaw-mount-wait");
266+
var retryStatus = document.getElementById("openclaw-mount-retry-status");
267+
var titleEl = document.getElementById("openclaw-mount-fallback-title");
268+
var reasonEl = document.getElementById("openclaw-mount-fallback-reason");
269+
var stepsEl = document.getElementById("openclaw-mount-fallback-steps");
254270
var rawDelay = Number(fallback.getAttribute("data-openclaw-mount-timeout-ms"));
255271
var delay = Number.isFinite(rawDelay) && rawDelay > 0 ? rawDelay : 12000;
256272
var timer;
273+
var retryCount = 0;
274+
var maxRetries = 5;
275+
var backoffSchedule = [2000, 4000, 8000, 16000, 32000];
276+
var retrying = false;
277+
var probeResult = null; // null = not probed, "reachable", "unreachable"
257278

258279
function appMounted() {
259280
try {
@@ -284,20 +305,158 @@ <h1 id="openclaw-mount-fallback-title">Control UI did not start</h1>
284305
panel.focus();
285306
}
286307
}
308+
// Probe gateway reachability to distinguish restart from extension blocking
309+
probeGateway();
310+
}
311+
312+
function probeGateway() {
313+
if (probeResult !== null) return; // already probed
314+
try {
315+
fetch(window.location.href, {
316+
method: "GET",
317+
cache: "no-store",
318+
headers: { "X-OpenClaw-Probe": "1" },
319+
}).then(
320+
function (res) {
321+
probeResult = res && res.ok ? "reachable" : "unreachable";
322+
updateFallbackMessage();
323+
},
324+
function () {
325+
probeResult = "unreachable";
326+
updateFallbackMessage();
327+
},
328+
);
329+
} catch (e) {
330+
probeResult = "unreachable";
331+
updateFallbackMessage();
332+
}
333+
}
334+
335+
function updateFallbackMessage() {
336+
if (probeResult === null || !titleEl || !reasonEl || !stepsEl) return;
337+
if (probeResult === "unreachable") {
338+
titleEl.textContent = "Control UI is starting up";
339+
reasonEl.textContent =
340+
"The gateway is restarting or temporarily unavailable. The app bundle will be retried automatically — please wait.";
341+
stepsEl.innerHTML =
342+
'<li>The gateway may take a few seconds to come back online.</li>' +
343+
'<li>If this persists after 30 seconds, try reloading the page.</li>' +
344+
'<li>See <a href="https://docs.openclaw.ai/web/control-ui#blank-control-ui-page" target="_blank" rel="noopener noreferrer">Control UI troubleshooting</a>.</li>';
345+
} else {
346+
titleEl.textContent = "Control UI did not start";
347+
reasonEl.textContent =
348+
"The browser loaded the static page, but the app bundle did not register the " +
349+
"openclaw-app web component. A browser extension or early content script may " +
350+
"be blocking module execution.";
351+
stepsEl.innerHTML =
352+
'<li>Try again in a clean browser profile or private window.</li>' +
353+
'<li>Disable extensions that run on all pages, then reload this dashboard.</li>' +
354+
'<li>See <a href="https://docs.openclaw.ai/web/control-ui#blank-control-ui-page" target="_blank" rel="noopener noreferrer">Control UI troubleshooting</a>.</li>';
355+
}
287356
}
288357

289358
function armFallbackTimer() {
290359
window.clearTimeout(timer);
291360
timer = window.setTimeout(showFallback, delay);
292361
}
293362

363+
function fetchAppBundle() {
364+
try {
365+
return fetch("/src/main.ts", { cache: "no-store" }).then(
366+
function (res) {
367+
return res && res.ok;
368+
},
369+
function () {
370+
return false;
371+
},
372+
);
373+
} catch (e) {
374+
return Promise.resolve(false);
375+
}
376+
}
377+
378+
function showRetryStatus(message) {
379+
if (!retryStatus) return;
380+
retryStatus.textContent = message;
381+
retryStatus.hidden = false;
382+
}
383+
384+
function hideRetryStatus() {
385+
if (!retryStatus) return;
386+
retryStatus.hidden = true;
387+
retryStatus.textContent = "";
388+
}
389+
390+
function performRetry() {
391+
if (retrying || appMounted()) return;
392+
if (retryCount >= maxRetries) {
393+
showRetryStatus(
394+
"Automatic retry limit reached. Use \u201cTry again\u201d to reload the page.",
395+
);
396+
return;
397+
}
398+
retrying = true;
399+
var attempt = retryCount + 1;
400+
var backoff = backoffSchedule[retryCount] || 32000;
401+
showRetryStatus("Retrying (attempt " + attempt + " of " + maxRetries + ")\u2026");
402+
fetchAppBundle().then(function (ok) {
403+
retrying = false;
404+
retryCount++;
405+
if (ok && appMounted()) {
406+
hideRetryStatus();
407+
hideFallback();
408+
return;
409+
}
410+
if (ok && !appMounted()) {
411+
// Bundle is fetchable but component hasn\u2019t registered yet \u2014 give it more time
412+
showRetryStatus(
413+
"Bundle fetched; waiting for app to initialize\u2026",
414+
);
415+
timer = window.setTimeout(performRetry, backoff);
416+
return;
417+
}
418+
// Fetch failed \u2014 gateway likely still restarting
419+
if (retryCount >= maxRetries) {
420+
showRetryStatus(
421+
"Automatic retry limit reached. Use \u201cTry again\u201d to reload the page.",
422+
);
423+
} else {
424+
showRetryStatus(
425+
"Gateway still unavailable. Next retry in " + Math.round(backoff / 1000) + "s\u2026",
426+
);
427+
timer = window.setTimeout(performRetry, backoff);
428+
}
429+
});
430+
}
431+
432+
function startRetrySequence() {
433+
hideFallback();
434+
hideRetryStatus();
435+
retryCount = 0;
436+
retrying = false;
437+
window.clearTimeout(timer);
438+
// Reset probe so we re-check gateway status on the next fallback
439+
probeResult = null;
440+
performRetry();
441+
// Arm a safety timer in case retries don\u2019t trigger fallback show
442+
timer = window.setTimeout(function () {
443+
if (!appMounted()) {
444+
showFallback();
445+
if (retryCount < maxRetries && !retrying) {
446+
performRetry();
447+
}
448+
}
449+
}, delay);
450+
}
451+
294452
armFallbackTimer();
295453

296454
if (window.customElements && typeof window.customElements.whenDefined === "function") {
297455
window.customElements.whenDefined(tagName).then(
298456
function () {
299457
window.clearTimeout(timer);
300458
hideFallback();
459+
hideRetryStatus();
301460
},
302461
function () {},
303462
);
@@ -310,8 +469,7 @@ <h1 id="openclaw-mount-fallback-title">Control UI did not start</h1>
310469
}
311470
if (wait) {
312471
wait.addEventListener("click", function () {
313-
hideFallback();
314-
armFallbackTimer();
472+
startRetrySequence();
315473
});
316474
}
317475
})();

0 commit comments

Comments
 (0)