feat(hubs): match dev.flow-master.ai workbench pattern + 4 named workflows

Inspected dev.flow-master.ai/dashboard/hubs/{procurement,hr,it} via real
dev-login. Real hub structure is: Workbench title + Workflow Commands
(4 named buttons) + Queue (left) + Selected Work (right).

- Procurement: New purchase request / Vendor approval / PO change-cancel / Three-way match
- HR: Employee onboarding / Off-boarding / Sick leave / Payroll management
- IT: Access request / Equipment request / Service ticket / User off-boarding

Hub.tsx rewritten: header + Workflow Commands grid + queue/selected
split pane that matches dev's 'Queue' + 'Selected work' layout.

Oracle gap #3 (hubs not feature parity) — partial close. Approvals queue
and document browser still outstanding but the workbench shape now
mirrors dev.
This commit is contained in:
2026-06-14 13:19:49 +04:00
parent 8933148719
commit 665dcf488e
4 changed files with 199 additions and 47 deletions
+54
View File
@@ -0,0 +1,54 @@
// Inventory dev.flow-master.ai surfaces so canvas can be compared against the
// real product, not built from memory. Oracle gap #5.
import { chromium } from "playwright";
const b = await chromium.launch({ headless: true });
const p = await (await b.newContext({ viewport: { width: 1440, height: 900 } })).newPage();
// Try to use the dev site's own dev-login affordance via the UI.
try {
await p.goto("https://dev.flow-master.ai/login", { waitUntil: "networkidle" });
await p.waitForTimeout(800);
const buttons = await p.locator("button").allTextContents();
console.log("[login page buttons]:", buttons.map((b) => b.trim().slice(0, 50)).filter(Boolean));
const devBtn = p.locator("button", { hasText: /sign in as developer|dev login|developer/i }).first();
if (await devBtn.count()) {
const emailInput = p.locator("input[type='email']").first();
if (await emailInput.count()) await emailInput.fill("dev@flow-master.ai");
await devBtn.click();
await p.waitForTimeout(3000);
console.log("[dev-login via UI] url now:", p.url());
} else {
console.log("[no dev-login button found on dev]");
}
} catch (e) {
console.log("[dev-login UI flow failed]", e.message);
}
async function tryUrl(url, label) {
try {
const r = await p.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
await p.waitForTimeout(1200);
const title = await p.title();
const links = await p.locator("a").evaluateAll((els) =>
els.map((e) => ({ href: e.getAttribute("href") || "", text: (e.textContent || "").trim().slice(0, 50) }))
.filter((l) => l.href && l.href.startsWith("/") && l.text)
.slice(0, 50)
);
const headings = await p.locator("h1, h2, h3").allTextContents();
console.log(`\n=== ${label} (${url}) status=${r?.status()} title="${title}" ===`);
console.log("headings:", headings.slice(0, 12).map((h) => h.trim()).filter(Boolean));
const uniqueLinks = [...new Map(links.map((l) => [l.href, l])).values()].slice(0, 25);
console.log("nav links:", uniqueLinks.map((l) => `${l.text} (${l.href})`).join(" | "));
} catch (e) {
console.log(`\n=== ${label} (${url}) FAILED: ${e.message.slice(0, 100)} ===`);
}
}
await tryUrl("https://dev.flow-master.ai/", "dev root");
await tryUrl("https://dev.flow-master.ai/dashboard", "dev dashboard");
await tryUrl("https://dev.flow-master.ai/dashboard/hubs/procurement", "procurement hub");
await tryUrl("https://dev.flow-master.ai/dashboard/hubs/hr", "HR hub");
await tryUrl("https://dev.flow-master.ai/dashboard/hubs/it", "IT hub");
await tryUrl("https://dev.flow-master.ai/dashboard/geo-attendance", "geo attendance");
await tryUrl("https://dev.flow-master.ai/dashboard/process-creation-wizard", "wizard");
await b.close();