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:
@@ -1,15 +1,14 @@
|
||||
// Command palette v2 — scenarios, steps, tour, scenes, recents.
|
||||
// Command palette — scenarios, steps, scenes, recents, real actions.
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { Command } from "cmdk";
|
||||
import { useApp, scenarioById } from "../state/store";
|
||||
import { Play, Branch, Bot, Check, Sparkles, Home, History as HistoryIcon, Layers, Search, Refresh } from "./icons";
|
||||
import { Play, Branch, Bot, Check, Home, History as HistoryIcon, Layers, Search, Refresh, Cog, User } from "./icons";
|
||||
|
||||
export default function CommandBar() {
|
||||
const open = useApp((s) => s.cmdOpen);
|
||||
const setOpen = useApp((s) => s.setCmdOpen);
|
||||
const setScenarioId = useApp((s) => s.setScenarioId);
|
||||
const setSelectedStepId = useApp((s) => s.setSelectedStepId);
|
||||
const startTour = useApp((s) => s.startTour);
|
||||
const setScene = useApp((s) => s.setScene);
|
||||
const recents = useApp((s) => s.recents);
|
||||
const scenarioId = useApp((s) => s.scenarioId);
|
||||
@@ -18,14 +17,16 @@ export default function CommandBar() {
|
||||
const mode = useApp((s) => s.mode);
|
||||
const setMode = useApp((s) => s.setMode);
|
||||
const refreshLive = useApp((s) => s.refreshLive);
|
||||
const startInstance = useApp((s) => s.startInstance);
|
||||
const executeAction = useApp((s) => s.executeAction);
|
||||
const pushToast = useApp((s) => s.pushToast);
|
||||
const setConsoleOpen = useApp((s) => s.setConsoleOpen);
|
||||
const setTheme = useApp((s) => s.setTheme);
|
||||
const theme = useApp((s) => s.theme);
|
||||
const actor = useApp((s) => s.actor);
|
||||
|
||||
const sc = scenarioById(scenarioId);
|
||||
|
||||
const previewAction = (label: string) => {
|
||||
pushToast("info", `${label} is preview-only. Wire to POST /api/runtime/transactions/{id}/actions to make it real.`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "k") {
|
||||
@@ -44,12 +45,15 @@ export default function CommandBar() {
|
||||
|
||||
const close = () => setOpen(false);
|
||||
|
||||
const canActLive = mode === "live" && actor?.user_id && sc?.live && sc.headlineTx;
|
||||
const firstActionId = sc?.steps.find((s) => s.state === "running")?.actions?.[0]?.id;
|
||||
|
||||
return (
|
||||
<div className="cmd-overlay" onClick={close}>
|
||||
<Command className="cmd" onClick={(e) => e.stopPropagation()} label="Command bar">
|
||||
<div className="cmd-input-row">
|
||||
<Search size={14} />
|
||||
<Command.Input autoFocus placeholder="Switch scenario · jump to step · start tour…" />
|
||||
<Command.Input autoFocus placeholder="Switch scenario · jump to step · start instance · execute action…" />
|
||||
<kbd className="kbd-hint">esc</kbd>
|
||||
</div>
|
||||
<Command.List>
|
||||
@@ -75,11 +79,70 @@ export default function CommandBar() {
|
||||
<Command.Item onSelect={() => { setScene("history"); close(); }}>
|
||||
<HistoryIcon size={13} /> Run History
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => { setScene("studio"); close(); }}>
|
||||
<Branch size={13} /> Process Studio
|
||||
<span className="cmd-hint">design & publish a new process</span>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => { setScene("settings"); close(); }}>
|
||||
<Cog size={13} /> Settings
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
|
||||
<Command.Group heading="Tour & demo">
|
||||
<Command.Item onSelect={() => { startTour(); close(); }}>
|
||||
<Sparkles size={13} /> Start guided tour <span className="cmd-hint">presenter mode</span>
|
||||
<Command.Group heading="Data mode">
|
||||
{mode === "live" ? (
|
||||
<Command.Item onSelect={() => { refreshLive(); close(); }}>
|
||||
<Refresh size={13} /> Refresh live scenarios
|
||||
<span className="cmd-hint">re-fetch demo.flow-master.ai</span>
|
||||
</Command.Item>
|
||||
) : (
|
||||
<Command.Item onSelect={() => { setMode("live"); close(); }}>
|
||||
<Refresh size={13} /> Switch to LIVE mode (fetch demo.flow-master.ai)
|
||||
<span className="cmd-hint">in-browser</span>
|
||||
</Command.Item>
|
||||
)}
|
||||
<Command.Item onSelect={() => { setMode("snapshot"); close(); }}>
|
||||
<Layers size={13} /> Switch to SNAPSHOT mode
|
||||
<span className="cmd-hint">bundled JSON</span>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => { setConsoleOpen(true); close(); }}>
|
||||
<Layers size={13} /> Open live API console
|
||||
<span className="cmd-hint">stream every fetch</span>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => { setTheme(theme === "dark" ? "light" : "dark"); close(); }}>
|
||||
<Cog size={13} /> Toggle theme
|
||||
<span className="cmd-hint">{theme === "dark" ? "→ light" : "→ dark"}</span>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
|
||||
<Command.Group heading="Real actions">
|
||||
{sc?.live ? (
|
||||
<Command.Item
|
||||
onSelect={async () => {
|
||||
close();
|
||||
if (mode !== "live") { pushToast("warn", "Switch to LIVE mode to start a real instance."); return; }
|
||||
await startInstance(sc.defKey, `MC demo · ${new Date().toLocaleString()}`);
|
||||
}}
|
||||
>
|
||||
<Play size={13} /> Start new instance of "{sc.defName}"
|
||||
<span className="cmd-hint">POST /api/runtime/transactions</span>
|
||||
</Command.Item>
|
||||
) : (
|
||||
<Command.Item disabled>
|
||||
<Play size={13} /> Start new instance
|
||||
<span className="cmd-hint">switch to a LIVE scenario first</span>
|
||||
</Command.Item>
|
||||
)}
|
||||
{canActLive && firstActionId && (
|
||||
<Command.Item
|
||||
onSelect={async () => { close(); await executeAction(sc!.headlineTx!, firstActionId); }}
|
||||
>
|
||||
<Check size={13} /> Execute "{firstActionId}" on headline tx
|
||||
<span className="cmd-hint">{sc?.headlineTx?.slice(0, 8)} · real</span>
|
||||
</Command.Item>
|
||||
)}
|
||||
<Command.Item onSelect={() => { setScene("settings"); close(); }}>
|
||||
<User size={13} /> Sign in as different user
|
||||
<span className="cmd-hint">Settings → identity</span>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
|
||||
@@ -112,33 +175,15 @@ export default function CommandBar() {
|
||||
</Command.Group>
|
||||
)}
|
||||
|
||||
<Command.Group heading="Data mode">
|
||||
{mode === "live" ? (
|
||||
<Command.Item onSelect={() => { refreshLive(); close(); }}>
|
||||
<Refresh size={13} /> Refresh live scenarios
|
||||
<span className="cmd-hint">re-fetch demo.flow-master.ai</span>
|
||||
</Command.Item>
|
||||
) : (
|
||||
<Command.Item onSelect={() => { setMode("live"); close(); }}>
|
||||
<Refresh size={13} /> Switch to LIVE mode (fetch demo.flow-master.ai)
|
||||
<span className="cmd-hint">in-browser</span>
|
||||
</Command.Item>
|
||||
)}
|
||||
<Command.Item onSelect={() => { setMode("snapshot"); close(); }}>
|
||||
<Layers size={13} /> Switch to SNAPSHOT mode
|
||||
<span className="cmd-hint">bundled JSON</span>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
|
||||
<Command.Group heading="Actions (preview-only)">
|
||||
<Command.Item onSelect={() => { previewAction("Start runtime instance"); close(); }}>
|
||||
<Play size={13} /> Start runtime instance <span className="cmd-hint">preview</span>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => { previewAction("Dispatch sidekick agent"); close(); }}>
|
||||
<Bot size={13} /> Dispatch sidekick agent <span className="cmd-hint">preview</span>
|
||||
</Command.Item>
|
||||
<Command.Item onSelect={() => { previewAction("Confirm awaiting agent runs"); close(); }}>
|
||||
<Check size={13} /> Confirm awaiting agent runs <span className="cmd-hint">{sc?.agentRuns.length ?? 0} pending · preview</span>
|
||||
<Command.Group heading="Agent">
|
||||
<Command.Item
|
||||
onSelect={async () => {
|
||||
close();
|
||||
pushToast("info", `Sidekick dispatch endpoint not yet wired — see Studio for process-level changes.`);
|
||||
}}
|
||||
>
|
||||
<Bot size={13} /> Dispatch sidekick agent
|
||||
<span className="cmd-hint">coming soon</span>
|
||||
</Command.Item>
|
||||
</Command.Group>
|
||||
</Command.List>
|
||||
|
||||
Reference in New Issue
Block a user