Home live scene reads the chain, not the window: chain block, shards proven, last lock, live colours, a plain unavailable state
The "LIVE DEVNET" scene on the home page showed blocks 21, proven 0, locked 3 and grey nodes while the devnet stood past chain block 140,000 with 2,090 shards paid and a checkpoint locking every 30 s. Four causes, all in site/index.html: - "blocks" was the scene's own spawn counter, never the chain; - "proven" counted blocks of the 90 s window whose shards were all paid, and proofs land a median 380 s behind the tip (median_proof_lag_s), so the window can never hold one; - "locked" counted locked checkpoint blocks inside the same 90 s window (always about 3); - the live path skipped the colour state machine, so every real block stayed grey, and a failed first fetch left the simulation running under a label that looked live, with no further polls. Now: /api/live carries state.height (the chain block number, the same field /api/stats reports, one indexed subquery); the scene's counters read chain block (state.height), shards proven (proving.paid_shards_total, the node's count of paid shard records, with the 10-minute count and the median lag in the note) and last lock (finality.latest_locked_index); live blocks take the observer's own words (pending, included, excluded, proven, locked checkpoint, final to its left), refreshed on every poll, with a legend in those words; the hero tile shows the chain block instead of the node's held-block count; polling never stops (2 s while fresh, 10 s while off) and the off state is labelled "simulated preview · live feed unavailable" or "observer offline, last update N ago" with a note that the counters count simulated blocks. Also found while verifying: the scene advanced a fixed 16 ms per frame, so it ran at double speed on 120 Hz displays; it now uses the real frame time, capped at 50 ms. /live had the same window bug in its "proven 0/16" counter; it now reads the observer's 10-minute truth (chain blocks with every shard paid, of the chain blocks planned in 10 min) with the lag and the chain total in its title. Verified against the live API through tools/site-serve.mjs in the built-in browser and headless Chromium: chain block 140,489, shards proven 2,130, last lock #6,784; the failed-fetch state shown by blocking /api/live. Screenshots: docs/plans/site-ui-3-shots/item0/ (1440 and 390, live and failed). Checks: identity grep 0 hits, link check 708 links 0 broken, site/api tests 14 pass, site build clean. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
82a1a5dc97
commit
cd1a0c0cf6
9 changed files with 85 additions and 33 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,
|
||||
|
|
|
|||
103
site/index.html
103
site/index.html
|
|
@ -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">
|
||||
|
|
@ -500,8 +510,8 @@ pre{margin:0;font-family:var(--f-mono);font-size:13px;line-height:1.6;color:var(
|
|||
<div class="pill"><b>Tunes itself, in public</b><span>Kernel variants raced every hour, hash per watt swept, every number in the bench table.</span></div>
|
||||
</div>
|
||||
<div class="cta" style="margin-top:24px;align-items:center">
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="btn dark">Windows <span data-dl-meta="miner-windows" style="font-weight:400;opacity:.8">v0.3.13 · 45.4 MB</span></a>
|
||||
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" class="btn dark">macOS <span data-dl-meta="miner-mac" style="font-weight:400;opacity:.8">v0.3.13 · 41.9 MB</span></a>
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="btn dark">Windows <span data-dl-meta="miner-windows" style="font-weight:400;opacity:.8">v0.3.14 · 45.4 MB</span></a>
|
||||
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" class="btn dark">macOS <span data-dl-meta="miner-mac" style="font-weight:400;opacity:.8">v0.3.14 · 41.9 MB</span></a>
|
||||
<a href="/miner#get" class="btn" style="color:#0C0C0E;border-color:#0C0C0E">Linux · HiveOS</a>
|
||||
</div>
|
||||
<p data-devnet-notice style="margin-top:14px;font-size:14px;color:#0C0C0E;font-weight:600">Devnet: coins have no value and the chain may be reset.</p>
|
||||
|
|
@ -682,19 +692,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');
|
||||
|
|
@ -704,7 +720,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
|
||||
|
|
@ -783,21 +799,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;});
|
||||
|
|
@ -813,10 +838,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();}
|
||||
});
|
||||
|
|
@ -829,19 +855,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