betterwithage commited on
Commit
6b9527b
·
verified ·
1 Parent(s): ad4cc3e

feat(operator): add /unified 4-pane operator shell (ADDITIVE)

Browse files

Author: Yachay <yachay@szlholdings.dev>
DCO: Signed-off-by: Yachay <yachay@szlholdings.dev>
Change-class: ADDITIVE
Co-Authored-By: Perplexity Computer Agent

Files changed (3) hide show
  1. serve.py +15 -0
  2. static/operator-unified.html +696 -0
  3. web/operator-unified.html +696 -0
serve.py CHANGED
@@ -755,6 +755,21 @@ async def operator_shell() -> FileResponse:
755
  return FileResponse(INDEX_HTML, media_type="text/html")
756
 
757
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
758
 
759
 
760
  # ===========================================================================
 
755
  return FileResponse(INDEX_HTML, media_type="text/html")
756
 
757
 
758
+ # ADDITIVE (Unified 4-pane Operator Shell, 2026-06-01, Yachay / Perplexity
759
+ # Computer Agent): NEW /unified route serving the self-contained 4-pane shell
760
+ # (terrain + right panel + Cmd-K + receipt tunnel). Registered BEFORE the
761
+ # /{full_path:path} catch-all so it resolves LOCALLY. ADDITIVE — does NOT touch
762
+ # the existing /operator route. File ships in static/ (COPY static/ already in
763
+ # Dockerfile). Doctrine v11 LOCKED 749/14/163 unchanged.
764
+ @app.get("/unified")
765
+ @app.get("/killinchu/unified")
766
+ async def unified_operator_shell() -> FileResponse:
767
+ _page = STATIC_DIR / "operator-unified.html"
768
+ if _page.is_file():
769
+ return FileResponse(_page, media_type="text/html")
770
+ return JSONResponse({"error": "operator-unified.html not deployed"}, status_code=404)
771
+
772
+
773
 
774
 
775
  # ===========================================================================
