a11oy / web /energy-harvest.html
betterwithage's picture
feat(energy): deploy 3 real energy surfaces (ops/3d/harvest) bound to live endpoints
fce4859 verified
Raw
History Blame
28.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>a11oy — Grid Energy Harvest · Proven Energy Engine · SZL Holdings</title>
<meta name="description" content="Honest grid/harvest view of the Proven Energy Engine: the Bekenstein-gated energy+information budget (F19/TH6, a proven inequality), the cumulative joules-est-over-time series (SAMPLE/ESTIMATE — no on-box meter behind the budget layer), and a client-side off-peak/normal/peak window heuristic (NOT a live tariff feed). No free-energy / perpetual-motion claims."/>
<!-- 0 runtime CDN (doctrine v11): uPlot (MIT) + Apache ECharts (Apache-2.0) vendored same-origin at /vendor/*. System fonts only. -->
<link rel="stylesheet" href="/vendor/uPlot.min.css"/>
<style>
:root{
--ground:#0a0a0a; --panel:#0e0e0e; --panel2:#080808;
--gold:#c9b787; --gold-bright:#d6c69a;
--holo:#39d8c8; --holo-soft:rgba(57,216,200,0.10); --holo-line:rgba(57,216,200,0.24);
--teal:#5fb3a3;
--cream:#f5f5f5; --paragraph:#9a9a9a; --muted:#888; --dim:#555;
--gold-line:rgba(201,183,135,0.15); --gold-soft:rgba(201,183,135,0.04);
--live:#5a8a6e; --warn:#c9a05f;
--green:#39d98a; --yellow:#f5c451; --red:#ff6a5a; --blue:#5bc8ff;
--mono:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
--display:ui-sans-serif,system-ui,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;background:var(--ground);color:var(--cream);font-family:var(--display);-webkit-font-smoothing:antialiased;}
.mono{font-family:var(--mono);}
a{color:inherit;text-decoration:none;}
.topbar{position:sticky;top:0;z-index:60;display:flex;align-items:center;gap:1rem;flex-wrap:wrap;padding:.5rem 1.1rem;background:rgba(10,10,10,.92);backdrop-filter:blur(10px);border-bottom:1px solid var(--holo-line);font-family:var(--mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--holo);}
.topbar .sep{color:var(--dim);}
.topbar .live{display:inline-flex;align-items:center;gap:.4rem;color:var(--cream);}
.live-dot{width:6px;height:6px;border-radius:50%;background:var(--holo);box-shadow:0 0 6px var(--holo);animation:pulse 2.2s ease-in-out infinite;}
@keyframes pulse{0%,100%{opacity:1;}50%{opacity:.35;}}
.switcher{margin-left:auto;display:flex;align-items:center;gap:.3rem;flex-wrap:wrap;}
.flag{padding:.22rem .55rem;border-radius:6px;border:1px solid transparent;color:var(--muted);transition:.15s;}
.flag:hover{color:var(--cream);border-color:var(--holo-line);background:var(--holo-soft);}
.flag.active{color:var(--ground);background:var(--holo);border-color:var(--holo);font-weight:600;}
.wrap{max-width:1320px;margin:0 auto;padding:1.6rem 1.4rem 4rem;}
h1{font-size:1.7rem;font-weight:600;margin:.2rem 0 .3rem;letter-spacing:-.01em;}
.badge{display:inline-block;font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--holo);border:1px solid var(--holo-line);background:var(--holo-soft);padding:.25rem .6rem;border-radius:5px;margin-left:.4rem;vertical-align:middle;}
.lede{color:var(--paragraph);font-size:.95rem;line-height:1.65;max-width:1180px;margin:.6rem 0 1rem;}
.lede b{color:var(--cream);font-weight:600;}
.econ{border:1px solid var(--gold-line);background:var(--gold-soft);border-radius:8px;padding:.7rem 1rem;margin:.4rem 0 1.2rem;font-family:var(--mono);font-size:12px;color:var(--paragraph);line-height:1.7;}
.econ .k{color:var(--gold);text-transform:uppercase;letter-spacing:.08em;font-size:10px;}
.econ .hi{color:var(--holo);}
.subnav{display:flex;gap:.5rem;flex-wrap:wrap;align-items:center;margin:.1rem 0 1.2rem;}
.subnav .lbl{font-family:var(--mono);font-size:10px;letter-spacing:.08em;text-transform:uppercase;color:var(--muted);margin-right:.2rem;}
.cards{display:grid;grid-template-columns:repeat(4,1fr);gap:.8rem;margin:0 0 1.2rem;}
.card{border:1px solid var(--gold-line);background:var(--panel);border-radius:8px;padding:.85rem 1rem;}
.card .lbl{font-family:var(--mono);font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);}
.card .big{font-size:1.5rem;font-weight:600;margin:.35rem 0 .15rem;color:var(--holo);overflow-wrap:anywhere;}
.card .sub{font-family:var(--mono);font-size:10.5px;color:var(--muted);}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:1rem;}
.panel{border:1px solid var(--gold-line);background:var(--panel);border-radius:10px;padding:1rem 1.1rem;margin-bottom:1rem;}
.panel h2{font-size:1.05rem;font-weight:600;margin:0 0 .2rem;display:flex;align-items:center;justify-content:space-between;gap:.6rem;flex-wrap:wrap;}
.panel h2 .meta{font-family:var(--mono);font-size:10px;color:var(--dim);text-transform:uppercase;letter-spacing:.06em;}
.chip{font-family:var(--mono);font-size:9px;letter-spacing:.06em;text-transform:uppercase;border-radius:4px;padding:.12rem .42rem;white-space:nowrap;border:1px solid var(--dim);color:var(--muted);}
.chip.measured{color:var(--green);border-color:rgba(57,217,138,.5);background:rgba(57,217,138,.08);}
.chip.sample,.chip.estimate{color:var(--warn);border-color:var(--warn);background:rgba(201,160,95,.06);}
.chip.modeled{color:var(--blue);border-color:rgba(91,200,255,.5);background:rgba(91,200,255,.07);}
.chip.unavailable{color:var(--red);border-color:rgba(255,106,90,.5);background:rgba(255,106,90,.07);}
.uarea{width:100%;height:300px;}
.echart{width:100%;height:300px;}
.win{display:grid;grid-template-columns:repeat(3,1fr);gap:.5rem;margin-top:.2rem;}
.win .cell{border:1px solid var(--gold-line);border-radius:7px;padding:.7rem;text-align:center;font-family:var(--mono);}
.win .cell .t{font-size:9.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--dim);}
.win .cell .h{font-size:9.5px;color:var(--muted);margin-top:.2rem;}
.win .cell .v{font-size:1rem;font-weight:600;margin-top:.3rem;color:var(--muted);}
.win .cell.on{border-color:var(--holo-line);background:var(--holo-soft);}
.win .cell.on .v{color:var(--holo);}
.win .cell.peak.on{border-color:rgba(255,106,90,.45);background:rgba(255,106,90,.07);}
.win .cell.peak.on .v{color:var(--red);}
.win .cell.normal.on{border-color:rgba(245,196,81,.45);background:rgba(245,196,81,.07);}
.win .cell.normal.on .v{color:var(--yellow);}
.simnote{border:1px solid var(--warn);background:rgba(201,160,95,.06);border-radius:8px;padding:.7rem .9rem;margin-top:.8rem;font-family:var(--mono);font-size:11px;color:#d8c39a;line-height:1.65;}
.simnote b{color:var(--warn);}
table{width:100%;border-collapse:collapse;margin-top:.6rem;font-family:var(--mono);font-size:11px;}
th,td{text-align:left;padding:.4rem .5rem;border-bottom:1px solid var(--gold-line);white-space:nowrap;}
th{color:var(--dim);font-weight:500;font-size:9.5px;letter-spacing:.08em;text-transform:uppercase;}
td{color:var(--paragraph);}
td.ok{color:var(--green);}
td.bad{color:var(--red);}
.scrollx{overflow-x:auto;}
.empty{font-family:var(--mono);font-size:11.5px;color:var(--muted);border:1px dashed var(--holo-line);border-radius:8px;padding:1.1rem 1rem;margin-top:.2rem;line-height:1.6;text-align:center;}
.empty b{color:var(--cream);}
.kv{font-family:var(--mono);font-size:12px;color:var(--paragraph);line-height:1.9;margin-top:.3rem;}
.kv .k{color:var(--dim);display:inline-block;min-width:150px;}
.kv .v{color:var(--cream);}
.footnote{margin-top:1.6rem;font-family:var(--mono);font-size:10.5px;color:var(--muted);line-height:1.7;border-top:1px solid var(--gold-line);padding-top:.9rem;}
.footnote b{color:var(--cream);}
@media(max-width:980px){.cards{grid-template-columns:repeat(2,1fr);}.grid2{grid-template-columns:1fr;}}
@media (prefers-reduced-motion: reduce){*{animation:none !important;transition:none !important;}.live-dot{animation:none;}}
</style>
</head>
<body>
<div class="topbar">
<span>SZL HOLDINGS</span><span class="sep">/</span><span>a11oy</span><span class="sep">/</span><span>GRID ENERGY HARVEST</span>
<span class="sep">/</span><span class="live"><span class="live-dot"></span><span id="livelbl">LIVE · RT</span></span>
<span class="switcher">
<a class="flag" href="/energy">Energy</a>
<a class="flag" href="/energy-ops">Live Energy</a>
<a class="flag" href="/energy-3d">3D Holographic</a>
<a class="flag active" href="/energy-harvest">Grid Harvest</a>
<a class="flag" href="/code">Code</a>
</span>
</div>
<div class="wrap">
<h1>Grid Energy Harvest<span class="badge">Bekenstein-gated budget (F19/TH6) · SAMPLE energy · heuristic window</span></h1>
<p class="lede">A live, <b>honest</b> grid/harvest view of the Proven Energy Engine. The cumulative series is the <b>Bekenstein-gated energy+information budget</b> — every task receipt's <code>joules_est</code> over time, each passing the gate <code>shannon_bits ≤ output_bytes·8</code> (<b>F19/TH6, a proven inequality</b>, locked-8 — not an assertion). <b>Energy figures are SAMPLE/ESTIMATE</b>: there is no on-box power meter behind the budget layer, so joules are labeled, never presented as measured. The off-peak / normal / peak window is a <b>client-side heuristic from the server clock — NOT a live tariff feed</b> (no real-time grid price is wired). <b>No free-energy / perpetual-motion claims</b>: the engine harvests WASTED energy and <i>proves bounded information work</i>; any harvest framing is MODELED/SAMPLE.</p>
<div class="econ"><span class="k">Why it matters:</span> harvest yield and energy efficiency are <span class="hi">measured-or-labeled, never assumed</span> — the only unacceptable outcome is over-claiming (fabricated joules, a fake tariff feed, or a perpetual-motion story). This surface refuses to over-claim: when the budget is empty it shows the structure with an honest <b>"no tasks yet"</b>, never fake bars.</div>
<div class="subnav">
<span class="lbl">Bound endpoints (poll ~5s):</span>
<span class="flag" style="border-color:var(--holo-line);background:var(--holo-soft);cursor:default">/v1/energy/harvest</span>
<span class="flag" style="border-color:var(--holo-line);background:var(--holo-soft);cursor:default">/v1/energy/budget</span>
<span class="flag" style="border-color:var(--holo-line);background:var(--holo-soft);cursor:default">/v1/energy/ledger</span>
</div>
<div class="cards">
<div class="card"><div class="lbl">Tasks gated</div><div class="big" id="c_tasks" style="color:var(--blue)"></div><div class="sub">Bekenstein receipts</div></div>
<div class="card"><div class="lbl">All within bound</div><div class="big" id="c_within" style="color:var(--green)"></div><div class="sub">F19/TH6 gate</div></div>
<div class="card"><div class="lbl">Total joules-est <span class="chip sample" id="c_joules_lbl">sample</span></div><div class="big" id="c_joules" style="color:var(--warn)"></div><div class="sub">estimate · not metered</div></div>
<div class="card"><div class="lbl">Power window</div><div class="big" id="c_window"></div><div class="sub" id="c_window_sub">client-side heuristic</div></div>
</div>
<div class="panel">
<h2>(1) Cumulative joules-est over time
<span style="display:flex;gap:.4rem;align-items:center"><span class="chip sample" id="area_lbl">sample</span><span class="meta">/v1/energy/harvest · budget.series</span></span>
</h2>
<div id="area_mount"><div class="uarea" id="area_chart"></div></div>
<div class="empty" id="area_empty" style="display:none"><b>No tasks yet.</b> The in-memory Bekenstein budget ledger is empty (it resets on process restart). Track a task via <code>/api/a11oy/v1/energy/budget?bytes=…</code> to populate the series — until then this is the honest empty-state, not a fabricated curve.</div>
<div class="simnote"><b>joules_est is SAMPLE/ESTIMATE</b> — no on-box meter is wired behind the budget layer (doctrine v11). The curve is the running sum of per-task <code>joules_est</code>; the chip reflects the payload's <code>joules_label</code>. The plot tint mirrors the current heuristic window (green=off-peak, amber=normal, red=peak) — a clock heuristic, NOT a tariff feed.</div>
</div>
<div class="grid2">
<div class="panel">
<h2>(2) Off-peak window<span class="meta">server-clock heuristic</span></h2>
<div class="win" id="win">
<div class="cell offpeak" data-w="off-peak"><div class="t">Off-peak</div><div class="h" id="win_h_off"></div><div class="v"></div></div>
<div class="cell normal" data-w="normal"><div class="t">Normal</div><div class="h" id="win_h_norm"></div><div class="v"></div></div>
<div class="cell peak" data-w="peak"><div class="t">Peak</div><div class="h" id="win_h_peak"></div><div class="v"></div></div>
</div>
<div class="simnote" id="win_note"><b>Client-side heuristic, NOT a live tariff feed.</b> The window is derived from the server clock (hour-of-day bands) — it mirrors the engine's off-peak provider but <b>no real-time grid price is wired</b>. No price or carbon figure is claimed as metered.</div>
</div>
<div class="panel">
<h2>(3) Bekenstein budget gauge
<span style="display:flex;gap:.4rem;align-items:center"><span class="chip measured" id="gauge_chip">proven</span><span class="meta">Σ shannon_bits ÷ Σ n·8</span></span>
</h2>
<div class="echart" id="gauge_chart" style="height:260px"></div>
<div class="empty" id="gauge_empty" style="display:none"><b>No tasks yet.</b> Nothing to gate — the F19/TH6 inequality holds vacuously over an empty ledger.</div>
<div class="simnote" id="gauge_note"><b>Proven inequality (F19/TH6, locked-8):</b> empirical byte-entropy is always ≤ n·8, so the needle stays at or below 100% of the ceiling unless an upstream over-claim occurs (then it reads OVER, in red). Kernel-checked in EnergyBudgetWitness.lean (0-sorry). <code>all_within_bound</code> is the real gate result.</div>
</div>
</div>
<div class="grid2">
<div class="panel">
<h2>(4) Per-task receipt ledger<span class="meta">/v1/energy/harvest · per receipt</span></h2>
<div class="scrollx" id="led_wrap" style="display:none">
<table>
<thead><tr>
<th>#</th><th>shannon_bits</th><th>bekenstein n·8</th><th>within</th><th>joules_est <span class="chip sample">est</span></th><th>Σ joules-est</th><th>ts</th>
</tr></thead>
<tbody id="led_body"></tbody>
</table>
</div>
<div class="empty" id="led_empty"><b>No per-task receipts yet.</b> The in-memory ledger is empty and resets on process restart. The table structure stays visible so the shape is honest — no placeholder rows.</div>
</div>
<div class="panel">
<h2>Harvest composition + signed ledger
<span style="display:flex;gap:.4rem;align-items:center"><span class="chip modeled" id="mix_chip">modeled</span><span class="meta">/v1/energy/ledger</span></span>
</h2>
<div class="echart" id="mix_chart" style="height:170px"></div>
<div class="kv" id="sl_kv" style="display:none">
<div><span class="k">Signed jobs</span><span class="v" id="sl_jobs"></span></div>
<div><span class="k">Joules MEASURED-billable</span><span class="v" id="sl_meas"></span> <span class="chip measured">measured</span></div>
<div><span class="k">kWh total</span><span class="v" id="sl_kwh"></span></div>
<div><span class="k">Receipt chain</span><span class="v" id="sl_chain"></span></div>
</div>
<div class="empty" id="sl_empty" style="display:none"><b>Signed ledger unavailable.</b> /v1/energy/ledger did not return a chain — showing no measured total (honest empty-state, never a fabricated number).</div>
<div class="simnote"><b>Two honesty tiers, never mixed:</b> the <span style="color:var(--warn)">SAMPLE/ESTIMATE</span> joules-est above come from the budget layer (no meter); the <span style="color:var(--green)">MEASURED-billable</span> joules here come only from real NVML deltas in the signed receipt chain. The donut splits MEASURED-billable vs SAMPLE-est headroom — a MODELED composition, not a grid source mix.</div>
</div>
</div>
<div class="footnote">
<b>Honest by design.</b> The Bekenstein gate is <b>F19 (locked-8) / TH6</b>, a machine-checked inequality (EnergyBudgetWitness.lean, 0-sorry) — Σ shannon_bits ≤ Σ output_bytes·8. <b>All joules-est figures are SAMPLE/ESTIMATE</b> (no on-box meter behind the budget layer); only the signed-ledger <code>joules_measured_billable</code> is MEASURED (real NVML deltas). The off-peak/normal/peak window is a <b>client-side clock heuristic, NOT a live tariff feed</b>. <b>No free-energy / perpetual-motion claims</b> — the engine harvests WASTED energy and proves bounded information work; harvest framing is MODELED/SAMPLE. Λ trust = <b>Conjecture 1</b> (advisory, never a theorem). <b>0 runtime CDN</b> — uPlot (MIT) + Apache ECharts (Apache-2.0) served same-origin from <code>/vendor/*</code>; system fonts. Poll ~5s; every value MEASURED-or-labeled, never fabricated.
</div>
</div>
<!-- 0 CDN: uPlot (MIT) + Apache ECharts (Apache-2.0) vendored same-origin. -->
<script src="/vendor/uPlot.iife.min.js"></script>
<script src="/vendor/echarts.min.js"></script>
<script>
const A="/api/a11oy";
const $=id=>document.getElementById(id);
const REDUCED = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
async function getJSON(u){try{const r=await fetch(u,{headers:{'accept':'application/json'}});if(!r.ok)return null;return await r.json();}catch(e){return null;}}
function fmt(n,d){if(n==null||isNaN(n))return '—';const x=Number(n);return d!=null?x.toFixed(d):String(x);}
function tsEpoch(s){const t=Date.parse(s);return isNaN(t)?null:Math.floor(t/1000);}
// ---- honesty chip helper ----------------------------------------------------
function setChip(el,label){
if(!el)return;
const l=String(label||'SAMPLE').toUpperCase();
el.textContent=l.toLowerCase();
el.className='chip '+({MEASURED:'measured',SAMPLE:'sample',ESTIMATE:'estimate',MODELED:'modeled',UNAVAILABLE:'unavailable'}[l]||'sample');
}
// ---- (2) off-peak window ----------------------------------------------------
const WIN_COLORS={'off-peak':'rgba(57,217,138,0.10)','normal':'rgba(245,196,81,0.08)','peak':'rgba(255,106,90,0.08)'};
let CURRENT_WINDOW='normal';
function renderWindow(tw){
if(!tw)return;
CURRENT_WINDOW=tw.window||'normal';
const bands=tw.bands||{};
$('win_h_off').textContent=bands['off-peak']||'';
$('win_h_norm').textContent=bands['normal']||'';
$('win_h_peak').textContent=bands['peak']||'';
const hh=(tw.local_hour!=null)?('NOW · '+String(tw.local_hour).padStart(2,'0')+':00'):'NOW';
document.querySelectorAll('#win .cell').forEach(c=>{
const on=c.dataset.w===CURRENT_WINDOW;
c.classList.toggle('on',on);
c.querySelector('.v').textContent=on?hh:'—';
});
$('c_window').textContent=CURRENT_WINDOW;
$('c_window').style.color=({'off-peak':'var(--green)','normal':'var(--yellow)','peak':'var(--red)'}[CURRENT_WINDOW])||'var(--holo)';
$('c_window_sub').textContent='heuristic · hour '+(tw.local_hour!=null?tw.local_hour:'—');
}
// ---- (1) cumulative joules-est area chart (uPlot, MIT) ----------------------
let uplot=null;
// background tint = current heuristic window (clock heuristic, NOT a tariff feed)
const windowTint={hooks:{draw:[u=>{
const {ctx}=u; const {left,top,width,height}=u.bbox;
ctx.save(); ctx.fillStyle=WIN_COLORS[CURRENT_WINDOW]||'transparent';
ctx.fillRect(left,top,width,height); ctx.restore();
}]}};
function buildUplot(){
const el=$('area_chart'); if(!el||!window.uPlot)return;
const w=el.clientWidth||el.parentElement.clientWidth||800;
const opts={
width:w, height:300,
plugins:[windowTint],
cursor:{points:{size:6}},
scales:{x:{time:true}},
axes:[
{stroke:'#888',grid:{stroke:'rgba(201,183,135,0.10)'},ticks:{stroke:'rgba(201,183,135,0.18)'},font:'11px ui-monospace,monospace'},
{stroke:'#888',grid:{stroke:'rgba(201,183,135,0.10)'},ticks:{stroke:'rgba(201,183,135,0.18)'},font:'11px ui-monospace,monospace',
label:'Σ joules-est (SAMPLE)',labelFont:'11px ui-monospace,monospace',labelSize:34},
],
series:[
{},
{label:'Σ joules-est',stroke:'#39d8c8',width:2,fill:'rgba(57,216,200,0.18)',
points:{show:false},
value:(u,v)=>v==null?'—':v.toFixed(3)+' J (est)'},
],
};
uplot=new uPlot(opts,[[],[]],el);
window.addEventListener('resize',()=>{ if(uplot) uplot.setSize({width:(el.clientWidth||w),height:300}); });
}
function renderArea(series,label){
setChip($('area_lbl'),label);
const rows=Array.isArray(series)?series.filter(r=>r&&r.ts):[];
const hasData=rows.length>0;
$('area_empty').style.display=hasData?'none':'block';
$('area_chart').style.display=hasData?'block':'none';
if(!hasData){return;}
if(!uplot)buildUplot();
// build x (epoch seconds) + y (cumulative joules-est). Fall back to index if ts unparseable.
let xs=[], ys=[]; let useIndex=false;
rows.forEach((r,i)=>{const e=tsEpoch(r.ts); if(e==null)useIndex=true;});
rows.forEach((r,i)=>{
const e=tsEpoch(r.ts);
xs.push(useIndex?i:(e!=null?e:i));
ys.push(Number(r.cumulative_joules_est!=null?r.cumulative_joules_est:r.joules_est)||0);
});
if(uplot){ uplot.setData([xs,ys]); }
}
// ---- (3) Bekenstein budget gauge (ECharts, Apache-2.0) ----------------------
let gaugeChart=null;
function buildGauge(){
const el=$('gauge_chart'); if(!el||!window.echarts)return;
gaugeChart=echarts.init(el,null,{renderer:'canvas'});
}
function renderGauge(sumSh,sumCeil,allWithin,taskCount){
const has=(taskCount>0)&&(sumCeil!=null)&&(sumCeil>0);
$('gauge_empty').style.display=has?'none':'block';
$('gauge_chart').style.display=has?'block':'none';
// chip reflects the REAL gate result
const gchip=$('gauge_chip');
if(allWithin===false){gchip.textContent='OVER';gchip.className='chip unavailable';}
else{gchip.textContent='within bound';gchip.className='chip measured';}
if(!has)return;
if(!gaugeChart)buildGauge();
const pct=Math.max(0,Math.min(sumSh/sumCeil,1.2))*100;
const over=sumSh>sumCeil+1e-9;
gaugeChart && gaugeChart.setOption({
animation:!REDUCED,
series:[{
type:'gauge',min:0,max:100,startAngle:210,endAngle:-30,
radius:'95%',center:['50%','62%'],
progress:{show:true,width:14,itemStyle:{color:over?'#ff6a5a':'#39d8c8'}},
axisLine:{lineStyle:{width:14,color:[[0.8,'rgba(57,216,200,0.18)'],[1,'rgba(255,106,90,0.25)']]}},
axisTick:{show:false},splitLine:{length:10,lineStyle:{color:'#555'}},
axisLabel:{color:'#888',fontSize:9,distance:14,formatter:v=>v+'%'},
pointer:{itemStyle:{color:over?'#ff6a5a':'#39d8c8'}},
anchor:{show:true,size:8,itemStyle:{color:over?'#ff6a5a':'#39d8c8'}},
title:{show:true,offsetCenter:[0,'34%'],color:'#888',fontSize:10,
fontFamily:'ui-monospace,monospace'},
detail:{valueAnimation:!REDUCED,offsetCenter:[0,'8%'],fontSize:22,
fontFamily:'ui-monospace,monospace',color:over?'#ff6a5a':'#39d8c8',
formatter:v=>v.toFixed(1)+'%'},
data:[{value:pct,name:over?'OVER CEILING':'of F19/TH6 ceiling'}],
}],
});
}
// ---- harvest composition donut (ECharts) -----------------------------------
let mixChart=null;
function renderMix(measBillable,sampleEst){
const el=$('mix_chart'); if(!el||!window.echarts)return;
if(!mixChart)mixChart=echarts.init(el,null,{renderer:'canvas'});
const m=Number(measBillable)||0, s=Number(sampleEst)||0;
if(m<=0&&s<=0){
mixChart.setOption({animation:false,
title:{text:'no joules yet',left:'center',top:'center',textStyle:{color:'#888',fontSize:11,fontFamily:'ui-monospace,monospace'}},
series:[]},true);
return;
}
mixChart.setOption({
animation:!REDUCED,
tooltip:{trigger:'item',formatter:p=>p.name+': '+p.value.toFixed(3)+' J'},
legend:{bottom:0,textStyle:{color:'#9a9a9a',fontSize:9,fontFamily:'ui-monospace,monospace'},itemWidth:9,itemHeight:9},
series:[{
type:'pie',radius:['46%','72%'],center:['50%','44%'],avoidLabelOverlap:true,
itemStyle:{borderColor:'#0e0e0e',borderWidth:2},
label:{show:false},labelLine:{show:false},
data:[
{value:m,name:'MEASURED-billable',itemStyle:{color:'#39d98a'}},
{value:s,name:'SAMPLE-est',itemStyle:{color:'#c9a05f'}},
],
}],
},true);
}
// ---- (4) ledger table -------------------------------------------------------
function renderLedger(series){
const rows=Array.isArray(series)?series.filter(r=>r&&r.ts):[];
if(!rows.length){$('led_wrap').style.display='none';$('led_empty').style.display='block';return;}
$('led_empty').style.display='none';$('led_wrap').style.display='block';
$('led_body').innerHTML=rows.map((r,i)=>{
const wb=r.within_bound===true;
return `<tr>
<td>${i+1}</td>
<td>${fmt(r.shannon_bits,1)}</td>
<td>${r.bekenstein_bound_bits==null?'—':r.bekenstein_bound_bits}</td>
<td class="${wb?'ok':'bad'}">${r.within_bound==null?'—':(wb?'✓':'✗')}</td>
<td>${fmt(r.joules_est,3)}</td>
<td>${fmt(r.cumulative_joules_est,3)}</td>
<td>${String(r.ts||'—')}</td>
</tr>`;
}).join('');
}
// ---- signed ledger summary --------------------------------------------------
function renderSignedLedger(sl){
const ok=sl&&sl.available;
$('sl_kv').style.display=ok?'block':'none';
$('sl_empty').style.display=ok?'none':'block';
if(!ok)return;
$('sl_jobs').textContent=sl.jobs!=null?sl.jobs:'—';
$('sl_meas').textContent=(sl.joules_measured_billable!=null?fmt(sl.joules_measured_billable,3):'0')+' J';
$('sl_kwh').textContent=sl.kwh_total!=null?fmt(sl.kwh_total,6)+' kWh':'—';
const chainOk=sl.chain_ok===true;
$('sl_chain').innerHTML=`<span style="color:${chainOk?'var(--green)':'var(--red)'}">${chainOk?'OK':'BROKEN'}</span> · len ${sl.chain_length!=null?sl.chain_length:'—'}`;
}
// ---- main poll loop ---------------------------------------------------------
async function loadAll(){
const [harvest,budget,ledger]=await Promise.all([
getJSON(A+"/v1/energy/harvest"),
getJSON(A+"/v1/energy/budget"),
getJSON(A+"/v1/energy/ledger"),
]);
if(!harvest){
$('livelbl').textContent='FALLBACK';
renderArea([],'UNAVAILABLE');renderLedger([]);
$('c_tasks').textContent='—';$('c_within').textContent='—';$('c_joules').textContent='—';
return;
}
$('livelbl').textContent='LIVE · RT';
const tw=harvest.tariff_window||null; renderWindow(tw);
const b=harvest.budget||{};
const series=b.series||[];
const jlabel=b.joules_label||harvest.total_joules_label||'SAMPLE';
// summary cards (prefer the live /budget summary, fall back to harvest budget block)
const bs=(budget&&budget.task_count!=null)?budget:b;
const tc=(bs.task_count!=null)?bs.task_count:series.length;
$('c_tasks').textContent=tc;
const allWithin=(bs.all_within_bound!=null)?bs.all_within_bound:(series.length?series.every(r=>r.within_bound):null);
$('c_within').textContent=(allWithin==null)?'—':(allWithin?'YES':'NO');
$('c_within').style.color=(allWithin===false)?'var(--red)':'var(--green)';
const tj=(bs.total_joules_est!=null)?bs.total_joules_est:(harvest.total_joules!=null?harvest.total_joules:series.reduce((a,r)=>a+(Number(r.joules_est)||0),0));
$('c_joules').textContent=fmt(tj,3)+' J';
setChip($('c_joules_lbl'),jlabel);
// (1) area
renderArea(series,jlabel);
// (3) gauge — Σ shannon vs Σ ceiling (prefer /budget summary; fall back to series sums)
const sumSh=(budget&&budget.total_shannon_bits!=null)?budget.total_shannon_bits:series.reduce((a,r)=>a+(Number(r.shannon_bits)||0),0);
const sumCeil=(budget&&budget.total_bekenstein_bound_bits!=null)?budget.total_bekenstein_bound_bits:series.reduce((a,r)=>a+(Number(r.bekenstein_bound_bits)||0),0);
renderGauge(sumSh,sumCeil,allWithin,tc);
// (4) ledger
renderLedger(series);
// signed ledger (prefer dedicated /ledger endpoint, fall back to harvest.signed_ledger)
let sl=harvest.signed_ledger||{available:false};
if(ledger){
const t=ledger.totals||ledger;
const chain=ledger.chain||ledger;
sl={available:true,
jobs:(t.jobs!=null?t.jobs:sl.jobs),
joules_measured_billable:(t.joules_measured_billable!=null?t.joules_measured_billable:sl.joules_measured_billable),
kwh_total:(t.kwh_total!=null?t.kwh_total:sl.kwh_total),
chain_ok:(chain.ok!=null?chain.ok:(ledger.chain_ok!=null?ledger.chain_ok:sl.chain_ok)),
chain_length:(chain.length!=null?chain.length:(ledger.chain_length!=null?ledger.chain_length:sl.chain_length))};
}
renderSignedLedger(sl);
// composition donut: MEASURED-billable vs SAMPLE-est
renderMix(sl&&sl.joules_measured_billable, tj);
}
buildUplot(); buildGauge();
loadAll();
setInterval(loadAll,5000);
</script>
</body>
</html>