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:
+45
-10
@@ -1,12 +1,15 @@
|
||||
// App shell — scene switcher + top bar + command palette + tour overlay.
|
||||
// App shell — scene switcher + top bar + command palette + console + toaster.
|
||||
import { useEffect } from "react";
|
||||
import { useApp, scenarioById } from "./state/store";
|
||||
import Landing from "./scenes/Landing";
|
||||
import MissionControl from "./scenes/MissionControl";
|
||||
import RunHistory from "./scenes/RunHistory";
|
||||
import Studio from "./scenes/Studio";
|
||||
import Settings from "./scenes/Settings";
|
||||
import CommandBar from "./components/CommandBar";
|
||||
import Tour from "./components/Tour";
|
||||
import Toaster from "./components/Toaster";
|
||||
import { Cmd, Home, Layers, History as HistoryIcon, Sparkles, Pulse, Refresh } from "./components/icons";
|
||||
import Console from "./components/Console";
|
||||
import { Cmd, Home, Layers, History as HistoryIcon, Pulse, Refresh, Branch, Cog, User } from "./components/icons";
|
||||
import { liveMeta } from "./data/scenarios";
|
||||
|
||||
export default function App() {
|
||||
@@ -15,13 +18,23 @@ export default function App() {
|
||||
const setCmdOpen = useApp((s) => s.setCmdOpen);
|
||||
const scenarioId = useApp((s) => s.scenarioId);
|
||||
const sc = scenarioById(scenarioId);
|
||||
const startTour = useApp((s) => s.startTour);
|
||||
const tourActive = useApp((s) => s.tour.active);
|
||||
const mode = useApp((s) => s.mode);
|
||||
const setMode = useApp((s) => s.setMode);
|
||||
const refreshLive = useApp((s) => s.refreshLive);
|
||||
const liveLoading = useApp((s) => s.liveLoading);
|
||||
const liveFetchedAt = useApp((s) => s.liveFetchedAt);
|
||||
const consoleOpen = useApp((s) => s.consoleOpen);
|
||||
const setConsoleOpen = useApp((s) => s.setConsoleOpen);
|
||||
const apiLogCount = useApp((s) => s.apiLog.length);
|
||||
const actor = useApp((s) => s.actor);
|
||||
const userEmail = useApp((s) => s.userEmail);
|
||||
const startPolling = useApp((s) => s.startPolling);
|
||||
const stopPolling = useApp((s) => s.stopPolling);
|
||||
|
||||
useEffect(() => {
|
||||
if (mode === "live") startPolling();
|
||||
return () => stopPolling();
|
||||
}, [mode, startPolling, stopPolling]);
|
||||
|
||||
const liveAge = liveFetchedAt ? `${Math.max(0, Math.floor((Date.now() - liveFetchedAt) / 1000))}s ago` : null;
|
||||
|
||||
@@ -43,13 +56,19 @@ export default function App() {
|
||||
<button role="tab" aria-selected={scene === "history"} className={`tab${scene === "history" ? " tab-sel" : ""}`} onClick={() => setScene("history")}>
|
||||
<HistoryIcon size={13} /> Runs
|
||||
</button>
|
||||
<button role="tab" aria-selected={scene === "studio"} className={`tab${scene === "studio" ? " tab-sel" : ""}`} onClick={() => setScene("studio")}>
|
||||
<Branch size={13} /> Studio
|
||||
</button>
|
||||
<button role="tab" aria-selected={scene === "settings"} className={`tab${scene === "settings" ? " tab-sel" : ""}`} onClick={() => setScene("settings")}>
|
||||
<Cog size={13} /> Settings
|
||||
</button>
|
||||
<button role="tab" aria-selected={false} className="tab" onClick={() => setScene("landing")}>
|
||||
<Home size={13} /> Home
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div className="topbar-mid">
|
||||
{sc && (
|
||||
{sc && scene === "mission" && (
|
||||
<div className="topbar-context">
|
||||
<span className="topbar-chip">
|
||||
<span className="dot dot-running" /> {sc.family.label}
|
||||
@@ -68,6 +87,15 @@ export default function App() {
|
||||
</div>
|
||||
|
||||
<div className="topbar-actions">
|
||||
<button
|
||||
className="link-btn user-btn"
|
||||
onClick={() => setScene("settings")}
|
||||
title={`Signed in as ${userEmail}${actor?.user_id ? ` (${actor.user_id.slice(0, 8)})` : ""}`}
|
||||
>
|
||||
<User size={12} />
|
||||
<span className="user-email">{userEmail.split("@")[0]}</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
className={`link-btn mode-toggle mode-${mode}`}
|
||||
onClick={() => setMode(mode === "live" ? "snapshot" : "live")}
|
||||
@@ -85,11 +113,16 @@ export default function App() {
|
||||
<Refresh size={12} /> Refresh
|
||||
</button>
|
||||
)}
|
||||
<button className="link-btn" onClick={startTour} disabled={tourActive}>
|
||||
<Sparkles size={13} /> Tour
|
||||
<button
|
||||
className={`link-btn${consoleOpen ? " is-on" : ""}`}
|
||||
onClick={() => setConsoleOpen(!consoleOpen)}
|
||||
title="Toggle live API console"
|
||||
>
|
||||
<Layers size={12} /> Console
|
||||
{apiLogCount > 0 && <span className="badge mono">{apiLogCount}</span>}
|
||||
</button>
|
||||
<button className="link-btn" onClick={() => setCmdOpen(true)}>
|
||||
<Cmd size={13} /> Command <kbd>⌘K</kbd>
|
||||
<Cmd size={13} /> <kbd>⌘K</kbd>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
@@ -99,10 +132,12 @@ export default function App() {
|
||||
{scene === "landing" && <Landing />}
|
||||
{scene === "mission" && <MissionControl />}
|
||||
{scene === "history" && <RunHistory />}
|
||||
{scene === "studio" && <Studio />}
|
||||
{scene === "settings" && <Settings />}
|
||||
</div>
|
||||
|
||||
<CommandBar />
|
||||
<Tour />
|
||||
<Console />
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user