mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 16:44:45 +00:00
fix(control-ui): add static mount fallback
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 `<openclaw-app>` 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 `<all_urls>` 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.
|
||||
|
||||
235
ui/index.html
235
ui/index.html
@@ -62,9 +62,244 @@
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
<style>
|
||||
body.openclaw-mount-fallback-active {
|
||||
margin: 0;
|
||||
min-width: 320px;
|
||||
color: #eef4f8;
|
||||
background: #101418;
|
||||
font-family:
|
||||
Inter,
|
||||
ui-sans-serif,
|
||||
system-ui,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
"Segoe UI",
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
body.openclaw-mount-fallback-active openclaw-app {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mount-fallback {
|
||||
box-sizing: border-box;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.mount-fallback:not([hidden]) {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.mount-fallback__panel {
|
||||
box-sizing: border-box;
|
||||
width: min(100%, 640px);
|
||||
border: 1px solid rgba(148, 163, 184, 0.28);
|
||||
border-radius: 8px;
|
||||
background: #151b21;
|
||||
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.36);
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.mount-fallback__eyebrow {
|
||||
margin: 0 0 10px;
|
||||
color: #9fb0bd;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mount-fallback__panel h1 {
|
||||
margin: 0;
|
||||
color: inherit;
|
||||
font-size: clamp(1.5rem, 3vw, 2rem);
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.mount-fallback__panel p {
|
||||
margin: 16px 0 0;
|
||||
color: #c8d3da;
|
||||
font-size: 1rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.mount-fallback__panel ul {
|
||||
margin: 18px 0 0;
|
||||
padding-left: 1.2rem;
|
||||
color: #c8d3da;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.mount-fallback__panel a {
|
||||
color: #8bd3ff;
|
||||
text-decoration-thickness: 0.08em;
|
||||
text-underline-offset: 0.18em;
|
||||
}
|
||||
|
||||
.mount-fallback__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.mount-fallback__button {
|
||||
min-height: 42px;
|
||||
border: 1px solid rgba(148, 163, 184, 0.36);
|
||||
border-radius: 6px;
|
||||
padding: 0 16px;
|
||||
color: inherit;
|
||||
background: transparent;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mount-fallback__button--primary {
|
||||
border-color: #66c2ff;
|
||||
color: #061019;
|
||||
background: #8bd3ff;
|
||||
}
|
||||
|
||||
.mount-fallback__button:focus-visible,
|
||||
.mount-fallback__panel a:focus-visible {
|
||||
outline: 3px solid #f9c74f;
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
html[data-theme-mode="light"] body.openclaw-mount-fallback-active {
|
||||
color: #151b21;
|
||||
background: #f5f7fa;
|
||||
}
|
||||
|
||||
html[data-theme-mode="light"] .mount-fallback__panel {
|
||||
border-color: rgba(71, 85, 105, 0.22);
|
||||
background: #ffffff;
|
||||
box-shadow: 0 24px 80px rgba(15, 23, 42, 0.14);
|
||||
}
|
||||
|
||||
html[data-theme-mode="light"] .mount-fallback__eyebrow,
|
||||
html[data-theme-mode="light"] .mount-fallback__panel p,
|
||||
html[data-theme-mode="light"] .mount-fallback__panel ul {
|
||||
color: #4b5963;
|
||||
}
|
||||
|
||||
html[data-theme-mode="light"] .mount-fallback__panel a {
|
||||
color: #0369a1;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<openclaw-app></openclaw-app>
|
||||
<section
|
||||
id="openclaw-mount-fallback"
|
||||
class="mount-fallback"
|
||||
data-openclaw-mount-timeout-ms="12000"
|
||||
role="alert"
|
||||
aria-labelledby="openclaw-mount-fallback-title"
|
||||
hidden
|
||||
>
|
||||
<div class="mount-fallback__panel">
|
||||
<p class="mount-fallback__eyebrow">OpenClaw Control UI</p>
|
||||
<h1 id="openclaw-mount-fallback-title">Control UI did not start</h1>
|
||||
<p>
|
||||
The browser loaded the static page, but the app bundle did not register the
|
||||
<code>openclaw-app</code> web component. A browser extension or early content script may
|
||||
be blocking module execution.
|
||||
</p>
|
||||
<ul>
|
||||
<li>Try again in a clean browser profile or private window.</li>
|
||||
<li>Disable extensions that run on all pages, then reload this dashboard.</li>
|
||||
<li>
|
||||
See
|
||||
<a
|
||||
href="https://docs.openclaw.ai/web/control-ui#blank-control-ui-page"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Control UI troubleshooting</a
|
||||
>.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="mount-fallback__actions">
|
||||
<button
|
||||
type="button"
|
||||
id="openclaw-mount-retry"
|
||||
class="mount-fallback__button mount-fallback__button--primary"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
<button type="button" id="openclaw-mount-dismiss" class="mount-fallback__button">
|
||||
Hide message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function () {
|
||||
var tagName = "openclaw-app";
|
||||
var app = document.querySelector(tagName);
|
||||
var fallback = document.getElementById("openclaw-mount-fallback");
|
||||
if (!app || !fallback) return;
|
||||
|
||||
var dismissed = false;
|
||||
var retry = document.getElementById("openclaw-mount-retry");
|
||||
var dismiss = document.getElementById("openclaw-mount-dismiss");
|
||||
var rawDelay = Number(fallback.getAttribute("data-openclaw-mount-timeout-ms"));
|
||||
var delay = Number.isFinite(rawDelay) && rawDelay > 0 ? rawDelay : 12000;
|
||||
|
||||
function appMounted() {
|
||||
try {
|
||||
return Boolean(
|
||||
app.isConnected &&
|
||||
window.customElements &&
|
||||
typeof window.customElements.get === "function" &&
|
||||
window.customElements.get(tagName),
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hideFallback() {
|
||||
fallback.hidden = true;
|
||||
document.body.classList.remove("openclaw-mount-fallback-active");
|
||||
}
|
||||
|
||||
function showFallback() {
|
||||
if (dismissed || appMounted()) return;
|
||||
fallback.hidden = false;
|
||||
document.body.classList.add("openclaw-mount-fallback-active");
|
||||
}
|
||||
|
||||
var timer = window.setTimeout(showFallback, delay);
|
||||
|
||||
if (window.customElements && typeof window.customElements.whenDefined === "function") {
|
||||
window.customElements.whenDefined(tagName).then(
|
||||
function () {
|
||||
window.clearTimeout(timer);
|
||||
hideFallback();
|
||||
},
|
||||
function () {},
|
||||
);
|
||||
}
|
||||
|
||||
if (retry) {
|
||||
retry.addEventListener("click", function () {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
if (dismiss) {
|
||||
dismiss.addEventListener("click", function () {
|
||||
dismissed = true;
|
||||
window.clearTimeout(timer);
|
||||
hideFallback();
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
62
ui/src/ui/mount-fallback.test.ts
Normal file
62
ui/src/ui/mount-fallback.test.ts
Normal file
@@ -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<string> {
|
||||
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<void> {
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user