Files
canvas-frontend/src/scenes/Landing.tsx
T
shad e3b4ed62c0 fix(oracle-r2): same-origin baseUrl + refresh actually re-fetches
Oracle round-2 review caught two real bugs:

1. Production baseUrl bypassed the nginx /api proxy
   - api.ts defaulted to https://demo.flow-master.ai in prod
   - Browser would hit cross-origin and CORS-fail
   - Now: baseUrl='' everywhere; nginx.conf already reverse-proxies /api/*
   - vite.config.ts proxy still handles dev

2. Refresh button didn't refresh
   - setMode('live') early-returned when already in live mode
   - Now: setMode() and refreshLive() share runLiveFetch(); refreshLive
     ignores the same-mode guard and always re-runs
   - 4 new vitest regressions in state/store.test.ts cover the contract
   - Smoke now asserts /api/ea2/work-items is called twice after Refresh

Also:
- buildScenarios.ts parallelized: cap N=6 candidates, Promise.all per-
  candidate fetches → live mode now ~3s instead of 30s
- CommandBar + LeftRail preview toasts now name the exact endpoint
  (/api/runtime/transactions/{id}/actions) in the visible text
- Landing 'Go live' button rebound to refreshLive() when already live;
  copy changed to 'Live · refresh'
- README: scenario table now renders (added separator row); deploy
  section points at the real ops PR + the actual overlay path
  (overlays/demo, not overlays/mc.flow-master.ai); CORS doc clarifies
  same-origin requirement

Constraint: browsers reject cross-origin → same-origin /api/* required
Rejected: dev/prod baseUrl divergence | created production bug
Confidence: high
Scope-risk: narrow
Not-tested: production image actually built + served by ops PR (gated by trusted updater + DNS)
2026-06-14 00:26:38 +04:00

134 lines
5.7 KiB
TypeScript

// Cinematic landing scene: brand, scenarios pick, start tour CTA.
import { motion } from "framer-motion";
import { useApp } from "../state/store";
import { liveMeta } from "../data/scenarios";
import { Sparkles, Arrow, Cmd, Bot, Pulse } from "../components/icons";
export default function Landing() {
const setScene = useApp((s) => s.setScene);
const setScenarioId = useApp((s) => s.setScenarioId);
const startTour = useApp((s) => s.startTour);
const setCmdOpen = useApp((s) => s.setCmdOpen);
const scenarios = useApp((s) => s.scenarios);
const mode = useApp((s) => s.mode);
const setMode = useApp((s) => s.setMode);
const refreshLive = useApp((s) => s.refreshLive);
const liveLoading = useApp((s) => s.liveLoading);
const liveTotals = useApp((s) => s.liveTotals);
return (
<div className="landing">
<div className="landing-bg" aria-hidden />
<div className="landing-grid" aria-hidden />
<header className="landing-top">
<div className="brand-lock">
<span className="brand-mark" />
<span className="brand-name">FlowMaster</span>
<span className="brand-divider" />
<span className="brand-sub">Mission Control</span>
</div>
<button className="link-btn" onClick={() => setCmdOpen(true)}>
<Cmd size={13} /> Command <kbd>K</kbd>
</button>
</header>
<main className="landing-main">
<motion.div
className="landing-hero"
initial={{ opacity: 0, y: 16 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.45 }}
>
<span className="hero-eyebrow"><Sparkles size={13} /> Business-as-code · Mission Control</span>
<h1 className="hero-title">
Every process. <span className="hl">One control surface.</span>
</h1>
<p className="hero-sub">
FlowMaster turns the operational map of a company into living,
typed processes backed by humans, agents, and rules and gives you a
single command-center to drive them. Procurement scenarios are real,
backed by EA2 on{" "}
<span className="mono">{liveMeta.fetchedFrom?.replace("https://", "") ?? "demo"}</span>;
AR, HCM, GL, and Service are industry blueprints showing how this
same shell extends to any process family.
</p>
<div className="hero-actions">
<button className="btn btn-primary btn-lg" onClick={startTour}>
<Sparkles size={14} /> Start guided tour
</button>
<button className="btn btn-ghost btn-lg" onClick={() => setScene("mission")}>
Skip to Mission Control <Arrow size={13} />
</button>
<button
className={`btn btn-ghost btn-lg${mode === "live" ? " is-on" : ""}`}
onClick={() => mode === "live" ? refreshLive() : setMode("live")}
disabled={liveLoading}
title={mode === "live" ? "Re-fetch live scenarios from demo.flow-master.ai" : "Toggle between bundled snapshot and a live in-browser fetch"}
>
{liveLoading ? <span className="spin" /> : <Pulse size={13} />}
{mode === "live" ? "Live · refresh" : "Go live"}
</button>
</div>
<div className="hero-stats">
<div className="stat">
<Pulse size={12} />
<span className="stat-v mono">{liveTotals?.workItems ?? liveMeta.workItems}</span>
<span className="stat-l">{mode === "live" ? "live work items (now)" : "snapshot work items"}</span>
</div>
<div className="stat">
<span className="stat-v mono">{liveTotals?.distinctDefs ?? liveMeta.distinctDefs}</span>
<span className="stat-l">process definitions</span>
</div>
<div className="stat">
<Bot size={12} />
<span className="stat-v mono">{scenarios.length}</span>
<span className="stat-l">scenarios in catalog</span>
</div>
<div className="stat">
<span className={`mode-pill mode-${mode}`}>{mode === "live" ? "LIVE" : "SNAPSHOT"}</span>
<span className="stat-l">data mode</span>
</div>
</div>
</motion.div>
<motion.div
className="landing-cards"
initial={{ opacity: 0, y: 18 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.12, duration: 0.45 }}
>
{scenarios.map((s) => (
<button
key={s.id}
className="sc-card"
style={{ ["--sc-accent" as string]: s.family.accent }}
onClick={() => { setScenarioId(s.id); setScene("mission"); }}
>
<div className="sc-card-top">
<span className="sc-card-mark" />
<span className={`tag ${s.live ? "tag-live" : "tag-syn"}`}>{s.live ? "live" : "blueprint"}</span>
</div>
<h3 className="sc-card-title">{s.family.label}</h3>
<p className="sc-card-sub">{s.family.subtitle}</p>
<div className="sc-card-meta">
<span>{s.steps.length} steps</span>
<span>·</span>
<span>{s.queue.length} cases</span>
<span>·</span>
<span className="mono">{s.version}</span>
</div>
<div className="sc-card-cta">Open <Arrow size={12} /></div>
</button>
))}
</motion.div>
</main>
<footer className="landing-foot">
<span className="foot-eyebrow">FlowMaster · Mission Control demo · synthesised on top of demo.flow-master.ai</span>
</footer>
</div>
);
}