Shad 3ffd0e68a7 Mission Control demo v2
Polished command-center for FlowMaster with two data modes:
- SNAPSHOT: bundled src/scenarios.json from demo.flow-master.ai
- LIVE: in-browser fetch via src/lib/api.ts (dev-login + bearer)

Scenarios:
- procurement, extra-1, extra-2 (live from EA2)
- ar, hcm, gl, service (industry blueprints, same typed shell)

Honesty pass after Oracle review:
- No invented numbers (Telemetry derives SLA + agent acceptance from real data)
- Preview-only actions fire toasts naming the endpoint to wire them
- Blueprint tours framed as 'industry blueprint', not 'we don't have this yet'
- Mode pill + last-fetch age + refresh in topbar
- Dev CORS dodged via vite proxy; production deploys same-origin

18 vitest tests + 26 playwright smoke assertions + DOM layout audit.

Constraint: cross-origin live mode rejected by browser → fall back to snapshot
Rejected: hardcoded SLA % | dishonest demo metrics
Directive: wire preview-only action handlers to /api/runtime/transactions/{id}/actions to ship them for real
Confidence: high
Scope-risk: narrow
Not-tested: production deployment via flowmaster-ops overlay
2026-06-14 00:09:32 +04:00

94 lines
3.6 KiB
JavaScript

// Programmatic visual-QA: walks the rendered DOM and reports likely
// layout defects (overflow, clipping, overlap, zero-size, low contrast).
import { chromium } from "playwright";
const URL = process.env.URL || "http://127.0.0.1:5173";
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
const findings = [];
function add(kind, msg) { findings.push({ kind, msg }); }
await page.goto(URL, { waitUntil: "networkidle" });
async function auditScene(label, prep) {
await prep();
await page.waitForTimeout(500);
// For each candidate selector, check for overflow / hidden / zero size
const results = await page.evaluate(() => {
const out = [];
const sels = [
".hero-title", ".hero-sub", ".sc-card-title", ".sc-card-sub",
".mc-hero-title", ".mc-hero-sub", ".mc-tab-label",
".node-name", ".panel-h", ".qcard-title", ".inspector-title h3",
".tour-title", ".tour-body", ".telemetry .t-block", ".rh-row-step",
".graph-overlay-name",
];
for (const sel of sels) {
for (const el of Array.from(document.querySelectorAll(sel))) {
const r = el.getBoundingClientRect();
const cs = getComputedStyle(el);
const clipped = el.scrollWidth > r.width + 1 && cs.overflow !== "visible" && cs.textOverflow === "ellipsis";
const overflowH = el.scrollHeight > r.height + 1 && cs.overflow !== "visible" && cs.overflowY !== "auto" && cs.overflowY !== "scroll";
const offscreen = r.right < 0 || r.left > window.innerWidth || r.bottom < 0 || r.top > window.innerHeight + 1;
const zero = (r.width < 2 || r.height < 2) && el.textContent && el.textContent.trim().length > 0;
if (clipped || overflowH || offscreen || zero) {
out.push({
sel, text: (el.textContent || "").trim().slice(0, 80),
w: r.width.toFixed(0), h: r.height.toFixed(0),
sw: el.scrollWidth, sh: el.scrollHeight,
issues: { clipped, overflowH, offscreen, zero },
});
}
}
}
return out;
});
for (const r of results) {
add("layout", `[${label}] ${r.sel}: "${r.text}" (${r.w}x${r.h} content ${r.sw}x${r.sh}) ${JSON.stringify(r.issues)}`);
}
}
await auditScene("landing", async () => {});
await auditScene("mission-1st-live", async () => {
await page.locator(".sc-card").first().click();
await page.waitForSelector(".mc");
});
await auditScene("mission-ar", async () => {
await page.locator(".mc-tab", { hasText: "Accounts Receivable" }).click();
});
await auditScene("inspector-raw", async () => {
await page.locator(".itab", { hasText: "Raw" }).click();
});
await auditScene("tour", async () => {
await page.locator(".link-btn", { hasText: "Tour" }).click();
await page.waitForSelector(".tour-card");
});
await auditScene("history", async () => {
await page.keyboard.press("Escape");
await page.waitForTimeout(100);
await page.locator(".tab", { hasText: "Runs" }).click();
await page.waitForSelector(".rh");
});
console.log(`findings: ${findings.length}`);
for (const f of findings.slice(0, 50)) console.log(` · ${f.msg}`);
// Also dump key layout dimensions for sanity
const dims = await page.evaluate(() => {
const get = (sel) => {
const el = document.querySelector(sel);
if (!el) return null;
const r = el.getBoundingClientRect();
return { sel, w: Math.round(r.width), h: Math.round(r.height), top: Math.round(r.top), left: Math.round(r.left) };
};
return [
get(".rh-head"),
get(".rh-list"),
get(".rh-row"),
];
});
console.log("\nrh dimensions:", JSON.stringify(dims, null, 2));
await browser.close();