Spaces:
Running
Running
File size: 5,794 Bytes
518343a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | // 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>
);
}
|