diff --git a/docs/design/live-dag/after-replay-1bps-desktop.png b/docs/design/live-dag/after-replay-1bps-desktop.png
new file mode 100644
index 00000000..cf4f0f83
Binary files /dev/null and b/docs/design/live-dag/after-replay-1bps-desktop.png differ
diff --git a/docs/design/live-dag/after-replay-4bps-20miners-desktop.png b/docs/design/live-dag/after-replay-4bps-20miners-desktop.png
new file mode 100644
index 00000000..1aac2638
Binary files /dev/null and b/docs/design/live-dag/after-replay-4bps-20miners-desktop.png differ
diff --git a/docs/design/live-dag/after-replay-4bps-20miners-mobile.png b/docs/design/live-dag/after-replay-4bps-20miners-mobile.png
new file mode 100644
index 00000000..91b58f00
Binary files /dev/null and b/docs/design/live-dag/after-replay-4bps-20miners-mobile.png differ
diff --git a/docs/design/live-dag/after-replay-hover-tooltip.png b/docs/design/live-dag/after-replay-hover-tooltip.png
new file mode 100644
index 00000000..c472d3f4
Binary files /dev/null and b/docs/design/live-dag/after-replay-hover-tooltip.png differ
diff --git a/docs/design/live-dag/after-replay-tap-tooltip-mobile.png b/docs/design/live-dag/after-replay-tap-tooltip-mobile.png
new file mode 100644
index 00000000..1d62c009
Binary files /dev/null and b/docs/design/live-dag/after-replay-tap-tooltip-mobile.png differ
diff --git a/docs/design/live-dag/before-desktop.png b/docs/design/live-dag/before-desktop.png
new file mode 100644
index 00000000..85b15ec1
Binary files /dev/null and b/docs/design/live-dag/before-desktop.png differ
diff --git a/docs/design/live-dag/before-mobile.png b/docs/design/live-dag/before-mobile.png
new file mode 100644
index 00000000..dbf6b429
Binary files /dev/null and b/docs/design/live-dag/before-mobile.png differ
diff --git a/site/live.html b/site/live.html
index e580cd0b..da9adc03 100644
--- a/site/live.html
+++ b/site/live.html
@@ -93,7 +93,7 @@ p{margin:0}
.viz-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px 20px;margin-bottom:12px}
.viz-stats{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:12px;color:var(--ash)}
.viz-stats b{color:var(--bone);font-weight:500;font-variant-numeric:tabular-nums}
-#dag{display:block;width:100%;height:clamp(240px,38vw,380px);border-radius:12px;background:var(--obsidian)}
+#dag{display:block;width:100%;height:clamp(240px,38vw,380px);border-radius:12px;background:var(--obsidian);touch-action:pan-y}
.legend{display:flex;flex-wrap:wrap;gap:16px 20px;margin-top:16px;font-size:13px;color:var(--ink-2)}
.legend span{display:inline-flex;align-items:center;gap:8px}
.sw{width:14px;height:14px;border-radius:4px;display:inline-block}
@@ -182,10 +182,12 @@ footer{border-top:1px solid var(--line);padding-block:48px 32px}
chain block
side block (blue or red, off the selected chain)
- locked checkpoint, final
+ locked checkpoint, final
just arrived
- parent edge
- 2ea5116eminer short id, one lane each
+ selected chain
+ first parent of a side block
+ 5d3fd79cone lane per miner, colour from its id
+ hover or tap a block for its id, scores and parents
@@ -259,66 +261,149 @@ footer{border-top:1px solid var(--line);padding-block:48px 32px}
function esc(s){return String(s).replace(/&/g,'&').replace(//g,'>');}
// ---------- DAG scene ----------
- var c=$('dag'),x=c.getContext('2d'),W,H,S,blocks=[],byHash={},lanesOf={},laneOrder=[],maxLanes=6,pxPerSec=40,targetPx=40;
- var live=false,serverNow=0,serverAt=0,frozenAt=null,note='',bps=0;
- 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?16:22;maxLanes=H<260?4:6;}
- size();window.addEventListener('resize',function(){size();draw(performance.now());});
- function laneCount(){return Math.max(1,Math.min(maxLanes,laneOrder.length));}
- function laneFor(m){m=m||'?';if(!(m in lanesOf)){lanesOf[m]=laneOrder.length;laneOrder.push(m);}return lanesOf[m]%laneCount();}
- function laneY(l){var n=laneCount();if(n===1)return H*0.5;return H*0.16+l*(H*0.62/(n-1));}
- function jitter(h){var v=0;for(var i=0;i<8;i++)v=(v*31+h.charCodeAt(i))%1000;return (v/1000-0.5)*S*0.5;}
+ // One canvas, one requestAnimationFrame loop. Lanes: one per miner in the visible window, most blocks on top,
+ // the least active share an "others" lane. Labels live in a left gutter, never on blocks. Edges go under blocks.
+ var c=$('dag'),x=c.getContext('2d'),W=0,H=0,S=22,G=78,narrow=false,level=0,maxLanes=8;
+ var blocks=[],byHash={},miners={},lanes=[],laneN=1,othersN=0;
+ var LV=[{S:22,s:14,pps:40,npps:28},{S:16,s:12,pps:26,npps:20},{S:12,s:10,pps:18,npps:14}];
+ var pxPerSec=40,targetPx=40,bps=0,live=false,serverNow=0,serverAt=0,frozenAt=null,note='';
+ var running=false,needSize=true,inView=true,ptrX=-1,ptrY=-1,ptrTouch=false,pin=null,laneLast=[],laneRun=[];
+ var TOP=20,BOT=12;
+ // 12 hues that sit beside ember on obsidian; the index comes from the short id, so a miner keeps its colour across reloads
+ var PAL=['#5CB8FF','#3FD39A','#F5C542','#C49BFF','#FF7AA8','#8FD15F','#FFB08A','#6F8CFF','#D9E25A','#FF8FD6','#4FD8D8','#E0A96D'];
+ function colour(id){var v=0;for(var i=0;i>>0;return PAL[v%PAL.length];}
+ function jitter(h){var v=0;for(var i=0;i<8;i++)v=(v*31+h.charCodeAt(i))%1000;return v/1000-0.5;}
function vnow(){if(frozenAt!==null)return frozenAt;return serverNow+(performance.now()-serverAt);}
+ function laneY(l){var u=H-TOP-BOT,p=u/Math.max(1,laneN);return TOP+(l+0.5)*p;}
+ function size(){
+ needSize=false;W=c.clientWidth;H=c.clientHeight;if(!W||!H)return;
+ c.width=Math.round(W*dpr);c.height=Math.round(H*dpr);x.setTransform(dpr,0,0,dpr,0,0);
+ narrow=W<560;G=narrow?36:80;applyLevel();
+ }
+ function applyLevel(){
+ var L=LV[level];S=narrow?L.s:L.S;targetPx=narrow?L.npps:L.pps;
+ maxLanes=Math.max(2,Math.min(14,Math.floor((H-TOP-BOT)/(S*1.7))));
+ layoutLanes();
+ }
+ function pickLevel(){
+ // projected blocks on screen at the base scale drives the step, with hysteresis so it never flickers
+ var proj=bps*(W-G)/(narrow?LV[0].npps:LV[0].pps),up=[48,110],down=[36,85],next=level;
+ if(level<2&&proj>up[level])next=level+1;else if(level>0&&projlaneN?ids.length-(laneN-1):0;
+ lanes=[];
+ ids.forEach(function(id,i){var m=miners[id],l=othersN&&i>=laneN-1?laneN-1:i;m.lane=l;if(m.y===undefined)m.y=laneY(l);if(!othersN||lcut;if(!keep)delete byHash[b.hash];return keep;});
blocks.sort(function(a,b){return a.ts-b.ts;});
+ for(var id in miners)miners[id].n=0;
+ blocks.forEach(function(b){var m=miners[b.miner];if(!m){m=miners[b.miner]={id:b.miner,n:0,col:colour(b.miner),tag:b.miner.slice(0,3),lane:0};}m.n++;});
+ for(id in miners)if(!miners[id].n)delete miners[id];
bps=d.state.blocks_per_second_60s||0;
- targetPx=Math.max(S*0.4,Math.min(420,bps*S*1.7));
- var chain=0;blocks.forEach(function(b){if(b.chain)chain++;});
- $('c-screen').textContent=blocks.length;$('c-chain').textContent=chain;$('c-side').textContent=blocks.length-chain;
+ pickLevel();layoutLanes();
+ var vis=vnow()-((W-G)/Math.max(8,targetPx))*1000,on=0,chain=0;
+ blocks.forEach(function(b){if(b.ts>vis){on++;if(b.chain)chain++;}});
+ $('c-screen').textContent=on;$('c-chain').textContent=chain;$('c-side').textContent=on-chain;
$('c-newest').textContent=blocks.length?compact(blocks[blocks.length-1].blue_score):'0';
var f=d.finality;$('c-locked').textContent=f&&f.latest_locked_index?('#'+f.latest_locked_index+' at blue '+compact(f.latest_locked_blue_score)):(f&&f.supported?'none yet':'n/a');
+ if(!running&&!document.hidden)kick();
}
function rrect(px,py,w,h,r){x.beginPath();x.moveTo(px+r,py);x.lineTo(px+w-r,py);x.quadraticCurveTo(px+w,py,px+w,py+r);x.lineTo(px+w,py+h-r);x.quadraticCurveTo(px+w,py+h,px+w-r,py+h);x.lineTo(px+r,py+h);x.quadraticCurveTo(px,py+h,px,py+h-r);x.lineTo(px,py+r);x.quadraticCurveTo(px,py,px+r,py);x.closePath();}
- function xOf(b,t){return Math.min(W-S,W-S*1.2-(t-b.ts)*pxPerSec/1000);}
+ // a cubic with horizontal tangents; the control offset is clamped so a link across many lanes reads as a gentle S, never a loop
+ function link(a,b){var d=Math.max(S*0.6,Math.min(48,Math.abs(b.px-a.px)*0.5));x.moveTo(a.px,a.py);x.bezierCurveTo(a.px+d,a.py,b.px-d,b.py,b.px,b.py);}
+ function hit(){
+ var best=null,bd=S*0.9,qx=ptrX,qy=ptrY;
+ if(pin){var p=byHash[pin];return p&&p.on?p:null;}
+ if(qx<0)return null;
+ for(var i=0;iG-S;}
x.clearRect(0,0,W,H);
- // lane rails and miner labels
- var n=laneCount();x.font='500 10px IBM Plex Mono, monospace';x.textAlign='left';
- for(var l=0;l2?' +'+(names.length-2):''),8,y-S*0.75);}}
- var pos={};blocks.forEach(function(b){pos[b.hash]={x:xOf(b,t),y:laneY(laneFor(b.miner))+b.j};});
- // edges
- blocks.forEach(function(b){var p=pos[b.hash];b.parents.forEach(function(h){var q=pos[h];if(!q)return;var both=b.chain&&byHash[h]&&byHash[h].chain;
- x.strokeStyle=both?'rgba(242,84,27,0.75)':'rgba(201,199,194,0.45)';x.lineWidth=both?2:1.25;x.beginPath();x.moveTo(p.x-S/2,p.y);var mx=(p.x+q.x)/2;x.bezierCurveTo(mx,p.y,mx,q.y,q.x+S/2,q.y);x.stroke();});});
- // lock line: everything left of the newest locked checkpoint is final (mirrors the homepage simulation)
- var lk=null;blocks.forEach(function(b){if(b.locked&&(!lk||b.ts>lk.ts))lk=b;});
- if(lk){var lp=pos[lk.hash];x.strokeStyle='rgba(242,84,27,0.55)';x.lineWidth=1.5;x.setLineDash([5,6]);x.beginPath();x.moveTo(lp.x,6);x.lineTo(lp.x,H-6);x.stroke();x.setLineDash([]);x.fillStyle='rgba(154,154,158,0.9)';x.font='500 10px IBM Plex Mono, monospace';x.textAlign='right';x.fillText('final',lp.x-8,14);}
+ // final region: everything left of the newest locked checkpoint
+ var lk=null,fx=-1;
+ for(i=0;ilk.ts))lk=b;if(b.final&&b.px>fx)fx=b.px;}
+ if(lk)fx=lk.px;
+ if(fx>G){x.fillStyle='rgba(242,84,27,0.06)';x.fillRect(G,0,fx-G,H);x.strokeStyle='rgba(255,179,92,0.55)';x.lineWidth=1;x.setLineDash([4,5]);x.beginPath();x.moveTo(fx,4);x.lineTo(fx,H-4);x.stroke();x.setLineDash([]);}
+ // lane rails
+ x.strokeStyle='rgba(42,42,48,0.75)';x.lineWidth=1;x.beginPath();for(i=0;iG-S*2)link(q,b);}
+ x.stroke();
+ // the selected chain as one path through the chain blocks
+ x.strokeStyle=level?'rgba(242,84,27,0.6)':'rgba(242,84,27,0.85)';x.lineWidth=narrow||level?1.5:2;x.beginPath();var prev=null;
+ for(i=0;iG-S*2)link(prev,b);prev=b;}
+ x.stroke();
+ // hovered block: all of its parent edges, bright
+ var hv=hit();
+ if(hv){x.strokeStyle='rgba(255,179,92,0.9)';x.lineWidth=1.5;x.beginPath();for(i=0;i0){var g=x.createRadialGradient(p.x,p.y,0,p.x,p.y,S*1.8);g.addColorStop(0,'rgba(255,179,92,'+(0.55*fl)+')');g.addColorStop(1,'rgba(255,179,92,0)');x.fillStyle=g;x.beginPath();x.arc(p.x,p.y,S*1.8,0,Math.PI*2);x.fill();}
- if(b.chain){x.fillStyle='#F2541B';rrect(p.x-h,p.y-h,S,S,S*0.24);x.fill();}
- else{x.fillStyle='#0C0C0E';rrect(p.x-h,p.y-h,S,S,S*0.24);x.fill();x.strokeStyle=fl>0?'rgba(255,179,92,0.95)':'rgba(154,154,158,0.9)';x.lineWidth=2;rrect(p.x-h,p.y-h,S,S,S*0.24);x.stroke();}
- if(fl>0&&b.chain){x.strokeStyle='rgba(255,179,92,'+fl+')';x.lineWidth=2;rrect(p.x-h-2,p.y-h-2,S+4,S+4,S*0.3);x.stroke();}
- if(b.locked){x.strokeStyle='#F2541B';x.lineWidth=3;x.beginPath();x.arc(p.x,p.y,S*0.95,0,Math.PI*2);x.stroke();}
- x.fillStyle=b.chain?'rgba(244,241,236,0.9)':'rgba(154,154,158,0.9)';x.font='500 '+(S<20?8:9)+'px IBM Plex Mono, monospace';x.textAlign='center';x.fillText(b.miner,p.x,p.y+h+11);});
- // tip marker
- if(blocks.length){var tip=blocks[blocks.length-1],tp=pos[tip.hash];x.fillStyle='rgba(154,154,158,0.9)';x.font='500 10px IBM Plex Mono, monospace';x.textAlign='right';x.fillText('tip',Math.min(W-6,tp.x+S*0.5),Math.max(12,tp.y-S*0.75));}
- if(!blocks.length){x.fillStyle='rgba(154,154,158,0.9)';x.font='500 13px IBM Plex Mono, monospace';x.textAlign='center';x.fillText(live?'waiting for the first block':'no observer data',W/2,H/2);}
+ var h=S/2,r=S*0.24,tip=null;
+ for(i=0;i0){var g=x.createRadialGradient(b.px,b.py,0,b.px,b.py,S*1.6);g.addColorStop(0,'rgba(255,179,92,'+(0.5*fl)+')');g.addColorStop(1,'rgba(255,179,92,0)');x.fillStyle=g;x.beginPath();x.arc(b.px,b.py,S*1.6,0,Math.PI*2);x.fill();}
+ if(b.chain){x.fillStyle='#F2541B';rrect(b.px-h,b.py-h,S,S,r);x.fill();x.strokeStyle=m?m.col:'#9A9A9E';x.lineWidth=1.5;rrect(b.px-h-1,b.py-h-1,S+2,S+2,r+1);x.stroke();tip=b;}
+ else{x.fillStyle='#0C0C0E';rrect(b.px-h,b.py-h,S,S,r);x.fill();x.strokeStyle=fl>0?'rgba(255,179,92,0.95)':'rgba(154,154,158,0.85)';x.lineWidth=1.5;rrect(b.px-h,b.py-h,S,S,r);x.stroke();}
+ if(fl>0&&b.chain){x.strokeStyle='rgba(255,179,92,'+fl+')';x.lineWidth=2;rrect(b.px-h-3,b.py-h-3,S+6,S+6,r+2);x.stroke();}
+ if(b.locked){x.strokeStyle='#FFB35C';x.lineWidth=2.5;x.beginPath();x.arc(b.px,b.py,S*0.95,0,Math.PI*2);x.stroke();}
+ if(hv===b){x.strokeStyle='#F4F1EC';x.lineWidth=1.5;rrect(b.px-h-3,b.py-h-3,S+6,S+6,r+2);x.stroke();}}
+ x.font='500 10px IBM Plex Mono, monospace';
+ // the chain tip carries its short id; nothing else is labelled unless hovered, pinned or locked
+ if(tip){x.fillStyle='rgba(244,241,236,0.92)';x.textAlign='right';var ly=tip.py-h-6;if(ly<10){ly=tip.py+h+12;}x.fillText(tip.hash.slice(0,8),Math.min(W-4,tip.px+h+2),ly);}
+ if(fx>G){var ft=lk?'final #'+lk.hash.slice(0,8):'final',fw=x.measureText(ft).width;x.fillStyle='#FFB35C';
+ if(fx-G>fw+14){x.textAlign='right';x.fillText(ft,fx-6,12);}else{x.textAlign='left';x.fillText(ft,fx+6,12);}}
+ // tooltip on the canvas, no DOM
+ if(hv){var l1=hv.hash.slice(0,8)+(hv.locked?' locked':(hv.chain?' chain':' side')),l2='blue '+compact(hv.blue_score)+' daa '+compact(hv.daa),l3=hv.parents.length+(hv.parents.length===1?' parent':' parents')+' '+hv.miner;
+ var tw=Math.max(x.measureText(l1).width,x.measureText(l2).width,x.measureText(l3).width)+20,th=52,tx=hv.px+h+10,tty=hv.py-th/2;
+ if(tx+tw>W-4)tx=hv.px-h-10-tw;if(tx