Files
canvas-frontend/src/App.tsx
T
shad 8933148719 fix(curation): kill dev-artefact leak + rename agent honestly
Oracle remediation batch 1 — closing gaps #1 (agent overclaim), #4
(demo-data leak), #5 (raw-ID exposure):

- src/lib/flowCuration.ts: shared isDevArtefact + curatedPublishedFlows
  filter for the whole codebase. Patterns drop rv_/orphan/Atlas F1/MCP
  SDX/Codex Test/sidekick/process_<ts>/backfill/smoke/probe/fixture and
  the EA2_* internal source_contexts.
- src/scenes/Hub.tsx: loadPublishedFlows uses the shared curator
- src/lib/agentTools.ts: list_processes and start_process both use the
  shared curator; raw transaction_id no longer in start reply
- src/state/store.ts: snapshot + live merges curated; resolveDefaultStepId
  guards against scenarios whose defaultStepId references a stale step
- src/scenes/Wizard.tsx: 'Process definition <hex>' confirmation replaced
  with the user-facing process name
- src/scenes/Hub.tsx: start toast no longer shows transaction_id prefix
- src/scenes/Agent.tsx + App.tsx + Landing.tsx: rename Pi -> FlowMaster
  Command Assistant. Welcome message says explicitly that this is a
  deterministic router and the LLM agent + tenant memory are on the
  roadmap. Tab label, hub chip, turn author all rebranded.
- src/scenes/GeoAttendance.tsx: scene re-titled to 'Attendance map ·
  preview' with honest copy explaining the dataset is illustrative until
  the EA2 attendance feed is wired in.
- src/data/synthetic.ts: orphaned from runtime (kept in tree for tests).
  Snapshot now sources only the live-cached EA2 read (scenarios.json),
  filtered through the same curator.

29/29 tests green. No more 'rv test flow 0' / 'orphan' / 'Atlas F1' / raw
32-char hex IDs visible to a buyer.
2026-06-14 13:11:54 +04:00

196 lines
8.5 KiB
TypeScript

