betterwithage commited on
Commit
69f4cbb
·
verified ·
1 Parent(s): cb09193

chore(sync): mirror front-door files to Space (hf-sync)

Browse files

Automated front-door sync from szl-holdings/a11oy main via hf-sync.
Added/updated: cathedral.html, console/docs.html, console/index.html, console/pricing.html, console/throne-room.html, console/throne-room.js, pages/api-keys.html, pages/audit.html, pages/ayni.html, pages/brain-dual.html, pages/brain-jack.html, pages/brain.html, pages/chaski.html, pages/codex-kernel.html, pages/company.html, pages/compliance.html, pages/console.html, pages/counter-uas.html, pages/cued-engagement.html, pages/docs.html, pages/evidence.html, pages/gap-report.html, pages/harvest.html, pages/hatun-mcp.html, pages/hub.html, pages/integrations.html, pages/landing.html, pages/mesh.html, pages/observability.html, pages/operator_organ.html, pages/pricing.html, pages/run-all.html, pages/sdk.html, pages/security.html, pages/status.html, pages/substrate.html, pages/superpowers.html, pages/throne-room.html, pages/throne-room.js, pages/uds.html, pages/upgrades.html, pages/wallpa.html, pages/warhacker.html, pages/wasi-rikuq.html, pages/wires.html, static/a11oy_cathedral.js
Deleted (gone from GitHub main): (none)

