Files
canvas-frontend/src/components/Console.tsx
T
shad dba1eb3328 feat(theme): promote industrial blueprint to the whole shell
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)
2026-06-14 02:12:11 +04:00

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>
);
}