fix(login): do not block login completion on EA2 scenario fetch
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

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.
This commit is contained in:
canvas-bot
2026-06-15 19:13:13 +04:00
parent 722b0ba3a2
commit 8d85a4c3ea
3 changed files with 114 additions and 1 deletions
+65
View File
@@ -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();
+46
View File
@@ -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();
+3 -1
View File
@@ -272,7 +272,9 @@ export const useApp = create<AppState>((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;