Files
canvas-bot db32934100
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled
fix(nginx): static proxy_pass hostname so DNS resolves once at config-load
The previous $variable form forced lazy per-request DNS resolution.
Per-request lookups against 10.43.0.10 → SERVFAIL → cold-cache 502
even when downstream was healthy. Switching to a literal hostname
makes nginx resolve once at startup and reuse the cached upstream IP
for the lifetime of the worker. Cloudflare anycast is stable so this
is safe.
2026-06-15 18:39:26 +04:00

121 lines
6.2 KiB
JavaScript

// Serve dist locally, screenshot the new topbar + scenes with all backends mocked.
import { chromium } from "playwright";
import http from "node:http";
import { readFileSync, statSync, writeFileSync } from "node:fs";
import { extname, join } from "node:path";
const ROOT = "/private/tmp/canvas-work/canvas-real/dist";
const MIME = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png", ".json": "application/json" };
const server = http.createServer((req, res) => {
let path = req.url.split("?")[0];
if (path === "/") path = "/index.html";
const full = join(ROOT, path);
try { statSync(full); res.setHeader("content-type", MIME[extname(full)] || "application/octet-stream"); res.end(readFileSync(full)); }
catch { res.setHeader("content-type", "text/html"); res.end(readFileSync(join(ROOT, "index.html"))); }
});
await new Promise((r) => server.listen(0, r));
const port = server.address().port;
const URL = `http://127.0.0.1:${port}/`;
console.log("serving on", URL);
const OUT = "/tmp/canvas-evidence/wave1-local";
import { mkdirSync } from "node:fs";
mkdirSync(OUT, { recursive: true });
const b = await chromium.launch({ headless: true });
const ctx = await b.newContext({ viewport: { width: 1440, height: 900 } });
const p = await ctx.newPage();
// Stub backend
await ctx.route("**/api/v1/auth/microsoft/login", (r) => r.fulfill({ status: 503, body: "" }));
await ctx.route("**/internal/dev-login-config", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"enabled":true}' }));
await ctx.route("**/api/v1/auth/dev-login", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"access_token":"T","refresh_token":"R"}' }));
await ctx.route("**/api/v1/auth/me", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"user_id":"u1","tenant_id":"t1","email":"ceo-head@flow-master.ai","display_name":"Mariana Cole"}' }));
await ctx.route("**/api/ea2/work-items**", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"items":[]}' }));
await ctx.route("**/api/ea2/flow/processes**", (r) => r.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [
{ _key: "k1", display_name: "Purchase requisition to PO", status: "published", kind: "definition" },
{ _key: "k2", display_name: "Laptop procurement (employee → manager → IT)", status: "published", kind: "definition" },
{ _key: "k3", display_name: "Customer refund approval", status: "published", kind: "definition" },
] }) }));
await ctx.route("**/api/ea2/flow/**", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"_key":"x","display_name":"X","status":"published","kind":"definition","config":{}}' }));
await ctx.route("**/api/ea2/edges/**", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"items":[]}' }));
await ctx.route("**/api/v1/llm/generate", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"text":"Sure — here are your published processes: purchase requisition to PO, laptop procurement, customer refund approval.","provider":"demo"}' }));
await ctx.route("**/api/ea2/apply-batch", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"applied":1,"result":{"ops":[{"key":"new1"},{"key":"new2"}]}}' }));
await ctx.route("**/api/runtime/transactions**", (r) => r.fulfill({ status: 200, contentType: "application/json", body: '{"items":[]}' }));
async function snap(name) { await p.screenshot({ path: `${OUT}/${name}.png`, fullPage: false }); }
// Pre-seed localStorage to skip the onboarding tour
await ctx.addInitScript(() => { localStorage.setItem("fm.canvas.tour.seen", String(Date.now())); });
await p.goto(URL, { waitUntil: "domcontentloaded" });
await p.waitForTimeout(1500);
await snap("01-login");
await p.locator(".persona-chip").filter({ hasText: /Mariana/ }).first().click();
await p.waitForTimeout(3500);
await snap("02-after-login-MISSION");
const shell = await p.evaluate(() => document.querySelector("[class*='shell-']")?.className);
console.log("post-login shell:", shell);
const tabCount = await p.locator(".tab").count();
console.log("topbar tab count:", tabCount);
const tabTexts = await p.locator(".tab").allTextContents();
console.log("tabs:", tabTexts);
// avatar/menu
const avatar = p.locator(".user-avatar").first();
console.log("user-avatar present:", await avatar.count() > 0);
if (await avatar.count()) {
await avatar.click();
await p.waitForTimeout(800);
await snap("03-user-menu-open");
await p.keyboard.press("Escape");
await p.waitForTimeout(400);
}
// Studio dogfood
await p.locator(".tab").filter({ hasText: /Studio/ }).first().click();
await p.waitForTimeout(1500);
await p.locator("textarea").first().fill("Laptop procurement for a regional store manager.");
await snap("04-studio-intake");
await p.locator("button.btn-primary").filter({ hasText: /draft/i }).first().click();
await p.waitForTimeout(2500);
await snap("05-studio-analyze");
await p.locator("button.btn-primary").filter({ hasText: /Confirm/i }).first().click();
await p.waitForTimeout(2500);
await snap("06-studio-after-confirm");
const toastsAfterConfirm = await p.locator(".toast, .toast-msg").allTextContents();
console.log("studio toasts:", toastsAfterConfirm.slice(-3));
// Assistant
await p.locator(".tab").filter({ hasText: /Assistant/ }).first().click();
await p.waitForTimeout(1500);
await snap("07-assistant");
const input = p.locator(".agent-input, input[placeholder*='Ask']").first();
if (await input.count()) {
await input.fill("list processes");
await input.press("Enter");
await p.waitForTimeout(3500);
await snap("08-assistant-list-processes");
const replies = await p.locator(".agent-msg-text, .agent-msg-body").allTextContents();
console.log("assistant last reply:", replies.slice(-1)[0]?.slice(0, 200));
}
// Chat
await p.locator(".tab").filter({ hasText: /Chat/ }).first().click();
await p.waitForTimeout(2500);
await snap("09-chat");
// Dark theme via user menu
await p.locator(".user-avatar").first().click();
await p.waitForTimeout(400);
await p.locator(".user-menu-item").filter({ hasText: /Switch to (dark|light)/i }).first().click();
await p.waitForTimeout(800);
await snap("10-theme-toggled");
await b.close();
server.close();
console.log(`\nArtifacts in ${OUT}`);