From 6f12cb27d9e6459de2c7701b3030b57737cee5eb Mon Sep 17 00:00:00 2001 From: Val Alexander <68980965+BunsDev@users.noreply.github.com> Date: Mon, 11 May 2026 07:01:02 -0500 Subject: [PATCH] fix(control-ui): add static mount fallback --- CHANGELOG.md | 1 + docs/web/control-ui.md | 10 ++ ui/index.html | 235 +++++++++++++++++++++++++++++++ ui/src/ui/mount-fallback.test.ts | 62 ++++++++ 4 files changed, 308 insertions(+) create mode 100644 ui/src/ui/mount-fallback.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index ac566c0fb76..bbba5a40c00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai - CI: add a non-blocking `plugin-inspector-advisory` artifact to Plugin Prerelease so release runs capture bundled plugin compatibility triage without changing the blocking gate. - Providers/fal: route GPT Image 2 and Nano Banana 2 reference-image edit requests to `/edit` with `image_urls` array, enforce NB2 edit geometry using `aspect_ratio` and `resolution` params, lift Fal edit mode input-image caps to 10 for GPT Image 2 and 14 for Nano Banana 2, and allow aspect-ratio hints in edit mode. (#77295) Thanks @leoge007. +- Control UI: show a plain HTML recovery panel when the app module never registers, giving blank dashboard pages a retry path and browser-extension troubleshooting link. Fixes #44107. Thanks @BunsDev. - Build: enable additional low-churn oxlint rules for promise, TypeScript, and runtime footgun checks. - Build: enable stricter Vitest lint rules for focused, disabled, conditional, hook, matcher, and expectation hazards. diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md index c615f44f3d9..a08137419c6 100644 --- a/docs/web/control-ui.md +++ b/docs/web/control-ui.md @@ -425,6 +425,16 @@ pnpm ui:dev Then point the UI at your Gateway WS URL (e.g. `ws://127.0.0.1:18789`). +## Blank Control UI page + +If the browser loads a blank dashboard and DevTools shows no useful error, an extension or early content script may have prevented the JavaScript module app from evaluating. The static page includes a plain HTML recovery panel that appears when `` is not registered after startup. + +Use the panel's **Try again** action after changing the browser environment, or reload manually after these checks: + +- Disable extensions that inject into all pages, especially extensions with `` content scripts. +- Try a private window, a clean browser profile, or another browser. +- Keep the Gateway running and verify the same dashboard URL after the browser change. + ## Debugging/testing: dev server + remote Gateway The Control UI is static files; the WebSocket target is configurable and can be different from the HTTP origin. This is handy when you want the Vite dev server locally but the Gateway runs elsewhere. diff --git a/ui/index.html b/ui/index.html index 2eb6385ef99..2a9e32ba07f 100644 --- a/ui/index.html +++ b/ui/index.html @@ -62,9 +62,244 @@ } catch (e) {} })(); + + + diff --git a/ui/src/ui/mount-fallback.test.ts b/ui/src/ui/mount-fallback.test.ts new file mode 100644 index 00000000000..7f2a7fe6e04 --- /dev/null +++ b/ui/src/ui/mount-fallback.test.ts @@ -0,0 +1,62 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +const indexHtmlPath = path.resolve(process.cwd(), "ui/index.html"); + +async function readIndexHtmlWithDelay(delayMs: number): Promise { + const html = await readFile(indexHtmlPath, "utf8"); + return html.replace( + 'data-openclaw-mount-timeout-ms="12000"', + `data-openclaw-mount-timeout-ms="${delayMs}"`, + ); +} + +function waitForWindowTimeout(window: Window, delayMs: number): Promise { + return new Promise((resolve) => { + window.setTimeout(resolve, delayMs); + }); +} + +function installFallbackShell(html: string): void { + const parsed = new DOMParser().parseFromString(html, "text/html"); + document.head.innerHTML = parsed.head.innerHTML; + document.body.innerHTML = parsed.body.innerHTML; + + const sentinel = Array.from(parsed.querySelectorAll("script:not([src])")).find((script) => + script.textContent?.includes("openclaw-mount-fallback"), + ); + expect(sentinel).toBeTruthy(); + window.eval(sentinel?.textContent ?? ""); +} + +describe("Control UI mount fallback", () => { + afterEach(() => { + document.head.innerHTML = ""; + document.body.innerHTML = ""; + }); + + it("shows the static troubleshooting panel when the app element is never registered", async () => { + installFallbackShell(await readIndexHtmlWithDelay(1)); + await waitForWindowTimeout(window, 10); + + const fallback = document.getElementById("openclaw-mount-fallback"); + expect(fallback?.hidden).toBe(false); + expect(document.body.classList.contains("openclaw-mount-fallback-active")).toBe(true); + expect(fallback?.textContent).toContain("Control UI did not start"); + expect(fallback?.textContent).toContain("Control UI troubleshooting"); + }); + + it("keeps the fallback hidden when the app element registers before the timeout", async () => { + installFallbackShell(await readIndexHtmlWithDelay(25)); + if (!window.customElements.get("openclaw-app")) { + window.customElements.define("openclaw-app", class extends HTMLElement {}); + } + await window.customElements.whenDefined("openclaw-app"); + await waitForWindowTimeout(window, 35); + + const fallback = document.getElementById("openclaw-mount-fallback"); + expect(fallback?.hidden).toBe(true); + expect(document.body.classList.contains("openclaw-mount-fallback-active")).toBe(false); + }); +});