diff --git a/qa/probe_hub_curation.mjs b/qa/probe_hub_curation.mjs new file mode 100644 index 0000000..786d96b --- /dev/null +++ b/qa/probe_hub_curation.mjs @@ -0,0 +1,20 @@ +import { chromium } from "playwright"; +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(); +p.on("response", async (r) => { + if (/\/api\/ea2\/flow\/processes/.test(r.url())) { + const t = await r.text().catch(() => ""); + console.log(`[catalogue ${r.status()}] ${r.url().replace("https://canvas.flow-master.ai","")} → ${t.slice(0,500)}`); + } +}); +await p.goto("https://canvas.flow-master.ai/", { waitUntil: "networkidle" }); +await p.waitForTimeout(800); +await p.locator(".persona-chip", { hasText: /Mariana/ }).click(); +await p.locator(".dev-login-btn").click(); +await p.waitForTimeout(2500); +await p.locator(".hub-chip", { hasText: /Procurement Hub/ }).click(); +await p.waitForTimeout(3000); +const flowCards = await p.locator(".hub-queue-row, .hub-flow-title").allTextContents(); +console.log(`flowCards: ${flowCards.length} | ${JSON.stringify(flowCards.slice(0,5))}`); +await b.close(); diff --git a/src/lib/agentTools.ts b/src/lib/agentTools.ts index 80ccb77..bccf781 100644 --- a/src/lib/agentTools.ts +++ b/src/lib/agentTools.ts @@ -1,7 +1,7 @@ import { api } from "./api"; import { wizardApi } from "./wizardApi"; import { chatApi } from "./chatApi"; -import { curatedPublishedFlows } from "./flowCuration"; +import { curatedPublishedFlows, fetchStartableFlows } from "./flowCuration"; export interface ToolResult { ok: boolean; @@ -70,10 +70,17 @@ const TOOLS: ToolDef[] = [ const processName = subjectMatch ? subjectMatch[1] : phrase; const subject = subjectMatch ? subjectMatch[3] : undefined; - const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, { - headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` }, + const token = sessionStorage.getItem("fm.mc.token.v1") || ""; + const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=500`, { + headers: { Authorization: `Bearer ${token}` }, }).then((r) => r.json()); - const items = curatedPublishedFlows((procs?.items || []) as any[]); + const fromList = curatedPublishedFlows((procs?.items || []) as any[]); + const directHits = await fetchStartableFlows(api.config.baseUrl, token); + const items = (() => { + const m = new Map(); + for (const it of [...directHits, ...fromList]) m.set(it._key, it); + return Array.from(m.values()); + })(); const norm = (s: string) => s.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim(); const target = norm(processName); const match = items.find((it) => norm(it.display_name || "").includes(target) || norm(it.name || "").includes(target)); @@ -128,12 +135,17 @@ const TOOLS: ToolDef[] = [ description: "List published processes for this tenant", matcher: /^(list|show|what)\s+(processes|workflows|flows)/i, async run() { - const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, { - headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` }, + const token = sessionStorage.getItem("fm.mc.token.v1") || ""; + const procs = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=500`, { + headers: { Authorization: `Bearer ${token}` }, }) .then((r) => r.json()) .catch(() => ({ items: [] })); - const published = curatedPublishedFlows((procs?.items || []) as any[]).slice(0, 10); + const fromList = curatedPublishedFlows((procs?.items || []) as any[]); + const directHits = await fetchStartableFlows(api.config.baseUrl, token); + const merged = new Map(); + for (const it of [...directHits, ...fromList]) merged.set(it._key, it); + const published = Array.from(merged.values()).slice(0, 10); if (!published.length) return { ok: true, display: "No published processes yet. Open the Studio to design one." }; const lines = published.map((it) => `• ${it.display_name}`).join("\n"); return { ok: true, display: `Here's what's published:\n${lines}` }; diff --git a/src/lib/flowCuration.ts b/src/lib/flowCuration.ts index fceef76..cad736b 100644 --- a/src/lib/flowCuration.ts +++ b/src/lib/flowCuration.ts @@ -36,7 +36,7 @@ const NON_BUSINESS_SOURCE_CONTEXTS = new Set([ "EA2_DRAFT_PROCESS:process_creation", ]); -const STARTABLE_FLOW_KEYS = new Set([ +export const STARTABLE_FLOW_KEYS = new Set([ "pr_to_po_def", ]); @@ -60,6 +60,21 @@ export function curatedPublishedFlows(items: T[]): T[] { ); } +export async function fetchStartableFlows(baseUrl: string, token: string): Promise { + const results = await Promise.all( + Array.from(STARTABLE_FLOW_KEYS).map(async (key) => { + try { + const r = await fetch(`${baseUrl}/api/ea2/flow/${key}`, { headers: { Authorization: `Bearer ${token}` } }); + if (!r.ok) return null; + const doc = await r.json(); + if (doc.kind !== "definition" || doc.status !== "published" || !doc.display_name) return null; + return doc as RawFlow; + } catch { return null; } + }) + ); + return results.filter((d): d is RawFlow => !!d); +} + export function friendlyShortId(id: string | undefined | null): string { if (!id) return ""; const trimmed = id.replace(HEX_SUFFIX, ""); diff --git a/src/scenes/Hub.tsx b/src/scenes/Hub.tsx index aaf0cce..94eaa1a 100644 --- a/src/scenes/Hub.tsx +++ b/src/scenes/Hub.tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from "react"; import { useApp } from "../state/store"; import { api } from "../lib/api"; import { wizardApi } from "../lib/wizardApi"; -import { curatedPublishedFlows } from "../lib/flowCuration"; +import { curatedPublishedFlows, fetchStartableFlows } from "../lib/flowCuration"; import { Branch, Layers, Pulse } from "../components/icons"; export type HubKey = "procurement" | "hr" | "it"; @@ -71,12 +71,17 @@ interface PublishedFlow { } async function loadPublishedFlows(): Promise { - const res = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, { - headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` }, + const token = sessionStorage.getItem("fm.mc.token.v1") || ""; + const res = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=500`, { + headers: { Authorization: `Bearer ${token}` }, }); if (!res.ok) throw new Error(`processes ${res.status}`); const body = await res.json(); - return curatedPublishedFlows((body?.items || []) as any[]).map((it) => ({ + const fromList = curatedPublishedFlows((body?.items || []) as any[]); + const directHits = await fetchStartableFlows(api.config.baseUrl, token); + const merged = new Map(); + for (const it of [...directHits, ...fromList]) merged.set(it._key, it); + return Array.from(merged.values()).map((it) => ({ _key: it._key, display_name: it.display_name!, description: it.description,