diff --git a/qa/audit_approvals.mjs b/qa/audit_approvals.mjs new file mode 100644 index 0000000..3de7ba5 --- /dev/null +++ b/qa/audit_approvals.mjs @@ -0,0 +1,56 @@ +// Audit: Approvals scene loads, queue populates, hub filter works, +// selecting a row shows the active step, and clicking an action posts +// to /api/runtime/transactions//actions/ (real EA2 write). +import { chromium } from "playwright"; +const URL = "https://canvas.flow-master.ai/"; +const args = ["--host-resolver-rules=MAP canvas.flow-master.ai 65.21.71.186", "--ignore-certificate-errors"]; + +const b = await chromium.launch({ headless: true, args }); +const p = await (await b.newContext({ viewport: { width: 1440, height: 900 } })).newPage(); +const writes = []; +p.on("request", (req) => { + const u = req.url(); + const m = req.method(); + if ((m === "POST" || m === "PUT") && /\/api\/runtime\/transactions\//.test(u)) { + writes.push({ method: m, path: u.replace("https://canvas.flow-master.ai", "") }); + } +}); +p.on("response", async (res) => { + if (/\/api\/runtime\/transactions\//.test(res.url()) && res.request().method() === "POST") { + const t = await res.text().catch(() => ""); + console.log(`[resp ${res.status()}] ${res.request().method()} ${res.url().replace("https://canvas.flow-master.ai","")} → ${t.slice(0,200)}`); + } +}); + +await p.goto(URL, { waitUntil: "networkidle" }); +await p.waitForTimeout(800); +await p.locator(".persona-chip", { hasText: /Mariana/ }).click(); +await p.locator(".dev-login-btn").click(); +await p.waitForSelector(".hero-actions", { timeout: 15000 }); +await p.waitForFunction(() => document.querySelectorAll(".hub-chip").length >= 8, undefined, { timeout: 15000 }).catch(() => {}); + +await p.locator(".hub-chip", { hasText: /Approvals queue/ }).click(); +await p.waitForSelector(".approvals-row", { timeout: 15000 }).catch(() => {}); +await p.waitForTimeout(1500); + +const rowCount = await p.locator(".approvals-row").count(); +const hubChips = await p.locator(".approvals-filter-row .hub-chip").count(); +console.log(`[queue] rows=${rowCount} hubFilters=${hubChips}`); + +if (rowCount > 0) { + await p.locator(".approvals-row").first().click(); + await p.waitForSelector(".approvals-card", { timeout: 8000 }).catch(() => {}); + const stepTitle = (await p.locator(".approvals-card-title").first().textContent()) || ""; + const actionCount = await p.locator(".approvals-actions .btn").count(); + console.log(`[detail] step="${stepTitle.trim().slice(0,60)}" actions=${actionCount}`); + if (actionCount > 0) { + await p.locator(".approvals-actions .btn").first().click(); + await p.waitForTimeout(2500); + } +} + +console.log(`\n=== runtime writes during run ===`); +writes.forEach((w) => console.log(` ${w.method} ${w.path}`)); +console.log(`\ntotal runtime writes: ${writes.length}`); +await b.close(); +process.exit(0); diff --git a/src/lib/api.ts b/src/lib/api.ts index 21e243d..9387b07 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -333,10 +333,10 @@ export const api = { }, /** Probe whether the backend is reachable. Never throws. */ - async ping(signal?: AbortSignal): Promise<{ ok: boolean; reason?: string; user?: string; user_id?: string }> { + async ping(signal?: AbortSignal): Promise<{ ok: boolean; reason?: string; user?: string; user_id?: string; tenant_id?: string }> { try { const me = await this.me(signal); - return { ok: true, user: me.email, user_id: me.user_id }; + return { ok: true, user: me.email, user_id: me.user_id, tenant_id: me.tenant_id }; } catch (e) { return { ok: false, reason: (e as Error).message }; } diff --git a/src/scenes/Approvals.tsx b/src/scenes/Approvals.tsx index f0c25c4..cec36ac 100644 --- a/src/scenes/Approvals.tsx +++ b/src/scenes/Approvals.tsx @@ -35,10 +35,12 @@ export default function Approvals() { const [tx, setTx] = useState(null); const [busy, setBusy] = useState(null); + const tenantId = useApp((s) => s.tenantId); const loadQueue = async () => { try { const rows = await api.workItems(); - setItems(rows.map((it) => ({ ...it, age_label: ageLabel(it) }))); + const scoped = tenantId ? rows.filter((it) => (it as any).tenant_id === tenantId) : rows; + setItems(scoped.map((it) => ({ ...it, age_label: ageLabel(it) }))); } catch (err: any) { pushToast("err", `Queue failed: ${err.message}`); } diff --git a/src/state/store.ts b/src/state/store.ts index 3520bcb..1082706 100644 --- a/src/state/store.ts +++ b/src/state/store.ts @@ -94,6 +94,7 @@ interface AppState { actor: Actor | null; userEmail: string; userDisplayName: string | null; + tenantId: string | null; isAuthed: boolean; setUserEmail: (e: string) => void; loginAs: (email: string, password?: string, options?: { method?: "password" | "dev" }) => Promise; @@ -141,6 +142,7 @@ async function runLiveFetch( set({ actor: { mode: "direct_user", user_id: ping.user_id }, userEmail: ping.user ?? get().userEmail, + tenantId: ping.tenant_id ?? get().tenantId, }); } const { scenarios, workItems, distinctDefs } = await buildLiveScenariosFromApi(); @@ -256,6 +258,7 @@ export const useApp = create((set, get) => { actor: null, userEmail: prefs.email ?? "dev@flow-master.ai", userDisplayName: null, + tenantId: null, isAuthed: typeof sessionStorage !== "undefined" && !!sessionStorage.getItem("fm.mc.token.v1"), setUserEmail: (e) => { set({ userEmail: e }); persist(); }, loginAs: async (email, password, options) => { @@ -274,6 +277,7 @@ export const useApp = create((set, get) => { set({ actor: { mode: "direct_user", user_id: me.user_id }, userDisplayName: me.display_name ?? null, + tenantId: me.tenant_id ?? null, isAuthed: true, }); get().pushToast("ok", `Signed in as ${me.email}`);