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