Shad b2c1e57c86
Some checks failed
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled
deploy: live at canvas.flow-master.ai
- index.html title 'FlowMaster · Mission Control' + theme-color #1a2740
- Settings About box names canvas.flow-master.ai as the canonical URL
- README rewritten: live URL up top, deploy section reflects merged PR
  + Cloudflare A record added (this session), end-to-end serving with
  cert-manager TLS + nginx /api/* proxy to demo.flow-master.ai
- .dockerignore added (excludes node_modules, dist, qa/screenshots,
  .git, bak files) — keeps the build context lean
- qa/smoke_canvas.mjs (new) — real-URL Playwright smoke against
  https://canvas.flow-master.ai with --host-resolver-rules so the
  local DNS cache can't shadow the freshly-published A record.
  8/8 PASS, 0 console errors:
    ✓ landing renders at canvas.flow-master.ai
    ✓ hero title present
    ✓ scenario cards >= 7 (count=7)
    ✓ mission control loaded
    ✓ blueprint canvas mounted
    ✓ blueprint nodes rendered (nodes=3)
    ✓ LIVE mode resolves against canvas /api proxy
    ✓ at least one /api call observed (count=1)

Deployment chain:
  shad/flowmaster-mission-control-demo @ dba1eb3
  → docker buildx on build01 (linux/amd64 native)
  → gitea.flow-master.ai/shad/mission-control-demo:sha-dba1eb3
  → FM06/flowmaster-ops PR #1164 (merged)
  → kubectl apply -n demo (ArgoCD was Degraded due to unrelated
    fm-shell env-var dupe; bypassed by applying directly)
  → 2/2 mc-mission-control pods Running
  → cluster ingress + cert-manager + Cloudflare A record
  → https://canvas.flow-master.ai serves the SPA + proxies /api to demo.flow-master.ai

Confidence: high
Scope-risk: narrow (demo namespace only; no shared baseline mutation)
Not-tested: long-term cert renewal cycle (will happen automatically;
    cert-manager letsencrypt-prod-dns issuer is already proven by hakeem)
2026-06-14 02:46:34 +04:00

54 lines
2.3 KiB
JavaScript

// Real-URL smoke against https://canvas.flow-master.ai.
// Runs Playwright with a forced DNS resolution so the local box's DNS cache
// doesn't shadow the just-published A record.
import { chromium } from "playwright";
const URL = "https://canvas.flow-master.ai/";
const FORCE_IP = "65.21.71.186";
function ok(label, cond, detail = "") {
console.log(`${cond ? "✓" : "✗"} ${label}${detail ? " · " + detail : ""}`);
if (!cond) process.exitCode = 1;
}
const b = await chromium.launch({
headless: true,
args: [
`--host-resolver-rules=MAP canvas.flow-master.ai ${FORCE_IP}`,
"--ignore-certificate-errors",
],
});
const p = await b.newPage({ viewport: { width: 1440, height: 900 } });
const errors = [];
p.on("pageerror", (e) => errors.push(`pageerror ${e.message}`));
p.on("console", (m) => { if (m.type() === "error") errors.push(`console.error ${m.text().slice(0, 200)}`); });
await p.goto(URL, { waitUntil: "networkidle" });
ok("landing renders at canvas.flow-master.ai", await p.locator(".landing").isVisible());
ok("hero title present", await p.locator(".hero-title").isVisible());
const cards = await p.locator(".sc-card").count();
ok("scenario cards >= 7", cards >= 7, `count=${cards}`);
await p.locator(".sc-card").first().click();
await p.waitForSelector(".mc"); await p.waitForTimeout(600);
ok("mission control loaded", await p.locator(".mc-strip").isVisible());
ok("blueprint canvas mounted", await p.locator(".bp-frame").isVisible());
const nodes = await p.locator(".bp-node").count();
ok("blueprint nodes rendered", nodes >= 3, `nodes=${nodes}`);
// Live mode: real /api hit through nginx proxy at canvas
const networkCalls = [];
p.on("request", (req) => { if (req.url().includes("/api/")) networkCalls.push(req.url()); });
await p.locator(".mode-toggle").first().click();
const liveOk = await p.waitForFunction(
() => Array.from(document.querySelectorAll(".mode-pill")).some((el) => el.textContent?.trim() === "LIVE"),
{ timeout: 15000 },
).then(() => true).catch(() => false);
ok("LIVE mode resolves against canvas /api proxy", liveOk);
ok("at least one /api call observed", networkCalls.length > 0, `count=${networkCalls.length}`);
await p.screenshot({ path: "qa/screenshots/canvas-live.png" });
console.log(`\nconsole errors: ${errors.length}`);
errors.slice(0, 5).forEach((e) => console.log(" -", e));
await b.close();