feat: real backend mutations + Studio + Settings + live API console
build-and-publish / test (push) Has been cancelled
build-and-publish / image (push) Has been cancelled

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:
2026-06-14 01:19:36 +04:00
parent 49639a0857
commit cb9291b225
13 changed files with 1334 additions and 305 deletions
+164
View File
@@ -0,0 +1,164 @@
// Settings scene — identity, backend URL, polling cadence, theme.
import { useState } from "react";
import { useApp } from "../state/store";
import { api } from "../lib/api";
import { User, Refresh, Cog, Pulse, Layers, Check } from "../components/icons";
const COMMON_EMAILS = [
"dev@flow-master.ai",
"procurement.operator@flowmaster.local",
"finance.lead@flowmaster.local",
"ops.admin@flowmaster.local",
];
export default function Settings() {
const userEmail = useApp((s) => s.userEmail);
const actor = useApp((s) => s.actor);
const userDisplayName = useApp((s) => s.userDisplayName);
const loginAs = useApp((s) => s.loginAs);
const pollEverySec = useApp((s) => s.pollEverySec);
const setPollEverySec = useApp((s) => s.setPollEverySec);
const theme = useApp((s) => s.theme);
const setTheme = useApp((s) => s.setTheme);
const consoleOpen = useApp((s) => s.consoleOpen);
const setConsoleOpen = useApp((s) => s.setConsoleOpen);
const mode = useApp((s) => s.mode);
const setMode = useApp((s) => s.setMode);
const refreshLive = useApp((s) => s.refreshLive);
const pushToast = useApp((s) => s.pushToast);
const [emailInput, setEmailInput] = useState(userEmail);
const [signingIn, setSigningIn] = useState(false);
const signIn = async (email: string) => {
setSigningIn(true);
try {
await loginAs(email);
} finally {
setSigningIn(false);
}
};
const clearToken = () => {
api.clearToken();
pushToast("info", "Cleared bearer token. Next request will re-login.");
};
return (
<div className="settings">
<header className="studio-head">
<div>
<div className="mc-hero-eyebrow"><Cog size={12} /> Settings</div>
<h2 className="mc-hero-title">Identity, backend & UI</h2>
<div className="mc-hero-sub">Everything here is persisted to localStorage and survives reloads.</div>
</div>
</header>
<div className="studio-grid">
<section className="studio-panel">
<h3 className="panel-h"><User size={12} /> Identity</h3>
<div className="settings-id">
<div className="i-field">
<span>Current</span>
<span className="mono">{actor?.user_id?.slice(0, 12) ?? "—"}</span>
</div>
<div className="i-field">
<span>Email</span>
<span>{userEmail}</span>
</div>
<div className="i-field">
<span>Display name</span>
<span>{userDisplayName ?? "—"}</span>
</div>
<div className="i-field">
<span>Backend</span>
<span className="mono">{api.config.baseUrl || "(same-origin via /api)"}</span>
</div>
</div>
<h4 className="i-h" style={{ marginTop: 16 }}>Sign in as</h4>
<div className="studio-rows">
<div className="studio-row">
<input
className="studio-input"
placeholder="someone@flow-master.ai"
value={emailInput}
onChange={(e) => setEmailInput(e.target.value)}
/>
<button className="btn btn-primary" onClick={() => signIn(emailInput)} disabled={signingIn || !emailInput.includes("@")}>
{signingIn ? <span className="spin" /> : <Check size={12} />} Sign in
</button>
</div>
<div className="quick-users">
{COMMON_EMAILS.map((e) => (
<button key={e} className="link-btn" onClick={() => { setEmailInput(e); signIn(e); }} disabled={signingIn}>
{e}
</button>
))}
</div>
</div>
<button className="link-btn" style={{ marginTop: 10 }} onClick={clearToken}>
<Refresh size={12} /> Clear bearer token
</button>
</section>
<section className="studio-panel">
<h3 className="panel-h"><Pulse size={12} /> Data mode & polling</h3>
<div className="i-field">
<span>Current mode</span>
<span className={`mode-pill mode-${mode}`}>{mode === "live" ? "LIVE" : "SNAPSHOT"}</span>
</div>
<div className="settings-row">
<button className="btn btn-primary" onClick={() => setMode(mode === "live" ? "snapshot" : "live")} disabled={signingIn}>
<Refresh size={12} /> {mode === "live" ? "Switch to snapshot" : "Switch to live"}
</button>
{mode === "live" && (
<button className="btn btn-ghost" onClick={refreshLive}>
<Refresh size={12} /> Refresh now
</button>
)}
</div>
<label className="studio-field" style={{ marginTop: 12 }}>
<span>Auto-refresh every (seconds)</span>
<input
type="number"
className="studio-input mono"
min={2}
max={120}
value={pollEverySec}
onChange={(e) => setPollEverySec(Number(e.target.value) || 8)}
/>
</label>
<div className="settings-hint">Polling only runs in LIVE mode. Currently {mode === "live" ? "polling" : "paused"}.</div>
</section>
<section className="studio-panel">
<h3 className="panel-h"><Cog size={12} /> Appearance</h3>
<div className="settings-row">
<button className={`btn ${theme === "dark" ? "btn-primary" : "btn-ghost"}`} onClick={() => setTheme("dark")}>
Dark
</button>
<button className={`btn ${theme === "light" ? "btn-primary" : "btn-ghost"}`} onClick={() => setTheme("light")}>
Light
</button>
</div>
<label className="settings-toggle" style={{ marginTop: 12 }}>
<input type="checkbox" checked={consoleOpen} onChange={(e) => setConsoleOpen(e.target.checked)} />
<span>Show live API console by default</span>
</label>
</section>
<section className="studio-panel">
<h3 className="panel-h"><Layers size={12} /> About</h3>
<p className="settings-text">
Mission Control demo. All actions are real backend calls the live
console shows every fetch. Source:{" "}
<a className="settings-link" href="https://gitea.flow-master.ai/shad/flowmaster-mission-control-demo" target="_blank" rel="noreferrer">
gitea.flow-master.ai/shad/flowmaster-mission-control-demo
</a>.
</p>
</section>
</div>
</div>
);
}