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
201 lines
6.7 KiB
JavaScript
201 lines
6.7 KiB
JavaScript
// 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)`);
|