diff --git a/docs/plans/site-ui-3-shots/item0/index-1440-dark-clip.png b/docs/plans/site-ui-3-shots/item0/index-1440-dark-clip.png new file mode 100644 index 000000000..7685b15c8 Binary files /dev/null and b/docs/plans/site-ui-3-shots/item0/index-1440-dark-clip.png differ diff --git a/docs/plans/site-ui-3-shots/item0/index-1440-dark-fail-clip.png b/docs/plans/site-ui-3-shots/item0/index-1440-dark-fail-clip.png new file mode 100644 index 000000000..735c57d64 Binary files /dev/null and b/docs/plans/site-ui-3-shots/item0/index-1440-dark-fail-clip.png differ diff --git a/docs/plans/site-ui-3-shots/item0/index-1440-dark-fail.jpg b/docs/plans/site-ui-3-shots/item0/index-1440-dark-fail.jpg new file mode 100644 index 000000000..d3f6d0f35 Binary files /dev/null and b/docs/plans/site-ui-3-shots/item0/index-1440-dark-fail.jpg differ diff --git a/docs/plans/site-ui-3-shots/item0/index-1440-dark.jpg b/docs/plans/site-ui-3-shots/item0/index-1440-dark.jpg new file mode 100644 index 000000000..96e03e66e Binary files /dev/null and b/docs/plans/site-ui-3-shots/item0/index-1440-dark.jpg differ diff --git a/docs/plans/site-ui-3-shots/item0/index-390-dark-clip.png b/docs/plans/site-ui-3-shots/item0/index-390-dark-clip.png new file mode 100644 index 000000000..fb4300f80 Binary files /dev/null and b/docs/plans/site-ui-3-shots/item0/index-390-dark-clip.png differ diff --git a/docs/plans/site-ui-3-shots/item0/index-390-dark.jpg b/docs/plans/site-ui-3-shots/item0/index-390-dark.jpg new file mode 100644 index 000000000..b32bc0ceb Binary files /dev/null and b/docs/plans/site-ui-3-shots/item0/index-390-dark.jpg differ diff --git a/site/api/live.mjs b/site/api/live.mjs index 10b1221af..57012a989 100644 --- a/site/api/live.mjs +++ b/site/api/live.mjs @@ -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, diff --git a/site/index.html b/site/index.html index 7f3d2473d..a2ec31a0d 100644 --- a/site/index.html +++ b/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(
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.
-Devnet: coins have no value and the chain may be reset.
@@ -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