// Multi-scenario live read from demo.flow-master.ai. // // Strategy: // 1. dev-login → bearer token // 2. /api/ea2/work-items?view=all → enumerate every definition_key // actually referenced by live work, then count cases per definition. // 3. For each top-N definition, fetch its graph (`/api/ea2/process-definitions/{k}/graph`) // and a few full runtime transactions. // 4. Classify into family buckets (procurement/AR/HCM/GL close/service ops) // using definition display_name + node labels. // 5. Write src/scenarios.json with the picked scenarios + the raw work-item // board so the UI can show a real "All work" view. // // READ-ONLY. Run: `node fetch_scenarios.mjs`. import { writeFileSync } from "node:fs"; const BASE = process.env.FM_BASE || "https://demo.flow-master.ai"; const EMAIL = process.env.FM_EMAIL || "dev@flow-master.ai"; const login = await fetch(`${BASE}/api/v1/auth/dev-login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: EMAIL }), }); if (!login.ok) throw new Error(`dev-login ${login.status}`); const token = (await login.json()).access_token; const H = { Authorization: `Bearer ${token}` }; const tryGet = async (path) => { try { const r = await fetch(`${BASE}${path}`, { headers: H }); if (!r.ok) return null; return await r.json(); } catch { return null; } }; // 1. Pull the full live work-item board. const board = await tryGet("/api/ea2/work-items?view=all"); const workItems = board?.items || []; console.log(`✓ ${workItems.length} live work-items`); // 2. Enumerate unique definition_keys + per-def metadata. const byDef = new Map(); for (const w of workItems) { const k = w.definition_key; if (!k) continue; if (!byDef.has(k)) { byDef.set(k, { key: k, hubs: new Set(), cases: [], statuses: {} }); } const r = byDef.get(k); if (w.hub) r.hubs.add(w.hub); r.cases.push(w); r.statuses[w.status] = (r.statuses[w.status] || 0) + 1; } console.log(`✓ ${byDef.size} distinct definitions in work board`); // 3. Filter to meaningful definitions (≥1 running OR ≥2 total cases). const candidates = [...byDef.values()].filter((d) => { const total = d.cases.length; const running = (d.statuses.running || 0) + (d.statuses.waiting_for_user || 0); return total >= 2 || running >= 1; }); candidates.sort((a, b) => b.cases.length - a.cases.length); console.log(`✓ ${candidates.length} candidates after filter`); // 4. Fetch graph + representative transaction per candidate. const enriched = []; for (const c of candidates.slice(0, 20)) { const graph = await tryGet(`/api/ea2/process-definitions/${c.key}/graph`); if (!graph?.process_definition?.config?.nodes?.length) { console.warn(` ! ${c.key.slice(0, 8)} no graph`); continue; } const headlineCase = c.cases.find((w) => w.status === "running") || c.cases.find((w) => w.status === "waiting_for_user") || c.cases.find((w) => w.status === "errored" || w.status === "failed") || c.cases[0]; let headlineRt = null; if (headlineCase?.transaction_id) { headlineRt = await tryGet(`/api/runtime/transactions/${headlineCase.transaction_id}`); } const recent = []; for (const w of c.cases.slice(0, 6)) { if (!w.transaction_id || w.transaction_id === headlineCase?.transaction_id) continue; const r = await tryGet(`/api/runtime/transactions/${w.transaction_id}`); if (r) recent.push(r); if (recent.length >= 3) break; } enriched.push({ key: c.key, name: graph.process_definition.display_name || graph.process_definition.name, hubs: [...c.hubs], statuses: c.statuses, cases: c.cases, graph, headlineCase, headlineRt, recent, }); console.log( ` ✓ ${c.key.slice(0, 8)} ${(graph.process_definition.display_name || "—").slice(0, 36)} nodes=${graph.process_definition.config.nodes.length} cases=${c.cases.length} rt=${headlineRt ? "live" : "no"}` ); } // 5. Classify into families. const families = [ { id: "procurement", label: "Procurement to Pay", subtitle: "Requisition → PO → 3-way match", accent: "#3b82f6", match: (e) => /procure|purchas|pr_to_po|atlas|requisition|po\b/i.test(`${e.name} ${e.hubs.join(" ")}`), }, { id: "ar", label: "Accounts Receivable", subtitle: "Refunds, credits & collections", accent: "#10b981", match: (e) => /refund|credit|collect|receivable|ar\b|invoice/i.test(e.name), }, { id: "hcm", label: "People Operations", subtitle: "Onboard · Offboard · Leave", accent: "#a855f7", match: (e) => /onboard|offboard|hire|hcm|employee|leave|payroll|hr\b/i.test(e.name), }, { id: "gl", label: "GL Close", subtitle: "Accruals, reconciliations, journals", accent: "#f59e0b", match: (e) => /close|ledger|journal|accrual|reconcil|gl\b/i.test(e.name), }, { id: "service", label: "Service Operations", subtitle: "Tickets, incidents, support", accent: "#ef4444", match: (e) => /ticket|incident|support|service|case\b/i.test(e.name), }, ]; const scenarios = []; const used = new Set(); for (const fam of families) { const pick = enriched .filter((e) => !used.has(e.key) && fam.match(e)) .sort((a, b) => b.cases.length - a.cases.length)[0]; if (!pick) { console.log(` ⚠ family ${fam.id} unmatched`); continue; } used.add(pick.key); scenarios.push({ family: fam, ...pick }); } // Top up to at least 4 with largest remaining. const remaining = enriched.filter((e) => !used.has(e.key)); remaining.sort((a, b) => b.cases.length - a.cases.length); while (scenarios.length < 4 && remaining.length) { const e = remaining.shift(); scenarios.push({ family: { id: `extra-${scenarios.length}`, label: e.name || "Process", subtitle: `${e.cases.length} live cases`, accent: "#64748b", match: () => false, }, ...e, }); used.add(e.key); } console.log( `\n→ ${scenarios.length} scenarios: ` + scenarios.map((s) => `${s.family.id}:${s.name?.slice(0, 24)}`).join(" | ") ); // 6. Write artifact. const out = { fetchedFrom: BASE, fetchedAt: new Date().toISOString(), totals: { workItems: workItems.length, distinctDefs: byDef.size, scenarios: scenarios.length, }, board: workItems, scenarios: scenarios.map((s) => ({ id: s.family.id, family: s.family, def_key: s.key, def_name: s.name, hubs: s.hubs, statuses: s.statuses, cases: s.cases, graph: s.graph, headlineTx: s.headlineCase?.transaction_id || null, headlineRt: s.headlineRt, recent: s.recent, })), }; writeFileSync(new URL("./src/scenarios.json", import.meta.url), JSON.stringify(out, null, 2)); console.log(`\n✓ wrote src/scenarios.json (${scenarios.length} scenarios, ${workItems.length} board items)`);