diff --git a/site/index.html b/site/index.html
index 5a6b2a20..e9d16c4e 100644
--- a/site/index.html
+++ b/site/index.html
@@ -195,6 +195,7 @@ footer .wrap{padding-block:48px 32px}
Economics
Litepaper
Log
+ Live
Get the miner
@@ -252,7 +253,7 @@ footer .wrap{padding-block:48px 32px}
-
simulated preview
+
simulated preview
blocks 0proven 0locked 0
@@ -481,14 +482,18 @@ footer .wrap{padding-block:48px 32px}
function size(){W=c.clientWidth;H=c.clientHeight;c.width=W*dpr;c.height=H*dpr;x.setTransform(dpr,0,0,dpr,0,0);S=W<600?18:26;GAP=W<600?S*2.5:S*3.1;speed=GAP/1000;k=Math.max(0.22,Math.min(1,(W/speed)/22000));}
size();window.addEventListener('resize',size);
function laneY(l){return H*0.14+l*(H*0.72/(lanes-1));}
- function spawn(px){n++;var 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};
- var cands=blocks.slice(-8).filter(function(q){return q.x
80)blocks.shift();cb.textContent=n;}
+ // live path state: when the devnet observer is fresh, real blocks (lb) replace the random spawner
+ var liveMode=false,liveQ=[],liveSeen={},liveLanes={},liveRate=1,byHash={};
+ 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);});}
+ else{var cands=blocks.slice(-8).filter(function(q){return q.x80){var old=blocks.shift();if(old.hash)delete byHash[old.hash];}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;
function frame(t){
- if(t-last>1000){spawn(W+S);last=t;}
+ if(liveMode){if(liveQ.length&&t-last>Math.max(60,1000/liveRate)){spawn(W+S,liveQ.shift());last=t;}}
+ else if(t-last>1000){spawn(W+S);last=t;}
var dt=16;
blocks.forEach(function(b){b.x-=speed*dt;var age=t-b.born;
if(b.state==='mined'&&age>1200*k)b.state='proving';
@@ -522,6 +527,19 @@ footer .wrap{padding-block:48px 32px}
var seedN=Math.ceil(W/GAP)+1,pv=Math.min(5,Math.max(2,Math.floor(seedN*0.45)));for(var i=0;ipv){b.state='proven';b.shards=[1,1,1,1];b.claimed=[true,true,true,true];proven++;if(b.cp){b.locked=true;locked++;}}else if(i>1){b.state='proving';b.claimed=[true,true,false,false];b.shards=[1,1,0,0];}}
cb.textContent=n;cp.textContent=proven;cl.textContent=locked;
requestAnimationFrame(frame);
+ // live path: one fetch on load; if the observer updated within 30 s, the label links to /live and real blocks feed the scene
+ var label=document.getElementById('viz-label'),labelHTML=label?label.innerHTML:'';
+ 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?'live devnet':labelHTML;}
+ 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){setLive(false);return false;}
+ setLive(true);liveRate=Math.max(1,(d.state.blocks_per_second_60s||0)*1.5);
+ var fresh=d.blocks.filter(function(b){return !liveSeen[b.hash];});if(first)fresh=fresh.slice(-12);
+ d.blocks.forEach(function(b){liveSeen[b.hash]=1;});
+ fresh.forEach(function(b){liveQ.push({hash:b.hash,parents:b.parents,lane:laneFor(b.miner||'?')});});
+ if(liveQ.length>40)liveQ=liveQ.slice(-40);return true;
+ }).catch(function(){setLive(false);return false;});}
+ pullLive(true).then(function(on){if(on)setInterval(function(){pullLive(false);},2000);});
})();
// hourly program ticker
diff --git a/site/vercel.json b/site/vercel.json
index 4b5cc5d2..a975ad43 100644
--- a/site/vercel.json
+++ b/site/vercel.json
@@ -9,6 +9,7 @@
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" },
{ "key": "Strict-Transport-Security", "value": "max-age=63072000; includeSubDomains; preload" }
] },
- { "source": "/journey.json", "headers": [ { "key": "Cache-Control", "value": "public, max-age=300" } ] }
+ { "source": "/journey.json", "headers": [ { "key": "Cache-Control", "value": "public, max-age=300" } ] },
+ { "source": "/api/live", "headers": [ { "key": "Cache-Control", "value": "public, max-age=1" } ] }
]
}