From b26ea1ee9c5d03eea06eee996cc3ba95c33fa41e Mon Sep 17 00:00:00 2001 From: Shad Date: Sun, 14 Jun 2026 11:35:15 +0400 Subject: [PATCH] fix(login): devLoginConfig() returns null when endpoint absent, not false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Vite minifier was tree-shaking the dev-login button because devLoginConfig() always returned {enabled: false} when the /internal/dev-login-config endpoint 404'd (which is the case on canvas, where the endpoint isn't mounted). That meant my prior 'default ON' fix never took effect — the useEffect unconditionally flipped state to false within microseconds of mount. Real fix: return {enabled: null} on absence. Only flip OFF when endpoint explicitly says enabled=false. Otherwise honor the source default. Confidence: high Scope-risk: narrow --- qa/probe_login.mjs | 21 +++++++++++++++++++++ src/lib/api.ts | 9 +++++---- src/scenes/Login.tsx | 6 ++---- 3 files changed, 28 insertions(+), 8 deletions(-) create mode 100644 qa/probe_login.mjs diff --git a/qa/probe_login.mjs b/qa/probe_login.mjs new file mode 100644 index 0000000..17728d0 --- /dev/null +++ b/qa/probe_login.mjs @@ -0,0 +1,21 @@ +import { chromium } from "playwright"; +const b = await chromium.launch({ + headless: true, + args: [ + "--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", + "--ignore-certificate-errors", + ], +}); +const p = await b.newPage({ viewport: { width: 1440, height: 900 } }); +p.on("console", (m) => console.log("console", m.type(), m.text().slice(0, 200))); +p.on("pageerror", (e) => console.log("pageerror", e.message)); +await p.goto("https://canvas.flow-master.ai/", { waitUntil: "networkidle" }); +await p.waitForTimeout(2000); +console.log("--- DOM ---"); +console.log(await p.evaluate(() => document.body.innerHTML.slice(0, 3000))); +console.log("--- visible text ---"); +console.log(await p.evaluate(() => document.body.innerText)); +console.log("--- buttons ---"); +const buttons = await p.$$eval("button, a", (els) => els.map((e) => `${e.tagName} disabled=${e.disabled} "${(e.innerText || e.textContent || "").slice(0, 60)}"`)); +buttons.forEach((b) => console.log(b)); +await b.close(); diff --git a/src/lib/api.ts b/src/lib/api.ts index 1e10f6b..44590b0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -210,17 +210,18 @@ export const api = { return data; }, - async devLoginConfig(signal?: AbortSignal): Promise<{ enabled: boolean }> { + async devLoginConfig(signal?: AbortSignal): Promise<{ enabled: boolean | null }> { try { const init: RequestInit = { method: "GET", headers: { "Accept": "application/json" }, signal }; const r = await instrumentedFetch("GET", `${this.config.baseUrl}/internal/dev-login-config`, init); if (r.ok) { - return await r.json() as { enabled: boolean }; + const body = await r.json() as { enabled: boolean }; + return { enabled: !!body.enabled }; } } catch { - // swallow + // network/parse error — caller treats null as "no opinion" } - return { enabled: false }; + return { enabled: null }; }, async workItems(signal?: AbortSignal): Promise { diff --git a/src/scenes/Login.tsx b/src/scenes/Login.tsx index f158c22..54ba939 100644 --- a/src/scenes/Login.tsx +++ b/src/scenes/Login.tsx @@ -14,12 +14,10 @@ export default function Login() { const [error, setError] = useState(null); useEffect(() => { - // The dev-login button is intentionally on by default on canvas - // (the user explicitly asked for it as a documented exception until - // we cut over fully to SSO). The endpoint config only flips it OFF. + // dev-login is on by default. Only the endpoint can flip it OFF. api.devLoginConfig() .then((cfg) => { if (cfg.enabled === false) setDevLoginEnabled(false); }) - .catch(() => { /* endpoint not present — keep default ON */ }); + .catch(() => { /* endpoint absent → keep ON */ }); }, []); const handleSubmit = async (e: React.FormEvent) => {