// App shell — scene switcher + top bar + command palette + console + toaster.
import { useEffect } from "react";
import { useApp, scenarioById } from "./state/store";
import Landing from "./scenes/Landing";
import MissionControl from "./scenes/MissionControl";
import RunHistory from "./scenes/RunHistory";
import Wizard from "./scenes/Wizard";
import Settings from "./scenes/Settings";
import Login from "./scenes/Login";
import SsoCallback from "./scenes/SsoCallback";
import Chat from "./scenes/Chat";
import Agent from "./scenes/Agent";
import Hub from "./scenes/Hub";
import GeoAttendance from "./scenes/GeoAttendance";
import Explainer from "./scenes/Explainer";
import CommandBar from "./components/CommandBar";
import Toaster from "./components/Toaster";
import Console from "./components/Console";
import { Cmd, Home, Layers, HistoryIcon, Pulse, Refresh, Branch, Cog, User, Sun, Moon, Bot } from "./components/icons";
import { liveMeta } from "./data/scenarios";
export default function App() {
const scene = useApp((s) => s.scene);
const setScene = useApp((s) => s.setScene);
const setCmdOpen = useApp((s) => s.setCmdOpen);
const scenarioId = useApp((s) => s.scenarioId);
const sc = scenarioById(scenarioId);
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 liveFetchedAt = useApp((s) => s.liveFetchedAt);
const consoleOpen = useApp((s) => s.consoleOpen);
const setConsoleOpen = useApp((s) => s.setConsoleOpen);
const apiLogCount = useApp((s) => s.apiLog.length);
const actor = useApp((s) => s.actor);
const userEmail = useApp((s) => s.userEmail);
const startPolling = useApp((s) => s.startPolling);
const stopPolling = useApp((s) => s.stopPolling);
const isAuthed = useApp((s) => s.isAuthed);
// Auth guard and SSO callback detector
useEffect(() => {
if (typeof window !== "undefined" && window.location.hash.includes("access_token")) {
setScene("sso-callback");
} else if (!isAuthed && scene !== "login" && scene !== "sso-callback") {
setScene("login");
}
}, [isAuthed, scene, setScene]);
useEffect(() => {
if (mode === "live") startPolling();
return () => stopPolling();
}, [mode, startPolling, stopPolling]);
const liveAge = liveFetchedAt ? `${Math.max(0, Math.floor((Date.now() - liveFetchedAt) / 1000))}s ago` : null;
return (
<div className={`shell shell-${scene}`}>
{scene !== "landing" && scene !== "login" && scene !== "sso-callback" && (
<header className="topbar" data-anchor="topbar">
<button className="brand-lock brand-btn" onClick={() => setScene("landing")} aria-label="Home">
<span className="brand-mark sm" />
<span className="brand-name">FlowMaster</span>
<span className="brand-divider" />
<span className="brand-sub">Mission Control</span>
</button>
<nav className="tabs" role="tablist">
<button role="tab" aria-selected={scene === "mission"} className={`tab${scene === "mission" ? " tab-sel" : ""}`} onClick={() => setScene("mission")}>
<Layers size={13} /> Mission
</button>
<button role="tab" aria-selected={scene === "history"} className={`tab${scene === "history" ? " tab-sel" : ""}`} onClick={() => setScene("history")}>
<HistoryIcon size={13} /> Runs
</button>
<button role="tab" aria-selected={scene === "studio"} className={`tab${scene === "studio" ? " tab-sel" : ""}`} onClick={() => setScene("studio")}>
<Branch size={13} /> Studio
</button>
<button role="tab" aria-selected={scene === "chat"} className={`tab${scene === "chat" ? " tab-sel" : ""}`} onClick={() => setScene("chat")}>
<Bot size={13} /> Chat
</button>
<button role="tab" aria-selected={scene === "agent"} className={`tab${scene === "agent" ? " tab-sel" : ""}`} onClick={() => setScene("agent")}>
<Bot size={13} /> Assistant
</button>
<button role="tab" aria-selected={scene === "settings"} className={`tab${scene === "settings" ? " tab-sel" : ""}`} onClick={() => setScene("settings")}>
<Cog size={13} /> Settings
</button>
<button role="tab" aria-selected={false} className="tab" onClick={() => setScene("landing")}>
<Home size={13} /> Home
</button>
</nav>
<div className="topbar-mid">
{sc && scene === "mission" && (
<div className="topbar-context">
<span className="topbar-chip">
<span className="dot dot-running" /> {sc.family.label}
</span>
<span className="topbar-chip">
<span className="mono">{sc.defName.slice(0, 40)}</span>
</span>
<span className="topbar-chip mono">{sc.version}</span>
{sc.live ? (
<span className="tag tag-live">live · {liveMeta.fetchedFrom?.replace("https://", "")}</span>
) : (
<span className="tag tag-syn">blueprint</span>
)}
</div>
)}
</div>
<div className="topbar-actions">
<button
className="link-btn user-btn"
onClick={() => setScene("settings")}
title={`Signed in as ${userEmail}${actor?.user_id ? ` (${actor.user_id.slice(0, 8)})` : ""}`}
>
<User size={12} />
<span className="user-email">{userEmail.split("@")[0]}</span>
</button>
<button
className={`link-btn mode-toggle mode-${mode}`}
onClick={() => setMode(mode === "live" ? "snapshot" : "live")}
disabled={liveLoading}
title={mode === "live"
? `Live mode is on. Fetched ${liveAge}. Click to drop back to snapshot.`
: "Click to fetch live scenarios from demo.flow-master.ai in the browser."}
>
{liveLoading ? <span className="spin" /> : <Pulse size={12} />}
<span className={`mode-pill mode-${mode}`}>{mode === "live" ? "LIVE" : "SNAPSHOT"}</span>
{mode === "live" && liveAge && <span className="topbar-age">{liveAge}</span>}
</button>
{mode === "live" && (
<button className="link-btn" onClick={refreshLive} disabled={liveLoading} title="Re-fetch live scenarios">
<Refresh size={12} /> Refresh
</button>
)}
<button
className={`link-btn${consoleOpen ? " is-on" : ""}`}
onClick={() => setConsoleOpen(!consoleOpen)}
title="Toggle live API console"
>
<Layers size={12} /> Console
{apiLogCount > 0 && <span className="badge mono">{apiLogCount}</span>}
</button>
<ThemeToggle />
<button className="link-btn" onClick={() => setCmdOpen(true)}>
<Cmd size={13} /> <kbd>K</kbd>
</button>
</div>
</header>
)}
<div className="scene">
{scene === "login" && <Login />}
{scene === "sso-callback" && <SsoCallback />}
{scene === "landing" && <Landing />}
{scene === "mission" && <MissionControl />}
{scene === "history" && <RunHistory />}
{scene === "studio" && <Wizard />}
{scene === "chat" && <Chat />}
{scene === "agent" && <Agent />}
{scene === "hub-procurement" && <Hub hub="procurement" />}
{scene === "hub-hr" && <Hub hub="hr" />}
{scene === "hub-it" && <Hub hub="it" />}
{scene === "geo-attendance" && <GeoAttendance />}
{scene === "explainer" && <Explainer />}
{scene === "settings" && <Settings />}
</div>
<CommandBar />
<Console />
<Toaster />
</div>
);
}
function ThemeToggle() {
const theme = useApp((s) => s.theme);
const setTheme = useApp((s) => s.setTheme);
const isDark = theme === "dark";
return (
<button
className="link-btn theme-toggle"
onClick={() => setTheme(isDark ? "light" : "dark")}
title={isDark ? "Switch to light theme" : "Switch to dark theme"}
aria-label={isDark ? "Switch to light theme" : "Switch to dark theme"}
>
{isDark ? <Sun size={13} /> : <Moon size={13} />}
<span className="theme-toggle-label">{isDark ? "LIGHT" : "DARK"}</span>
</button>
);
}