From 8d85a4c3ea49a31d4ffb9132aa10382c8c5bd4a9 Mon Sep 17 00:00:00 2001 From: canvas-bot Date: Mon, 15 Jun 2026 19:13:13 +0400 Subject: [PATCH] fix(login): do not block login completion on EA2 scenario fetch If buildLiveScenariosFromApi hangs or errors on any EA2 read, loginAs would await it forever, leaving the user stuck on 'VERIFYING...' even after auth+identity succeed. Make refreshLive fire-and-forget after the toast; the Mission scene re-fetches on mount and surfaces failures via liveError. --- qa/_measure_topbar.mjs | 65 ++++++++++++++++++++++++++++++++++++++++++ qa/_probe_dom.mjs | 46 ++++++++++++++++++++++++++++++ src/state/store.ts | 4 ++- 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 qa/_measure_topbar.mjs create mode 100644 qa/_probe_dom.mjs diff --git a/qa/_measure_topbar.mjs b/qa/_measure_topbar.mjs new file mode 100644 index 0000000..44b3a48 --- /dev/null +++ b/qa/_measure_topbar.mjs @@ -0,0 +1,65 @@ +// DOM-introspection — measure topbar item count, sizes, overlap. +import { chromium } from "playwright"; + +const URL = "https://canvas.flow-master.ai/"; +const b = await chromium.launch({ headless: true }); +const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); +const p = await ctx.newPage(); +await ctx.addInitScript(() => localStorage.setItem("fm.canvas.tour.seen", String(Date.now()))); + +await p.goto(URL, { waitUntil: "domcontentloaded" }); +await p.waitForTimeout(2500); + +console.log("=== LOGIN PAGE ==="); +const loginHas = await p.evaluate(() => ({ + topbar: !!document.querySelector(".topbar"), + personaChips: document.querySelectorAll(".persona-chip").length, + ssoBtn: !!document.querySelector(".sso-btn"), + emailInput: !!document.querySelector("input[type='email']"), + banner: document.querySelector(".global-banner, .login-banner")?.textContent?.slice(0, 100) || null, +})); +console.log(JSON.stringify(loginHas, null, 2)); + +console.log("\n=== POST-LOGIN ==="); +await p.locator(".persona-chip").filter({ hasText: /Mariana/ }).click(); +await p.waitForSelector(".topbar", { timeout: 15000 }).catch(() => {}); +await p.waitForTimeout(3000); + +const topbar = await p.evaluate(() => { + const tb = document.querySelector(".topbar"); + if (!tb) return null; + const items = Array.from(tb.children).map((el) => ({ + tag: el.tagName.toLowerCase(), + class: el.className, + text: el.textContent?.trim().slice(0, 50), + width: el.getBoundingClientRect().width, + })); + const tabs = Array.from(tb.querySelectorAll(".tab")).map((el) => el.textContent?.trim()); + const actions = Array.from(tb.querySelectorAll(".topbar-actions > *")).map((el) => ({ + tag: el.tagName.toLowerCase(), + class: el.className.slice(0, 40), + text: el.textContent?.trim().slice(0, 30), + })); + return { + totalWidth: tb.getBoundingClientRect().width, + children: items, + tabs, + actionsCount: actions.length, + actions, + overflows: tb.scrollWidth > tb.clientWidth, + }; +}); +console.log(JSON.stringify(topbar, null, 2)); + +console.log("\n=== USER MENU ==="); +await p.locator(".user-avatar").first().click().catch(() => {}); +await p.waitForTimeout(800); +const menu = await p.evaluate(() => { + const m = document.querySelector(".user-menu-pop"); + if (!m) return { open: false }; + const items = Array.from(m.querySelectorAll(".user-menu-item")).map((el) => el.textContent?.trim().slice(0, 50)); + return { open: true, items, width: m.getBoundingClientRect().width }; +}); +console.log(JSON.stringify(menu, null, 2)); + +await b.close(); diff --git a/qa/_probe_dom.mjs b/qa/_probe_dom.mjs new file mode 100644 index 0000000..1eef659 --- /dev/null +++ b/qa/_probe_dom.mjs @@ -0,0 +1,46 @@ +import { chromium } from "playwright"; + +const b = await chromium.launch({ headless: true }); +const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); +const p = await ctx.newPage(); +const calls = []; +p.on("response", async (r) => { + const u = r.url(); + if (u.includes("/api/")) { + calls.push({ status: r.status(), method: r.request().method(), url: u.replace("https://canvas.flow-master.ai", "") }); + } +}); +p.on("console", (m) => { if (m.type() === "error") console.log("CONSOLE-ERR:", m.text().slice(0, 200)); }); +p.on("pageerror", (e) => console.log("PAGE-ERR:", e.message.slice(0, 200))); +await ctx.addInitScript(() => localStorage.setItem("fm.canvas.tour.seen", String(Date.now()))); + +await p.goto("https://canvas.flow-master.ai/", { waitUntil: "domcontentloaded" }); +await p.waitForTimeout(2500); +await p.locator(".persona-chip").filter({ hasText: /Mariana/ }).click(); +// Live auth can take 6-10s; wait for shell to flip +await p.waitForFunction(() => !document.querySelector("[class*='shell-login']"), null, { timeout: 30000 }).catch(() => {}); +await p.waitForTimeout(3000); + +const state = await p.evaluate(() => { + const shell = document.querySelector("[class*='shell-']")?.className; + const topbar = document.querySelector("header.topbar"); + const tabs = Array.from(document.querySelectorAll(".tab")).map(t => t.textContent?.trim()); + const headerByTag = document.querySelector("header")?.outerHTML?.slice(0, 200); + return { + shell, + hasTopbar: !!topbar, + headerByTag, + tabCount: tabs.length, + tabs, + sceneText: document.querySelector(".scene")?.innerText?.slice(0, 300) || null, + }; +}); +console.log(JSON.stringify(state, null, 2)); + +await p.screenshot({ path: "/tmp/canvas-evidence/measure.png", fullPage: false }); +console.log("screenshot /tmp/canvas-evidence/measure.png"); + +console.log("\n=== api calls ==="); +for (const c of calls) console.log(` ${c.status} ${c.method} ${c.url.slice(0, 100)}`); + +await b.close(); diff --git a/src/state/store.ts b/src/state/store.ts index 652b6d5..a25ff06 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -272,7 +272,9 @@ export const useApp = create((set, get) => { isAuthed: true, }); get().pushToast("ok", `Signed in as ${me.email}`); - if (get().mode === "live") await get().refreshLive(); + // Fire-and-forget the live scenario fetch. Login completion must NOT + // block on every EA2 read; the Mission scene re-fetches on mount. + void get().refreshLive().catch(() => { /* surfaced via liveError */ }); } catch (e) { get().pushToast("err", `Login failed: ${(e as Error).message.slice(0, 80)}`); throw e;