Mission Control demo v2
Polished command-center for FlowMaster with two data modes:
- SNAPSHOT: bundled src/scenarios.json from demo.flow-master.ai
- LIVE: in-browser fetch via src/lib/api.ts (dev-login + bearer)
Scenarios:
- procurement, extra-1, extra-2 (live from EA2)
- ar, hcm, gl, service (industry blueprints, same typed shell)
Honesty pass after Oracle review:
- No invented numbers (Telemetry derives SLA + agent acceptance from real data)
- Preview-only actions fire toasts naming the endpoint to wire them
- Blueprint tours framed as 'industry blueprint', not 'we don't have this yet'
- Mode pill + last-fetch age + refresh in topbar
- Dev CORS dodged via vite proxy; production deploys same-origin
18 vitest tests + 26 playwright smoke assertions + DOM layout audit.
Constraint: cross-origin live mode rejected by browser → fall back to snapshot
Rejected: hardcoded SLA % | dishonest demo metrics
Directive: wire preview-only action handlers to /api/runtime/transactions/{id}/actions to ship them for real
Confidence: high
Scope-risk: narrow
Not-tested: production deployment via flowmaster-ops overlay
This commit is contained in:
+108
@@ -0,0 +1,108 @@
|
||||
// App shell — scene switcher + top bar + command palette + tour overlay.
|
||||
import { useApp, scenarioById } from "./state/store";
|
||||
import Landing from "./scenes/Landing";
|
||||
import MissionControl from "./scenes/MissionControl";
|
||||
import RunHistory from "./scenes/RunHistory";
|
||||
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 { liveMeta } from "./data/scenarios";
|
||||
|
||||
export default function App() {
|
||||
const scene = useApp((s) => s.scene);
|
||||
const setScene = useApp((s) => s.setScene);
|
||||
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 liveLoading = useApp((s) => s.liveLoading);
|
||||
const liveFetchedAt = useApp((s) => s.liveFetchedAt);
|
||||
|
||||
const liveAge = liveFetchedAt ? `${Math.max(0, Math.floor((Date.now() - liveFetchedAt) / 1000))}s ago` : null;
|
||||
|
||||
return (
|
||||
<div className={`shell shell-${scene}`}>
|
||||
{scene !== "landing" && (
|
||||
<header className="topbar" data-anchor="topbar">
|
||||
<button className="brand-lock brand-btn" onClick={() => setScene("landing")} aria-label="Home">
|
||||
<span className="brand-mark sm" />
|
||||
<span className="brand-name">FlowMaster</span>
|
||||
<span className="brand-divider" />
|
||||
<span className="brand-sub">Mission Control</span>
|
||||
</button>
|
||||
|
||||
<nav className="tabs" role="tablist">
|
||||
<button role="tab" aria-selected={scene === "mission"} className={`tab${scene === "mission" ? " tab-sel" : ""}`} onClick={() => setScene("mission")}>
|
||||
<Layers size={13} /> Mission
|
||||
</button>
|
||||
<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={false} className="tab" onClick={() => setScene("landing")}>
|
||||
<Home size={13} /> Home
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div className="topbar-mid">
|
||||
{sc && (
|
||||
<div className="topbar-context">
|
||||
<span className="topbar-chip">
|
||||
<span className="dot dot-running" /> {sc.family.label}
|
||||
</span>
|
||||
<span className="topbar-chip">
|
||||
<span className="mono">{sc.defName.slice(0, 40)}</span>
|
||||
</span>
|
||||
<span className="topbar-chip mono">{sc.version}</span>
|
||||
{sc.live ? (
|
||||
<span className="tag tag-live">live · {liveMeta.fetchedFrom?.replace("https://", "")}</span>
|
||||
) : (
|
||||
<span className="tag tag-syn">blueprint</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="topbar-actions">
|
||||
<button
|
||||
className={`link-btn mode-toggle mode-${mode}`}
|
||||
onClick={() => setMode(mode === "live" ? "snapshot" : "live")}
|
||||
disabled={liveLoading}
|
||||
title={mode === "live"
|
||||
? `Live mode is on. Fetched ${liveAge}. Click to drop back to snapshot.`
|
||||
: "Click to fetch live scenarios from demo.flow-master.ai in the browser."}
|
||||
>
|
||||
{liveLoading ? <span className="spin" /> : <Pulse size={12} />}
|
||||
<span className={`mode-pill mode-${mode}`}>{mode === "live" ? "LIVE" : "SNAPSHOT"}</span>
|
||||
{mode === "live" && liveAge && <span className="topbar-age">{liveAge}</span>}
|
||||
</button>
|
||||
{mode === "live" && (
|
||||
<button className="link-btn" onClick={() => setMode("live")} disabled={liveLoading} title="Re-fetch live scenarios">
|
||||
<Refresh size={12} /> Refresh
|
||||
</button>
|
||||
)}
|
||||
<button className="link-btn" onClick={startTour} disabled={tourActive}>
|
||||
<Sparkles size={13} /> Tour
|
||||
</button>
|
||||
<button className="link-btn" onClick={() => setCmdOpen(true)}>
|
||||
<Cmd size={13} /> Command <kbd>⌘K</kbd>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
)}
|
||||
|
||||
<div className="scene">
|
||||
{scene === "landing" && <Landing />}
|
||||
{scene === "mission" && <MissionControl />}
|
||||
{scene === "history" && <RunHistory />}
|
||||
</div>
|
||||
|
||||
<CommandBar />
|
||||
<Tour />
|
||||
<Toaster />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user