a11oy / web /console /src /App.tsx
betterwithage's picture
sync(space): complete build context — fix BUILD_ERROR (CTO)
518343a verified
Raw
History Blame Contribute Delete
5.79 kB
// Copyright 2026 SZL Holdings
// SPDX-License-Identifier: Apache-2.0
//
// App.tsx — the a11oy operator console SPA. Five working routes, each backed by
// a real a11oy `serve` HTTP endpoint via A11oyClient. Self-contained: the only
// dependencies are react, react-dom, and wouter. It does not import the legacy
// 471-import surface (web/src/App.tsx); that surface is tracked separately as
// engineering debt and is left in place per Operating Principle #10 (annotate,
// do not silently delete).
//
// The a11oy base URL is read from VITE_A11OY_BASE_URL (default same origin).
//
// Authored for SZL Holdings. Signed-off per repository DCO.
import { useEffect, useState } from "react";
import { Link, Route, Router, Switch, useRoute } from "wouter";
import { A11oyClient, CONSOLE_ROUTES, type OperationalReceipt } from "./a11oyClient.ts";
// The SPA is deployed at /console/ (Vite base=/console/). Wouter must strip
// that prefix from browser pathnames before matching routes.
const ROUTER_BASE = "/console";
const BASE = (import.meta.env?.VITE_A11OY_BASE_URL as string | undefined) ?? "";
const client = new A11oyClient(BASE || window.location.origin);
function useAsync<T>(fn: () => Promise<T>, deps: unknown[] = []): { data: T | null; error: string | null; loading: boolean } {
const [data, setData] = useState<T | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let live = true;
setLoading(true);
fn()
.then((d) => { if (live) { setData(d); setError(null); } })
.catch((e) => { if (live) setError(String(e)); })
.finally(() => { if (live) setLoading(false); });
return () => { live = false; };
// eslint-disable-next-line react-hooks/exhaustive-deps
}, deps);
return { data, error, loading };
}
function HealthRoute() {
const health = useAsync(() => client.health(), []);
const ready = useAsync(() => client.readiness(), []);
return (
<section>
<h2>Health</h2>
{health.loading ? <p>loading…</p> : health.error ? <p className="err">{health.error}</p> : (
<pre>{JSON.stringify(health.data, null, 2)}</pre>
)}
<h3>Readiness</h3>
{ready.loading ? <p>loading…</p> : ready.error ? <p className="err">{ready.error}</p> : (
<pre>{JSON.stringify(ready.data, null, 2)}</pre>
)}
</section>
);
}
function LedgerRoute() {
const page = useAsync(() => client.ledger(20), []);
return (
<section>
<h2>Proof Ledger</h2>
{page.loading ? <p>loading…</p> : page.error ? <p className="err">{page.error}</p> : (
<>
<p>{page.data?.count} of {page.data?.total} receipts</p>
<ul>
{page.data?.receipts.map((r: OperationalReceipt, i) => (
<li key={i}>
<Link href={`/receipt/${encodeURIComponent(String(r.merkle_root ?? r.receipt_id ?? i))}`}>
{String(r.receipt_id ?? r.merkle_root ?? `#${i}`)}
</Link>
</li>
))}
</ul>
</>
)}
</section>
);
}
function ReceiptRoute() {
const [, params] = useRoute("/receipt/:hash");
const hash = params?.hash ?? "";
const out = useAsync(() => client.receipt(hash), [hash]);
return (
<section>
<h2>Receipt {hash}</h2>
{out.loading ? <p>loading…</p> : out.error ? <p className="err">{out.error}</p> : (
<pre>{JSON.stringify(out.data?.receipt, null, 2)}</pre>
)}
</section>
);
}
function VerifyRoute() {
const [text, setText] = useState("[]");
const [result, setResult] = useState<string>("");
const run = async () => {
try {
const ledger = JSON.parse(text);
const r = await client.verify(ledger);
setResult(JSON.stringify(r, null, 2));
} catch (e) {
setResult(String(e));
}
};
return (
<section>
<h2>Verify</h2>
<p>Paste a receipt ledger array; POSTs to <code>/v1/verify</code>.</p>
<textarea value={text} onChange={(e) => setText(e.target.value)} rows={8} cols={60} />
<div><button onClick={run}>Verify chain</button></div>
<pre>{result}</pre>
</section>
);
}
function PolicyRoute() {
const [severity, setSeverity] = useState("medium");
const [result, setResult] = useState<string>("");
const run = async () => {
const r = await client.evaluatePolicy({ actionId: "console-action", severity: severity as never, confidence: 0.8 });
setResult(JSON.stringify(r, null, 2));
};
return (
<section>
<h2>Policy</h2>
<p>Evaluate a severity against the real threshold gate (<code>/v1/policy/evaluate</code>).</p>
<select value={severity} onChange={(e) => setSeverity(e.target.value)}>
{["low", "medium", "high", "critical"].map((s) => <option key={s} value={s}>{s}</option>)}
</select>
<button onClick={run}>Evaluate</button>
<pre>{result}</pre>
</section>
);
}
export default function App() {
return (
<Router base={ROUTER_BASE}>
<div className="console">
<header>
<h1>a11oy operator console</h1>
<nav>
{CONSOLE_ROUTES.map((r) => (
<Link key={r.path} href={r.path.replace(":hash", "")}>{r.label}</Link>
))}
</nav>
</header>
<main>
<Switch>
<Route path="/" component={HealthRoute} />
<Route path="/ledger" component={LedgerRoute} />
<Route path="/receipt/:hash" component={ReceiptRoute} />
<Route path="/verify" component={VerifyRoute} />
<Route path="/policy" component={PolicyRoute} />
<Route>404 — not a console route</Route>
</Switch>
</main>
</div>
</Router>
);
}