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:
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user