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
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
// MissionControl scene: scenario tabs + graph + rails + telemetry strip.
|
|
import { useApp, scenarioById } from "../state/store";
|
|
import ProcessGraph from "../components/ProcessGraph";
|
|
import LeftRail from "../components/LeftRail";
|
|
import Inspector from "../components/Inspector";
|
|
import Telemetry from "../components/Telemetry";
|
|
|
|
export default function MissionControl() {
|
|
const scenarioId = useApp((s) => s.scenarioId);
|
|
const setScenarioId = useApp((s) => s.setScenarioId);
|
|
const scenarios = useApp((s) => s.scenarios);
|
|
const liveLoading = useApp((s) => s.liveLoading);
|
|
const liveError = useApp((s) => s.liveError);
|
|
const sc = scenarioById(scenarioId);
|
|
|
|
return (
|
|
<div className="mc">
|
|
{liveLoading && (
|
|
<div className="mc-banner mc-banner-info">
|
|
<span className="spin" /> Fetching live scenarios from demo.flow-master.ai…
|
|
</div>
|
|
)}
|
|
{liveError && (
|
|
<div className="mc-banner mc-banner-err">
|
|
Live mode failed: <span className="mono">{liveError}</span> · showing snapshot
|
|
</div>
|
|
)}
|
|
<div className="mc-strip" role="tablist" aria-label="Scenarios">
|
|
{scenarios.map((s) => (
|
|
<button
|
|
key={s.id}
|
|
role="tab"
|
|
aria-selected={s.id === scenarioId}
|
|
className={`mc-tab${s.id === scenarioId ? " mc-tab-sel" : ""}`}
|
|
style={{ ["--accent" as string]: s.family.accent }}
|
|
onClick={() => setScenarioId(s.id)}
|
|
>
|
|
<span className="mc-tab-dot" />
|
|
<span className="mc-tab-label">{s.family.label}</span>
|
|
<span className={`mc-tab-mark ${s.live ? "is-live" : "is-syn"}`}>{s.live ? "live" : "bp"}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div className="mc-hero">
|
|
<div>
|
|
<div className="mc-hero-eyebrow">{sc?.family.subtitle}</div>
|
|
<h2 className="mc-hero-title">{sc?.defName}</h2>
|
|
<div className="mc-hero-sub">{sc?.tagline}</div>
|
|
</div>
|
|
<div className="mc-hero-kpis">
|
|
{sc?.kpis.slice(0, 4).map((k) => (
|
|
<div className="mc-kpi" key={k.label}>
|
|
<div className="mc-kpi-v">{k.value}</div>
|
|
<div className="mc-kpi-l">{k.label}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mc-body">
|
|
<LeftRail />
|
|
<main className="mc-main">
|
|
<ProcessGraph />
|
|
</main>
|
|
<Inspector />
|
|
</div>
|
|
|
|
<Telemetry />
|
|
</div>
|
|
);
|
|
}
|