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:
2026-06-14 00:09:32 +04:00
commit 3ffd0e68a7
45 changed files with 15603 additions and 0 deletions
+134
View File
@@ -0,0 +1,134 @@
// Bottom telemetry strip.
// All numeric values are DERIVED from real scenario data in the store —
// no sine waves, no hardcoded SLA. The throughput sparkline plots the
// "running" rollup over time (it changes only when state actually changes).
// The "ui tick" dot is a UI heartbeat labelled as such.
import { useEffect, useMemo, useState } from "react";
import { useApp, scenarioById } from "../state/store";
import { liveMeta } from "../data/scenarios";
import { Pulse, Spark } from "./icons";
function Sparkline({ values, color, label }: { values: number[]; color: string; label: string }) {
const w = 120, h = 28, pad = 2;
if (!values.length) return <svg width={w} height={h} aria-label={label} />;
const max = Math.max(...values, 1);
const min = Math.min(...values, 0);
const span = Math.max(1, max - min);
const pts = values
.map((v, i) => {
const x = pad + ((w - pad * 2) * i) / Math.max(1, values.length - 1);
const y = h - pad - ((v - min) / span) * (h - pad * 2);
return `${x.toFixed(1)},${y.toFixed(1)}`;
})
.join(" ");
return (
<svg width={w} height={h} className="spark" aria-label={label}>
<polyline points={pts} fill="none" stroke={color} strokeWidth={1.6} />
</svg>
);
}
const HIST_LEN = 24;
export default function Telemetry() {
const scenarioId = useApp((s) => s.scenarioId);
const scenarios = useApp((s) => s.scenarios);
const mode = useApp((s) => s.mode);
const sc = scenarioById(scenarioId);
// Real rollup across every loaded scenario.
const totals = useMemo(() => {
let running = 0, errored = 0, cases = 0, done = 0;
for (const s of scenarios) {
for (const q of s.queue) {
cases += 1;
if (q.status === "running" || q.status === "waiting_for_user" || q.status === "waiting_for_agent") running += 1;
else if (q.status === "errored" || q.status === "failed") errored += 1;
else if (q.status === "completed" || q.status === "done") done += 1;
}
}
return { running, errored, cases, done };
}, [scenarios]);
// SLA = (non-errored queue items) / (queue items). Real, derived.
const sla = totals.cases === 0 ? 1 : 1 - totals.errored / totals.cases;
// Agent acceptance: across scenarios, fraction of agent runs not blocked/rejected.
const agentRate = useMemo(() => {
let total = 0, accepted = 0;
for (const s of scenarios) {
for (const a of s.agentRuns) {
total += 1;
if (a.status !== "proposed") accepted += 1;
}
}
return total === 0 ? 1 : accepted / total;
}, [scenarios]);
// Sparkline = history of `running` totals. Updates only when totals change.
const [history, setHistory] = useState<number[]>(() => Array(HIST_LEN).fill(totals.running));
useEffect(() => {
const t = setInterval(() => {
setHistory((h) => [...h.slice(1), totals.running]);
}, 1500);
return () => clearInterval(t);
}, [totals.running]);
return (
<div className="telemetry" data-anchor="telemetry">
<div className="t-block">
<span className="t-eyebrow"><Pulse size={11} /> running over time</span>
<Sparkline values={history} color="var(--run)" label="running cases over the last 36 seconds" />
<span className="t-v mono">{totals.running} now</span>
</div>
<div className="t-divider" />
<div className="t-block">
<span className="t-eyebrow">cases</span>
<span className="t-v mono">{totals.cases}</span>
</div>
<div className="t-block">
<span className="t-eyebrow">running</span>
<span className="t-v mono" style={{ color: "var(--run)" }}>{totals.running}</span>
</div>
<div className="t-block">
<span className="t-eyebrow">errored</span>
<span className="t-v mono" style={{ color: totals.errored > 0 ? "var(--block)" : "var(--text-2)" }}>{totals.errored}</span>
</div>
<div className="t-divider" />
<div className="t-block" title="derived: non-errored / total queue items">
<span className="t-eyebrow"><Spark size={11} /> sla (derived)</span>
<Gauge value={sla} />
<span className="t-v mono">{(sla * 100).toFixed(1)}%</span>
</div>
<div className="t-divider" />
<div className="t-block" title="derived: fraction of agent runs not in 'proposed' state">
<span className="t-eyebrow">agent acceptance (derived)</span>
<Gauge value={agentRate} accent="var(--ok)" />
<span className="t-v mono">{(agentRate * 100).toFixed(0)}%</span>
</div>
<div className="t-spacer" />
<div className="t-block">
<span className="t-eyebrow">scenario</span>
<span className="t-v">{sc?.family.label ?? "—"}</span>
</div>
<div className="t-block">
<span className="t-eyebrow">mode</span>
<span className={`mode-pill mode-${mode}`}>{mode === "live" ? "LIVE" : "SNAPSHOT"}</span>
</div>
<div className="t-block">
<span className="t-eyebrow">source</span>
<span className="t-v mono">{liveMeta.fetchedFrom?.replace("https://", "") ?? "—"}</span>
</div>
<div className="t-tick" aria-label="ui tick" title="UI heartbeat — pulses every 1.5s, does not represent backend activity" />
</div>
);
}
function Gauge({ value, accent = "var(--run)" }: { value: number; accent?: string }) {
const w = 60, h = 12;
return (
<div className="gauge" style={{ width: w, height: h }}>
<div className="gauge-fill" style={{ width: `${Math.min(100, Math.max(0, value * 100))}%`, background: accent }} />
</div>
);
}