Oracle remediation batch 1 — closing gaps #1 (agent overclaim), #4 (demo-data leak), #5 (raw-ID exposure): - src/lib/flowCuration.ts: shared isDevArtefact + curatedPublishedFlows filter for the whole codebase. Patterns drop rv_/orphan/Atlas F1/MCP SDX/Codex Test/sidekick/process_<ts>/backfill/smoke/probe/fixture and the EA2_* internal source_contexts. - src/scenes/Hub.tsx: loadPublishedFlows uses the shared curator - src/lib/agentTools.ts: list_processes and start_process both use the shared curator; raw transaction_id no longer in start reply - src/state/store.ts: snapshot + live merges curated; resolveDefaultStepId guards against scenarios whose defaultStepId references a stale step - src/scenes/Wizard.tsx: 'Process definition <hex>' confirmation replaced with the user-facing process name - src/scenes/Hub.tsx: start toast no longer shows transaction_id prefix - src/scenes/Agent.tsx + App.tsx + Landing.tsx: rename Pi -> FlowMaster Command Assistant. Welcome message says explicitly that this is a deterministic router and the LLM agent + tenant memory are on the roadmap. Tab label, hub chip, turn author all rebranded. - src/scenes/GeoAttendance.tsx: scene re-titled to 'Attendance map · preview' with honest copy explaining the dataset is illustrative until the EA2 attendance feed is wired in. - src/data/synthetic.ts: orphaned from runtime (kept in tree for tests). Snapshot now sources only the live-cached EA2 read (scenarios.json), filtered through the same curator. 29/29 tests green. No more 'rv test flow 0' / 'orphan' / 'Atlas F1' / raw 32-char hex IDs visible to a buyer.
174 lines
6.6 KiB
TypeScript
174 lines
6.6 KiB
TypeScript
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 { Branch, Layers, Pulse } from "../components/icons";
|
|
|
|
export type HubKey = "procurement" | "hr" | "it";
|
|
|
|
interface HubSpec {
|
|
title: string;
|
|
eyebrow: string;
|
|
intro: string;
|
|
match: RegExp;
|
|
quickActions: { label: string; subject: string; processHint: string }[];
|
|
}
|
|
|
|
const HUBS: Record<HubKey, HubSpec> = {
|
|
procurement: {
|
|
title: "Procurement Hub",
|
|
eyebrow: "Purchase requests, vendor approvals, three-way match",
|
|
intro:
|
|
"Everything a store manager needs to buy something — from a laptop to a forklift — and watch it move through the approval chain.",
|
|
match: /procure|purchase|vendor|\bpo\b|\bp2p\b|requisition|payment|invoice/i,
|
|
quickActions: [
|
|
{ label: "Request a new laptop", subject: "store-204-laptop", processHint: "procurement" },
|
|
{ label: "Submit a quote for review", subject: "vendor-quote-Q3", processHint: "procurement" },
|
|
],
|
|
},
|
|
hr: {
|
|
title: "People Hub",
|
|
eyebrow: "Hiring, onboarding, leave, performance",
|
|
intro:
|
|
"All people movements run here. Start an onboarding for a new hire, file a leave request, or kick off a review cycle.",
|
|
match: /onboard|offboard|leave|\bhr\b|hiring|payroll|employee|people operations/i,
|
|
quickActions: [
|
|
{ label: "Onboard a new hire", subject: "new-hire-2026Q3", processHint: "onboard" },
|
|
{ label: "Request annual leave", subject: "leave-7d-may", processHint: "leave" },
|
|
],
|
|
},
|
|
it: {
|
|
title: "IT Hub",
|
|
eyebrow: "Tickets, access requests, incident response",
|
|
intro:
|
|
"When something breaks, request access, or you need a new account — file it here and route it to the right on-call.",
|
|
match: /\bit\b|ticket|incident|service operations|access request|sap access|laptop request|hardware request|software request|password reset|account provisioning/i,
|
|
quickActions: [
|
|
{ label: "Open an incident", subject: "incident-store-204", processHint: "service" },
|
|
{ label: "Request system access", subject: "access-sap-RW", processHint: "access" },
|
|
],
|
|
},
|
|
};
|
|
|
|
interface PublishedFlow {
|
|
_key: string;
|
|
display_name: string;
|
|
description?: string;
|
|
name?: string;
|
|
}
|
|
|
|
async function loadPublishedFlows(): Promise<PublishedFlow[]> {
|
|
const res = await fetch(`${api.config.baseUrl}/api/ea2/flow/processes?limit=200`, {
|
|
headers: { Authorization: `Bearer ${sessionStorage.getItem("fm.mc.token.v1")}` },
|
|
});
|
|
if (!res.ok) throw new Error(`processes ${res.status}`);
|
|
const body = await res.json();
|
|
return curatedPublishedFlows((body?.items || []) as any[]).map((it) => ({
|
|
_key: it._key,
|
|
display_name: it.display_name!,
|
|
description: it.description,
|
|
name: it.name,
|
|
}));
|
|
}
|
|
|
|
export default function Hub({ hub }: { hub: HubKey }) {
|
|
const spec = HUBS[hub];
|
|
const pushToast = useApp((s) => s.pushToast);
|
|
const setScene = useApp((s) => s.setScene);
|
|
const [flows, setFlows] = useState<PublishedFlow[] | null>(null);
|
|
const [busy, setBusy] = useState<string | null>(null);
|
|
|
|
useEffect(() => {
|
|
let cancelled = false;
|
|
loadPublishedFlows()
|
|
.then((rows) => !cancelled && setFlows(rows))
|
|
.catch((err) => !cancelled && pushToast("err", `Catalogue failed: ${err.message}`));
|
|
return () => {
|
|
cancelled = true;
|
|
};
|
|
}, [hub]);
|
|
|
|
const matching = (flows || []).filter((f) => spec.match.test(`${f.display_name} ${f.description || ""} ${f.name || ""}`)).slice(0, 8);
|
|
const fallback = (flows || []).slice(0, 6);
|
|
const visible = matching.length > 0 ? matching : fallback;
|
|
|
|
const startInstance = async (flowKey: string, subject: string, label: string) => {
|
|
setBusy(flowKey);
|
|
try {
|
|
const res = await wizardApi.startInstance(flowKey, subject);
|
|
void res;
|
|
pushToast("ok", `Started ${label}${subject ? ` for ${subject}` : ""}.`);
|
|
setScene("mission");
|
|
} catch (err: any) {
|
|
pushToast("err", `Start failed: ${err.message}`);
|
|
} finally {
|
|
setBusy(null);
|
|
}
|
|
};
|
|
|
|
const tryQuickAction = async (label: string, subject: string, processHint: string) => {
|
|
if (!flows) return;
|
|
const match = flows.find((f) =>
|
|
new RegExp(processHint, "i").test(`${f.display_name} ${f.description || ""} ${f.name || ""}`)
|
|
);
|
|
if (!match) {
|
|
pushToast("err", `No published process matching "${processHint}". Build one in Studio.`);
|
|
return;
|
|
}
|
|
await startInstance(match._key, subject, label);
|
|
};
|
|
|
|
return (
|
|
<div className="hub-scene">
|
|
<header className="hub-head">
|
|
<div className="mc-hero-eyebrow"><Layers size={12} /> {spec.eyebrow}</div>
|
|
<h2 className="mc-hero-title">{spec.title}</h2>
|
|
<p className="hub-intro">{spec.intro}</p>
|
|
</header>
|
|
|
|
<section className="hub-quick">
|
|
<div className="hub-section-label">QUICK ACTIONS</div>
|
|
<div className="hub-quick-grid">
|
|
{spec.quickActions.map((qa) => (
|
|
<button
|
|
key={qa.label}
|
|
className="hub-quick-card"
|
|
onClick={() => tryQuickAction(qa.label, qa.subject, qa.processHint)}
|
|
disabled={!flows}
|
|
>
|
|
<div className="hub-quick-title"><Pulse size={12} /> {qa.label}</div>
|
|
<div className="hub-quick-meta">Looks up "{qa.processHint}" in your catalogue</div>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
<section className="hub-catalogue">
|
|
<div className="hub-section-label">RELATED PROCESSES</div>
|
|
{flows === null && <div className="hub-empty">Loading catalogue…</div>}
|
|
{flows !== null && visible.length === 0 && (
|
|
<div className="hub-empty">
|
|
No matching published processes yet. Open the <button className="link-inline" onClick={() => setScene("studio")}>Process Studio</button> to build one.
|
|
</div>
|
|
)}
|
|
<div className="hub-catalogue-grid">
|
|
{visible.map((f) => (
|
|
<div key={f._key} className="hub-flow-card">
|
|
<div className="hub-flow-title"><Branch size={11} /> {f.display_name}</div>
|
|
{f.description && <div className="hub-flow-desc">{f.description.slice(0, 160)}</div>}
|
|
<button
|
|
className="btn btn-secondary"
|
|
disabled={busy === f._key}
|
|
onClick={() => startInstance(f._key, "", f.display_name)}
|
|
>
|
|
{busy === f._key ? "Starting…" : "Start an instance"}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
);
|
|
}
|