Keeps the served front-door (pages/*.html, console/*.html) identical
to GitHub main so an HF factory rebuild never drops a GitHub edit or
keeps serving a page that was deleted on GitHub.

Files changed (1) hide show
  1. pages/harvest.html +815 -0
pages/harvest.html ADDED
@@ -0,0 +1,815 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <!-- SPDX-License-Identifier: Apache-2.0 © 2026 Lutar, Stephen P. — SZL Holdings · Doctrine v11+ -->
3
+ <!--
4
+ Allodial Harvest Console — web/harvest.html
5
+ Honest labels only:
6
+ • posture → real /api/a11oy/v1/harvest/posture
7
+ • sovereign banner → real /api/a11oy/v1/formula/sovereign
8
+ • joules: SAMPLE until on-box NVML provides MEASURED
9
+ • Λ = Conjecture 1 (advisory, not proven)
10
+ • NO free-energy claim — harvests WASTED grid energy
11
+ 0 runtime CDN — all CSS/JS inline.
12
+ Auto-refreshes every 30 s. Degrades honestly to "—" / "feed down" on 404/error.
13
+ -->
14
+ <html lang="en">
15
+ <head>
16
+ <meta charset="utf-8"/>
17
+ <meta name="viewport" content="width=device-width, initial-scale=1"/>
18
+ <title>a11oy · Allodial Harvest Console</title>
19
+ <style>
20
+ /* ── design tokens ───────────────────────────────────────────── */
21
+ :root{
22
+ --bg:#0a0b10; --panel:#12141d; --panel2:#171a26; --ink:#e8eaf0;
23
+ --mut:#9aa0b4; --line:#252938; --mono:'SF Mono',ui-monospace,'JetBrains Mono',Menlo,Consolas,monospace;
24
+
25
+ /* posture colours */
26
+ --c-neg:#c94eff; /* negative-price – vivid purple */
27
+ --c-cur:#3fb950; /* curtailed-renew – green */
28
+ --c-chp:#20808d; /* cheap – teal */
29
+ --c-nor:#9aa0b4; /* normal – grey */
30
+ --c-exp:#d29922; /* expensive – amber */
31
+
32
+ /* semantic */
33
+ --ok:#3fb950; --warn:#d29922; --bad:#f85149;
34
+ --gold:#d9b35c;
35
+
36
+ /* badges */
37
+ --badge-sample:#2a1f00;
38
+ --badge-sample-ink:#d29922;
39
+ --badge-measured:#0e2b0e;
40
+ --badge-measured-ink:#3fb950;
41
+ --badge-sovereign:#0e1e2b;
42
+ --badge-sovereign-ink:#7cc4ff;
43
+ --badge-half:#1c1620;
44
+ --badge-half-ink:#b090d0;
45
+ --badge-advisory:#231a00;
46
+ --badge-advisory-ink:#d9b35c;
47
+ }
48
+
49
+ /* ── reset & base ────────────────────────────────────────────── */
50
+ *{box-sizing:border-box;margin:0;padding:0}
51
+ html{scroll-behavior:smooth}
52
+ body{
53
+ background:var(--bg);color:var(--ink);
54
+ font:15px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
55
+ min-height:100vh;
56
+ }
57
+ a{color:var(--gold);text-decoration:none}
58
+ a:hover{text-decoration:underline}
59
+
60
+ /* ── layout ──────────────────────────────────────────────────── */
61
+ .wrap{max-width:1080px;margin:0 auto;padding:0 20px 80px}
62
+
63
+ /* ── header ──────────────────────────────────────────────────── */
64
+ header{
65
+ border-bottom:1px solid var(--line);
66
+ padding:14px 20px;
67
+ display:flex;align-items:center;gap:14px;flex-wrap:wrap;
68
+ background:linear-gradient(180deg,#10121b,var(--bg));
69
+ position:sticky;top:0;z-index:100;backdrop-filter:blur(6px);
70
+ }
71
+ .brand{font-weight:700;letter-spacing:.5px;color:var(--gold);font-size:18px}
72
+ .brand span{font-weight:400;opacity:.6}
73
+ .hdr-right{margin-left:auto;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
74
+ .ts{font-size:12px;color:var(--mut);font-family:var(--mono)}
75
+ button.refresh{
76
+ background:transparent;border:1px solid var(--line);color:var(--mut);
77
+ border-radius:8px;padding:5px 12px;font-size:12.5px;cursor:pointer;
78
+ transition:border-color .15s,color .15s;
79
+ }
80
+ button.refresh:hover{border-color:var(--gold);color:var(--gold)}
81
+ .tick{font-size:11px;color:var(--mut);font-family:var(--mono)}
82
+
83
+ /* ── nav ─────────────────────────────────────────────────────── */
84
+ nav{
85
+ display:flex;flex-wrap:wrap;gap:8px;padding:10px 20px;
86
+ border-bottom:1px solid var(--line);background:var(--panel);
87
+ }
88
+ nav a{
89
+ font-size:12px;padding:4px 10px;border:1px solid var(--line);
90
+ border-radius:999px;color:var(--mut);transition:border-color .15s,color .15s;
91
+ }
92
+ nav a:hover{border-color:var(--gold);color:var(--gold);text-decoration:none}
93
+ nav a.here{background:var(--gold);color:#0a0b10;border-color:var(--gold);font-weight:600}
94
+
95
+ /* ── section spacing ─────────────────────────────────────────── */
96
+ section{margin-top:28px}
97
+ h2{font-size:16px;font-weight:700;color:var(--gold);margin-bottom:12px;letter-spacing:.3px;text-transform:uppercase}
98
+
99
+ /* ── badges ──────────────────────────────────────────────────── */
100
+ .badge{
101
+ display:inline-block;padding:2px 9px;border-radius:999px;
102
+ font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;
103
+ vertical-align:middle;
104
+ }
105
+ .badge-sample {background:var(--badge-sample);color:var(--badge-sample-ink)}
106
+ .badge-measured {background:var(--badge-measured);color:var(--badge-measured-ink)}
107
+ .badge-sov {background:var(--badge-sovereign);color:var(--badge-sovereign-ink)}
108
+ .badge-half {background:var(--badge-half);color:var(--badge-half-ink)}
109
+ .badge-advisory {background:var(--badge-advisory);color:var(--badge-advisory-ink)}
110
+ .badge-live {background:rgba(63,185,80,.15);color:var(--ok)}
111
+ .badge-down {background:rgba(248,81,73,.15);color:var(--bad)}
112
+ .badge-up {background:rgba(63,185,80,.15);color:var(--ok)}
113
+
114
+ /* ── status HERO ─────────────────────────────────────────────── */
115
+ .hero{
116
+ border-radius:16px;padding:28px 28px 24px;margin-top:24px;
117
+ border:1px solid var(--line);position:relative;overflow:hidden;
118
+ transition:border-color .4s,background .4s;
119
+ }
120
+ .hero::before{
121
+ content:'';position:absolute;inset:0;
122
+ border-radius:16px;pointer-events:none;opacity:.08;
123
+ }
124
+ .hero.p-neg{border-color:var(--c-neg);background:linear-gradient(135deg,#180a22,#0a0b10)}
125
+ .hero.p-neg::before{background:var(--c-neg)}
126
+ .hero.p-cur{border-color:var(--c-cur);background:linear-gradient(135deg,#0d1f0f,#0a0b10)}
127
+ .hero.p-cur::before{background:var(--c-cur)}
128
+ .hero.p-chp{border-color:var(--c-chp);background:linear-gradient(135deg,#091a1d,#0a0b10)}
129
+ .hero.p-chp::before{background:var(--c-chp)}
130
+ .hero.p-nor{border-color:var(--line);background:var(--panel)}
131
+ .hero.p-exp{border-color:var(--c-exp);background:linear-gradient(135deg,#1e1200,#0a0b10)}
132
+ .hero.p-exp::before{background:var(--c-exp)}
133
+
134
+ .hero-inner{display:flex;align-items:flex-start;gap:20px;flex-wrap:wrap}
135
+ .hero-status{
136
+ font-size:28px;font-weight:800;letter-spacing:1px;text-transform:uppercase;
137
+ line-height:1.1;
138
+ }
139
+ .p-neg .hero-status{color:var(--c-neg)}
140
+ .p-cur .hero-status{color:var(--c-cur)}
141
+ .p-chp .hero-status{color:var(--c-chp)}
142
+ .p-nor .hero-status{color:var(--c-nor)}
143
+ .p-exp .hero-status{color:var(--c-exp)}
144
+
145
+ .hero-right{flex:1;min-width:220px}
146
+ .hero-price{font-size:22px;font-weight:700;margin:4px 0;font-family:var(--mono)}
147
+ .hero-note{font-size:13.5px;color:var(--mut);margin-bottom:10px}
148
+ .drivers{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
149
+ .driver{
150
+ font-size:11.5px;background:var(--panel2);border:1px solid var(--line);
151
+ border-radius:6px;padding:3px 9px;color:var(--mut);
152
+ }
153
+ .hero-meta{display:flex;align-items:center;gap:10px;margin-top:16px;flex-wrap:wrap}
154
+ .soak-hard{
155
+ font-size:12px;font-weight:600;padding:3px 10px;border-radius:6px;
156
+ background:rgba(201,78,255,.12);color:var(--c-neg);border:1px solid rgba(201,78,255,.3);
157
+ }
158
+ .wasted{font-size:13px;color:var(--mut)}
159
+ .hero-loading{font-size:16px;color:var(--mut)}
160
+
161
+ /* ── sovereign banner ────────────────────────────────────────── */
162
+ .sov-banner{
163
+ border-radius:12px;padding:14px 20px;margin-top:8px;
164
+ display:flex;align-items:center;gap:14px;flex-wrap:wrap;
165
+ border:1px solid var(--line);transition:border-color .4s,background .4s;
166
+ }
167
+ .sov-banner.sov{
168
+ background:rgba(124,196,255,.05);border-color:rgba(124,196,255,.3);
169
+ }
170
+ .sov-banner.half{
171
+ background:rgba(176,144,208,.04);border-color:rgba(176,144,208,.3);
172
+ }
173
+ .sov-banner.loading{background:var(--panel)}
174
+ .sov-icon{font-size:22px;line-height:1}
175
+ .sov-text strong{font-size:15px;display:block}
176
+ .sov-text p{font-size:12.5px;color:var(--mut);margin-top:2px}
177
+
178
+ /* ── cards ────────────────────────────────────────────────────── */
179
+ .card{
180
+ background:var(--panel);border:1px solid var(--line);
181
+ border-radius:12px;padding:18px 20px;margin-bottom:14px;
182
+ }
183
+
184
+ /* ── feeds panel ─────────────────────────────────────────────── */
185
+ .feeds-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px}
186
+ .feed-row{
187
+ background:var(--panel2);border:1px solid var(--line);border-radius:10px;
188
+ padding:12px 14px;display:flex;align-items:flex-start;gap:12px;
189
+ }
190
+ .feed-dot{
191
+ width:9px;height:9px;border-radius:50%;margin-top:5px;flex-shrink:0;
192
+ }
193
+ .feed-dot.live{background:var(--ok);box-shadow:0 0 6px var(--ok)}
194
+ .feed-dot.down{background:var(--bad)}
195
+ .feed-dot.loading{background:var(--mut);animation:pulse 1.2s infinite}
196
+ @keyframes pulse{0%,100%{opacity:.4}50%{opacity:1}}
197
+ .feed-name{font-size:13px;font-weight:600}
198
+ .feed-val{font-family:var(--mono);font-size:14px;font-weight:700;margin:2px 0}
199
+ .feed-unit{font-size:11.5px;color:var(--mut)}
200
+ .feed-badges{display:flex;gap:5px;flex-wrap:wrap;margin-top:5px}
201
+ .feed-note{font-size:11.5px;color:var(--mut);margin-top:4px;line-height:1.4}
202
+
203
+ /* ── soak panel ──────────────────────────────────────────────── */
204
+ .soak-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px;margin-bottom:14px}
205
+ .soak-kpi{
206
+ background:var(--panel2);border:1px solid var(--line);border-radius:10px;padding:14px;
207
+ }
208
+ .soak-kpi .k{font-size:11.5px;color:var(--mut);text-transform:uppercase;letter-spacing:.4px}
209
+ .soak-kpi .v{font-family:var(--mono);font-size:20px;font-weight:700;margin:4px 0;color:var(--ink)}
210
+ .soak-kpi .d{font-size:11.5px;color:var(--mut);line-height:1.4}
211
+ .soak-jobs{margin-top:12px}
212
+ .soak-jobs h3{font-size:13px;font-weight:600;color:var(--mut);margin-bottom:8px}
213
+ .job-list{display:flex;flex-wrap:wrap;gap:6px}
214
+ .job-chip{
215
+ background:var(--panel2);border:1px solid var(--line);border-radius:6px;
216
+ font-family:var(--mono);font-size:11.5px;padding:3px 9px;color:var(--ink);
217
+ }
218
+ .soak-reason{
219
+ margin-top:12px;font-size:12.5px;color:var(--mut);
220
+ border-top:1px solid var(--line);padding-top:10px;
221
+ }
222
+ .soak-admit{
223
+ display:inline-flex;align-items:center;gap:6px;
224
+ font-size:13px;font-weight:600;padding:4px 12px;border-radius:8px;
225
+ background:rgba(63,185,80,.1);color:var(--ok);border:1px solid rgba(63,185,80,.25);
226
+ margin-bottom:10px;
227
+ }
228
+ .soak-admit.deny{
229
+ background:rgba(248,81,73,.1);color:var(--bad);border-color:rgba(248,81,73,.25);
230
+ }
231
+
232
+ /* ── receipt panel ───────────────────────────────────────────── */
233
+ .receipt-table{width:100%;border-collapse:collapse;font-size:13.5px}
234
+ .receipt-table td,.receipt-table th{
235
+ padding:8px 10px;border-bottom:1px solid var(--line);vertical-align:top;
236
+ text-align:left;
237
+ }
238
+ .receipt-table th{
239
+ color:var(--mut);font-size:11.5px;font-weight:600;
240
+ text-transform:uppercase;letter-spacing:.4px;
241
+ }
242
+ .receipt-table td.lbl{color:var(--mut);width:44%}
243
+ .receipt-table td.val{font-family:var(--mono);font-weight:600}
244
+ .joules-row .val{font-size:16px}
245
+ .receipt-note{
246
+ margin-top:12px;padding:10px 14px;border-left:3px solid var(--gold);
247
+ background:rgba(217,179,92,.05);border-radius:0 8px 8px 0;
248
+ font-size:12.5px;color:var(--mut);
249
+ }
250
+
251
+ /* ── world panel ─────────────────────────────────────────────── */
252
+ .world-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}
253
+ .zone-card{
254
+ background:var(--panel2);border:1px solid var(--line);border-radius:10px;
255
+ padding:12px;text-align:center;transition:border-color .2s;
256
+ }
257
+ .zone-card.best{border-color:var(--ok);background:rgba(63,185,80,.05)}
258
+ .zone-name{font-size:12px;font-weight:600;color:var(--mut);text-transform:uppercase;letter-spacing:.5px}
259
+ .zone-share{font-family:var(--mono);font-size:20px;font-weight:700;margin:4px 0}
260
+ .zone-card.best .zone-share{color:var(--ok)}
261
+ .zone-best-tag{font-size:11px;color:var(--ok);margin-top:2px}
262
+
263
+ /* ── lambda advisory ─────────────────────────────────────────── */
264
+ .lambda-note{
265
+ font-size:12px;padding:10px 14px;border-left:3px solid var(--badge-advisory-ink);
266
+ background:rgba(217,179,92,.04);border-radius:0 8px 8px 0;color:var(--mut);
267
+ margin-top:14px;
268
+ }
269
+
270
+ /* ── error / empty states ────────────────────────────────────── */
271
+ .feed-unavail{
272
+ font-size:13px;color:var(--mut);
273
+ padding:14px;text-align:center;
274
+ border:1px dashed var(--line);border-radius:10px;
275
+ }
276
+ .skeleton{background:var(--panel2);border-radius:6px;height:1em;animation:pulse 1.2s infinite}
277
+
278
+ /* ── footer ──────────────────────────────────────────────────── */
279
+ footer{
280
+ border-top:1px solid var(--line);color:var(--mut);
281
+ font-size:11.5px;padding:18px 20px;line-height:1.7;
282
+ margin-top:40px;
283
+ }
284
+ footer strong{color:var(--ink)}
285
+
286
+ /* ── responsive ──────────────────────────────────────────────── */
287
+ @media(max-width:600px){
288
+ .hero{padding:18px 16px 14px}
289
+ .hero-status{font-size:20px}
290
+ .hero-price{font-size:16px}
291
+ .sov-banner{padding:10px 14px}
292
+ .wrap{padding:0 12px 60px}
293
+ header{padding:10px 12px}
294
+ nav{padding:8px 12px}
295
+ }
296
+
297
+ /* ── reduced motion ──────────────────────────────────────────── */
298
+ @media(prefers-reduced-motion:reduce){
299
+ *{animation:none!important;transition:none!important}
300
+ }
301
+ </style>
302
+ </head>
303
+ <body>
304
+
305
+ <header>
306
+ <span class="brand">a<span>11</span>oy</span>
307
+ <span style="color:var(--mut);font-size:13px">· Allodial Harvest Console</span>
308
+ <div class="hdr-right">
309
+ <span class="ts" id="ts">—</span>
310
+ <button class="refresh" id="btn-refresh" onclick="loadAll(true)">↻ Refresh</button>
311
+ <span class="tick" id="tick" title="next auto-refresh">—</span>
312
+ </div>
313
+ </header>
314
+
315
+ <nav>
316
+ <a href="/">home</a>
317
+ <a href="/hub">hub</a>
318
+ <a href="/status">status</a>
319
+ <a href="/console">console</a>
320
+ <a href="/harvest" class="here">harvest</a>
321
+ <a href="/formulas">formulas</a>
322
+ </nav>
323
+
324
+ <div class="wrap">
325
+
326
+ <!-- ────────────────────── STATUS HERO ───────────────────────── -->
327
+ <section id="sec-posture">
328
+ <div class="hero p-nor" id="hero">
329
+ <div class="hero-inner">
330
+ <div>
331
+ <div class="hero-status" id="hero-label">loading…</div>
332
+ </div>
333
+ <div class="hero-right">
334
+ <div class="hero-price" id="hero-price">—</div>
335
+ <div class="hero-note" id="hero-note">fetching posture…</div>
336
+ <div class="drivers" id="drivers"></div>
337
+ <div class="hero-meta">
338
+ <span class="wasted" id="wasted-label">—</span>
339
+ <span id="soak-hard-badge"></span>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ </section>
345
+
346
+ <!-- ────────────────────── SOVEREIGN BANNER ─────────────────── -->
347
+ <section id="sec-sovereign" style="margin-top:14px">
348
+ <div class="sov-banner loading" id="sov-banner">
349
+ <span class="sov-icon">⏳</span>
350
+ <div class="sov-text">
351
+ <strong id="sov-title">Checking sovereignty…</strong>
352
+ <p id="sov-desc">Reading /api/a11oy/v1/formula/sovereign</p>
353
+ </div>
354
+ </div>
355
+ </section>
356
+
357
+ <!-- ────────────────────── FEEDS PANEL ──────────────────────── -->
358
+ <section id="sec-feeds">
359
+ <h2>Energy Feeds</h2>
360
+ <div class="feeds-grid" id="feeds-grid">
361
+ <!-- populated by JS -->
362
+ <div class="feed-unavail">Loading feed data…</div>
363
+ </div>
364
+ </section>
365
+
366
+ <!-- ────────────────────── SOAK PANEL ───────────────────────── -->
367
+ <section id="sec-soak">
368
+ <h2>Formula-Bounded Soak Plan</h2>
369
+ <div class="card" id="soak-card">
370
+ <div class="feed-unavail">Loading soak plan…</div>
371
+ </div>
372
+ <div class="lambda-note">
373
+ <span class="badge badge-advisory">Λ = Conjecture 1</span>
374
+ &nbsp;The Ouroboros iteration bound is derived from Λ, which is currently
375
+ <strong>advisory (conjecture)</strong> — not a proven theorem. Plans using
376
+ this bound are clearly labelled as best-effort estimates.
377
+ </div>
378
+ </section>
379
+
380
+ <!-- ────────────────────── RECEIPT PANEL ────────────────────── -->
381
+ <section id="sec-receipt">
382
+ <h2>Latest Receipt</h2>
383
+ <div class="card" id="receipt-card">
384
+ <div class="feed-unavail">Loading receipt…</div>
385
+ </div>
386
+ </section>
387
+
388
+ <!-- ────────────────────── WORLD PANEL ──────────────────────── -->
389
+ <section id="sec-world">
390
+ <h2>Follow the Wind — Global Zone Shares</h2>
391
+ <div class="card">
392
+ <div id="world-best" style="margin-bottom:14px;font-size:13.5px;color:var(--mut)">Loading…</div>
393
+ <div class="world-grid" id="world-grid"></div>
394
+ </div>
395
+ </section>
396
+
397
+ </div><!-- .wrap -->
398
+
399
+ <footer>
400
+ <strong>Allodial sovereign compute — harvests wasted grid energy, proves bounded work.</strong><br>
401
+ No free-energy claim. The tagline is "harvest WASTED grid energy" — not "free energy".<br>
402
+ Joules are <strong>SAMPLE</strong> until metered by on-box NVML; they become
403
+ <strong>MEASURED</strong> only on a sovereign GPU node.<br>
404
+ Λ = Conjecture 1 (advisory). sovereign:true only when a local node serves.
405
+ <span class="badge badge-advisory" style="margin-left:4px">Free to use</span><br><br>
406
+ <span style="font-family:var(--mono)">Doctrine v11+ · SLSA L1 (honest) · Apache-2.0 · SZL Holdings · a11oy</span>
407
+ </footer>
408
+
409
+ <script>
410
+ /* ──────────────────────────────────────────────────────────────
411
+ Allodial Harvest Console — vanilla JS, 0 runtime CDN.
412
+ All data from REAL backend endpoints; degrades honestly.
413
+ ────────────────────────────────────────────────────────────── */
414
+ "use strict";
415
+
416
+ const BASE = ""; /* same-origin; no hard-coded host */
417
+ const ENDPOINTS = {
418
+ posture : "/api/a11oy/v1/harvest/posture",
419
+ plan : "/api/a11oy/v1/harvest/plan?bytes=0",
420
+ receipt : "/api/a11oy/v1/harvest/receipt",
421
+ world : "/api/a11oy/v1/harvest/world",
422
+ sovereign: "/api/a11oy/v1/formula/sovereign",
423
+ healthz : "/api/a11oy/healthz",
424
+ };
425
+
426
+ /* ── helpers ──────────────────────────────────────────────────── */
427
+ function q(id){ return document.getElementById(id); }
428
+
429
+ function safeFetch(url, label){
430
+ return fetch(url, {cache:"no-store"})
431
+ .then(r => {
432
+ if(!r.ok) throw new Error(`${r.status} ${r.statusText}`);
433
+ return r.json();
434
+ })
435
+ .catch(e => {
436
+ console.warn(`[harvest] ${label} failed:`, e.message);
437
+ return null;
438
+ });
439
+ }
440
+
441
+ function fmtNum(v){
442
+ if(v == null || v === "") return "—";
443
+ const n = Number(v);
444
+ if(isNaN(n)) return String(v);
445
+ return n.toLocaleString("en-US", {maximumFractionDigits:4});
446
+ }
447
+
448
+ function esc(s){ /* XSS-safe text */
449
+ if(s == null) return "—";
450
+ return String(s)
451
+ .replace(/&/g,"&amp;").replace(/</g,"&lt;")
452
+ .replace(/>/g,"&gt;").replace(/"/g,"&quot;");
453
+ }
454
+
455
+ /* ── posture config ───────────────────────────────────────────── */
456
+ const POSTURE_CFG = {
457
+ "negative-price" : {cls:"p-neg", label:"HARVESTING NOW", color:"var(--c-neg)", note:"The grid is paying to offload power — maximum harvest window."},
458
+ "curtailed-renewable": {cls:"p-cur", label:"RENEWABLE SURPLUS", color:"var(--c-cur)", note:"Excess renewable generation curtailed — clean harvest opportunity."},
459
+ "cheap" : {cls:"p-chp", label:"CHEAP ENERGY", color:"var(--c-chp)", note:"Below-average grid price — cost-effective harvest window."},
460
+ "normal" : {cls:"p-nor", label:"NORMAL CONDITIONS", color:"var(--c-nor)", note:"Grid at normal price and mix — standard operating mode."},
461
+ "expensive" : {cls:"p-exp", label:"EXPENSIVE — STANDING BY", color:"var(--c-exp)", note:"Grid price elevated — harvest paused to avoid wasteful spend."},
462
+ };
463
+
464
+ function getPostureCfg(p){
465
+ return POSTURE_CFG[p] || {cls:"p-nor", label: esc(p)||"UNKNOWN", color:"var(--c-nor)", note:"Posture value not recognised."};
466
+ }
467
+
468
+ /* ── posture hero ─────────────────────────────────────────────── */
469
+ function renderPosture(d){
470
+ const hero = q("hero");
471
+ if(!d){
472
+ hero.className="hero p-nor";
473
+ q("hero-label").textContent="feed down";
474
+ q("hero-price").textContent="—";
475
+ q("hero-note").textContent="Posture feed unavailable — showing last known state.";
476
+ q("drivers").innerHTML="";
477
+ q("wasted-label").textContent="—";
478
+ q("soak-hard-badge").innerHTML="";
479
+ return;
480
+ }
481
+
482
+ const cfg = getPostureCfg(d.posture);
483
+ hero.className = "hero " + cfg.cls;
484
+ q("hero-label").textContent = cfg.label;
485
+
486
+ /* price */
487
+ let priceHtml = "—";
488
+ if(d.price != null && d.price_unit){
489
+ priceHtml = `<span style="color:${cfg.color}">${fmtNum(d.price)}</span> <span style="font-size:14px;color:var(--mut)">${esc(d.price_unit)}</span>`;
490
+ } else if(d.price != null){
491
+ priceHtml = `<span style="color:${cfg.color}">${fmtNum(d.price)}</span>`;
492
+ }
493
+ q("hero-price").innerHTML = priceHtml;
494
+ q("hero-note").textContent = cfg.note;
495
+
496
+ /* drivers */
497
+ const drvs = Array.isArray(d.drivers) ? d.drivers : [];
498
+ q("drivers").innerHTML = drvs.length
499
+ ? drvs.map(dr => `<span class="driver">${esc(typeof dr === "string" ? dr : JSON.stringify(dr))}</span>`).join("")
500
+ : '<span class="driver" style="color:var(--mut)">no drivers reported</span>';
501
+
502
+ /* wasted energy available */
503
+ if(d.wasted_energy_available != null){
504
+ q("wasted-label").textContent = `Wasted energy available: ${fmtNum(d.wasted_energy_available)} ${d.wasted_energy_unit||""}`;
505
+ } else {
506
+ q("wasted-label").textContent = "Wasted energy: not reported";
507
+ }
508
+
509
+ /* soak_hard flag */
510
+ if(d.soak_hard === true){
511
+ q("soak-hard-badge").innerHTML = '<span class="soak-hard">SOAK HARD</span>';
512
+ } else {
513
+ q("soak-hard-badge").innerHTML="";
514
+ }
515
+ }
516
+
517
+ /* ── sovereign banner ─────────────────────────────────────────── */
518
+ function renderSovereign(d){
519
+ const banner = q("sov-banner");
520
+ if(!d){
521
+ banner.className="sov-banner loading";
522
+ q("sov-icon").textContent="⚠";
523
+ q("sov-title").textContent="Sovereignty gate unreachable";
524
+ q("sov-desc").textContent="Cannot verify sovereign state — feed down. Status: HALF-STATE assumed.";
525
+ return;
526
+ }
527
+
528
+ /* Accept both {sovereign:true/false} and {gate:{sovereign:...}} shapes */
529
+ const isSov = d.sovereign === true
530
+ || (d.gate && d.gate.sovereign === true)
531
+ || (d.result && d.result.sovereign === true);
532
+
533
+ if(isSov){
534
+ banner.className="sov-banner sov";
535
+ q("sov-icon").textContent="⚡";
536
+ q("sov-title").innerHTML = '<span class="badge badge-sov">SOVEREIGN</span> — running on own metal';
537
+ q("sov-desc").textContent = d.node_id
538
+ ? `Node: ${d.node_id}. Compute fully local — no external routing. Data stays on-prem.`
539
+ : "Compute fully local — no external routing. Data stays on-prem.";
540
+ } else {
541
+ banner.className="sov-banner half";
542
+ q("sov-icon").textContent="🔗";
543
+ q("sov-title").innerHTML = '<span class="badge badge-half">HALF-STATE</span> — routed externally';
544
+ q("sov-desc").textContent = "This request is handled via an external or managed node. Sovereignty not established. Joules will remain SAMPLE until a local sovereign GPU is active.";
545
+ }
546
+ }
547
+
548
+ /* ── feeds panel ──────────────────────────────────────────────── */
549
+ const KNOWN_FEEDS = [
550
+ {feed:"awattar", display:"aWATTar", desc:"Real-time day-ahead electricity price (DE/AT market)"},
551
+ {feed:"energy_charts", display:"Energy-Charts", desc:"Renewable share + grid frequency (DE)"},
552
+ {feed:"uk_carbon", display:"UK Carbon Intensity", desc:"Real-time UK grid carbon intensity (gCO₂/kWh)"},
553
+ {feed:"open_meteo", display:"Open-Meteo", desc:"Weather (wind/solar irradiance) for harvest forecast"},
554
+ {feed:"caiso", display:"CAISO", desc:"California ISO real-time system price (US West)"},
555
+ ];
556
+
557
+ function renderFeeds(readings){
558
+ const grid = q("feeds-grid");
559
+ if(!readings || !Array.isArray(readings) || readings.length === 0){
560
+ grid.innerHTML = `<div class="feed-unavail">Posture feed down — no reading data available.</div>`;
561
+ return;
562
+ }
563
+
564
+ /* Build a lookup from actual readings */
565
+ const byFeed = {};
566
+ readings.forEach(r => { byFeed[(r.feed||"").toLowerCase()] = r; });
567
+
568
+ /* Merge known feeds + any extra returned */
569
+ const allFeedIds = new Set([
570
+ ...KNOWN_FEEDS.map(f => f.feed.toLowerCase()),
571
+ ...Object.keys(byFeed),
572
+ ]);
573
+
574
+ const rows = [];
575
+ KNOWN_FEEDS.forEach(kf => {
576
+ const key = kf.feed.toLowerCase();
577
+ allFeedIds.delete(key);
578
+ rows.push({display: kf.display, desc: kf.desc, data: byFeed[key] || null});
579
+ });
580
+ /* extras not in known list */
581
+ allFeedIds.forEach(id => {
582
+ rows.push({display: id, desc:"", data: byFeed[id]||null});
583
+ });
584
+
585
+ grid.innerHTML = rows.map(row => {
586
+ const r = row.data;
587
+ const reachable = r ? r.reachable !== false : false;
588
+ const dotCls = !r ? "loading" : reachable ? "live" : "down";
589
+ const statusBadge = !r
590
+ ? `<span class="badge" style="background:#1c2030;color:var(--mut)">no data</span>`
591
+ : reachable
592
+ ? `<span class="badge badge-live">LIVE</span>`
593
+ : `<span class="badge badge-down">DOWN</span>`;
594
+ const measuredBadge = r && r.measured === true
595
+ ? `<span class="badge badge-measured">MEASURED</span>`
596
+ : r ? `<span class="badge badge-sample">SAMPLE</span>` : "";
597
+ const val = r && r.value != null ? fmtNum(r.value) : "—";
598
+ const unit = r && r.unit ? esc(r.unit) : "";
599
+ const note = r && r.note ? `<div class="feed-note">${esc(r.note)}</div>` : (row.desc ? `<div class="feed-note">${esc(row.desc)}</div>` : "");
600
+
601
+ return `<div class="feed-row">
602
+ <span class="feed-dot ${dotCls}" title="${reachable ? "live" : "down"}"></span>
603
+ <div style="flex:1;min-width:0">
604
+ <div class="feed-name">${esc(row.display)}</div>
605
+ <div class="feed-val">${esc(val)} <span class="feed-unit">${unit}</span></div>
606
+ <div class="feed-badges">${statusBadge}${measuredBadge}</div>
607
+ ${note}
608
+ </div>
609
+ </div>`;
610
+ }).join("");
611
+ }
612
+
613
+ /* ── soak panel ───────────────────────────────────────────────── */
614
+ function renderSoak(d){
615
+ const card = q("soak-card");
616
+ if(!d){
617
+ card.innerHTML = `<div class="feed-unavail">Soak plan feed unavailable — cannot determine formula-bounded plan. Will retry on next refresh.</div>`;
618
+ return;
619
+ }
620
+
621
+ const admitted = d.admit === true || d.admit === "true";
622
+ const admitHtml = admitted
623
+ ? `<span class="soak-admit">✓ ADMIT — wasted energy available, plan within bounds</span>`
624
+ : `<span class="soak-admit deny">✗ HOLD — soak not admitted (posture or bounds)</span>`;
625
+
626
+ const bekBits = d.bekenstein_cap_bits != null ? fmtNum(d.bekenstein_cap_bits) : "—";
627
+ const landauJ = d.landauer_floor_joules != null ? fmtNum(d.landauer_floor_joules) : "—";
628
+ const ouroboros = d.ouroboros_max_iters != null ? fmtNum(d.ouroboros_max_iters) : "—";
629
+ const soakVal = d.soak != null ? esc(String(d.soak)) : "—";
630
+
631
+ const jobs = Array.isArray(d.admitted_ids) ? d.admitted_ids : [];
632
+ const jobsHtml = jobs.length
633
+ ? `<div class="soak-jobs"><h3>Admitted job IDs</h3><div class="job-list">${jobs.map(j=>`<span class="job-chip">${esc(String(j))}</span>`).join("")}</div></div>`
634
+ : `<div class="soak-jobs"><h3>Admitted job IDs</h3><div class="job-list"><span style="color:var(--mut);font-size:12.5px">none admitted</span></div></div>`;
635
+
636
+ const reasonHtml = d.reason
637
+ ? `<div class="soak-reason">Reason: ${esc(d.reason)}</div>`
638
+ : "";
639
+
640
+ card.innerHTML = `
641
+ ${admitHtml}
642
+ <div class="soak-grid">
643
+ <div class="soak-kpi">
644
+ <div class="k">Bekenstein Cap</div>
645
+ <div class="v">${bekBits}</div>
646
+ <div class="d">bits — max information throughput proven by the Bekenstein bound. We only run as much as the proven info budget allows.</div>
647
+ </div>
648
+ <div class="soak-kpi">
649
+ <div class="k">Landauer Floor</div>
650
+ <div class="v">${landauJ}</div>
651
+ <div class="d">joules — thermodynamic minimum energy per bit-erasure (kT ln2). Computations below this cost are physically impossible.</div>
652
+ </div>
653
+ <div class="soak-kpi">
654
+ <div class="k">Ouroboros Bound</div>
655
+ <div class="v">${ouroboros}</div>
656
+ <div class="d">max iterations — derived from Λ (Conjecture 1, advisory). Prevents unbounded self-modification loops. <span class="badge badge-advisory">Λ advisory</span></div>
657
+ </div>
658
+ <div class="soak-kpi">
659
+ <div class="k">Soak</div>
660
+ <div class="v">${soakVal}</div>
661
+ <div class="d">whether the system is soaking this cycle (consuming admitted wasted energy).</div>
662
+ </div>
663
+ </div>
664
+ ${jobsHtml}
665
+ ${reasonHtml}
666
+ `;
667
+ }
668
+
669
+ /* ── receipt panel ────────────────────────────────────────────── */
670
+ function renderReceipt(d){
671
+ const card = q("receipt-card");
672
+ if(!d){
673
+ card.innerHTML = `<div class="feed-unavail">Receipt feed unavailable — no receipt data. Will retry.</div>`;
674
+ return;
675
+ }
676
+
677
+ const joulLabel = (d.joules_label||"").toLowerCase();
678
+ const joulMeasured = joulLabel === "measured";
679
+ const joulBadge = joulMeasured
680
+ ? `<span class="badge badge-measured">MEASURED</span>`
681
+ : `<span class="badge badge-sample">SAMPLE</span>`;
682
+ const joulsVal = d.joules_est != null ? fmtNum(d.joules_est) : "—";
683
+
684
+ const priceRow = d.price_measured != null
685
+ ? `<tr><td class="lbl">Price (measured)</td><td class="val">${esc(String(d.price_measured))}</td></tr>`
686
+ : "";
687
+
688
+ const rows = [
689
+ ["Energy source", d.energy_source],
690
+ ["Posture", d.posture],
691
+ ].map(([l,v]) => v != null
692
+ ? `<tr><td class="lbl">${esc(l)}</td><td class="val">${esc(String(v))}</td></tr>`
693
+ : ""
694
+ ).join("");
695
+
696
+ /* extra keys except the ones we've already shown */
697
+ const skip = new Set(["energy_source","posture","price_measured","joules_est","joules_label","status","fetchedAt","doctrine","citations"]);
698
+ const extras = Object.entries(d)
699
+ .filter(([k]) => !skip.has(k) && d[k] != null)
700
+ .map(([k,v]) => `<tr><td class="lbl">${esc(k)}</td><td class="val">${esc(typeof v==="object"?JSON.stringify(v):String(v))}</td></tr>`)
701
+ .join("");
702
+
703
+ card.innerHTML = `
704
+ <table class="receipt-table">
705
+ <thead><tr><th>Field</th><th>Value</th></tr></thead>
706
+ <tbody>
707
+ ${rows}
708
+ ${priceRow}
709
+ <tr class="joules-row">
710
+ <td class="lbl">Joules (estimated) ${joulBadge}</td>
711
+ <td class="val">${esc(joulsVal)}</td>
712
+ </tr>
713
+ ${extras}
714
+ </tbody>
715
+ </table>
716
+ <div class="receipt-note">
717
+ Joules become <strong>MEASURED</strong> only when on-box NVML is active on a sovereign GPU node.
718
+ Until then all joule readings are <strong>SAMPLE</strong> — statistically estimated, not directly metered.
719
+ This is an honest label, not a placeholder: the system does real work, the accounting is just not yet metered.
720
+ </div>
721
+ `;
722
+ }
723
+
724
+ /* ── world panel ──────────────────────────────────────────────── */
725
+ function renderWorld(d){
726
+ const best = q("world-best");
727
+ const grid = q("world-grid");
728
+ if(!d || !d.shares){
729
+ best.textContent = "World feed unavailable — cannot show zone renewable shares.";
730
+ grid.innerHTML = "";
731
+ return;
732
+ }
733
+
734
+ const bz = d.best_zone || "—";
735
+ const bs = d.best_share != null ? Math.round(d.best_share*100)+"%" : "—";
736
+ best.innerHTML = `Best zone: <strong style="color:var(--ok)">${esc(bz)}</strong>
737
+ <span style="font-family:var(--mono);color:var(--ok);font-weight:700"> ${esc(bs)}</span>
738
+ renewable share — follow the wind to this zone for greenest harvest.`;
739
+
740
+ const zones = Object.entries(d.shares)
741
+ .sort((a,b) => (b[1]||0)-(a[1]||0));
742
+
743
+ grid.innerHTML = zones.map(([zone, share]) => {
744
+ const pct = share != null ? Math.round(share*100)+"%" : "—";
745
+ const isBest = zone === d.best_zone;
746
+ return `<div class="zone-card${isBest?" best":""}">
747
+ <div class="zone-name">${esc(zone)}</div>
748
+ <div class="zone-share">${esc(pct)}</div>
749
+ ${isBest ? `<div class="zone-best-tag">★ best</div>` : ""}
750
+ </div>`;
751
+ }).join("");
752
+ }
753
+
754
+ /* ── timestamp ────────────────────────────────────────────────── */
755
+ function updateTs(){
756
+ const now = new Date();
757
+ q("ts").textContent = now.toUTCString().replace("GMT","UTC");
758
+ }
759
+
760
+ /* ── countdown tick ───────────────────────────────────────────── */
761
+ let _nextRefresh = Date.now() + 30000;
762
+ function startTick(){
763
+ setInterval(()=>{
764
+ const s = Math.max(0, Math.round((_nextRefresh - Date.now())/1000));
765
+ q("tick").textContent = `↺ ${s}s`;
766
+ }, 1000);
767
+ }
768
+
769
+ /* ── main load ────────────────────────────────────────────────── */
770
+ async function loadAll(manual){
771
+ _nextRefresh = Date.now() + 30000;
772
+ updateTs();
773
+ q("btn-refresh").textContent = "loading…";
774
+ q("btn-refresh").disabled = true;
775
+
776
+ try {
777
+ /* parallel fetch all endpoints */
778
+ const [posture, plan, receipt, world, sovereign] = await Promise.all([
779
+ safeFetch(ENDPOINTS.posture, "posture"),
780
+ safeFetch(ENDPOINTS.plan, "plan"),
781
+ safeFetch(ENDPOINTS.receipt, "receipt"),
782
+ safeFetch(ENDPOINTS.world, "world"),
783
+ safeFetch(ENDPOINTS.sovereign, "sovereign"),
784
+ ]);
785
+
786
+ renderPosture(posture);
787
+ renderFeeds(posture ? posture.readings : null);
788
+ renderSovereign(sovereign);
789
+ renderSoak(plan);
790
+ renderReceipt(receipt);
791
+ renderWorld(world);
792
+ updateTs();
793
+ } finally {
794
+ q("btn-refresh").textContent = "↻ Refresh";
795
+ q("btn-refresh").disabled = false;
796
+ }
797
+ }
798
+
799
+ /* ── auto-refresh every 30 s ──────────────────────────────────── */
800
+ function scheduleRefresh(){
801
+ setTimeout(async ()=>{
802
+ await loadAll(false);
803
+ scheduleRefresh();
804
+ }, 30000);
805
+ }
806
+
807
+ /* ── boot ─────────────────────────────────────────────────────── */
808
+ (async()=>{
809
+ startTick();
810
+ await loadAll(false);
811
+ scheduleRefresh();
812
+ })();
813
+ </script>
814
+ </body>
815
+ </html>