// Post-login EA2 trace + screenshot bundle. Uses dev-login (the SSO 502 is // the user's actual issue) and walks Mission Control so polling fires. import { chromium } from "playwright"; import { writeFileSync, mkdirSync } from "node:fs"; const URL = "https://canvas.flow-master.ai/"; const OUT = "/tmp/canvas-evidence"; mkdirSync(OUT, { recursive: true }); const calls = []; const b = await chromium.launch({ headless: true }); const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } }); const p = await ctx.newPage(); p.on("response", async (res) => { const u = res.url(); if (!u.includes("/api/")) return; calls.push({ url: u, status: res.status(), ts: Date.now() }); }); console.log("[1/5] goto landing"); await p.goto(URL, { waitUntil: "domcontentloaded" }); await p.waitForTimeout(2000); await p.screenshot({ path: `${OUT}/01-login.png`, fullPage: false }); console.log("[2/5] try dev-login as CEO persona"); const ceoChip = p.locator(".persona-chip").filter({ hasText: /Mariana/ }); const ceoVisible = await ceoChip.count(); console.log(` CEO persona chip count: ${ceoVisible}`); if (ceoVisible) { await ceoChip.first().click(); await p.waitForTimeout(3000); } await p.screenshot({ path: `${OUT}/02-after-login.png`, fullPage: false }); console.log("[3/5] check URL + page title"); console.log(` url: ${p.url()}`); console.log(` title: ${await p.title()}`); const headers = await p.evaluate(() => ({ body: document.body?.innerText?.slice(0, 200) ?? "", scenes: document.querySelector("[class*='shell']")?.className ?? "n/a", })); console.log(` scene: ${headers.scenes}`); console.log(` body[0..200]: ${JSON.stringify(headers.body)}`); console.log("[4/5] try to reach Mission Control"); const missionLink = p.locator("a, button").filter({ hasText: /mission control/i }).first(); if (await missionLink.count()) { await missionLink.click(); await p.waitForTimeout(4000); } await p.screenshot({ path: `${OUT}/03-mission.png`, fullPage: false }); console.log("[5/5] dump network"); const byStatus = {}; const byHost = {}; const byPath = {}; for (const c of calls) { byStatus[c.status] = (byStatus[c.status] || 0) + 1; const url = new URL(c.url); byHost[url.host] = (byHost[url.host] || 0) + 1; const path = url.pathname.split("/").slice(0, 4).join("/"); byPath[path] = (byPath[path] || 0) + 1; } const report = { ts: new Date().toISOString(), url: URL, total: calls.length, byStatus, byHost, byPath, ea2Calls: calls.filter((c) => c.url.includes("/api/ea2/")).length, ea2Ok: calls.filter((c) => c.url.includes("/api/ea2/") && c.status === 200).length, authCalls: calls.filter((c) => c.url.includes("/api/v1/auth/")).length, authOk: calls.filter((c) => c.url.includes("/api/v1/auth/") && c.status === 200).length, first10: calls.slice(0, 10), last10: calls.slice(-10), }; writeFileSync(`${OUT}/network-report.json`, JSON.stringify(report, null, 2)); console.log("\n=== SUMMARY ==="); console.log(`total /api/ calls: ${report.total}`); console.log(`by status: ${JSON.stringify(report.byStatus)}`); console.log(`by path (top):`); for (const [pa, n] of Object.entries(byPath).sort((a, b) => b[1] - a[1]).slice(0, 8)) { console.log(` ${n}\t${pa}`); } console.log(`EA2 200/total: ${report.ea2Ok}/${report.ea2Calls}`); console.log(`auth 200/total: ${report.authOk}/${report.authCalls}`); console.log(`screenshots → ${OUT}`); await b.close();