Merge site-ui-3 item 0: the home page's live scene reads the chain (block height, shards proven, last lock), the failed-feed state is labelled
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
11b8263512
9 changed files with 83 additions and 31 deletions
BIN
docs/plans/site-ui-3-shots/item0/index-1440-dark-clip.png
Normal file
BIN
docs/plans/site-ui-3-shots/item0/index-1440-dark-clip.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
docs/plans/site-ui-3-shots/item0/index-1440-dark-fail-clip.png
Normal file
BIN
docs/plans/site-ui-3-shots/item0/index-1440-dark-fail-clip.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/plans/site-ui-3-shots/item0/index-1440-dark-fail.jpg
Normal file
BIN
docs/plans/site-ui-3-shots/item0/index-1440-dark-fail.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 557 KiB |
BIN
docs/plans/site-ui-3-shots/item0/index-1440-dark.jpg
Normal file
BIN
docs/plans/site-ui-3-shots/item0/index-1440-dark.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 555 KiB |
BIN
docs/plans/site-ui-3-shots/item0/index-390-dark-clip.png
Normal file
BIN
docs/plans/site-ui-3-shots/item0/index-390-dark-clip.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/plans/site-ui-3-shots/item0/index-390-dark.jpg
Normal file
BIN
docs/plans/site-ui-3-shots/item0/index-390-dark.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 352 KiB |
|
|
@ -1,5 +1,5 @@
|
|||
// Igneum live devnet feed. GET /api/live returns what tools/observer wrote to Neon:
|
||||
// {now, state, blocks (last 90 s, or ?window=N up to 300), miners (last 10 min), events (last 30), finality (checkpoints, newest lock),
|
||||
// {now, state (incl. height = chain block number), blocks (last 90 s, or ?window=N up to 300), miners (last 10 min), events (last 30), finality (checkpoints, newest lock),
|
||||
// proving (activation and 10-minute counts; every chain block carries shards: [{i, state, prover, lag, payout}])}.
|
||||
// Five queries, all on indexed columns. Cached one second at the edge.
|
||||
// LIVE_TABLE_PREFIX (default empty) reads a test observer's tables (fintest_live_*, provtest_live_*) instead of the devnet's.
|
||||
|
|
@ -43,7 +43,8 @@ export default async function handler(req, res) {
|
|||
const [head, blockRows, minerRows, checkpointRows, proofRows] = await Promise.all([
|
||||
sql(`SELECT now() AS now,
|
||||
(SELECT row_to_json(s) FROM ${T}live_state s WHERE s.id = 1) AS state,
|
||||
(SELECT json_agg(e) FROM (SELECT ts, kind, text FROM ${T}live_events ORDER BY ts DESC LIMIT 30) e) AS events`),
|
||||
(SELECT json_agg(e) FROM (SELECT ts, kind, text FROM ${T}live_events ORDER BY ts DESC LIMIT 30) e) AS events,
|
||||
(SELECT max(number) FROM ${T}live_blocks WHERE number IS NOT NULL AND received_at > now() - interval '10 minutes') AS height`),
|
||||
sql(`SELECT hash, blue_score, daa_score, timestamp_ms, parent_hashes, is_chain_block, vote_key_hash, received_at, color
|
||||
FROM ${T}live_blocks
|
||||
WHERE received_at > now() - ($1 || ' seconds')::interval
|
||||
|
|
@ -78,6 +79,9 @@ export default async function handler(req, res) {
|
|||
age_s: age_s === null ? null : Math.round(age_s * 10) / 10,
|
||||
network: s ? s.network : null,
|
||||
node_version: s ? s.node_version : null,
|
||||
// 6 Oct 2026: height is the chain block number (the EVM block number of the newest chain block the observer planned),
|
||||
// the same field /api/stats reports; block_count counts every DAG block the node holds (pruned nodes hold fewer)
|
||||
height: num(head[0].height),
|
||||
block_count: s ? num(s.block_count) : null,
|
||||
header_count: s ? num(s.header_count) : null,
|
||||
blue_score: s ? num(s.blue_score) : null,
|
||||
|
|
|
|||
|
|
@ -204,6 +204,8 @@ section:not(.light)+section:not(.light){padding-top:0;scroll-margin-top:var(--sm
|
|||
.blk.new{animation:pop .4s ease}
|
||||
@keyframes pop{from{transform:scale(.4);opacity:0}to{transform:scale(1);opacity:1}}
|
||||
.legend{display:flex;flex-wrap:wrap;gap:20px;margin-top:24px;font-size:14px;color:var(--ink-2)}
|
||||
.viz-note{margin:14px 0 0;font-size:13px;line-height:1.5;color:var(--ash);max-width:80ch}
|
||||
.legend[hidden]{display:none}
|
||||
.legend span{display:inline-flex;align-items:center;gap:8px}
|
||||
.sw{width:14px;height:14px;border-radius:4px;display:inline-block}
|
||||
pre{margin:0;font-family:var(--f-mono);font-size:13px;line-height:1.6;color:var(--ink-2);white-space:pre-wrap;min-height:9.6em}
|
||||
|
|
@ -372,7 +374,7 @@ pre{margin:0;font-family:var(--f-mono);font-size:13px;line-height:1.6;color:var(
|
|||
<div class="tile"><div class="v" id="ls-miners">0</div><div class="k">vote keys active in 10 min (a card runs several)</div></div>
|
||||
<div class="tile"><div class="v" id="ls-bps">0.0</div><div class="k">blocks / s</div></div>
|
||||
<div class="tile"><div class="v" id="ls-hash">0.0 MH/s</div><div class="k">hash rate</div></div>
|
||||
<div class="tile"><div class="v" id="ls-blocks">0</div><div class="k">devnet blocks</div></div>
|
||||
<div class="tile"><div class="v" id="ls-blocks">0</div><div class="k">chain block</div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -394,18 +396,26 @@ pre{margin:0;font-family:var(--f-mono);font-size:13px;line-height:1.6;color:var(
|
|||
<h2>Watch the chain prove itself</h2>
|
||||
<p>Blocks every second, proven in shards, locked every 30 seconds. First live lock 4 Oct 2026: checkpoint 242, 77.4% of all weight, two hours after genesis.</p>
|
||||
</div>
|
||||
<div class="card viz reveal" aria-label="Animated block DAG, simulated preview">
|
||||
<div class="card viz reveal" id="viz-card" aria-label="Block DAG: the live devnet while the observer is fresh, otherwise a simulated preview">
|
||||
<div class="viz-head">
|
||||
<div class="eyebrow" id="viz-label"><span class="dot" style="background:var(--ember);margin-right:8px"></span>simulated preview</div>
|
||||
<div class="viz-stats mono"><span>blocks <b id="c-blocks">0</b></span><span>proven <b id="c-proven">0</b></span><span>locked <b id="c-locked">0</b></span></div>
|
||||
<div class="viz-stats mono"><span><span id="k-blocks">blocks</span> <b id="c-blocks">0</b></span><span><span id="k-proven">proven</span> <b id="c-proven">0</b></span><span><span id="k-locked">locked</span> <b id="c-locked">0</b></span></div>
|
||||
</div>
|
||||
<canvas id="chain" aria-hidden="true"></canvas>
|
||||
<div class="legend">
|
||||
<div class="legend" id="legend-sim">
|
||||
<span><i class="sw" style="border:2px solid var(--line-2)"></i>mined</span>
|
||||
<span><i class="sw" style="background:var(--molten)"></i>shards being proven</span>
|
||||
<span><i class="sw" style="background:var(--ember)"></i>proven</span>
|
||||
<span><i class="sw" style="border:3px solid var(--ember);border-radius:50%;background:transparent"></i>locked checkpoint, final</span>
|
||||
</div>
|
||||
<div class="legend" id="legend-live" hidden>
|
||||
<span><i class="sw" style="border:2px solid var(--line-2)"></i>pending</span>
|
||||
<span><i class="sw" style="border:2px solid var(--ember)"></i>included</span>
|
||||
<span><i class="sw" style="border:2px solid var(--line-2);opacity:.45"></i>excluded</span>
|
||||
<span><i class="sw" style="background:var(--ember)"></i>proven</span>
|
||||
<span><i class="sw" style="border:3px solid var(--ember);border-radius:50%;background:transparent"></i>locked checkpoint, final to its left</span>
|
||||
</div>
|
||||
<p class="viz-note" id="viz-note" hidden></p>
|
||||
</div>
|
||||
<div class="grid" style="margin-top:24px">
|
||||
<div class="card reveal" style="display:flex;flex-direction:column;gap:16px">
|
||||
|
|
@ -683,19 +693,25 @@ pre{margin:0;font-family:var(--f-mono);font-size:13px;line-height:1.6;color:var(
|
|||
|
||||
// live observer: ONE fetch of /api/live every 2 s feeds the hero, the stats strip and the chain scene.
|
||||
// Subscribers get (fresh, d): the sampled new blocks since the last pull and the whole response, or ([], null) when off or stale.
|
||||
var live={on:false,rate:1.3,seen:{},minerOf:{},subs:[]};
|
||||
// live.reason says why the feed is off, in plain words, for the scene's label: 'connecting', 'live feed unavailable' (the fetch
|
||||
// failed or the function errored), or 'observer offline, last update N s ago' (the observer's row is older than 30 s).
|
||||
var live={on:false,rate:1.3,seen:{},minerOf:{},subs:[],reason:'connecting'};
|
||||
live.sub=function(fn){live.subs.push(fn);};
|
||||
function emitLive(fresh,d){live.subs.forEach(function(fn){fn(fresh,d);});}
|
||||
function pullLive(first){return fetch('/api/live',{cache:'no-store'}).then(function(r){return r.ok?r.json():null;}).then(function(d){
|
||||
if(!d||!d.ok||!d.state||d.state.stale||!d.blocks||!d.miners){live.on=false;emitLive([],null);return false;}
|
||||
live.on=true;
|
||||
function fmtAge(sec){sec=Math.round(sec);return sec<90?sec+' s':sec<5400?Math.round(sec/60)+' min':Math.round(sec/3600)+' h';}
|
||||
function liveOff(reason){live.on=false;live.reason=reason;emitLive([],null);return false;}
|
||||
function pullLive(first){return fetch('/api/live',{cache:'no-store'}).then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.json();}).then(function(d){
|
||||
if(!d||!d.ok)return liveOff('live feed unavailable');
|
||||
if(!d.state||!d.blocks||!d.miners)return liveOff('live feed unavailable');
|
||||
if(d.state.stale)return liveOff('observer offline'+(d.state.age_s!==null&&d.state.age_s!==undefined?', last update '+fmtAge(d.state.age_s)+' ago':''));
|
||||
live.on=true;live.reason=null;
|
||||
var fresh=d.blocks.filter(function(b){return !live.seen[b.hash];});if(first)fresh=fresh.slice(-12);
|
||||
// the response is a 90 s window, so both maps are rebuilt from it and stay bounded
|
||||
var seen={},minerOf={};d.blocks.forEach(function(b){seen[b.hash]=1;minerOf[b.hash]=b.miner||'?';});live.seen=seen;live.minerOf=minerOf;
|
||||
/* the scenes keep a calm pace; faster chains are sampled, the counters stay true */
|
||||
if(fresh.length>3){var step=fresh.length/3,pick=[];for(var i=0;i<3;i++)pick.push(fresh[Math.floor(i*step)]);fresh=pick;}
|
||||
emitLive(fresh,d);return true;
|
||||
}).catch(function(){live.on=false;emitLive([],null);return false;});}
|
||||
}).catch(function(){return liveOff('live feed unavailable');});}
|
||||
|
||||
// live stats strip under the hero buttons: shown only while the observer is fresh
|
||||
var strip=document.getElementById('livestrip');
|
||||
|
|
@ -705,7 +721,7 @@ pre{margin:0;font-family:var(--f-mono);font-size:13px;line-height:1.6;color:var(
|
|||
lsSet('ls-miners',s.miners_10m!==null&&s.miners_10m!==undefined?s.miners_10m:d.miners.length);
|
||||
lsSet('ls-bps',s.blocks_60s!==null&&s.blocks_60s!==undefined?(s.blocks_60s/60).toFixed(1):'pending');
|
||||
lsSet('ls-hash',fmtHash(s.hashes_per_second_estimate));
|
||||
lsSet('ls-blocks',s.block_count!==null&&s.block_count!==undefined?Number(s.block_count).toLocaleString('en-GB'):'pending');});
|
||||
lsSet('ls-blocks',s.height!==null&&s.height!==undefined?Number(s.height).toLocaleString('en-GB'):'pending');});
|
||||
|
||||
// hero background: a block graph that grows left to right, proves, and locks.
|
||||
// Live: each dot is one miner seen in the last ten minutes (at most 24, no ids drawn); it flashes when that
|
||||
|
|
@ -784,21 +800,30 @@ pre{margin:0;font-family:var(--f-mono);font-size:13px;line-height:1.6;color:var(
|
|||
function laneY(l){return H*0.14+l*(H*0.72/(lanes-1));}
|
||||
// live path state: when the devnet observer is fresh, real blocks (lb) from the shared poller replace the random spawner
|
||||
var liveMode=false,liveQ=[],liveLanes={},byHash={};
|
||||
// a live block takes the observer's words: color pending / blue (included) / red (excluded), shards planned / proving / verified / paid,
|
||||
// locked = a locked checkpoint block, final = at or before the newest lock. Re-applied on every poll so a block on screen updates.
|
||||
function applyLive(b,src){b.color=src.color||'pending';b.chain=!!src.chain;b.isFinal=!!src.final;var sh=src.shards||[];
|
||||
var done=function(x){return x.state==='verified'||x.state==='paid';};
|
||||
var allProven=sh.length>0&&sh.every(done),anyProving=sh.some(function(x){return x.state==='proving'||done(x);});
|
||||
if(allProven){if(b.state!=='proven'){b.state='proven';b.glow=1;}}else if(anyProving)b.state='proving';else b.state='mined';
|
||||
b.shards=[0,0,0,0];sh.slice(0,4).forEach(function(x,i){b.shards[i]=done(x)?1:0;});
|
||||
if(src.locked&&!b.locked){b.locked=true;ripples.push({x:b.x,y:b.y,r:S,a:1});ripples.push({x:b.x,y:b.y,r:S*0.5,a:1});}}
|
||||
function spawn(px,lb){n++;var lane=lb?lb.lane:Math.floor(Math.random()*lanes);var b={id:n,x:px,y:laneY(lane),born:performance.now(),shards:[0,0,0,0],claimed:[false,false,false,false],state:'mined',cp:n%6===0,parents:[],glow:0,flash:0,hash:lb?lb.hash:null};
|
||||
if(lb){byHash[lb.hash]=b;lb.parents.forEach(function(h){var q=byHash[h];if(q&&blocks.indexOf(q)>=0&&b.parents.length<4)b.parents.push(q);});if(!b.parents.length){var prev=blocks.slice(-6).filter(function(q){return q.x<px-S;}).sort(function(a2,b2){return b2.x-a2.x;});if(prev.length)b.parents.push(prev[0]);if(prev.length>1&&Math.random()<0.5)b.parents.push(prev[1]);}}
|
||||
if(lb){byHash[lb.hash]=b;if(lb.src)applyLive(b,lb.src);lb.parents.forEach(function(h){var q=byHash[h];if(q&&blocks.indexOf(q)>=0&&b.parents.length<4)b.parents.push(q);});if(!b.parents.length){var prev=blocks.slice(-6).filter(function(q){return q.x<px-S;}).sort(function(a2,b2){return b2.x-a2.x;});if(prev.length)b.parents.push(prev[0]);if(prev.length>1&&Math.random()<0.5)b.parents.push(prev[1]);}}
|
||||
else{var cands=blocks.slice(-8).filter(function(q){return q.x<px-S;}).sort(function(a2,b2){return b2.x-a2.x;});
|
||||
if(cands.length)b.parents.push(cands[0]);cands.slice(1).forEach(function(q){if(Math.random()<0.4&&b.parents.length<3)b.parents.push(q);});}
|
||||
blocks.push(b);if(blocks.length>80){var old=blocks.shift();if(old.hash)delete byHash[old.hash];}cb.textContent=n;}
|
||||
blocks.push(b);if(blocks.length>80){var old=blocks.shift();if(old.hash)delete byHash[old.hash];}if(!liveMode)cb.textContent=n;}
|
||||
function rr(px,py,w,h,r){x.beginPath();x.moveTo(px+r,py);x.arcTo(px+w,py,px+w,py+h,r);x.arcTo(px+w,py+h,px,py+h,r);x.arcTo(px,py+h,px,py,r);x.arcTo(px,py,px+w,py,r);x.closePath();}
|
||||
var last=0,lastShard=0;
|
||||
var last=0,lastShard=0,prevT=0;
|
||||
function frame(t){
|
||||
if(liveMode){if(liveQ.length&&t-last>Math.max(60,1000/live.rate)){spawn(W+S,liveQ.shift());last=t;}}
|
||||
else if(t-last>1000){spawn(W+S);last=t;}
|
||||
var dt=16;
|
||||
// real frame time, capped: a fixed 16 ms ran the scene at double speed on 120 Hz displays (found 6 Oct 2026)
|
||||
var dt=prevT?Math.min(50,t-prevT):16;prevT=t;
|
||||
var sp=liveMode?speed*live.rate:speed;blocks.forEach(function(b){b.x-=sp*dt;var age=t-b.born;
|
||||
if(!liveMode&&b.state==='mined'&&age>1200*k)b.state='proving';
|
||||
if(!liveMode&&b.state==='proving'){for(var i=0;i<4;i++){if(!b.claimed[i]&&Math.random()<0.012/k){b.claimed[i]=true;var fromTop=Math.random()<0.5;sparks.push({x:Math.random()*W,y:fromTop?-6:H+6,tx:b.x,ty:b.y,b:b,i:i,p:0});}}
|
||||
if(b.shards.every(function(v){return v>=1;})){b.state='proven';b.glow=1;proven++;cp.textContent=proven;if(b.cp)setTimeout(function(){b.locked=true;locked++;cl.textContent=locked;ripples.push({x:b.x,y:b.y,r:S,a:1});ripples.push({x:b.x,y:b.y,r:S*0.5,a:1});},1800*k);}}
|
||||
if(b.shards.every(function(v){return v>=1;})){b.state='proven';b.glow=1;proven++;if(!liveMode)cp.textContent=proven;if(b.cp)setTimeout(function(){b.locked=true;locked++;if(!liveMode)cl.textContent=locked;ripples.push({x:b.x,y:b.y,r:S,a:1});ripples.push({x:b.x,y:b.y,r:S*0.5,a:1});},1800*k);}}
|
||||
if(b.glow>0)b.glow-=0.02;});
|
||||
sparks.forEach(function(sp){sp.p+=Math.min(0.08,0.035/k);sp.tx=sp.b.x;sp.ty=sp.b.y;if(sp.p>=1){sp.done=true;sp.b.shards[sp.i]=1;}});
|
||||
sparks=sparks.filter(function(sp){return !sp.done;});
|
||||
|
|
@ -814,10 +839,11 @@ pre{margin:0;font-family:var(--f-mono);font-size:13px;line-height:1.6;color:var(
|
|||
// blocks
|
||||
blocks.forEach(function(b){var h=S/2;
|
||||
// proving v0: a faint second glint behind a real block once every shard of it is verified on the devnet (nothing before activation)
|
||||
if(b.sealed){var g2=x.createRadialGradient(b.x,b.y,S*0.45,b.x,b.y,S*1.35);g2.addColorStop(0,'rgba(255,179,92,0.18)');g2.addColorStop(1,'rgba(255,179,92,0)');x.fillStyle=g2;x.beginPath();x.arc(b.x,b.y,S*1.35,0,Math.PI*2);x.fill();}
|
||||
if(b.glow>0){var g=x.createRadialGradient(b.x,b.y,0,b.x,b.y,S*1.6);g.addColorStop(0,'rgba(242,84,27,'+(0.45*b.glow)+')');g.addColorStop(1,'rgba(242,84,27,0)');x.fillStyle=g;x.beginPath();x.arc(b.x,b.y,S*1.6,0,Math.PI*2);x.fill();}
|
||||
if(b.state==='proven'){x.fillStyle='#F2541B';rr(b.x-h,b.y-h,S,S,S*0.24);x.fill();}
|
||||
else{x.fillStyle='#0C0C0E';rr(b.x-h,b.y-h,S,S,S*0.24);x.fill();x.strokeStyle=b.state==='proving'?'rgba(255,179,92,0.9)':'rgba(58,58,66,1)';x.lineWidth=2;rr(b.x-h,b.y-h,S,S,S*0.24);x.stroke();
|
||||
else{var ex=b.color==='red';if(ex)x.globalAlpha=0.45;x.fillStyle='#0C0C0E';rr(b.x-h,b.y-h,S,S,S*0.24);x.fill();
|
||||
// live: included (blue or on the selected chain) takes an ember outline, excluded (red) is dimmed, pending stays grey
|
||||
x.strokeStyle=b.state==='proving'?'rgba(255,179,92,0.9)':(liveMode&&!ex&&(b.color==='blue'||b.chain))?'rgba(242,84,27,0.75)':'rgba(58,58,66,1)';x.lineWidth=2;rr(b.x-h,b.y-h,S,S,S*0.24);x.stroke();if(ex)x.globalAlpha=1;
|
||||
if(b.state==='proving'){var q=S/2-3;for(var i=0;i<4;i++){if(b.shards[i]>=1){x.fillStyle='#FFB35C';var qx=b.x-h+3+(i%2)*q,qy=b.y-h+3+Math.floor(i/2)*q;rr(qx,qy,q-1,q-1,2);x.fill();}}}}
|
||||
if(b.locked){var pulse=0.8+0.2*Math.sin(t/350);x.strokeStyle='rgba(242,84,27,'+pulse+')';x.lineWidth=3;x.beginPath();x.arc(b.x,b.y,S*1.15,0,Math.PI*2);x.stroke();x.fillStyle='#F4F1EC';x.beginPath();x.arc(b.x,b.y,S*0.16,0,Math.PI*2);x.fill();}
|
||||
});
|
||||
|
|
@ -830,19 +856,38 @@ pre{margin:0;font-family:var(--f-mono);font-size:13px;line-height:1.6;color:var(
|
|||
cb.textContent=n;cp.textContent=proven;cl.textContent=locked;
|
||||
requestAnimationFrame(frame);
|
||||
// live path: the shared poller above feeds sampled real blocks; while fresh, the label links to /live
|
||||
var label=document.getElementById('viz-label'),labelHTML=label?label.innerHTML:'';
|
||||
var label=document.getElementById('viz-label'),note=document.getElementById('viz-note'),legSim=document.getElementById('legend-sim'),legLive=document.getElementById('legend-live');
|
||||
var kb=document.getElementById('k-blocks'),kp=document.getElementById('k-proven'),kl=document.getElementById('k-locked');
|
||||
var fmtN=function(v){return v===null||v===undefined?'pending':Number(v).toLocaleString('en-GB');};
|
||||
function laneFor(m){if(!(m in liveLanes))liveLanes[m]=Object.keys(liveLanes).length%lanes;return liveLanes[m];}
|
||||
function setLive(on){if(on===liveMode)return;liveMode=on;if(label)label.innerHTML=on?'<span class="dot" style="margin-right:8px"></span><a href="/live" style="color:var(--molten)">live devnet</a>':labelHTML;}
|
||||
// three label states: live (the observer is fresh), off with the reason (the simulation runs and its counters count simulated blocks), connecting
|
||||
function setLive(on){
|
||||
if(on!==liveMode){liveMode=on;if(!on){cb.textContent=n;cp.textContent=proven;cl.textContent=locked;}}
|
||||
if(legSim)legSim.hidden=on;if(legLive)legLive.hidden=!on;
|
||||
if(kb)kb.textContent=on?'chain block':'blocks';if(kp)kp.textContent=on?'shards proven':'proven';if(kl)kl.textContent=on?'last lock':'locked';
|
||||
if(!label)return;
|
||||
if(on){label.innerHTML='<span class="dot" style="margin-right:8px"></span><a href="/live" style="color:var(--molten)">live devnet</a>';return;}
|
||||
var r=live.reason||'live feed unavailable';
|
||||
label.innerHTML='<span class="dot" style="background:var(--ash);animation:none;margin-right:8px"></span>simulated preview'+(r==='connecting'?'':' · '+r);
|
||||
if(note){note.hidden=false;note.textContent=r==='connecting'?'Connecting to the devnet observer. Until it answers, this is a simulation and its counters count simulated blocks.':r.charAt(0).toUpperCase()+r.slice(1)+'. This is a simulation and its counters count simulated blocks. The live page shows the last stored state.';}}
|
||||
live.sub(function(fresh,d){setLive(live.on);if(!live.on)return;
|
||||
// live counters come from the observer, never from the simulation: proven = blocks in the window every shard of which is verified, locked = locked checkpoints in the window
|
||||
if(d&&d.blocks){var pv=d.proving&&d.proving.supported&&d.proving.active;cp.textContent=pv?String(d.blocks.filter(function(b){return b.proven;}).length):'not active';cl.textContent=String(d.blocks.filter(function(b){return b.locked;}).length);}
|
||||
fresh.forEach(function(b){liveQ.push({hash:b.hash,parents:b.parents||[],lane:laneFor(b.miner||'?')});});
|
||||
if(liveQ.length>6)liveQ=liveQ.slice(-6);
|
||||
// the glint above: only the per-block shards data, only while the proving layer is active
|
||||
if(d&&d.proving&&d.proving.supported&&d.proving.active)d.blocks.forEach(function(b){if(b.proven){var q=byHash[b.hash];if(q)q.sealed=true;}});});
|
||||
// live counters come from the chain, never from the 90 s window: the chain block number, the shards proven and paid on the chain
|
||||
// as the observer's node counts them (proofs land minutes behind the tip, so the blocks on screen are still pending), the latest locked checkpoint
|
||||
var s=d.state,pv=d.proving,fn=d.finality;
|
||||
cb.textContent=fmtN(s.height);
|
||||
cp.textContent=!pv||!pv.supported?'no proving layer':!pv.active?'not active':fmtN(pv.paid_shards_total);
|
||||
cl.textContent=!fn||!fn.supported?'no finality':!fn.active?'paused':fn.latest_locked_index!==null&&fn.latest_locked_index!==undefined?'#'+fmtN(fn.latest_locked_index):'none yet';
|
||||
if(note){var lag=pv&&pv.active&&pv.median_proof_lag_s?Math.round(pv.median_proof_lag_s):null;
|
||||
note.hidden=false;note.textContent='One node is read every 2 s. Chain block is the EVM block number. Shards proven is every shard paid on the chain so far'+(pv&&pv.active?' ('+fmtN(pv.shards_paid_10m||0)+' in the last 10 minutes'+(lag?', landing a median '+lag+' s behind the tip, so the newest blocks on screen are still pending':'')+')':'')+'. Last lock is the latest checkpoint the finality rule locked.';}
|
||||
// blocks already on screen take their newest words (pending to included, a shard proven, a checkpoint locked)
|
||||
d.blocks.forEach(function(src){var q=byHash[src.hash];if(q)applyLive(q,src);});
|
||||
fresh.forEach(function(b){liveQ.push({hash:b.hash,parents:b.parents||[],lane:laneFor(b.miner||'?'),src:b});});
|
||||
if(liveQ.length>6)liveQ=liveQ.slice(-6);});
|
||||
setLive(false);
|
||||
})();
|
||||
// one fetch on load; only if the observer is fresh does the 2 s poll start (it feeds the hero, the strip and the chain scene)
|
||||
pullLive(true).then(function(on){if(on)setInterval(function(){pullLive(false);},2000);});
|
||||
// one fetch on load, then every 2 s while the feed is fresh and every 10 s while it is off, so a feed that comes back is picked up
|
||||
function pollLive(on){setTimeout(function(){pullLive(false).then(pollLive);},on?2000:10000);}
|
||||
pullLive(true).then(pollLive);
|
||||
|
||||
// hourly program ticker
|
||||
var ops=['rotl','rotr','xor','add','sub','mul','mulhi','mad','or'];
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@ main{padding-bottom:var(--sec)}
|
|||
<div class="card" aria-label="Block DAG, finality and proving, live">
|
||||
<div class="viz-head">
|
||||
<div class="eyebrow"><span class="dot" style="margin-right:8px"></span>blocks, finality, proving</div>
|
||||
<div class="viz-stats mono"><span>on screen <b id="c-screen">0</b></span><span>chain <b id="c-chain">0</b></span><span>identities <b id="c-idents">0</b></span><span>last lock <b id="c-locked">none</b></span><span>proven <b id="c-proven">n/a</b></span></div>
|
||||
<div class="viz-stats mono"><span>on screen <b id="c-screen">0</b></span><span>chain <b id="c-chain">0</b></span><span>identities <b id="c-idents">0</b></span><span>last lock <b id="c-locked">none</b></span><span>proven <b id="c-proven">pending</b></span></div>
|
||||
</div>
|
||||
<canvas id="dag" aria-hidden="true"></canvas>
|
||||
<div class="legend">
|
||||
|
|
@ -402,7 +402,10 @@ main{padding-bottom:var(--sec)}
|
|||
blocks.forEach(function(b){if(b.ts>vis){on++;if(b.chain){chain++;if(b.shards.length){planned++;if(b.proven)proven++;}}}});
|
||||
$('c-screen').textContent=on;$('c-chain').textContent=chain;$('c-idents').textContent=idents;
|
||||
var f=fin;$('c-locked').textContent=f&&f.latest_locked_index?('#'+f.latest_locked_index+(f.checkpoints&&f.checkpoints.length?', '+lockAge(f):'')):(f&&f.supported?'none yet':'n/a');
|
||||
$('c-proven').textContent=prov&&prov.supported&&prov.active?(proven+'/'+planned):(prov&&prov.supported?'not active':'n/a');
|
||||
// 6 Oct 2026: proofs land a median of minutes behind the tip, so the on-screen window (proven+'/'+planned) read 0 for ever; the
|
||||
// counter now reads the observer's 10-minute truth: chain blocks with every shard paid, of the chain blocks planned in 10 min
|
||||
var pe=$('c-proven');if(prov&&prov.supported&&prov.active){pe.textContent=(prov.blocks_fully_proven_10m||0)+'/'+(prov.blocks_10m||0)+' in 10 min';pe.title=(prov.shards_paid_10m||0)+' shards paid in the last 10 minutes'+(prov.median_proof_lag_s?', landing a median '+Math.round(prov.median_proof_lag_s)+' s behind the tip':'')+'; '+(prov.paid_shards_total!==null&&prov.paid_shards_total!==undefined?Number(prov.paid_shards_total).toLocaleString('en-GB')+' paid on the chain so far':'');}
|
||||
else{pe.textContent=prov&&prov.supported?'not active':'no proving layer';pe.title='';}
|
||||
if(!running&&!document.hidden)kick();
|
||||
}
|
||||
function lockAge(f){var cps=f.checkpoints||[],lk=null;for(var i=cps.length-1;i>=0;i--)if(cps[i].state==='locked'){lk=cps[i];break;}
|
||||
|
|
|
|||
Loading…
Reference in a new issue