feat(approvals): tenant-scoped queue (no cross-tenant work-items)
Plumbed me.tenant_id through api.ping + loginAs into the store as tenantId. Approvals.loadQueue filters /api/ea2/work-items by tenant_id matching the signed-in user so the queue only shows actionable rows. Probe of EA2 work-items returned 73/80 in canvas's tenant (a0000000-...), 6 in hms_dev, 1 in hub-cnt-f56ce0 -- those 7 are now hidden.
This commit is contained in:
@@ -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/<id>/actions/<id> (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);
|
||||
+2
-2
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -35,10 +35,12 @@ export default function Approvals() {
|
||||
const [tx, setTx] = useState<RuntimeTransaction | null>(null);
|
||||
const [busy, setBusy] = useState<string | null>(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}`);
|
||||
}
|
||||
|
||||
@@ -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<void>;
|
||||
@@ -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<AppState>((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<AppState>((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}`);
|
||||
|
||||
Reference in New Issue
Block a user