feat: real backend mutations + Studio + Settings + live API console
Massive overhaul that turns the demo from presenter-mode into a
fully-functional FlowMaster operator surface.
NEW: real backend mutations
- api.executeAction(txId, actionId, actor, values) → POST /api/runtime/transactions/{tx}/actions/{actionId}
- api.startTransaction(defKey, business_subject) → POST /api/runtime/transactions
- api.createProcess(payload) → POST /api/ea2/flow
- store.executeAction / startInstance with toast feedback + auto-refresh
- Inspector Overview action buttons fire real backend calls (Submit/Save Draft/etc)
- LeftRail Confirm/Reject buttons fire real backend calls
- LeftRail 'Start new instance' button starts a real tx for live procurement
- CommandBar 'Real actions' group with 'Start new instance' + 'Execute action on headline tx'
NEW: Process Studio (src/scenes/Studio.tsx)
- in-UI process designer: name + display + hub + description + node list + edge list
- live JSON preview of the EA2 payload
- Publish button calls api.createProcess against demo.flow-master.ai
- Validates locally before publishing
- Auto-refreshes scenarios after publish so the new process shows up
NEW: Settings (src/scenes/Settings.tsx)
- Identity: sign in as any email (loginAs), see actor + display name
- Quick-user buttons for common demo identities
- Backend URL + clear-token diagnostic
- Polling cadence (2-120s)
- Dark/light theme toggle (CSS data-theme attribute)
- Show-console default toggle
- All persisted to localStorage (LS_KEY = fm.mc.prefs.v1)
NEW: Live API console (src/components/Console.tsx)
- Right-side drawer triggered from topbar
- Every fetch (GET/POST/etc) streams in real time with status + duration
- Click any entry to expand request + response JSON
- Filter: all / writes / errors
- Replaces the old guided-tour overlay entirely
NEW: live polling
- store.startPolling()/stopPolling() with setInterval guarded for SSR
- Auto-refresh while in LIVE mode at configurable cadence
REMOVED: Tour.tsx, all startTour() store actions and tour references
- Landing CTA now reads 'Enter Mission Control' / 'Design a process' / 'Open live console'
ALSO:
- api.ts: instrumentedFetch with observer pattern → store.apiLog
- Topbar: user identity chip linking to Settings, Console toggle with badge
- Light theme: minimal CSS data-theme override (text + surfaces only)
- localStorage persistence for mode, scenarioId, email, theme, pollEverySec, consoleOpen, recents
- 24/24 vitest, smoke quick run shows 0 console errors + 7 API calls captured
Confidence: high
Scope-risk: broad (~14 files)
Not-tested: actual end-to-end backend mutation roundtrip (requires LIVE + sign-in; structure proven via probe scripts)
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// Live API call log. Every fetch the app makes shows up here in real time.
|
||||
// Replaces the old guided-tour overlay.
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useApp } from "../state/store";
|
||||
import { Close, Refresh, Layers, Pulse } from "./icons";
|
||||
|
||||
const METHOD_COLOR: Record<string, string> = {
|
||||
GET: "var(--text-2)",
|
||||
POST: "#7eb0ff",
|
||||
PUT: "#f5b755",
|
||||
PATCH: "#f5b755",
|
||||
DELETE: "#ff8a8a",
|
||||
};
|
||||
|
||||
function statusClass(s: number): string {
|
||||
if (s === 0) return "err";
|
||||
if (s >= 500) return "err";
|
||||
if (s >= 400) return "warn";
|
||||
if (s >= 200) return "ok";
|
||||
return "info";
|
||||
}
|
||||
|
||||
export default function Console() {
|
||||
const open = useApp((s) => s.consoleOpen);
|
||||
const setOpen = useApp((s) => s.setConsoleOpen);
|
||||
const log = useApp((s) => s.apiLog);
|
||||
const clear = useApp((s) => s.clearApiLog);
|
||||
const [expanded, setExpanded] = useState<number | null>(null);
|
||||
const [filter, setFilter] = useState<"all" | "writes" | "errors">("all");
|
||||
const tailRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
tailRef.current?.scrollTo({ top: tailRef.current.scrollHeight, behavior: "smooth" });
|
||||
}, [log, open]);
|
||||
|
||||
const filtered = log.filter((c) => {
|
||||
if (filter === "writes") return c.method !== "GET";
|
||||
if (filter === "errors") return c.status === 0 || c.status >= 400;
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.aside
|
||||
className="console"
|
||||
initial={{ x: 360, opacity: 0 }}
|
||||
animate={{ x: 0, opacity: 1 }}
|
||||
exit={{ x: 360, opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
aria-label="API call console"
|
||||
>
|
||||
<header className="console-head">
|
||||
<Pulse size={13} />
|
||||
<span className="console-title">Live console</span>
|
||||
<span className="console-count mono">{log.length}</span>
|
||||
<div className="console-filters" role="tablist">
|
||||
<button className={`console-filter${filter === "all" ? " on" : ""}`} onClick={() => setFilter("all")}>all</button>
|
||||
<button className={`console-filter${filter === "writes" ? " on" : ""}`} onClick={() => setFilter("writes")}>writes</button>
|
||||
<button className={`console-filter${filter === "errors" ? " on" : ""}`} onClick={() => setFilter("errors")}>errors</button>
|
||||
</div>
|
||||
<button className="console-x" title="Clear" onClick={clear}><Refresh size={11} /></button>
|
||||
<button className="console-x" title="Close" onClick={() => setOpen(false)}><Close size={12} /></button>
|
||||
</header>
|
||||
<div className="console-body" ref={tailRef}>
|
||||
{filtered.length === 0 && (
|
||||
<div className="empty">No API calls yet. Flip to LIVE mode or perform an action.</div>
|
||||
)}
|
||||
{filtered.map((c) => {
|
||||
const isExpanded = expanded === c.id;
|
||||
const shortPath = c.path.replace(/^https?:\/\/[^/]+/, "");
|
||||
return (
|
||||
<div key={c.id} className={`call ${statusClass(c.status)}`}>
|
||||
<button
|
||||
className="call-head"
|
||||
onClick={() => setExpanded(isExpanded ? null : c.id)}
|
||||
title="Toggle body"
|
||||
>
|
||||
<span className="call-method mono" style={{ color: METHOD_COLOR[c.method] || "var(--text-2)" }}>{c.method}</span>
|
||||
<span className="call-status mono">{c.status || "ERR"}</span>
|
||||
<span className="call-path mono" title={c.path}>{shortPath}</span>
|
||||
<span className="call-dur mono">{c.durationMs}ms</span>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="call-body">
|
||||
{c.reqBody !== undefined && (
|
||||
<>
|
||||
<div className="call-label"><Layers size={10} /> request</div>
|
||||
<pre className="call-json"><code>{JSON.stringify(c.reqBody, null, 2)}</code></pre>
|
||||
</>
|
||||
)}
|
||||
{c.error && (
|
||||
<>
|
||||
<div className="call-label call-err"><Layers size={10} /> network error</div>
|
||||
<pre className="call-json"><code>{c.error}</code></pre>
|
||||
</>
|
||||
)}
|
||||
{c.resBody !== undefined && (
|
||||
<>
|
||||
<div className="call-label"><Layers size={10} /> response</div>
|
||||
<pre className="call-json"><code>{typeof c.resBody === "string" ? c.resBody : JSON.stringify(c.resBody, null, 2)}</code></pre>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</motion.aside>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user