cmms-embed: fix cmms-config validation rejecting the real server response
Some checks failed
wasm-build.yml / cmms-embed: fix cmms-config validation rejecting the real server response (push) Failing after 0s
wasm-build.yml / cmms-embed: fix cmms-config validation rejecting the real server response (release) Failing after 0s
release / meta (push) Has been cancelled
release / build (push) Has been cancelled
release / publish-wasm (push) Has been cancelled
release / deploy-demo (push) Has been cancelled
release / deploy-editor (push) Has been cancelled
release / Build all tools + KiCad e2e (Ubicloud) (push) Has been cancelled

Browser-verified bug (2026-09-05, reported via the prod-side session):
/api/cmms-config now returns 200 with aiEnabled:true, but the widget still
never mounted. Root cause: isCmmsConfig() required entityType/entityId as
strict strings, but the actual response omits them entirely - the whole
payload failed validation and silently fell back to NO_WIDGET, with nothing
logged to say why.

Fix: only dark/aiEnabled/aiEditorKind actually gate anything - validate
just those, and default csrfToken/entityType/entityId to "" when absent
(they only feed the iframe's query string, harmless empty). Also added a
.catch() + console.error on the widget's own boot effect, so a future
mistake here logs instead of silently no-op'ing.

Verified against the exact captured response body
({"dark":true,"aiEnabled":true,"aiEditorKind":"kicad","csrfToken":"..."}):
now resolves aiEnabled:true. tsc --noEmit clean, vitest 469/469 passing.

Also investigated the separately-reported "Uncaught Error: undefined
self-hosted:8:1" console error: built the pre-patch source
(0146488) and the patched source with an identical toolchain/base and
diffed the eager main chunk - same file list, same
wasm/collab/index.ts dynamic/static-import warning, ~4KB size delta
matching only this patch's own new code. yjs/Y.Doc was ALREADY present
in the pre-patch eager main chunk (grepped the unpatched build output
directly), so this patch did not newly pull collab/yjs code into eager
load - whatever's throwing predates this patch's chunk composition. Not
provable further without a live browser (not available in this repo's
env, see CLAUDE.md) - not a fix, just contrary evidence to "this patch's
widget code caused it" for the prod-side session to browser-verify.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
AI_Assistant 2026-09-05 21:44:13 +10:00
commit 213de59f94
2 changed files with 47 additions and 22 deletions

View file

@ -20,18 +20,27 @@ export function CmmsAiWidget(): React.ReactElement | null {
React.useEffect(() => { React.useEffect(() => {
let cancelled = false; let cancelled = false;
fetchCmmsConfig().then((c) => { // fetchCmmsConfig() never rejects (its own internal .catch() falls back
if (cancelled) return; // to NO_WIDGET) — this .catch() guards the callback body itself instead
setConfig(c); // (setConfig/installAiBridge/setTheme), so a mistake here becomes a
if (c.aiEnabled) installAiBridge(); // logged no-op, not a silent unhandled rejection with nothing to say why
// Belt-and-braces alongside the `?theme=` hand-off (lib/theme.ts) — that // the widget didn't appear.
// param already wins whenever edit.php sets it, so this only matters if fetchCmmsConfig()
// it's ever absent. Skip if the URL param already ran; setTheme is .then((c) => {
// otherwise idempotent (persists + toggles the same `.dark` class). if (cancelled) return;
if (c.dark && !new URLSearchParams(window.location.search).get("theme")) { setConfig(c);
setTheme("dark"); if (c.aiEnabled) installAiBridge();
} // Belt-and-braces alongside the `?theme=` hand-off (lib/theme.ts) —
}); // that param already wins whenever edit.php sets it, so this only
// matters if it's ever absent. Skip if the URL param already ran;
// setTheme is otherwise idempotent (persists + toggles `.dark`).
if (c.dark && !new URLSearchParams(window.location.search).get("theme")) {
setTheme("dark");
}
})
.catch((err: unknown) => {
console.error("[cmms-ai-widget] boot failed:", err);
});
return () => { return () => {
cancelled = true; cancelled = true;
}; };

View file

@ -15,17 +15,22 @@ export interface CmmsConfig {
entityId: string; entityId: string;
} }
function isCmmsConfig(v: unknown): v is CmmsConfig { // Only `dark` / `aiEnabled` / `aiEditorKind` actually gate behavior — a
// bug found 2026-09-05 in browser verification required all six fields as
// strict strings, so the real server response (which omits `entityType` /
// `entityId`) failed validation and silently fell back to NO_WIDGET despite
// `aiEnabled: true` — the widget never mounted and nothing logged why.
// `csrfToken` / `entityType` / `entityId` are only used to build the iframe's
// query string (harmless as `""`), so a missing one shouldn't disable the
// widget entirely.
function isCmmsConfigCore(v: unknown): v is Pick<CmmsConfig, "dark" | "aiEnabled" | "aiEditorKind"> {
if (!v || typeof v !== "object") return false; if (!v || typeof v !== "object") return false;
const c = v as Record<string, unknown>; const c = v as Record<string, unknown>;
return ( return typeof c.dark === "boolean" && typeof c.aiEnabled === "boolean" && c.aiEditorKind === "kicad";
typeof c.dark === "boolean" && }
typeof c.aiEnabled === "boolean" &&
c.aiEditorKind === "kicad" && function optionalString(v: unknown): string {
typeof c.csrfToken === "string" && return typeof v === "string" ? v : "";
typeof c.entityType === "string" &&
typeof c.entityId === "string"
);
} }
const NO_WIDGET: CmmsConfig = { const NO_WIDGET: CmmsConfig = {
@ -47,7 +52,18 @@ export function fetchCmmsConfig(): Promise<CmmsConfig> {
if (!cached) { if (!cached) {
cached = fetch(`${API_BASE_URL}/api/cmms-config`, { credentials: "include" }) cached = fetch(`${API_BASE_URL}/api/cmms-config`, { credentials: "include" })
.then((r) => (r.ok ? r.json() : null)) .then((r) => (r.ok ? r.json() : null))
.then((v) => (isCmmsConfig(v) ? v : NO_WIDGET)) .then((v): CmmsConfig => {
if (!isCmmsConfigCore(v)) return NO_WIDGET;
const c = v as Record<string, unknown>;
return {
dark: v.dark,
aiEnabled: v.aiEnabled,
aiEditorKind: v.aiEditorKind,
csrfToken: optionalString(c.csrfToken),
entityType: optionalString(c.entityType),
entityId: optionalString(c.entityId),
};
})
.catch(() => NO_WIDGET); .catch(() => NO_WIDGET);
} }
return cached; return cached;