Every scene now reads as the same FM doctrine: paper canvas + navy
frame + amber accent, 1px hairlines, square edges, monospace
uppercase labels, no glass, no shadows, no gradients.
WHAT CHANGED
- src/index.css rewritten end-to-end. Doctrinal hex tokens declared
at :root, legacy --bg/--surface/--text/--primary/--border aliases
repointed at doctrine values so existing component classes inherit
the blueprint palette without per-component churn. Global
* { border-radius: 0 } + box-shadow strip-out. All low-opacity
tints via color-mix(in srgb, var(--bp-navy) X%, transparent) so no
rgba() literals survive.
- Topbar redone as a 44px instrument strip: navy brand-lock with
amber mark, uppercase mono tabs, amber-on-paper selected tab,
square link buttons, mode pill with currentColor border.
- Mission Control hero + scenario tab strip + KPI cards retoken
with amber underline on selected.
- Left rail: 1px-bordered KPI grid, queue cards stack as a single
bordered list, agent supervision actions span the full width.
- Inspector: tabbed nav with amber selected, hairline-separated
fields, square rule + run + evidence cards.
- Command palette: paper bg, amber-bordered selected item, mono
caps headings.
- Live API console: paper drawer, mono call rows, amber filter chip,
color-tokenised METHOD_COLOR map.
- Toaster: left-border accent on paper surface.
- Telemetry: navy gauges, mono row, square tick.
- Studio + Settings: shared studio-grid layout — paper panels in a
1px navy grid with no margins between, mono inputs.
- Run History: mono table rows with amber selected filter chip.
- Landing: mono hero, square brand mark, stats strip as one
bordered row, scenario cards in a 1px grid (no glow, no shadow,
no gradient).
- Family accents in synthetic.ts and buildScenarios.ts retoken to
doctrine hex. src/scenarios.json snapshot patched in place.
- Console.tsx METHOD_COLOR map → var(--bp-muted/info/amber/err).
AUDIT
- qa/palette_audit.mjs upgraded: scans 31 source files (.ts/.tsx/
.json + index.css), catches rgb()/rgba()/hsl()/hsla() literals,
and refuses named CSS colors (white/red/blue/...) as background/
color/fill/stroke/border values. Hex regex uses (?![0-9a-fA-F])
lookahead so deploy-id text like '#d3f1a' is not a false positive.
Result: 0 non-doctrinal literals anywhere in src/.
- qa/smoke.mjs + qa/smoke_blueprint.mjs hasText matches converted
to case-insensitive regex because the doctrine uppercases every
user-visible label via text-transform: uppercase.
- qa/snap_all_scenes.mjs captures 9 fresh 1440x900 screenshots
in qa/screenshots/v4/ (landing, mission procurement, mission AR
blueprint, inspector raw, command palette, studio, settings,
run history, mission live with console).
VERIFY
- tsc -b clean
- vite build green (CSS 41 KB / 8 KB gz, JS 851 KB / 230 KB gz)
- vitest 5 files / 24 tests green
- main smoke 27/27, 0 console errors
- blueprint smoke 15/15, 0 console errors
- palette audit clean (31 files)
ORACLE-REVIEWED
Round 1 PASS with <promise>VERIFIED</promise>. Two non-blocking
audit hardening notes landed in this same commit: scan JSON,
catch rgb/hsl/named CSS colors.
Confidence: high
Scope-risk: moderate (theme sweep, all scenes touched)
Not-tested: pixel-level visual diff vs prior theme (Oracle could
not inspect images this round; relied on programmatic checks)
117 lines
4.9 KiB
TypeScript
117 lines
4.9 KiB
TypeScript
// Live API call log. Every fetch the app makes shows up here in real time.
|
|
// Replaces the old guided-tour overlay.
|
|
import { useEffect, useRef, useState } from "react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { useApp } from "../state/store";
|
|
import { Close, Refresh, Layers, Pulse } from "./icons";
|
|
|
|
const METHOD_COLOR: Record<string, string> = {
|
|
GET: "var(--bp-muted)",
|
|
POST: "var(--bp-info)",
|
|
PUT: "var(--bp-amber)",
|
|
PATCH: "var(--bp-amber)",
|
|
DELETE: "var(--bp-err)",
|
|
};
|
|
|
|
function statusClass(s: number): string {
|
|
if (s === 0) return "err";
|
|
if (s >= 500) return "err";
|
|
if (s >= 400) return "warn";
|
|
if (s >= 200) return "ok";
|
|
return "info";
|
|
}
|
|
|
|
export default function Console() {
|
|
const open = useApp((s) => s.consoleOpen);
|
|
const setOpen = useApp((s) => s.setConsoleOpen);
|
|
const log = useApp((s) => s.apiLog);
|
|
const clear = useApp((s) => s.clearApiLog);
|
|
const [expanded, setExpanded] = useState<number | null>(null);
|
|
const [filter, setFilter] = useState<"all" | "writes" | "errors">("all");
|
|
const tailRef = useRef<HTMLDivElement | null>(null);
|
|
|
|
useEffect(() => {
|
|
if (!open) return;
|
|
tailRef.current?.scrollTo({ top: tailRef.current.scrollHeight, behavior: "smooth" });
|
|
}, [log, open]);
|
|
|
|
const filtered = log.filter((c) => {
|
|
if (filter === "writes") return c.method !== "GET";
|
|
if (filter === "errors") return c.status === 0 || c.status >= 400;
|
|
return true;
|
|
});
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{open && (
|
|
<motion.aside
|
|
className="console"
|
|
initial={{ x: 360, opacity: 0 }}
|
|
animate={{ x: 0, opacity: 1 }}
|
|
exit={{ x: 360, opacity: 0 }}
|
|
transition={{ duration: 0.2 }}
|
|
aria-label="API call console"
|
|
>
|
|
<header className="console-head">
|
|
<Pulse size={13} />
|
|
<span className="console-title">Live console</span>
|
|
<span className="console-count mono">{log.length}</span>
|
|
<div className="console-filters" role="tablist">
|
|
<button className={`console-filter${filter === "all" ? " on" : ""}`} onClick={() => setFilter("all")}>all</button>
|
|
<button className={`console-filter${filter === "writes" ? " on" : ""}`} onClick={() => setFilter("writes")}>writes</button>
|
|
<button className={`console-filter${filter === "errors" ? " on" : ""}`} onClick={() => setFilter("errors")}>errors</button>
|
|
</div>
|
|
<button className="console-x" title="Clear" onClick={clear}><Refresh size={11} /></button>
|
|
<button className="console-x" title="Close" onClick={() => setOpen(false)}><Close size={12} /></button>
|
|
</header>
|
|
<div className="console-body" ref={tailRef}>
|
|
{filtered.length === 0 && (
|
|
<div className="empty">No API calls yet. Flip to LIVE mode or perform an action.</div>
|
|
)}
|
|
{filtered.map((c) => {
|
|
const isExpanded = expanded === c.id;
|
|
const shortPath = c.path.replace(/^https?:\/\/[^/]+/, "");
|
|
return (
|
|
<div key={c.id} className={`call ${statusClass(c.status)}`}>
|
|
<button
|
|
className="call-head"
|
|
onClick={() => setExpanded(isExpanded ? null : c.id)}
|
|
title="Toggle body"
|
|
>
|
|
<span className="call-method mono" style={{ color: METHOD_COLOR[c.method] || "var(--text-2)" }}>{c.method}</span>
|
|
<span className="call-status mono">{c.status || "ERR"}</span>
|
|
<span className="call-path mono" title={c.path}>{shortPath}</span>
|
|
<span className="call-dur mono">{c.durationMs}ms</span>
|
|
</button>
|
|
{isExpanded && (
|
|
<div className="call-body">
|
|
{c.reqBody !== undefined && (
|
|
<>
|
|
<div className="call-label"><Layers size={10} /> request</div>
|
|
<pre className="call-json"><code>{JSON.stringify(c.reqBody, null, 2)}</code></pre>
|
|
</>
|
|
)}
|
|
{c.error && (
|
|
<>
|
|
<div className="call-label call-err"><Layers size={10} /> network error</div>
|
|
<pre className="call-json"><code>{c.error}</code></pre>
|
|
</>
|
|
)}
|
|
{c.resBody !== undefined && (
|
|
<>
|
|
<div className="call-label"><Layers size={10} /> response</div>
|
|
<pre className="call-json"><code>{typeof c.resBody === "string" ? c.resBody : JSON.stringify(c.resBody, null, 2)}</code></pre>
|
|
</>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</motion.aside>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
}
|