Spaces:
Running
Running
| // 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> | |
| ); | |
| } | |