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
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:
parent
bf1e7a512e
commit
213de59f94
2 changed files with 47 additions and 22 deletions
|
|
@ -20,18 +20,27 @@ export function CmmsAiWidget(): React.ReactElement | null {
|
|||
|
||||
React.useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchCmmsConfig().then((c) => {
|
||||
if (cancelled) return;
|
||||
setConfig(c);
|
||||
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 the same `.dark` class).
|
||||
if (c.dark && !new URLSearchParams(window.location.search).get("theme")) {
|
||||
setTheme("dark");
|
||||
}
|
||||
});
|
||||
// fetchCmmsConfig() never rejects (its own internal .catch() falls back
|
||||
// to NO_WIDGET) — this .catch() guards the callback body itself instead
|
||||
// (setConfig/installAiBridge/setTheme), so a mistake here becomes a
|
||||
// logged no-op, not a silent unhandled rejection with nothing to say why
|
||||
// the widget didn't appear.
|
||||
fetchCmmsConfig()
|
||||
.then((c) => {
|
||||
if (cancelled) return;
|
||||
setConfig(c);
|
||||
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 () => {
|
||||
cancelled = true;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -15,17 +15,22 @@ export interface CmmsConfig {
|
|||
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;
|
||||
const c = v as Record<string, unknown>;
|
||||
return (
|
||||
typeof c.dark === "boolean" &&
|
||||
typeof c.aiEnabled === "boolean" &&
|
||||
c.aiEditorKind === "kicad" &&
|
||||
typeof c.csrfToken === "string" &&
|
||||
typeof c.entityType === "string" &&
|
||||
typeof c.entityId === "string"
|
||||
);
|
||||
return typeof c.dark === "boolean" && typeof c.aiEnabled === "boolean" && c.aiEditorKind === "kicad";
|
||||
}
|
||||
|
||||
function optionalString(v: unknown): string {
|
||||
return typeof v === "string" ? v : "";
|
||||
}
|
||||
|
||||
const NO_WIDGET: CmmsConfig = {
|
||||
|
|
@ -47,7 +52,18 @@ export function fetchCmmsConfig(): Promise<CmmsConfig> {
|
|||
if (!cached) {
|
||||
cached = fetch(`${API_BASE_URL}/api/cmms-config`, { credentials: "include" })
|
||||
.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);
|
||||
}
|
||||
return cached;
|
||||
|
|
|
|||
Loading…
Reference in a new issue