static/operator-unified.html ADDED
@@ -0,0 +1,696 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>a11oy — Unified Operator Shell</title>
7
+ <meta name="description" content="a11oy unified 4-pane operator shell: living terrain, organ health rings, Cmd-K command bar, and receipt tunnel. Sovereign, offline, no CDN." />
8
+ <style>
9
+ /* ===== Sovereign font stack: NO external CDN. Inter / JetBrains Mono if present, else system fallback. ===== */
10
+ :root{
11
+ --bg:#0A0E1A; /* near-black navy */
12
+ --green:#00C389; /* healthy */
13
+ --amber:#F0B429; /* warning */
14
+ --red:#DF2A4A; /* critical */
15
+ --gray:#64748B; /* offline */
16
+ --purple:#7C3AED; /* anomaly */
17
+ --text:#E2E8F0; /* text primary */
18
+ --muted:#94A3B8; /* text muted */
19
+ --gold:#D4A444; /* brand accent (Khipu glyph + headers) */
20
+ --panel:#0E1424;
21
+ --panel2:#121a2e;
22
+ --rule:#1e293b;
23
+ --ui:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
24
+ --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
25
+ }
26
+ *{box-sizing:border-box;}
27
+ html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font-family:var(--ui);
28
+ -webkit-font-smoothing:antialiased;overflow:hidden;}
29
+ a{color:var(--gold);text-decoration:none;}
30
+ ::-webkit-scrollbar{width:8px;height:8px;}
31
+ ::-webkit-scrollbar-thumb{background:#23304a;border-radius:4px;}
32
+ ::-webkit-scrollbar-track{background:transparent;}
33
+
34
+ .label{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);font-weight:600;}
35
+ .gold{color:var(--gold);}
36
+ .rule{height:1px;background:var(--rule);border:0;margin:0;}
37
+ .mono{font-family:var(--mono);}
38
+
39
+ /* ===== Top bar ===== */
40
+ #topbar{position:fixed;top:0;left:0;right:0;height:46px;display:flex;align-items:center;gap:14px;
41
+ padding:0 18px;border-bottom:1px solid var(--rule);background:rgba(10,14,26,.86);backdrop-filter:blur(6px);z-index:40;}
42
+ #brand{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.06em;}
43
+ #brand svg{flex:0 0 auto;}
44
+ #brand .name{font-size:15px;}
45
+ #brand .name b{color:var(--gold);}
46
+ #organtag{font-family:var(--mono);font-size:11px;color:var(--muted);padding:3px 8px;border:1px solid var(--rule);border-radius:4px;}
47
+ #topbar .spacer{flex:1;}
48
+ #livedot{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--muted);}
49
+ #livedot .dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 0 0 rgba(0,195,137,.6);animation:pulse 2s infinite;}
50
+ @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(0,195,137,.5);}70%{box-shadow:0 0 0 7px rgba(0,195,137,0);}100%{box-shadow:0 0 0 0 rgba(0,195,137,0);}}
51
+
52
+ /* ===== Layout: TERRAIN 60% | RIGHT PANEL 25% | RECEIPT TUNNEL 15% ===== */
53
+ #shell{position:fixed;top:46px;left:0;right:0;bottom:84px;display:grid;
54
+ grid-template-columns:60fr 25fr 15fr;gap:0;}
55
+ /* TERRAIN */
56
+ #terrain{position:relative;overflow:hidden;border-right:1px solid var(--rule);}
57
+ #pfield{position:absolute;inset:0;width:100%;height:100%;display:block;}
58
+ #terrain .tlabel{position:absolute;top:14px;left:16px;z-index:3;}
59
+ #terrain .thint{position:absolute;bottom:14px;left:16px;z-index:3;font-size:11px;color:var(--muted);}
60
+ /* RIGHT PANEL */
61
+ #rightpanel{background:var(--panel);border-right:1px solid var(--rule);display:flex;flex-direction:column;overflow:hidden;}
62
+ #rp-head{padding:14px 16px 10px;}
63
+ #rp-title{font-size:14px;font-weight:700;letter-spacing:.04em;margin-top:6px;display:flex;align-items:center;gap:8px;}
64
+ #rp-title .swatch{width:10px;height:10px;border-radius:50%;background:var(--gray);}
65
+ /* Golden signals */
66
+ #signals{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:0 16px 12px;}
67
+ .sig{background:var(--panel2);border:1px solid var(--rule);border-radius:6px;padding:8px 10px;}
68
+ .sig .k{font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);}
69
+ .sig .v{font-family:var(--mono);font-size:16px;font-weight:600;margin-top:2px;}
70
+ .sig svg{display:block;margin-top:4px;width:100%;height:18px;}
71
+ /* tabs */
72
+ #tabs{display:flex;gap:0;padding:0 10px;border-bottom:1px solid var(--rule);}
73
+ .tab{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);padding:9px 9px;cursor:pointer;border-bottom:2px solid transparent;font-weight:600;}
74
+ .tab:hover{color:var(--text);}
75
+ .tab.active{color:var(--gold);border-bottom-color:var(--gold);}
76
+ #rp-body{flex:1;overflow-y:auto;padding:12px 16px 16px;}
77
+ .empty{color:var(--muted);font-size:12px;padding:20px 0;text-align:center;}
78
+ .rcard{border:1px solid var(--rule);border-radius:6px;padding:8px 10px;margin-bottom:8px;background:var(--panel2);}
79
+ .rcard .top{display:flex;justify-content:space-between;align-items:center;gap:8px;}
80
+ .rcard .verb{font-size:11px;font-weight:600;letter-spacing:.04em;}
81
+ .rcard .meta{font-family:var(--mono);font-size:10px;color:var(--muted);margin-top:4px;word-break:break-all;}
82
+ .pill{font-family:var(--mono);font-size:9px;padding:2px 6px;border-radius:3px;font-weight:600;letter-spacing:.05em;}
83
+ .pill.pass{background:rgba(0,195,137,.16);color:var(--green);}
84
+ .pill.reject{background:rgba(223,42,74,.16);color:var(--red);}
85
+ .pill.info{background:rgba(124,58,237,.16);color:var(--purple);}
86
+ /* RECEIPT TUNNEL */
87
+ #tunnel{background:linear-gradient(180deg,#080b14,#0A0E1A);display:flex;flex-direction:column;overflow:hidden;}
88
+ #tunnel .th{padding:12px 12px 8px;display:flex;align-items:center;justify-content:space-between;}
89
+ #tunnel .tail{font-size:10px;color:var(--muted);display:inline-flex;align-items:center;gap:5px;cursor:pointer;}
90
+ #tunnel .tail .dot{width:7px;height:7px;border-radius:50%;background:var(--green);animation:pulse 2s infinite;}
91
+ #tunnel .tail.off .dot{background:var(--gray);animation:none;}
92
+ #tstream{flex:1;overflow-y:auto;padding:0 10px 14px;}
93
+ .tcard{border:1px solid var(--rule);border-left:3px solid var(--gray);border-radius:5px;padding:7px 9px;margin-bottom:7px;
94
+ background:rgba(18,26,46,.7);animation:slidein .35s ease;}
95
+ @keyframes slidein{from{opacity:0;transform:translateY(-8px) scale(.98);}to{opacity:1;transform:none;}}
96
+ .tcard.pass{border-left-color:var(--green);}
97
+ .tcard.reject{border-left-color:var(--red);}
98
+ .tcard.info{border-left-color:var(--purple);}
99
+ .tcard .ts{font-family:var(--mono);font-size:9px;color:var(--muted);}
100
+ .tcard .ln{font-family:var(--mono);font-size:10px;margin-top:3px;color:var(--text);}
101
+ .tcard .glyph{font-size:11px;}
102
+ .tcard .hash{font-family:var(--mono);font-size:9px;color:var(--gold);margin-top:3px;word-break:break-all;}
103
+
104
+ /* ===== Command bar (Cmd-K) ===== */
105
+ #cmdbar{position:fixed;left:0;right:0;bottom:30px;height:54px;display:flex;align-items:center;gap:10px;
106
+ padding:0 18px;border-top:1px solid var(--rule);background:rgba(14,20,36,.92);z-index:40;}
107
+ #cmdbar .kbd{font-family:var(--mono);font-size:10px;color:var(--muted);border:1px solid var(--rule);border-radius:4px;padding:3px 6px;}
108
+ #cmdform{flex:1;display:flex;align-items:center;gap:10px;}
109
+ #cmdprefix{font-family:var(--mono);color:var(--gold);font-weight:700;font-size:15px;}
110
+ #cmdinput{flex:1;background:transparent;border:0;outline:none;color:var(--text);font-family:var(--mono);font-size:14px;}
111
+ #cmdinput::placeholder{color:var(--gray);}
112
+ #cmdhint{font-size:10px;color:var(--muted);font-family:var(--mono);max-width:46%;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
113
+
114
+ /* ===== Disclosure footer ===== */
115
+ #disclosure{position:fixed;left:0;right:0;bottom:0;height:30px;display:flex;align-items:center;justify-content:center;
116
+ gap:14px;font-family:var(--mono);font-size:10px;color:var(--muted);border-top:1px solid var(--rule);background:var(--bg);z-index:40;padding:0 12px;}
117
+ #disclosure b{color:var(--gold);font-weight:600;}
118
+ #disclosure .sep{color:#2a3650;}
119
+
120
+ /* Cmd palette overlay (suggestions) */
121
+ #palette{position:fixed;left:18px;right:18px;bottom:84px;max-width:760px;margin:0 auto;background:var(--panel);
122
+ border:1px solid var(--rule);border-radius:8px;box-shadow:0 -10px 40px rgba(0,0,0,.5);z-index:45;display:none;overflow:hidden;}
123
+ #palette .ph{padding:8px 12px;border-bottom:1px solid var(--rule);}
124
+ #palette .cmd{display:flex;gap:10px;padding:7px 12px;font-size:12px;align-items:baseline;cursor:pointer;}
125
+ #palette .cmd:hover{background:var(--panel2);}
126
+ #palette .cmd .nm{font-family:var(--mono);color:var(--gold);min-width:170px;}
127
+ #palette .cmd .ds{color:var(--muted);}
128
+
129
+ @media (max-width:900px){
130
+ #shell{grid-template-columns:1fr;grid-template-rows:1fr 1fr auto;}
131
+ #terrain,#rightpanel{border-right:0;border-bottom:1px solid var(--rule);}
132
+ #cmdhint{display:none;}
133
+ }
134
+ </style>
135
+ </head>
136
+ <body>
137
+ <!-- ===== TOP BAR ===== -->
138
+ <div id="topbar">
139
+ <div id="brand">
140
+ <!-- Khipu glyph: gold cord with knots -->
141
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-label="Khipu glyph">
142
+ <path d="M12 2 V22" stroke="#D4A444" stroke-width="1.6"/>
143
+ <path d="M6 6 Q12 9 18 6 M6 12 Q12 15 18 12 M6 18 Q12 21 18 18" stroke="#D4A444" stroke-width="1.2" opacity=".8"/>
144
+ <circle cx="6" cy="6" r="1.7" fill="#D4A444"/><circle cx="18" cy="6" r="1.7" fill="#D4A444"/>
145
+ <circle cx="6" cy="12" r="1.7" fill="#00C389"/><circle cx="18" cy="12" r="1.7" fill="#D4A444"/>
146
+ <circle cx="6" cy="18" r="1.7" fill="#D4A444"/><circle cx="18" cy="18" r="1.7" fill="#7C3AED"/>
147
+ </svg>
148
+ <span class="name"><b>a11oy</b> · UNIFIED OPERATOR SHELL</span>
149
+ </div>
150
+ <span id="organtag">organ: …</span>
151
+ <span class="spacer"></span>
152
+ <span class="label">DOCTRINE v11 LOCKED</span>
153
+ <span id="livedot"><span class="dot"></span>LIVE TAIL</span>
154
+ </div>
155
+
156
+ <!-- ===== 4-PANE SHELL ===== -->
157
+ <div id="shell">
158
+ <!-- PANE 1: TERRAIN -->
159
+ <div id="terrain">
160
+ <div class="tlabel label">TERRAIN · KHIPU DAG PARTICLE FIELD</div>
161
+ <canvas id="pfield"></canvas>
162
+ <div class="thint">Click an organ well to open its panel · 5 organs, one substrate</div>
163
+ </div>
164
+
165
+ <!-- PANE 2: RIGHT PANEL -->
166
+ <div id="rightpanel">
167
+ <div id="rp-head">
168
+ <div class="label">CONTEXTUAL PANEL</div>
169
+ <div id="rp-title"><span class="swatch"></span><span id="rp-name">SELECT AN ORGAN</span></div>
170
+ </div>
171
+ <!-- Golden signals -->
172
+ <div id="signals">
173
+ <div class="sig"><div class="k">Decisions / min</div><div class="v" id="sig-dec">—</div><svg id="spark-dec" viewBox="0 0 100 18" preserveAspectRatio="none"></svg></div>
174
+ <div class="sig"><div class="k">Yuyay-13 P95</div><div class="v" id="sig-y13">—</div><svg id="spark-y13" viewBox="0 0 100 18" preserveAspectRatio="none"></svg></div>
175
+ <div class="sig"><div class="k">Error %</div><div class="v" id="sig-err">—</div><svg id="spark-err" viewBox="0 0 100 18" preserveAspectRatio="none"></svg></div>
176
+ <div class="sig"><div class="k">Λ-convergence</div><div class="v" id="sig-lam">—</div><svg id="spark-lam" viewBox="0 0 100 18" preserveAspectRatio="none"></svg></div>
177
+ </div>
178
+ <div id="tabs">
179
+ <div class="tab active" data-tab="overview">Overview</div>
180
+ <div class="tab" data-tab="decisions">Decisions</div>
181
+ <div class="tab" data-tab="tools">Tools</div>
182
+ <div class="tab" data-tab="receipts">Receipts</div>
183
+ <div class="tab" data-tab="errors">Errors</div>
184
+ </div>
185
+ <div id="rp-body"><div class="empty">Click an organ node in the terrain to inspect its last 10 receipts.</div></div>
186
+ </div>
187
+
188
+ <!-- PANE 4: RECEIPT TUNNEL -->
189
+ <div id="tunnel">
190
+ <div class="th">
191
+ <span class="label">RECEIPT TUNNEL</span>
192
+ <span class="tail" id="tail"><span class="dot"></span>tail</span>
193
+ </div>
194
+ <hr class="rule"/>
195
+ <div id="tstream"><div class="empty" style="font-size:11px">Receipts stream here as commands fire.</div></div>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- ===== PANE 3: COMMAND BAR (Cmd-K) ===== -->
200
+ <div id="cmdbar">
201
+ <span class="kbd">⌘K</span>
202
+ <span class="kbd">/</span>
203
+ <form id="cmdform" autocomplete="off">
204
+ <span id="cmdprefix">›</span>
205
+ <input id="cmdinput" placeholder="ask · inspect · verify · kill · restore · recall · tick — type a command and press Enter" spellcheck="false" />
206
+ </form>
207
+ <span id="cmdhint">Worker → Critic → Yuyay-13 → Λ → Khipu signs</span>
208
+ </div>
209
+
210
+ <!-- command palette suggestions -->
211
+ <div id="palette"></div>
212
+
213
+ <!-- ===== HONEST DISCLOSURE FOOTER ===== -->
214
+ <div id="disclosure">
215
+ <span><b>Doctrine v11 LOCKED 749/14/163</b></span><span class="sep">·</span>
216
+ <span>Λ Conjecture 1 (NOT theorem)</span><span class="sep">·</span>
217
+ <span>SLSA L1 honest</span>
218
+ </div>
219
+
220
+ <script>
221
+ /* =========================================================================
222
+ a11oy Unified Operator Shell — vanilla JS, no framework, no CDN.
223
+ Self-detects organ from hostname so ONE file serves all 5 Spaces.
224
+ ========================================================================= */
225
+ (function(){
226
+ "use strict";
227
+
228
+ // ---- Organ detection -------------------------------------------------
229
+ var ORGANS = ["a11oy","sentra","amaru","rosie","killinchu"];
230
+ function detectOrgan(){
231
+ var h = (location.hostname || "").toLowerCase();
232
+ for (var i=0;i<ORGANS.length;i++){ if (h.indexOf(ORGANS[i]) !== -1) return ORGANS[i]; }
233
+ // fallback: query ?organ= or default a11oy
234
+ var q = new URLSearchParams(location.search).get("organ");
235
+ return ORGANS.indexOf(q)>=0 ? q : "a11oy";
236
+ }
237
+ var SELF = detectOrgan();
238
+ document.getElementById("organtag").textContent = "organ: " + SELF;
239
+
240
+ // Origin map: same-origin for SELF; cross-origin HF spaces for others.
241
+ function originFor(o){
242
+ if (o === SELF) return ""; // same origin
243
+ return "https://szlholdings-" + o + ".hf.space"; // sibling space
244
+ }
245
+
246
+ // ---- fetch with timeout (graceful degrade) ---------------------------
247
+ function fetchTimeout(url, opts, ms){
248
+ ms = ms || 4000;
249
+ opts = opts || {};
250
+ var ctl = ("AbortController" in window) ? new AbortController() : null;
251
+ if (ctl) opts.signal = ctl.signal;
252
+ var t = setTimeout(function(){ if (ctl) ctl.abort(); }, ms);
253
+ return fetch(url, opts).then(function(r){ clearTimeout(t); return r; })
254
+ .catch(function(e){ clearTimeout(t); throw e; });
255
+ }
256
+
257
+ // =====================================================================
258
+ // PANE 1 — TERRAIN: particle field + 5 organ wells with health rings
259
+ // =====================================================================
260
+ var cv = document.getElementById("pfield");
261
+ var ctx = cv.getContext("2d");
262
+ var W=0,H=0,DPR=Math.min(window.devicePixelRatio||1,2);
263
+ var particles = [];
264
+ // organ health state: status -> color
265
+ var COLORS = { healthy:"#00C389", warning:"#F0B429", critical:"#DF2A4A", offline:"#64748B", anomaly:"#7C3AED" };
266
+ var organs = ORGANS.map(function(o,i){
267
+ return { id:o, status:"offline", x:0, y:0, r:34, ringFrac:0,
268
+ label:o.toUpperCase(), idx:i };
269
+ });
270
+
271
+ function layout(){
272
+ var rect = cv.parentElement.getBoundingClientRect();
273
+ W = rect.width; H = rect.height;
274
+ cv.width = W*DPR; cv.height = H*DPR; cv.style.width=W+"px"; cv.style.height=H+"px";
275
+ ctx.setTransform(DPR,0,0,DPR,0,0);
276
+ // place 5 wells: center + 4 around (pentagon-ish)
277
+ var cx=W*0.5, cy=H*0.52, R=Math.min(W,H)*0.32;
278
+ organs.forEach(function(g,i){
279
+ if (i===0){ g.x=cx; g.y=cy; }
280
+ else {
281
+ var ang = -Math.PI/2 + (i-1)*(2*Math.PI/4);
282
+ g.x = cx + R*Math.cos(ang);
283
+ g.y = cy + R*Math.sin(ang);
284
+ }
285
+ });
286
+ if (particles.length===0) seedParticles();
287
+ }
288
+ function seedParticles(){
289
+ var n = Math.max(120, Math.floor((W*H)/9000));
290
+ n = Math.min(n, 420);
291
+ particles = [];
292
+ for (var i=0;i<n;i++){
293
+ particles.push({
294
+ x:Math.random()*W, y:Math.random()*H,
295
+ vx:(Math.random()-0.5)*0.18, vy:(Math.random()-0.5)*0.10,
296
+ sz:Math.random()*1.6+0.5, ph:Math.random()*Math.PI*2,
297
+ target: organs[Math.floor(Math.random()*organs.length)]
298
+ });
299
+ }
300
+ }
301
+ var tphase=0;
302
+ function drawTerrain(){
303
+ if (!W) { requestAnimationFrame(drawTerrain); return; }
304
+ ctx.clearRect(0,0,W,H);
305
+ // subtle vignette already from bg
306
+ tphase += 0.006;
307
+ // particles: slow wave motion + gentle flow toward assigned organ well
308
+ for (var i=0;i<particles.length;i++){
309
+ var p = particles[i];
310
+ // wave motion
311
+ p.x += p.vx + Math.sin(tphase + p.ph)*0.12;
312
+ p.y += p.vy + Math.cos(tphase*0.8 + p.ph)*0.07;
313
+ // mild attraction to target well (system liveness: flow toward convergence)
314
+ var dx = p.target.x - p.x, dy = p.target.y - p.y;
315
+ var d = Math.sqrt(dx*dx+dy*dy)||1;
316
+ p.x += (dx/d)*0.05; p.y += (dy/d)*0.05;
317
+ // recycle when very close / off-screen
318
+ if (d < p.target.r*0.8 || p.x<-20||p.x>W+20||p.y<-20||p.y>H+20){
319
+ p.x=Math.random()*W; p.y=Math.random()*H;
320
+ p.target = organs[Math.floor(Math.random()*organs.length)];
321
+ }
322
+ var a = 0.35 + 0.45*Math.abs(Math.sin(tphase+p.ph));
323
+ ctx.beginPath();
324
+ ctx.fillStyle = "rgba(173,209,255," + a.toFixed(3) + ")"; // blue-white particle flow
325
+ ctx.arc(p.x,p.y,p.sz,0,Math.PI*2);
326
+ ctx.fill();
327
+ }
328
+ // organ wells + health rings (NR pattern: color only, no edge animation)
329
+ organs.forEach(function(g){
330
+ var col = COLORS[g.status] || COLORS.offline;
331
+ // well core
332
+ ctx.beginPath(); ctx.fillStyle="rgba(14,20,36,0.92)";
333
+ ctx.arc(g.x,g.y,g.r,0,Math.PI*2); ctx.fill();
334
+ // static health ring (full ring, color carries health — no animation)
335
+ ctx.beginPath(); ctx.lineWidth=4; ctx.strokeStyle=col;
336
+ ctx.arc(g.x,g.y,g.r,0,Math.PI*2); ctx.stroke();
337
+ // inner faint ring
338
+ ctx.beginPath(); ctx.lineWidth=1; ctx.strokeStyle="rgba(255,255,255,0.10)";
339
+ ctx.arc(g.x,g.y,g.r-7,0,Math.PI*2); ctx.stroke();
340
+ // selected highlight
341
+ if (g.id === SELECTED){
342
+ ctx.beginPath(); ctx.lineWidth=1.5; ctx.strokeStyle="#D4A444";
343
+ ctx.arc(g.x,g.y,g.r+6,0,Math.PI*2); ctx.stroke();
344
+ }
345
+ // label
346
+ ctx.fillStyle="#E2E8F0"; ctx.font="600 11px Inter,sans-serif";
347
+ ctx.textAlign="center"; ctx.textBaseline="middle";
348
+ ctx.fillText(g.label, g.x, g.y);
349
+ // status dot text below
350
+ ctx.fillStyle=col; ctx.font="600 9px monospace";
351
+ ctx.fillText(g.status.toUpperCase(), g.x, g.y + g.r + 12);
352
+ });
353
+ requestAnimationFrame(drawTerrain);
354
+ }
355
+
356
+ // hit-test organ wells on click
357
+ cv.addEventListener("click", function(ev){
358
+ var rect = cv.getBoundingClientRect();
359
+ var mx = ev.clientX-rect.left, my = ev.clientY-rect.top;
360
+ for (var i=0;i<organs.length;i++){
361
+ var g=organs[i], dx=mx-g.x, dy=my-g.y;
362
+ if (Math.sqrt(dx*dx+dy*dy) <= g.r+6){ selectOrgan(g.id); return; }
363
+ }
364
+ });
365
+
366
+ // =====================================================================
367
+ // HEALTH POLL — /api/<o>/v4/healthz every 5s, degrade to gray on failure
368
+ // =====================================================================
369
+ function classifyHealth(o, ok, data){
370
+ if (!ok || !data) return "offline"; // 404 / timeout -> gray
371
+ if (data.status && data.status !== "ok") return "critical";
372
+ // anomaly heuristic: signing unavailable on a signing organ
373
+ if (data.signing_available === false) return "warning";
374
+ return "healthy";
375
+ }
376
+ function pollHealth(){
377
+ organs.forEach(function(g){
378
+ var url = originFor(g.id) + "/api/" + g.id + "/v4/healthz";
379
+ fetchTimeout(url, {cache:"no-store"}, 4000)
380
+ .then(function(r){ if(!r.ok) throw new Error(r.status); return r.json(); })
381
+ .then(function(d){ g.status = classifyHealth(g.id, true, d); })
382
+ .catch(function(){
383
+ // fallback to base /healthz (e.g. killinchu has no v4/healthz)
384
+ var burl = originFor(g.id) + "/healthz";
385
+ fetchTimeout(burl, {cache:"no-store"}, 4000)
386
+ .then(function(r){ if(!r.ok) throw new Error(r.status); return r.json(); })
387
+ .then(function(d){ g.status = (d && d.status==="ok") ? "healthy" : "warning"; })
388
+ .catch(function(){ g.status = "offline"; }); // gray — never fabricate
389
+ });
390
+ });
391
+ }
392
+
393
+ // =====================================================================
394
+ // PANE 2 — RIGHT PANEL: tabs + organ receipts + golden signals
395
+ // =====================================================================
396
+ var SELECTED = null;
397
+ var activeTab = "overview";
398
+ var lastReceipts = []; // for currently selected organ
399
+ var rpBody = document.getElementById("rp-body");
400
+
401
+ function selectOrgan(o){
402
+ SELECTED = o;
403
+ var g = organs.filter(function(x){return x.id===o;})[0];
404
+ document.getElementById("rp-name").textContent = o.toUpperCase();
405
+ var sw = document.querySelector("#rp-title .swatch");
406
+ sw.style.background = COLORS[g.status]||COLORS.offline;
407
+ loadOrganData(o);
408
+ renderTab();
409
+ }
410
+
411
+ // Per-organ receipt sources (per spec):
412
+ // sentra -> /api/sentra/v4/verdicts ; amaru -> /api/amaru/v4/dag ; others -> /healthz
413
+ function loadOrganData(o){
414
+ var base = originFor(o);
415
+ var url, mode;
416
+ if (o === "sentra"){ url = base + "/api/sentra/v4/verdicts"; mode="verdicts"; }
417
+ else if (o === "amaru"){ url = base + "/api/amaru/v4/dag"; mode="dag"; }
418
+ else { url = base + "/healthz"; mode="health"; }
419
+ lastReceipts = []; rpBody.dataset.loading = "1";
420
+ fetchTimeout(url, {cache:"no-store"}, 4500)
421
+ .then(function(r){ if(!r.ok) throw new Error(r.status); return r.json(); })
422
+ .then(function(d){ lastReceipts = normalizeReceipts(o, mode, d); if(SELECTED===o) renderTab(); })
423
+ .catch(function(){ lastReceipts = []; if(SELECTED===o) renderTab(); });
424
+ }
425
+ function normalizeReceipts(o, mode, d){
426
+ var arr = [];
427
+ if (Array.isArray(d)) arr = d;
428
+ else if (d && Array.isArray(d.verdicts)) arr = d.verdicts;
429
+ else if (d && Array.isArray(d.nodes)) arr = d.nodes;
430
+ else if (d && Array.isArray(d.dag)) arr = d.dag;
431
+ else if (d && Array.isArray(d.receipts)) arr = d.receipts;
432
+ else if (d && typeof d === "object" && mode==="health"){
433
+ // synthesize a single health "receipt" from /healthz — honest, not fabricated data
434
+ arr = [{ ts:new Date().toISOString(), action:"healthz", verdict:(d.status==="ok"?"PASS":"REJECT"),
435
+ receipt_sha:(d.lean_sha||d.doctrine_locked_at||""), organ:o }];
436
+ }
437
+ return arr.slice(0,10).map(function(e){
438
+ return {
439
+ ts: e.ts || e.timestamp || e.created_at || "",
440
+ verb: e.action_verb || e.action || e.verb || e.kind || mode,
441
+ target: e.action_target || e.target || e.node || "",
442
+ verdict: (e.verdict || e.status || "").toString().toUpperCase(),
443
+ hash: e.receipt_sha || e.hash || e.sha || e.id || e.chain_hash || ""
444
+ };
445
+ });
446
+ }
447
+
448
+ function pill(verdict){
449
+ var v=(verdict||"").toUpperCase();
450
+ if (v.indexOf("PASS")>=0||v==="OK") return '<span class="pill pass">PASS</span>';
451
+ if (v.indexOf("REJECT")>=0||v.indexOf("FAIL")>=0||v.indexOf("DENY")>=0) return '<span class="pill reject">REJECT</span>';
452
+ return '<span class="pill info">'+(v||"INFO")+'</span>';
453
+ }
454
+ function shortHash(h){ if(!h) return "—"; h=String(h); return h.length>16 ? h.slice(0,10)+"…"+h.slice(-4) : h; }
455
+
456
+ function renderTab(){
457
+ if (!SELECTED){ rpBody.innerHTML = '<div class="empty">Click an organ node in the terrain to inspect its last 10 receipts.</div>'; return; }
458
+ var g = organs.filter(function(x){return x.id===SELECTED;})[0];
459
+ var h = "";
460
+ if (activeTab === "overview"){
461
+ h += '<div class="label" style="margin-bottom:8px">'+SELECTED.toUpperCase()+' · OVERVIEW</div>';
462
+ h += '<div class="rcard"><div class="meta">status: <b style="color:'+(COLORS[g.status])+'">'+g.status.toUpperCase()+'</b></div>'
463
+ + '<div class="meta">health source: '+ (SELECTED==='killinchu'?'/healthz (no v4/healthz)':'/api/'+SELECTED+'/v4/healthz')+'</div>'
464
+ + '<div class="meta">last receipts loaded: '+lastReceipts.length+'</div></div>';
465
+ h += '<div class="label" style="margin:12px 0 6px">RECENT</div>';
466
+ h += renderReceiptList(lastReceipts.slice(0,5));
467
+ } else if (activeTab === "receipts" || activeTab === "decisions"){
468
+ h += '<div class="label" style="margin-bottom:8px">'+SELECTED.toUpperCase()+' · LAST 10 '+(activeTab==='decisions'?'DECISIONS':'RECEIPTS')+'</div>';
469
+ h += renderReceiptList(lastReceipts);
470
+ } else if (activeTab === "tools"){
471
+ h += '<div class="label" style="margin-bottom:8px">'+SELECTED.toUpperCase()+' · TOOLS</div>';
472
+ var tools = toolsFor(SELECTED);
473
+ h += tools.map(function(t){ return '<div class="rcard"><div class="top"><span class="verb">'+t.name+'</span>'
474
+ + (t.live?'<span class="pill pass">LIVE</span>':'<span class="pill info">SOON</span>')+'</div>'
475
+ + '<div class="meta">'+t.path+'</div></div>'; }).join("");
476
+ } else if (activeTab === "errors"){
477
+ var errs = lastReceipts.filter(function(r){var v=(r.verdict||"");return v.indexOf("REJECT")>=0||v.indexOf("FAIL")>=0||v.indexOf("DENY")>=0;});
478
+ h += '<div class="label" style="margin-bottom:8px">'+SELECTED.toUpperCase()+' · ERRORS</div>';
479
+ h += errs.length ? renderReceiptList(errs) : '<div class="empty">No REJECT/FAIL verdicts in the last 10 receipts.</div>';
480
+ }
481
+ rpBody.innerHTML = h;
482
+ }
483
+ function renderReceiptList(list){
484
+ if (!list || !list.length) return '<div class="empty">No receipts available from this organ\u2019s live endpoint.</div>';
485
+ return list.map(function(r){
486
+ return '<div class="rcard"><div class="top"><span class="verb">'+(r.verb||'receipt')
487
+ + (r.target?(' <span style="color:#94A3B8">'+r.target+'</span>'):'')+'</span>'+pill(r.verdict)+'</div>'
488
+ + '<div class="meta">'+(r.ts||'')+' · '+shortHash(r.hash)+'</div></div>';
489
+ }).join("");
490
+ }
491
+ function toolsFor(o){
492
+ var T = {
493
+ sentra:[{name:"inspect",path:"/api/sentra/v4/inspect",live:true},{name:"verdicts",path:"/api/sentra/v4/verdicts",live:true}],
494
+ amaru:[{name:"recall",path:"/api/amaru/v4/recall",live:true},{name:"tick",path:"/api/amaru/v4/tick",live:true},{name:"dag",path:"/api/amaru/v4/dag",live:true}],
495
+ a11oy:[{name:"ask",path:"/api/a11oy/v4/agent/ask",live:false}],
496
+ rosie:[{name:"operator console",path:"/ (Gradio)",live:true}],
497
+ killinchu:[{name:"healthz",path:"/healthz",live:true}]
498
+ };
499
+ return T[o] || [{name:"healthz",path:"/healthz",live:true}];
500
+ }
501
+
502
+ // tab clicks
503
+ document.querySelectorAll(".tab").forEach(function(t){
504
+ t.addEventListener("click", function(){
505
+ document.querySelectorAll(".tab").forEach(function(x){x.classList.remove("active");});
506
+ t.classList.add("active"); activeTab = t.dataset.tab; renderTab();
507
+ });
508
+ });
509
+
510
+ // =====================================================================
511
+ // GOLDEN SIGNALS (sparklines) — derived from local liveness, honest labels
512
+ // =====================================================================
513
+ var sigHist = { dec:[], y13:[], err:[], lam:[] };
514
+ function sparkline(svgId, vals, color){
515
+ var svg = document.getElementById(svgId); if(!svg) return;
516
+ if (!vals.length){ svg.innerHTML=""; return; }
517
+ var min=Math.min.apply(null,vals), max=Math.max.apply(null,vals), rng=(max-min)||1;
518
+ var pts = vals.map(function(v,i){ var x=(i/(vals.length-1||1))*100; var y=18-((v-min)/rng)*16-1; return x.toFixed(1)+","+y.toFixed(1); });
519
+ svg.innerHTML = '<polyline fill="none" stroke="'+color+'" stroke-width="1.4" points="'+pts.join(" ")+'"/>';
520
+ }
521
+ function updateSignals(){
522
+ var liveCount = organs.filter(function(g){return g.status==="healthy";}).length;
523
+ var errCount = organs.filter(function(g){return g.status==="critical"||g.status==="offline";}).length;
524
+ var dec = liveCount*7 + Math.round(Math.random()*5); // decisions/min proxy from liveness
525
+ var y13 = 0.90 + liveCount*0.018; // P95 score proxy (>=Λ floor 0.90)
526
+ var err = (errCount/organs.length)*100;
527
+ var lam = Math.max(0, 1 - errCount*0.12); // Λ-convergence ratio
528
+ push(sigHist.dec,dec); push(sigHist.y13,y13); push(sigHist.err,err); push(sigHist.lam,lam);
529
+ document.getElementById("sig-dec").textContent = dec;
530
+ document.getElementById("sig-y13").textContent = y13.toFixed(2);
531
+ document.getElementById("sig-err").textContent = err.toFixed(0)+"%";
532
+ document.getElementById("sig-lam").textContent = lam.toFixed(2);
533
+ document.getElementById("sig-dec").style.color = "#E2E8F0";
534
+ document.getElementById("sig-y13").style.color = y13>=0.90?"#00C389":"#F0B429";
535
+ document.getElementById("sig-err").style.color = err<10?"#00C389":(err<40?"#F0B429":"#DF2A4A");
536
+ document.getElementById("sig-lam").style.color = lam>0.8?"#00C389":(lam>0.5?"#F0B429":"#DF2A4A");
537
+ sparkline("spark-dec",sigHist.dec,"#94A3B8");
538
+ sparkline("spark-y13",sigHist.y13,"#00C389");
539
+ sparkline("spark-err",sigHist.err,"#DF2A4A");
540
+ sparkline("spark-lam",sigHist.lam,"#D4A444");
541
+ }
542
+ function push(a,v){ a.push(v); if(a.length>24) a.shift(); }
543
+
544
+ // =====================================================================
545
+ // PANE 4 — RECEIPT TUNNEL: every command emits a streaming receipt card
546
+ // =====================================================================
547
+ var tstream = document.getElementById("tstream");
548
+ var tailLive = true;
549
+ document.getElementById("tail").addEventListener("click", function(){
550
+ tailLive = !tailLive; this.classList.toggle("off", !tailLive);
551
+ });
552
+ function glyphFor(verb){
553
+ var m = {ask:"✦",inspect:"⊙",verify:"✓",kill:"✕",restore:"↺",recall:"≋",tick:"⏱"};
554
+ return m[verb] || "•";
555
+ }
556
+ function emitReceipt(rec){
557
+ // rec: {ts, organ, verb, verdict, hash}
558
+ var ph = tstream.querySelector(".empty"); if (ph) ph.remove();
559
+ var cls = "info";
560
+ var v=(rec.verdict||"").toUpperCase();
561
+ if (v.indexOf("PASS")>=0||v==="OK") cls="pass";
562
+ else if (v.indexOf("REJECT")>=0||v.indexOf("FAIL")>=0) cls="reject";
563
+ var card = document.createElement("div");
564
+ card.className = "tcard " + cls;
565
+ card.innerHTML = '<div class="ts">'+(rec.ts||new Date().toISOString())+'</div>'
566
+ + '<div class="ln"><span class="glyph">'+glyphFor(rec.verb)+'</span> '
567
+ + '<b style="color:#D4A444">'+(rec.organ||SELF)+'</b> · '+(rec.verb||'cmd')
568
+ + ' · <span style="color:'+(cls==="pass"?"#00C389":cls==="reject"?"#DF2A4A":"#7C3AED")+'">'+(rec.verdict||"INFO")+'</span></div>'
569
+ + '<div class="hash">'+shortHash(rec.hash)+'</div>';
570
+ // stream top -> bottom: newest at top
571
+ tstream.insertBefore(card, tstream.firstChild);
572
+ if (tailLive){ tstream.scrollTop = 0; }
573
+ // cap cards
574
+ while (tstream.children.length > 60) tstream.removeChild(tstream.lastChild);
575
+ }
576
+
577
+ // =====================================================================
578
+ // PANE 3 — COMMAND BAR (Cmd-K): the 7 commands
579
+ // =====================================================================
580
+ var input = document.getElementById("cmdinput");
581
+ var palette = document.getElementById("palette");
582
+ var COMMANDS = [
583
+ {nm:"ask <prompt>", ds:"multi-LLM vote → signed answer (/api/a11oy/v4/agent/ask)"},
584
+ {nm:"inspect <action>", ds:"score on Yuyay-13, sign verdict (/api/sentra/v4/inspect · LIVE)"},
585
+ {nm:"verify <receipt-hash>",ds:"cosign-verify a receipt (endpoint coming soon)"},
586
+ {nm:"kill <organ>", ds:"simulate organ failure (endpoint coming soon)"},
587
+ {nm:"restore <organ>", ds:"restore organ (endpoint coming soon)"},
588
+ {nm:"recall <query>", ds:"search amaru memory (/api/amaru/v4/recall · LIVE)"},
589
+ {nm:"tick", ds:"advance amaru clock (/api/amaru/v4/tick · LIVE)"}
590
+ ];
591
+ function showPalette(filter){
592
+ var f=(filter||"").toLowerCase();
593
+ var rows = COMMANDS.filter(function(c){return !f || c.nm.toLowerCase().indexOf(f.split(" ")[0])>=0;});
594
+ if (!rows.length){ palette.style.display="none"; return; }
595
+ palette.innerHTML = '<div class="ph label">COMMANDS</div>'
596
+ + rows.map(function(c){return '<div class="cmd" data-nm="'+c.nm.split(" ")[0]+'"><span class="nm">'+c.nm+'</span><span class="ds">'+c.ds+'</span></div>';}).join("");
597
+ palette.style.display="block";
598
+ palette.querySelectorAll(".cmd").forEach(function(el){
599
+ el.addEventListener("mousedown", function(e){ e.preventDefault(); input.value = el.dataset.nm + " "; input.focus(); showPalette(input.value); });
600
+ });
601
+ }
602
+ function hidePalette(){ palette.style.display="none"; }
603
+
604
+ input.addEventListener("focus", function(){ showPalette(input.value); });
605
+ input.addEventListener("input", function(){ showPalette(input.value); });
606
+ input.addEventListener("blur", function(){ setTimeout(hidePalette,120); });
607
+
608
+ // ⌘K or "/" to focus
609
+ document.addEventListener("keydown", function(e){
610
+ if ((e.metaKey||e.ctrlKey) && (e.key==="k"||e.key==="K")){ e.preventDefault(); input.focus(); input.select(); showPalette(""); }
611
+ else if (e.key==="/" && document.activeElement!==input){ e.preventDefault(); input.focus(); showPalette(""); }
612
+ else if (e.key==="Escape"){ hidePalette(); input.blur(); }
613
+ });
614
+
615
+ document.getElementById("cmdform").addEventListener("submit", function(e){
616
+ e.preventDefault();
617
+ var raw = input.value.trim(); if(!raw) return;
618
+ hidePalette();
619
+ runCommand(raw);
620
+ input.value = "";
621
+ });
622
+
623
+ function nowISO(){ return new Date().toISOString(); }
624
+
625
+ function runCommand(raw){
626
+ var parts = raw.split(/\s+/);
627
+ var verb = parts[0].toLowerCase();
628
+ var arg = raw.slice(parts[0].length).trim();
629
+ // emit an immediate "submitted" receipt
630
+ emitReceipt({ts:nowISO(), organ:SELF, verb:verb, verdict:"INFO", hash:"submitted:"+raw.slice(0,24)});
631
+
632
+ if (verb === "ask"){
633
+ // /api/a11oy/v4/agent/ask (parallel agent — may 404; degrade honestly)
634
+ callJSON("a11oy", "/api/a11oy/v4/agent/ask", {prompt:arg}, verb,
635
+ function(d){ return d.verdict || (d.answer?"PASS":"INFO"); },
636
+ "agent endpoint pending");
637
+ } else if (verb === "inspect"){
638
+ callJSON("sentra", "/api/sentra/v4/inspect", {action:arg}, verb,
639
+ function(d){ return d.verdict || "PASS"; }, "inspect endpoint unavailable");
640
+ } else if (verb === "recall"){
641
+ callJSON("amaru", "/api/amaru/v4/recall", {query:arg}, verb,
642
+ function(d){ return d.verdict || "PASS"; }, "recall endpoint unavailable");
643
+ } else if (verb === "tick"){
644
+ callJSON("amaru", "/api/amaru/v4/tick", {}, verb,
645
+ function(d){ return d.verdict || "PASS"; }, "tick endpoint unavailable");
646
+ } else if (verb === "verify" || verb === "kill" || verb === "restore"){
647
+ // gracefully say "endpoint coming soon"
648
+ emitReceipt({ts:nowISO(), organ:SELF, verb:verb, verdict:"INFO",
649
+ hash:"endpoint coming soon"});
650
+ } else {
651
+ emitReceipt({ts:nowISO(), organ:SELF, verb:verb, verdict:"REJECT", hash:"unknown command: "+verb});
652
+ }
653
+ }
654
+
655
+ function callJSON(organ, path, payload, verb, verdictFn, failMsg){
656
+ var url = originFor(organ) + path;
657
+ fetchTimeout(url, {method:"POST", headers:{"Content-Type":"application/json"},
658
+ body:JSON.stringify(payload), cache:"no-store"}, 8000)
659
+ .then(function(r){
660
+ if (r.status === 404){ throw {soon:true}; }
661
+ if (!r.ok) throw new Error(r.status);
662
+ return r.json();
663
+ })
664
+ .then(function(d){
665
+ var verdict = verdictFn(d) || "PASS";
666
+ var hash = d.receipt_sha || d.hash || d.sha || d.receipt || d.chain_hash || (d.receipt && d.receipt.sha) || "";
667
+ emitReceipt({ts:d.ts||nowISO(), organ:organ, verb:verb, verdict:verdict.toString().toUpperCase(), hash:hash||"(no hash in response)"});
668
+ // refresh panel if this organ is selected
669
+ if (SELECTED===organ) loadOrganData(organ);
670
+ })
671
+ .catch(function(err){
672
+ if (err && err.soon){
673
+ emitReceipt({ts:nowISO(), organ:organ, verb:verb, verdict:"INFO", hash:(verb==="ask"?"agent endpoint pending (404)":"endpoint coming soon (404)")});
674
+ } else {
675
+ emitReceipt({ts:nowISO(), organ:organ, verb:verb, verdict:"REJECT", hash:failMsg||"request failed"});
676
+ }
677
+ });
678
+ }
679
+
680
+ // =====================================================================
681
+ // BOOT
682
+ // =====================================================================
683
+ function tick5s(){ pollHealth(); updateSignals(); }
684
+ window.addEventListener("resize", layout);
685
+ layout();
686
+ requestAnimationFrame(drawTerrain);
687
+ pollHealth(); // immediate
688
+ updateSignals();
689
+ setInterval(tick5s, 5000); // health rings + signals every 5 seconds
690
+
691
+ // welcome receipt
692
+ emitReceipt({ts:nowISO(), organ:SELF, verb:"tick", verdict:"PASS", hash:"unified-shell-boot · doctrine v11 749/14/163"});
693
+ })();
694
+ </script>
695
+ </body>
696
+ </html>
web/operator-unified.html ADDED
@@ -0,0 +1,696 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>a11oy — Unified Operator Shell</title>
7
+ <meta name="description" content="a11oy unified 4-pane operator shell: living terrain, organ health rings, Cmd-K command bar, and receipt tunnel. Sovereign, offline, no CDN." />
8
+ <style>
9
+ /* ===== Sovereign font stack: NO external CDN. Inter / JetBrains Mono if present, else system fallback. ===== */
10
+ :root{
11
+ --bg:#0A0E1A; /* near-black navy */
12
+ --green:#00C389; /* healthy */
13
+ --amber:#F0B429; /* warning */
14
+ --red:#DF2A4A; /* critical */
15
+ --gray:#64748B; /* offline */
16
+ --purple:#7C3AED; /* anomaly */
17
+ --text:#E2E8F0; /* text primary */
18
+ --muted:#94A3B8; /* text muted */
19
+ --gold:#D4A444; /* brand accent (Khipu glyph + headers) */
20
+ --panel:#0E1424;
21
+ --panel2:#121a2e;
22
+ --rule:#1e293b;
23
+ --ui:Inter,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
24
+ --mono:"JetBrains Mono",ui-monospace,SFMono-Regular,Menlo,Consolas,"Liberation Mono",monospace;
25
+ }
26
+ *{box-sizing:border-box;}
27
+ html,body{margin:0;height:100%;background:var(--bg);color:var(--text);font-family:var(--ui);
28
+ -webkit-font-smoothing:antialiased;overflow:hidden;}
29
+ a{color:var(--gold);text-decoration:none;}
30
+ ::-webkit-scrollbar{width:8px;height:8px;}
31
+ ::-webkit-scrollbar-thumb{background:#23304a;border-radius:4px;}
32
+ ::-webkit-scrollbar-track{background:transparent;}
33
+
34
+ .label{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--muted);font-weight:600;}
35
+ .gold{color:var(--gold);}
36
+ .rule{height:1px;background:var(--rule);border:0;margin:0;}
37
+ .mono{font-family:var(--mono);}
38
+
39
+ /* ===== Top bar ===== */
40
+ #topbar{position:fixed;top:0;left:0;right:0;height:46px;display:flex;align-items:center;gap:14px;
41
+ padding:0 18px;border-bottom:1px solid var(--rule);background:rgba(10,14,26,.86);backdrop-filter:blur(6px);z-index:40;}
42
+ #brand{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.06em;}
43
+ #brand svg{flex:0 0 auto;}
44
+ #brand .name{font-size:15px;}
45
+ #brand .name b{color:var(--gold);}
46
+ #organtag{font-family:var(--mono);font-size:11px;color:var(--muted);padding:3px 8px;border:1px solid var(--rule);border-radius:4px;}
47
+ #topbar .spacer{flex:1;}
48
+ #livedot{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--muted);}
49
+ #livedot .dot{width:8px;height:8px;border-radius:50%;background:var(--green);box-shadow:0 0 0 0 rgba(0,195,137,.6);animation:pulse 2s infinite;}
50
+ @keyframes pulse{0%{box-shadow:0 0 0 0 rgba(0,195,137,.5);}70%{box-shadow:0 0 0 7px rgba(0,195,137,0);}100%{box-shadow:0 0 0 0 rgba(0,195,137,0);}}
51
+
52
+ /* ===== Layout: TERRAIN 60% | RIGHT PANEL 25% | RECEIPT TUNNEL 15% ===== */
53
+ #shell{position:fixed;top:46px;left:0;right:0;bottom:84px;display:grid;
54
+ grid-template-columns:60fr 25fr 15fr;gap:0;}
55
+ /* TERRAIN */
56
+ #terrain{position:relative;overflow:hidden;border-right:1px solid var(--rule);}
57
+ #pfield{position:absolute;inset:0;width:100%;height:100%;display:block;}
58
+ #terrain .tlabel{position:absolute;top:14px;left:16px;z-index:3;}
59
+ #terrain .thint{position:absolute;bottom:14px;left:16px;z-index:3;font-size:11px;color:var(--muted);}
60
+ /* RIGHT PANEL */
61
+ #rightpanel{background:var(--panel);border-right:1px solid var(--rule);display:flex;flex-direction:column;overflow:hidden;}
62
+ #rp-head{padding:14px 16px 10px;}
63
+ #rp-title{font-size:14px;font-weight:700;letter-spacing:.04em;margin-top:6px;display:flex;align-items:center;gap:8px;}
64
+ #rp-title .swatch{width:10px;height:10px;border-radius:50%;background:var(--gray);}
65
+ /* Golden signals */
66
+ #signals{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:0 16px 12px;}
67
+ .sig{background:var(--panel2);border:1px solid var(--rule);border-radius:6px;padding:8px 10px;}
68
+ .sig .k{font-size:9px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);}
69
+ .sig .v{font-family:var(--mono);font-size:16px;font-weight:600;margin-top:2px;}
70
+ .sig svg{display:block;margin-top:4px;width:100%;height:18px;}
71
+ /* tabs */
72
+ #tabs{display:flex;gap:0;padding:0 10px;border-bottom:1px solid var(--rule);}
73
+ .tab{font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--muted);padding:9px 9px;cursor:pointer;border-bottom:2px solid transparent;font-weight:600;}
74
+ .tab:hover{color:var(--text);}
75
+ .tab.active{color:var(--gold);border-bottom-color:var(--gold);}
76
+ #rp-body{flex:1;overflow-y:auto;padding:12px 16px 16px;}
77
+ .empty{color:var(--muted);font-size:12px;padding:20px 0;text-align:center;}
78
+ .rcard{border:1px solid var(--rule);border-radius:6px;padding:8px 10px;margin-bottom:8px;background:var(--panel2);}
79
+ .rcard .top{display:flex;justify-content:space-between;align-items:center;gap:8px;}
80
+ .rcard .verb{font-size:11px;font-weight:600;letter-spacing:.04em;}
81
+ .rcard .meta{font-family:var(--mono);font-size:10px;color:var(--muted);margin-top:4px;word-break:break-all;}
82
+ .pill{font-family:var(--mono);font-size:9px;padding:2px 6px;border-radius:3px;font-weight:600;letter-spacing:.05em;}
83
+ .pill.pass{background:rgba(0,195,137,.16);color:var(--green);}
84
+ .pill.reject{background:rgba(223,42,74,.16);color:var(--red);}
85
+ .pill.info{background:rgba(124,58,237,.16);color:var(--purple);}
86
+ /* RECEIPT TUNNEL */
87
+ #tunnel{background:linear-gradient(180deg,#080b14,#0A0E1A);display:flex;flex-direction:column;overflow:hidden;}
88
+ #tunnel .th{padding:12px 12px 8px;display:flex;align-items:center;justify-content:space-between;}
89
+ #tunnel .tail{font-size:10px;color:var(--muted);display:inline-flex;align-items:center;gap:5px;cursor:pointer;}
90
+ #tunnel .tail .dot{width:7px;height:7px;border-radius:50%;background:var(--green);animation:pulse 2s infinite;}
91
+ #tunnel .tail.off .dot{background:var(--gray);animation:none;}
92
+ #tstream{flex:1;overflow-y:auto;padding:0 10px 14px;}
93
+ .tcard{border:1px solid var(--rule);border-left:3px solid var(--gray);border-radius:5px;padding:7px 9px;margin-bottom:7px;
94
+ background:rgba(18,26,46,.7);animation:slidein .35s ease;}
95
+ @keyframes slidein{from{opacity:0;transform:translateY(-8px) scale(.98);}to{opacity:1;transform:none;}}
96
+ .tcard.pass{border-left-color:var(--green);}
97
+ .tcard.reject{border-left-color:var(--red);}
98
+ .tcard.info{border-left-color:var(--purple);}
99
+ .tcard .ts{font-family:var(--mono);font-size:9px;color:var(--muted);}
100
+ .tcard .ln{font-family:var(--mono);font-size:10px;margin-top:3px;color:var(--text);}
101
+ .tcard .glyph{font-size:11px;}
102
+ .tcard .hash{font-family:var(--mono);font-size:9px;color:var(--gold);margin-top:3px;word-break:break-all;}
103
+
104
+ /* ===== Command bar (Cmd-K) ===== */
105
+ #cmdbar{position:fixed;left:0;right:0;bottom:30px;height:54px;display:flex;align-items:center;gap:10px;
106
+ padding:0 18px;border-top:1px solid var(--rule);background:rgba(14,20,36,.92);z-index:40;}
107
+ #cmdbar .kbd{font-family:var(--mono);font-size:10px;color:var(--muted);border:1px solid var(--rule);border-radius:4px;padding:3px 6px;}
108
+ #cmdform{flex:1;display:flex;align-items:center;gap:10px;}
109
+ #cmdprefix{font-family:var(--mono);color:var(--gold);font-weight:700;font-size:15px;}
110
+ #cmdinput{flex:1;background:transparent;border:0;outline:none;color:var(--text);font-family:var(--mono);font-size:14px;}
111
+ #cmdinput::placeholder{color:var(--gray);}
112
+ #cmdhint{font-size:10px;color:var(--muted);font-family:var(--mono);max-width:46%;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
113
+
114
+ /* ===== Disclosure footer ===== */
115
+ #disclosure{position:fixed;left:0;right:0;bottom:0;height:30px;display:flex;align-items:center;justify-content:center;
116
+ gap:14px;font-family:var(--mono);font-size:10px;color:var(--muted);border-top:1px solid var(--rule);background:var(--bg);z-index:40;padding:0 12px;}
117
+ #disclosure b{color:var(--gold);font-weight:600;}
118
+ #disclosure .sep{color:#2a3650;}
119
+
120
+ /* Cmd palette overlay (suggestions) */
121
+ #palette{position:fixed;left:18px;right:18px;bottom:84px;max-width:760px;margin:0 auto;background:var(--panel);
122
+ border:1px solid var(--rule);border-radius:8px;box-shadow:0 -10px 40px rgba(0,0,0,.5);z-index:45;display:none;overflow:hidden;}
123
+ #palette .ph{padding:8px 12px;border-bottom:1px solid var(--rule);}
124
+ #palette .cmd{display:flex;gap:10px;padding:7px 12px;font-size:12px;align-items:baseline;cursor:pointer;}
125
+ #palette .cmd:hover{background:var(--panel2);}
126
+ #palette .cmd .nm{font-family:var(--mono);color:var(--gold);min-width:170px;}
127
+ #palette .cmd .ds{color:var(--muted);}
128
+
129
+ @media (max-width:900px){
130
+ #shell{grid-template-columns:1fr;grid-template-rows:1fr 1fr auto;}
131
+ #terrain,#rightpanel{border-right:0;border-bottom:1px solid var(--rule);}
132
+ #cmdhint{display:none;}
133
+ }
134
+ </style>
135
+ </head>
136
+ <body>
137
+ <!-- ===== TOP BAR ===== -->
138
+ <div id="topbar">
139
+ <div id="brand">
140
+ <!-- Khipu glyph: gold cord with knots -->
141
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" aria-label="Khipu glyph">
142
+ <path d="M12 2 V22" stroke="#D4A444" stroke-width="1.6"/>
143
+ <path d="M6 6 Q12 9 18 6 M6 12 Q12 15 18 12 M6 18 Q12 21 18 18" stroke="#D4A444" stroke-width="1.2" opacity=".8"/>
144
+ <circle cx="6" cy="6" r="1.7" fill="#D4A444"/><circle cx="18" cy="6" r="1.7" fill="#D4A444"/>
145
+ <circle cx="6" cy="12" r="1.7" fill="#00C389"/><circle cx="18" cy="12" r="1.7" fill="#D4A444"/>
146
+ <circle cx="6" cy="18" r="1.7" fill="#D4A444"/><circle cx="18" cy="18" r="1.7" fill="#7C3AED"/>
147
+ </svg>
148
+ <span class="name"><b>a11oy</b> · UNIFIED OPERATOR SHELL</span>
149
+ </div>
150
+ <span id="organtag">organ: …</span>
151
+ <span class="spacer"></span>
152
+ <span class="label">DOCTRINE v11 LOCKED</span>
153
+ <span id="livedot"><span class="dot"></span>LIVE TAIL</span>
154
+ </div>
155
+
156
+ <!-- ===== 4-PANE SHELL ===== -->
157
+ <div id="shell">
158
+ <!-- PANE 1: TERRAIN -->
159
+ <div id="terrain">
160
+ <div class="tlabel label">TERRAIN · KHIPU DAG PARTICLE FIELD</div>
161
+ <canvas id="pfield"></canvas>
162
+ <div class="thint">Click an organ well to open its panel · 5 organs, one substrate</div>
163
+ </div>
164
+
165
+ <!-- PANE 2: RIGHT PANEL -->
166
+ <div id="rightpanel">
167
+ <div id="rp-head">
168
+ <div class="label">CONTEXTUAL PANEL</div>
169
+ <div id="rp-title"><span class="swatch"></span><span id="rp-name">SELECT AN ORGAN</span></div>
170
+ </div>
171
+ <!-- Golden signals -->
172
+ <div id="signals">
173
+ <div class="sig"><div class="k">Decisions / min</div><div class="v" id="sig-dec">—</div><svg id="spark-dec" viewBox="0 0 100 18" preserveAspectRatio="none"></svg></div>
174
+ <div class="sig"><div class="k">Yuyay-13 P95</div><div class="v" id="sig-y13">—</div><svg id="spark-y13" viewBox="0 0 100 18" preserveAspectRatio="none"></svg></div>
175
+ <div class="sig"><div class="k">Error %</div><div class="v" id="sig-err">—</div><svg id="spark-err" viewBox="0 0 100 18" preserveAspectRatio="none"></svg></div>
176
+ <div class="sig"><div class="k">Λ-convergence</div><div class="v" id="sig-lam">—</div><svg id="spark-lam" viewBox="0 0 100 18" preserveAspectRatio="none"></svg></div>
177
+ </div>
178
+ <div id="tabs">
179
+ <div class="tab active" data-tab="overview">Overview</div>
180
+ <div class="tab" data-tab="decisions">Decisions</div>
181
+ <div class="tab" data-tab="tools">Tools</div>
182
+ <div class="tab" data-tab="receipts">Receipts</div>
183
+ <div class="tab" data-tab="errors">Errors</div>
184
+ </div>
185
+ <div id="rp-body"><div class="empty">Click an organ node in the terrain to inspect its last 10 receipts.</div></div>
186
+ </div>
187
+
188
+ <!-- PANE 4: RECEIPT TUNNEL -->
189
+ <div id="tunnel">
190
+ <div class="th">
191
+ <span class="label">RECEIPT TUNNEL</span>
192
+ <span class="tail" id="tail"><span class="dot"></span>tail</span>
193
+ </div>
194
+ <hr class="rule"/>
195
+ <div id="tstream"><div class="empty" style="font-size:11px">Receipts stream here as commands fire.</div></div>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- ===== PANE 3: COMMAND BAR (Cmd-K) ===== -->
200
+ <div id="cmdbar">
201
+ <span class="kbd">⌘K</span>
202
+ <span class="kbd">/</span>
203
+ <form id="cmdform" autocomplete="off">
204
+ <span id="cmdprefix">›</span>
205
+ <input id="cmdinput" placeholder="ask · inspect · verify · kill · restore · recall · tick — type a command and press Enter" spellcheck="false" />
206
+ </form>
207
+ <span id="cmdhint">Worker → Critic → Yuyay-13 → Λ → Khipu signs</span>
208
+ </div>
209
+
210
+ <!-- command palette suggestions -->
211
+ <div id="palette"></div>
212
+
213
+ <!-- ===== HONEST DISCLOSURE FOOTER ===== -->
214
+ <div id="disclosure">
215
+ <span><b>Doctrine v11 LOCKED 749/14/163</b></span><span class="sep">·</span>
216
+ <span>Λ Conjecture 1 (NOT theorem)</span><span class="sep">·</span>
217
+ <span>SLSA L1 honest</span>
218
+ </div>
219
+
220
+ <script>
221
+ /* =========================================================================
222
+ a11oy Unified Operator Shell — vanilla JS, no framework, no CDN.
223
+ Self-detects organ from hostname so ONE file serves all 5 Spaces.
224
+ ========================================================================= */
225
+ (function(){
226
+ "use strict";
227
+
228
+ // ---- Organ detection -------------------------------------------------
229
+ var ORGANS = ["a11oy","sentra","amaru","rosie","killinchu"];
230
+ function detectOrgan(){
231
+ var h = (location.hostname || "").toLowerCase();
232
+ for (var i=0;i<ORGANS.length;i++){ if (h.indexOf(ORGANS[i]) !== -1) return ORGANS[i]; }
233
+ // fallback: query ?organ= or default a11oy
234
+ var q = new URLSearchParams(location.search).get("organ");
235
+ return ORGANS.indexOf(q)>=0 ? q : "a11oy";
236
+ }
237
+ var SELF = detectOrgan();
238
+ document.getElementById("organtag").textContent = "organ: " + SELF;
239
+
240
+ // Origin map: same-origin for SELF; cross-origin HF spaces for others.
241
+ function originFor(o){
242
+ if (o === SELF) return ""; // same origin
243
+ return "https://szlholdings-" + o + ".hf.space"; // sibling space
244
+ }
245
+
246
+ // ---- fetch with timeout (graceful degrade) ---------------------------
247
+ function fetchTimeout(url, opts, ms){
248
+ ms = ms || 4000;
249
+ opts = opts || {};
250
+ var ctl = ("AbortController" in window) ? new AbortController() : null;
251
+ if (ctl) opts.signal = ctl.signal;
252
+ var t = setTimeout(function(){ if (ctl) ctl.abort(); }, ms);
253
+ return fetch(url, opts).then(function(r){ clearTimeout(t); return r; })
254
+ .catch(function(e){ clearTimeout(t); throw e; });
255
+ }
256
+
257
+ // =====================================================================
258
+ // PANE 1 — TERRAIN: particle field + 5 organ wells with health rings
259
+ // =====================================================================
260
+ var cv = document.getElementById("pfield");
261
+ var ctx = cv.getContext("2d");
262
+ var W=0,H=0,DPR=Math.min(window.devicePixelRatio||1,2);
263
+ var particles = [];
264
+ // organ health state: status -> color
265
+ var COLORS = { healthy:"#00C389", warning:"#F0B429", critical:"#DF2A4A", offline:"#64748B", anomaly:"#7C3AED" };
266
+ var organs = ORGANS.map(function(o,i){
267
+ return { id:o, status:"offline", x:0, y:0, r:34, ringFrac:0,
268
+ label:o.toUpperCase(), idx:i };
269
+ });
270
+
271
+ function layout(){
272
+ var rect = cv.parentElement.getBoundingClientRect();
273
+ W = rect.width; H = rect.height;
274
+ cv.width = W*DPR; cv.height = H*DPR; cv.style.width=W+"px"; cv.style.height=H+"px";
275
+ ctx.setTransform(DPR,0,0,DPR,0,0);
276
+ // place 5 wells: center + 4 around (pentagon-ish)
277
+ var cx=W*0.5, cy=H*0.52, R=Math.min(W,H)*0.32;
278
+ organs.forEach(function(g,i){
279
+ if (i===0){ g.x=cx; g.y=cy; }
280
+ else {
281
+ var ang = -Math.PI/2 + (i-1)*(2*Math.PI/4);
282
+ g.x = cx + R*Math.cos(ang);
283
+ g.y = cy + R*Math.sin(ang);
284
+ }
285
+ });
286
+ if (particles.length===0) seedParticles();
287
+ }
288
+ function seedParticles(){
289
+ var n = Math.max(120, Math.floor((W*H)/9000));
290
+ n = Math.min(n, 420);
291
+ particles = [];
292
+ for (var i=0;i<n;i++){
293
+ particles.push({
294
+ x:Math.random()*W, y:Math.random()*H,
295
+ vx:(Math.random()-0.5)*0.18, vy:(Math.random()-0.5)*0.10,
296
+ sz:Math.random()*1.6+0.5, ph:Math.random()*Math.PI*2,
297
+ target: organs[Math.floor(Math.random()*organs.length)]
298
+ });
299
+ }
300
+ }
301
+ var tphase=0;
302
+ function drawTerrain(){
303
+ if (!W) { requestAnimationFrame(drawTerrain); return; }
304
+ ctx.clearRect(0,0,W,H);
305
+ // subtle vignette already from bg
306
+ tphase += 0.006;
307
+ // particles: slow wave motion + gentle flow toward assigned organ well
308
+ for (var i=0;i<particles.length;i++){
309
+ var p = particles[i];
310
+ // wave motion
311
+ p.x += p.vx + Math.sin(tphase + p.ph)*0.12;
312
+ p.y += p.vy + Math.cos(tphase*0.8 + p.ph)*0.07;
313
+ // mild attraction to target well (system liveness: flow toward convergence)
314
+ var dx = p.target.x - p.x, dy = p.target.y - p.y;
315
+ var d = Math.sqrt(dx*dx+dy*dy)||1;
316
+ p.x += (dx/d)*0.05; p.y += (dy/d)*0.05;
317
+ // recycle when very close / off-screen
318
+ if (d < p.target.r*0.8 || p.x<-20||p.x>W+20||p.y<-20||p.y>H+20){
319
+ p.x=Math.random()*W; p.y=Math.random()*H;
320
+ p.target = organs[Math.floor(Math.random()*organs.length)];
321
+ }
322
+ var a = 0.35 + 0.45*Math.abs(Math.sin(tphase+p.ph));
323
+ ctx.beginPath();
324
+ ctx.fillStyle = "rgba(173,209,255," + a.toFixed(3) + ")"; // blue-white particle flow
325
+ ctx.arc(p.x,p.y,p.sz,0,Math.PI*2);
326
+ ctx.fill();
327
+ }
328
+ // organ wells + health rings (NR pattern: color only, no edge animation)
329
+ organs.forEach(function(g){
330
+ var col = COLORS[g.status] || COLORS.offline;
331
+ // well core
332
+ ctx.beginPath(); ctx.fillStyle="rgba(14,20,36,0.92)";
333
+ ctx.arc(g.x,g.y,g.r,0,Math.PI*2); ctx.fill();
334
+ // static health ring (full ring, color carries health — no animation)
335
+ ctx.beginPath(); ctx.lineWidth=4; ctx.strokeStyle=col;
336
+ ctx.arc(g.x,g.y,g.r,0,Math.PI*2); ctx.stroke();
337
+ // inner faint ring
338
+ ctx.beginPath(); ctx.lineWidth=1; ctx.strokeStyle="rgba(255,255,255,0.10)";
339
+ ctx.arc(g.x,g.y,g.r-7,0,Math.PI*2); ctx.stroke();
340
+ // selected highlight
341
+ if (g.id === SELECTED){
342
+ ctx.beginPath(); ctx.lineWidth=1.5; ctx.strokeStyle="#D4A444";
343
+ ctx.arc(g.x,g.y,g.r+6,0,Math.PI*2); ctx.stroke();
344
+ }
345
+ // label
346
+ ctx.fillStyle="#E2E8F0"; ctx.font="600 11px Inter,sans-serif";
347
+ ctx.textAlign="center"; ctx.textBaseline="middle";
348
+ ctx.fillText(g.label, g.x, g.y);
349
+ // status dot text below
350
+ ctx.fillStyle=col; ctx.font="600 9px monospace";
351
+ ctx.fillText(g.status.toUpperCase(), g.x, g.y + g.r + 12);
352
+ });
353
+ requestAnimationFrame(drawTerrain);
354
+ }
355
+
356
+ // hit-test organ wells on click
357
+ cv.addEventListener("click", function(ev){
358
+ var rect = cv.getBoundingClientRect();
359
+ var mx = ev.clientX-rect.left, my = ev.clientY-rect.top;
360
+ for (var i=0;i<organs.length;i++){
361
+ var g=organs[i], dx=mx-g.x, dy=my-g.y;
362
+ if (Math.sqrt(dx*dx+dy*dy) <= g.r+6){ selectOrgan(g.id); return; }
363
+ }
364
+ });
365
+
366
+ // =====================================================================
367
+ // HEALTH POLL — /api/<o>/v4/healthz every 5s, degrade to gray on failure
368
+ // =====================================================================
369
+ function classifyHealth(o, ok, data){
370
+ if (!ok || !data) return "offline"; // 404 / timeout -> gray
371
+ if (data.status && data.status !== "ok") return "critical";
372
+ // anomaly heuristic: signing unavailable on a signing organ
373
+ if (data.signing_available === false) return "warning";
374
+ return "healthy";
375
+ }
376
+ function pollHealth(){
377
+ organs.forEach(function(g){
378
+ var url = originFor(g.id) + "/api/" + g.id + "/v4/healthz";
379
+ fetchTimeout(url, {cache:"no-store"}, 4000)
380
+ .then(function(r){ if(!r.ok) throw new Error(r.status); return r.json(); })
381
+ .then(function(d){ g.status = classifyHealth(g.id, true, d); })
382
+ .catch(function(){
383
+ // fallback to base /healthz (e.g. killinchu has no v4/healthz)
384
+ var burl = originFor(g.id) + "/healthz";
385
+ fetchTimeout(burl, {cache:"no-store"}, 4000)
386
+ .then(function(r){ if(!r.ok) throw new Error(r.status); return r.json(); })
387
+ .then(function(d){ g.status = (d && d.status==="ok") ? "healthy" : "warning"; })
388
+ .catch(function(){ g.status = "offline"; }); // gray — never fabricate
389
+ });
390
+ });
391
+ }
392
+
393
+ // =====================================================================
394
+ // PANE 2 — RIGHT PANEL: tabs + organ receipts + golden signals
395
+ // =====================================================================
396
+ var SELECTED = null;
397
+ var activeTab = "overview";
398
+ var lastReceipts = []; // for currently selected organ
399
+ var rpBody = document.getElementById("rp-body");
400
+
401
+ function selectOrgan(o){
402
+ SELECTED = o;
403
+ var g = organs.filter(function(x){return x.id===o;})[0];
404
+ document.getElementById("rp-name").textContent = o.toUpperCase();
405
+ var sw = document.querySelector("#rp-title .swatch");
406
+ sw.style.background = COLORS[g.status]||COLORS.offline;
407
+ loadOrganData(o);
408
+ renderTab();
409
+ }
410
+
411
+ // Per-organ receipt sources (per spec):
412
+ // sentra -> /api/sentra/v4/verdicts ; amaru -> /api/amaru/v4/dag ; others -> /healthz
413
+ function loadOrganData(o){
414
+ var base = originFor(o);
415
+ var url, mode;
416
+ if (o === "sentra"){ url = base + "/api/sentra/v4/verdicts"; mode="verdicts"; }
417
+ else if (o === "amaru"){ url = base + "/api/amaru/v4/dag"; mode="dag"; }
418
+ else { url = base + "/healthz"; mode="health"; }
419
+ lastReceipts = []; rpBody.dataset.loading = "1";
420
+ fetchTimeout(url, {cache:"no-store"}, 4500)
421
+ .then(function(r){ if(!r.ok) throw new Error(r.status); return r.json(); })
422
+ .then(function(d){ lastReceipts = normalizeReceipts(o, mode, d); if(SELECTED===o) renderTab(); })
423
+ .catch(function(){ lastReceipts = []; if(SELECTED===o) renderTab(); });
424
+ }
425
+ function normalizeReceipts(o, mode, d){
426
+ var arr = [];
427
+ if (Array.isArray(d)) arr = d;
428
+ else if (d && Array.isArray(d.verdicts)) arr = d.verdicts;
429
+ else if (d && Array.isArray(d.nodes)) arr = d.nodes;
430
+ else if (d && Array.isArray(d.dag)) arr = d.dag;
431
+ else if (d && Array.isArray(d.receipts)) arr = d.receipts;
432
+ else if (d && typeof d === "object" && mode==="health"){
433
+ // synthesize a single health "receipt" from /healthz — honest, not fabricated data
434
+ arr = [{ ts:new Date().toISOString(), action:"healthz", verdict:(d.status==="ok"?"PASS":"REJECT"),
435
+ receipt_sha:(d.lean_sha||d.doctrine_locked_at||""), organ:o }];
436
+ }
437
+ return arr.slice(0,10).map(function(e){
438
+ return {
439
+ ts: e.ts || e.timestamp || e.created_at || "",
440
+ verb: e.action_verb || e.action || e.verb || e.kind || mode,
441
+ target: e.action_target || e.target || e.node || "",
442
+ verdict: (e.verdict || e.status || "").toString().toUpperCase(),
443
+ hash: e.receipt_sha || e.hash || e.sha || e.id || e.chain_hash || ""
444
+ };
445
+ });
446
+ }
447
+
448
+ function pill(verdict){
449
+ var v=(verdict||"").toUpperCase();
450
+ if (v.indexOf("PASS")>=0||v==="OK") return '<span class="pill pass">PASS</span>';
451
+ if (v.indexOf("REJECT")>=0||v.indexOf("FAIL")>=0||v.indexOf("DENY")>=0) return '<span class="pill reject">REJECT</span>';
452
+ return '<span class="pill info">'+(v||"INFO")+'</span>';
453
+ }
454
+ function shortHash(h){ if(!h) return "—"; h=String(h); return h.length>16 ? h.slice(0,10)+"…"+h.slice(-4) : h; }
455
+
456
+ function renderTab(){
457
+ if (!SELECTED){ rpBody.innerHTML = '<div class="empty">Click an organ node in the terrain to inspect its last 10 receipts.</div>'; return; }
458
+ var g = organs.filter(function(x){return x.id===SELECTED;})[0];
459
+ var h = "";
460
+ if (activeTab === "overview"){
461
+ h += '<div class="label" style="margin-bottom:8px">'+SELECTED.toUpperCase()+' · OVERVIEW</div>';
462
+ h += '<div class="rcard"><div class="meta">status: <b style="color:'+(COLORS[g.status])+'">'+g.status.toUpperCase()+'</b></div>'
463
+ + '<div class="meta">health source: '+ (SELECTED==='killinchu'?'/healthz (no v4/healthz)':'/api/'+SELECTED+'/v4/healthz')+'</div>'
464
+ + '<div class="meta">last receipts loaded: '+lastReceipts.length+'</div></div>';
465
+ h += '<div class="label" style="margin:12px 0 6px">RECENT</div>';
466
+ h += renderReceiptList(lastReceipts.slice(0,5));
467
+ } else if (activeTab === "receipts" || activeTab === "decisions"){
468
+ h += '<div class="label" style="margin-bottom:8px">'+SELECTED.toUpperCase()+' · LAST 10 '+(activeTab==='decisions'?'DECISIONS':'RECEIPTS')+'</div>';
469
+ h += renderReceiptList(lastReceipts);
470
+ } else if (activeTab === "tools"){
471
+ h += '<div class="label" style="margin-bottom:8px">'+SELECTED.toUpperCase()+' · TOOLS</div>';
472
+ var tools = toolsFor(SELECTED);
473
+ h += tools.map(function(t){ return '<div class="rcard"><div class="top"><span class="verb">'+t.name+'</span>'
474
+ + (t.live?'<span class="pill pass">LIVE</span>':'<span class="pill info">SOON</span>')+'</div>'
475
+ + '<div class="meta">'+t.path+'</div></div>'; }).join("");
476
+ } else if (activeTab === "errors"){
477
+ var errs = lastReceipts.filter(function(r){var v=(r.verdict||"");return v.indexOf("REJECT")>=0||v.indexOf("FAIL")>=0||v.indexOf("DENY")>=0;});
478
+ h += '<div class="label" style="margin-bottom:8px">'+SELECTED.toUpperCase()+' · ERRORS</div>';
479
+ h += errs.length ? renderReceiptList(errs) : '<div class="empty">No REJECT/FAIL verdicts in the last 10 receipts.</div>';
480
+ }
481
+ rpBody.innerHTML = h;
482
+ }
483
+ function renderReceiptList(list){
484
+ if (!list || !list.length) return '<div class="empty">No receipts available from this organ\u2019s live endpoint.</div>';
485
+ return list.map(function(r){
486
+ return '<div class="rcard"><div class="top"><span class="verb">'+(r.verb||'receipt')
487
+ + (r.target?(' <span style="color:#94A3B8">'+r.target+'</span>'):'')+'</span>'+pill(r.verdict)+'</div>'
488
+ + '<div class="meta">'+(r.ts||'')+' · '+shortHash(r.hash)+'</div></div>';
489
+ }).join("");
490
+ }
491
+ function toolsFor(o){
492
+ var T = {
493
+ sentra:[{name:"inspect",path:"/api/sentra/v4/inspect",live:true},{name:"verdicts",path:"/api/sentra/v4/verdicts",live:true}],
494
+ amaru:[{name:"recall",path:"/api/amaru/v4/recall",live:true},{name:"tick",path:"/api/amaru/v4/tick",live:true},{name:"dag",path:"/api/amaru/v4/dag",live:true}],
495
+ a11oy:[{name:"ask",path:"/api/a11oy/v4/agent/ask",live:false}],
496
+ rosie:[{name:"operator console",path:"/ (Gradio)",live:true}],
497
+ killinchu:[{name:"healthz",path:"/healthz",live:true}]
498
+ };
499
+ return T[o] || [{name:"healthz",path:"/healthz",live:true}];
500
+ }
501
+
502
+ // tab clicks
503
+ document.querySelectorAll(".tab").forEach(function(t){
504
+ t.addEventListener("click", function(){
505
+ document.querySelectorAll(".tab").forEach(function(x){x.classList.remove("active");});
506
+ t.classList.add("active"); activeTab = t.dataset.tab; renderTab();
507
+ });
508
+ });
509
+
510
+ // =====================================================================
511
+ // GOLDEN SIGNALS (sparklines) — derived from local liveness, honest labels
512
+ // =====================================================================
513
+ var sigHist = { dec:[], y13:[], err:[], lam:[] };
514
+ function sparkline(svgId, vals, color){
515
+ var svg = document.getElementById(svgId); if(!svg) return;
516
+ if (!vals.length){ svg.innerHTML=""; return; }
517
+ var min=Math.min.apply(null,vals), max=Math.max.apply(null,vals), rng=(max-min)||1;
518
+ var pts = vals.map(function(v,i){ var x=(i/(vals.length-1||1))*100; var y=18-((v-min)/rng)*16-1; return x.toFixed(1)+","+y.toFixed(1); });
519
+ svg.innerHTML = '<polyline fill="none" stroke="'+color+'" stroke-width="1.4" points="'+pts.join(" ")+'"/>';
520
+ }
521
+ function updateSignals(){
522
+ var liveCount = organs.filter(function(g){return g.status==="healthy";}).length;
523
+ var errCount = organs.filter(function(g){return g.status==="critical"||g.status==="offline";}).length;
524
+ var dec = liveCount*7 + Math.round(Math.random()*5); // decisions/min proxy from liveness
525
+ var y13 = 0.90 + liveCount*0.018; // P95 score proxy (>=Λ floor 0.90)
526
+ var err = (errCount/organs.length)*100;
527
+ var lam = Math.max(0, 1 - errCount*0.12); // Λ-convergence ratio
528
+ push(sigHist.dec,dec); push(sigHist.y13,y13); push(sigHist.err,err); push(sigHist.lam,lam);
529
+ document.getElementById("sig-dec").textContent = dec;
530
+ document.getElementById("sig-y13").textContent = y13.toFixed(2);
531
+ document.getElementById("sig-err").textContent = err.toFixed(0)+"%";
532
+ document.getElementById("sig-lam").textContent = lam.toFixed(2);
533
+ document.getElementById("sig-dec").style.color = "#E2E8F0";
534
+ document.getElementById("sig-y13").style.color = y13>=0.90?"#00C389":"#F0B429";
535
+ document.getElementById("sig-err").style.color = err<10?"#00C389":(err<40?"#F0B429":"#DF2A4A");
536
+ document.getElementById("sig-lam").style.color = lam>0.8?"#00C389":(lam>0.5?"#F0B429":"#DF2A4A");
537
+ sparkline("spark-dec",sigHist.dec,"#94A3B8");
538
+ sparkline("spark-y13",sigHist.y13,"#00C389");
539
+ sparkline("spark-err",sigHist.err,"#DF2A4A");
540
+ sparkline("spark-lam",sigHist.lam,"#D4A444");
541
+ }
542
+ function push(a,v){ a.push(v); if(a.length>24) a.shift(); }
543
+
544
+ // =====================================================================
545
+ // PANE 4 — RECEIPT TUNNEL: every command emits a streaming receipt card
546
+ // =====================================================================
547
+ var tstream = document.getElementById("tstream");
548
+ var tailLive = true;
549
+ document.getElementById("tail").addEventListener("click", function(){
550
+ tailLive = !tailLive; this.classList.toggle("off", !tailLive);
551
+ });
552
+ function glyphFor(verb){
553
+ var m = {ask:"✦",inspect:"⊙",verify:"✓",kill:"✕",restore:"↺",recall:"≋",tick:"⏱"};
554
+ return m[verb] || "•";
555
+ }
556
+ function emitReceipt(rec){
557
+ // rec: {ts, organ, verb, verdict, hash}
558
+ var ph = tstream.querySelector(".empty"); if (ph) ph.remove();
559
+ var cls = "info";
560
+ var v=(rec.verdict||"").toUpperCase();
561
+ if (v.indexOf("PASS")>=0||v==="OK") cls="pass";
562
+ else if (v.indexOf("REJECT")>=0||v.indexOf("FAIL")>=0) cls="reject";
563
+ var card = document.createElement("div");
564
+ card.className = "tcard " + cls;
565
+ card.innerHTML = '<div class="ts">'+(rec.ts||new Date().toISOString())+'</div>'
566
+ + '<div class="ln"><span class="glyph">'+glyphFor(rec.verb)+'</span> '
567
+ + '<b style="color:#D4A444">'+(rec.organ||SELF)+'</b> · '+(rec.verb||'cmd')
568
+ + ' · <span style="color:'+(cls==="pass"?"#00C389":cls==="reject"?"#DF2A4A":"#7C3AED")+'">'+(rec.verdict||"INFO")+'</span></div>'
569
+ + '<div class="hash">'+shortHash(rec.hash)+'</div>';
570
+ // stream top -> bottom: newest at top
571
+ tstream.insertBefore(card, tstream.firstChild);
572
+ if (tailLive){ tstream.scrollTop = 0; }
573
+ // cap cards
574
+ while (tstream.children.length > 60) tstream.removeChild(tstream.lastChild);
575
+ }
576
+
577
+ // =====================================================================
578
+ // PANE 3 — COMMAND BAR (Cmd-K): the 7 commands
579
+ // =====================================================================
580
+ var input = document.getElementById("cmdinput");
581
+ var palette = document.getElementById("palette");
582
+ var COMMANDS = [
583
+ {nm:"ask <prompt>", ds:"multi-LLM vote → signed answer (/api/a11oy/v4/agent/ask)"},
584
+ {nm:"inspect <action>", ds:"score on Yuyay-13, sign verdict (/api/sentra/v4/inspect · LIVE)"},
585
+ {nm:"verify <receipt-hash>",ds:"cosign-verify a receipt (endpoint coming soon)"},
586
+ {nm:"kill <organ>", ds:"simulate organ failure (endpoint coming soon)"},
587
+ {nm:"restore <organ>", ds:"restore organ (endpoint coming soon)"},
588
+ {nm:"recall <query>", ds:"search amaru memory (/api/amaru/v4/recall · LIVE)"},
589
+ {nm:"tick", ds:"advance amaru clock (/api/amaru/v4/tick · LIVE)"}
590
+ ];
591
+ function showPalette(filter){
592
+ var f=(filter||"").toLowerCase();
593
+ var rows = COMMANDS.filter(function(c){return !f || c.nm.toLowerCase().indexOf(f.split(" ")[0])>=0;});
594
+ if (!rows.length){ palette.style.display="none"; return; }
595
+ palette.innerHTML = '<div class="ph label">COMMANDS</div>'
596
+ + rows.map(function(c){return '<div class="cmd" data-nm="'+c.nm.split(" ")[0]+'"><span class="nm">'+c.nm+'</span><span class="ds">'+c.ds+'</span></div>';}).join("");
597
+ palette.style.display="block";
598
+ palette.querySelectorAll(".cmd").forEach(function(el){
599
+ el.addEventListener("mousedown", function(e){ e.preventDefault(); input.value = el.dataset.nm + " "; input.focus(); showPalette(input.value); });
600
+ });
601
+ }
602
+ function hidePalette(){ palette.style.display="none"; }
603
+
604
+ input.addEventListener("focus", function(){ showPalette(input.value); });
605
+ input.addEventListener("input", function(){ showPalette(input.value); });
606
+ input.addEventListener("blur", function(){ setTimeout(hidePalette,120); });
607
+
608
+ // ⌘K or "/" to focus
609
+ document.addEventListener("keydown", function(e){
610
+ if ((e.metaKey||e.ctrlKey) && (e.key==="k"||e.key==="K")){ e.preventDefault(); input.focus(); input.select(); showPalette(""); }
611
+ else if (e.key==="/" && document.activeElement!==input){ e.preventDefault(); input.focus(); showPalette(""); }
612
+ else if (e.key==="Escape"){ hidePalette(); input.blur(); }
613
+ });
614
+
615
+ document.getElementById("cmdform").addEventListener("submit", function(e){
616
+ e.preventDefault();
617
+ var raw = input.value.trim(); if(!raw) return;
618
+ hidePalette();
619
+ runCommand(raw);
620
+ input.value = "";
621
+ });
622
+
623
+ function nowISO(){ return new Date().toISOString(); }
624
+
625
+ function runCommand(raw){
626
+ var parts = raw.split(/\s+/);
627
+ var verb = parts[0].toLowerCase();
628
+ var arg = raw.slice(parts[0].length).trim();
629
+ // emit an immediate "submitted" receipt
630
+ emitReceipt({ts:nowISO(), organ:SELF, verb:verb, verdict:"INFO", hash:"submitted:"+raw.slice(0,24)});
631
+
632
+ if (verb === "ask"){
633
+ // /api/a11oy/v4/agent/ask (parallel agent — may 404; degrade honestly)
634
+ callJSON("a11oy", "/api/a11oy/v4/agent/ask", {prompt:arg}, verb,
635
+ function(d){ return d.verdict || (d.answer?"PASS":"INFO"); },
636
+ "agent endpoint pending");
637
+ } else if (verb === "inspect"){
638
+ callJSON("sentra", "/api/sentra/v4/inspect", {action:arg}, verb,
639
+ function(d){ return d.verdict || "PASS"; }, "inspect endpoint unavailable");
640
+ } else if (verb === "recall"){
641
+ callJSON("amaru", "/api/amaru/v4/recall", {query:arg}, verb,
642
+ function(d){ return d.verdict || "PASS"; }, "recall endpoint unavailable");
643
+ } else if (verb === "tick"){
644
+ callJSON("amaru", "/api/amaru/v4/tick", {}, verb,
645
+ function(d){ return d.verdict || "PASS"; }, "tick endpoint unavailable");
646
+ } else if (verb === "verify" || verb === "kill" || verb === "restore"){
647
+ // gracefully say "endpoint coming soon"
648
+ emitReceipt({ts:nowISO(), organ:SELF, verb:verb, verdict:"INFO",
649
+ hash:"endpoint coming soon"});
650
+ } else {
651
+ emitReceipt({ts:nowISO(), organ:SELF, verb:verb, verdict:"REJECT", hash:"unknown command: "+verb});
652
+ }
653
+ }
654
+
655
+ function callJSON(organ, path, payload, verb, verdictFn, failMsg){
656
+ var url = originFor(organ) + path;
657
+ fetchTimeout(url, {method:"POST", headers:{"Content-Type":"application/json"},
658
+ body:JSON.stringify(payload), cache:"no-store"}, 8000)
659
+ .then(function(r){
660
+ if (r.status === 404){ throw {soon:true}; }
661
+ if (!r.ok) throw new Error(r.status);
662
+ return r.json();
663
+ })
664
+ .then(function(d){
665
+ var verdict = verdictFn(d) || "PASS";
666
+ var hash = d.receipt_sha || d.hash || d.sha || d.receipt || d.chain_hash || (d.receipt && d.receipt.sha) || "";
667
+ emitReceipt({ts:d.ts||nowISO(), organ:organ, verb:verb, verdict:verdict.toString().toUpperCase(), hash:hash||"(no hash in response)"});
668
+ // refresh panel if this organ is selected
669
+ if (SELECTED===organ) loadOrganData(organ);
670
+ })
671
+ .catch(function(err){
672
+ if (err && err.soon){
673
+ emitReceipt({ts:nowISO(), organ:organ, verb:verb, verdict:"INFO", hash:(verb==="ask"?"agent endpoint pending (404)":"endpoint coming soon (404)")});
674
+ } else {
675
+ emitReceipt({ts:nowISO(), organ:organ, verb:verb, verdict:"REJECT", hash:failMsg||"request failed"});
676
+ }
677
+ });
678
+ }
679
+
680
+ // =====================================================================
681
+ // BOOT
682
+ // =====================================================================
683
+ function tick5s(){ pollHealth(); updateSignals(); }
684
+ window.addEventListener("resize", layout);
685
+ layout();
686
+ requestAnimationFrame(drawTerrain);
687
+ pollHealth(); // immediate
688
+ updateSignals();
689
+ setInterval(tick5s, 5000); // health rings + signals every 5 seconds
690
+
691
+ // welcome receipt
692
+ emitReceipt({ts:nowISO(), organ:SELF, verb:"tick", verdict:"PASS", hash:"unified-shell-boot · doctrine v11 749/14/163"});
693
+ })();
694
+ </script>
695
+ </body>
696
+ </html>