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
+66
View File
@@ -0,0 +1,66 @@
// Guided tour overlay. Reads the active scenario's tour script and
// anchors a step-card to the named region.
import { useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { useApp, scenarioById } from "../state/store";
import { Sparkles, ChevL, ChevR, Close } from "./icons";
export default function Tour() {
const tour = useApp((s) => s.tour);
const scenarioId = useApp((s) => s.scenarioId);
const endTour = useApp((s) => s.endTour);
const tourPrev = useApp((s) => s.tourPrev);
const tourNext = useApp((s) => s.tourNext);
const sc = scenarioById(scenarioId);
const step = sc?.tour[tour.index] ?? null;
useEffect(() => {
if (!tour.active) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "ArrowRight" || e.key === "Enter") { e.preventDefault(); tourNext(); }
if (e.key === "ArrowLeft") { e.preventDefault(); tourPrev(); }
if (e.key === "Escape") { e.preventDefault(); endTour(); }
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [tour.active, tourPrev, tourNext, endTour]);
if (!tour.active || !step || !sc) return null;
const last = tour.index === sc.tour.length - 1;
const first = tour.index === 0;
return (
<AnimatePresence>
<motion.div
key={step.id}
className={`tour-card tour-anchor-${step.anchor}`}
initial={{ opacity: 0, y: 8, scale: 0.98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: -6 }}
transition={{ duration: 0.22 }}
>
<div className="tour-head">
<Sparkles size={14} />
<span className="tour-eyebrow">Guided tour · {tour.index + 1} / {sc.tour.length}</span>
<button className="tour-close" onClick={endTour} aria-label="End tour"><Close size={13} /></button>
</div>
<h3 className="tour-title">{step.title}</h3>
<p className="tour-body">{step.body}</p>
<div className="tour-actions">
<button className="btn btn-ghost" onClick={tourPrev} disabled={first}>
<ChevL size={13} /> Back
</button>
{last ? (
<button className="btn btn-primary" onClick={endTour}>Finish</button>
) : (
<button className="btn btn-primary" onClick={tourNext}>
Next <ChevR size={13} />
</button>
)}
</div>
</motion.div>
</AnimatePresence>
);
}