igneum/site/live-dag.js
igneum-labs a4fe3d8e08 site-ui-4: the EMBER 02 renderer on the home fold and /live, the proof inspector, the compact copies for the app (prototype, not deployed)
renderer pack EMBER 02 supplied by the project lead, 7 Oct 2026: src/live-dag.js
and src/proof-core.js adopted byte for byte (sha256 582f2e73... and
c2a094a6...), reviewed as third-party code (one same-origin fetch of
/api/live when poll is on, no eval, no CDN, no font download, tokens
read from :root in both themes, checkpoints placed only on an exact
blue score, proof never implies inclusion, a lock only when reported),
its tests/verify.py run on this Mac (73 of 73) and the module mounted
against the real observer feed (state live, only /api/live requested,
zero records promoted). Home: the pack's full mode in the one-screen
fold with the legend row and the newest block's caption, fed by the
page's own reader. /live: the pack in the graph slot, the four miner
stats (blocks in window, your blocks, proven, latest checkpoint), the
All blocks / Selected chain / Your blocks tabs, Pause and Follow, the
window stepper, the inspector column with IgneumProof's shard scene,
the proof shards list, inclusion, miner key, blue score, DAA, parents,
header time, checkpoint, finality, and the three-beat Motion, with
meaning block; the observatory's six tiles, identities, event stream,
block activity, light client and table stay. site.css gains --included
and --excluded. The standalone demo is the pack's standalone.html; the
10-second recording is from this branch's /live.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 08:59:46 +00:00

342 lines
34 KiB
JavaScript

/* Igneum DAG / Ember 2.0
* Replacement for the supplied live-dag.js. No framework, network writes or synthetic records.
* Existing mount/push options and public methods are retained; see README.md for additions.
* Every node, parent edge, proof segment and checkpoint is derived from the observer reply.
* Motion interpolates presentation ONLY. It never promotes proof, inclusion or finality.
*/
(function (root) {
'use strict';
var instances = new WeakMap();
var TAU = Math.PI * 2;
function clamp(n, a, b) { return Math.max(a, Math.min(b, n)); }
function finite(v) { return typeof v === 'number' && Number.isFinite(v); }
function num(v) { return finite(v) ? v : null; }
function fmt(v) { return v == null ? 'unknown' : Number(v).toLocaleString('en-GB'); }
function rgba(c, a) {
var s = String(c).trim(), h = s.replace('#', ''), rgb;
if (/^#[0-9a-f]{3}$/i.test(s)) h = h.split('').map(function (x) { return x + x; }).join('');
if (/^[0-9a-f]{6}$/i.test(h)) return 'rgba(' + parseInt(h.slice(0,2),16) + ',' + parseInt(h.slice(2,4),16) + ',' + parseInt(h.slice(4,6),16) + ',' + a + ')';
rgb = s.match(/^rgba?\(\s*([\d.]+)[,\s]+([\d.]+)[,\s]+([\d.]+)/i);
return rgb ? 'rgba(' + rgb[1] + ',' + rgb[2] + ',' + rgb[3] + ',' + a + ')' : s;
}
function rounded(ctx, x, y, w, h, r) {
r = Math.max(0, Math.min(r, w/2, h/2)); ctx.beginPath(); ctx.moveTo(x+r,y);
ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);
ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();
}
function cloneBlock(b) {
return {hash:b.hash,ts:b.ts,blue_score:b.blue_score,blue:b.blue_score,daa:b.daa,
parents:b.parents.slice(),chain:b.chain,color:b.color,miner:b.miner,locked:b.locked,
final:b.final,proven:b.proven,shards:b.shards.map(function(s){return {state:s.state};})};
}
function normalize(b) {
if (!b || typeof b.hash !== 'string' || !b.hash.length || b.hash.length>256 || !finite(b.ts) || b.ts<0) return null;
return {hash:b.hash,ts:b.ts,blue_score:num(b.blue_score),daa:num(b.daa),
parents:Array.isArray(b.parents)?Array.from(new Set(b.parents.filter(function(p){return typeof p==='string' && p!==b.hash;}))).slice(0,128):[],
chain:b.chain===true,color:['blue','red','pending'].includes(b.color)?b.color:'pending',
miner:typeof b.miner==='string'?b.miner.slice(0,256):'unknown',locked:b.locked===true,final:b.final===true,proven:b.proven===true,
shards:Array.isArray(b.shards)?b.shards.slice(0,256).map(function(s){return {state:s && ['planned','proving','verified','paid'].includes(s.state)?s.state:'unknown'};}):[]};
}
function mount(canvas, opts) {
opts=opts||{};
if (!canvas || typeof canvas.getContext!=='function') throw new TypeError('IgneumDag.mount requires a canvas element.');
if (instances.has(canvas)) instances.get(canvas).destroy();
var ctx=canvas.getContext('2d'); if(!ctx) throw new Error('A 2D canvas context is required.');
var doc=canvas.ownerDocument, compact=!!opts.compact;
var windowS=clamp(finite(opts.window)?opts.window:(compact?90:120),30,300);
var maxBlocks=clamp(finite(opts.maxBlocks)?opts.maxBlocks:6000,100,20000);
var lag=finite(opts.lagMs)?clamp(opts.lagMs,0,10000):2500;
var fps=clamp(finite(opts.fps)?opts.fps:30,10,60), minFrame=1000/fps;
var mq=root.matchMedia?root.matchMedia('(prefers-reduced-motion: reduce)'):null;
var reduced=mq?mq.matches:false, manualMotion=true;
var mediaTheme=root.matchMedia?root.matchMedia('(prefers-color-scheme: dark)'):null;
var col={}, W=0,H=0,dpr=1,padL=100,padR=22,padT=57,padB=37;
var model=new Map(), view=new Map(), blocks=[], cps=[], latestCps=[], lanes=[], laneOf=new Map();
var state='connecting',reason='Waiting for observer',destroyed=false,paused=false,following=true,engaged=false;
var fixedRight=0,pausedRight=0,staleRight=null,reducedRight=Date.now()-lag, lastGood=0, lastData=null;
var selected=null,hover=null,filter='all',queuedCount=0,invalid=0,omitted=0;
var visible=true,raf=0,lastPaint=-Infinity,frameCount=0,lastT=0,freezeT=0;
var pollTimer=0,healthTimer=0,timeout=0,controller=null,inflight=false,pollAgain=false,failures=0;
var remove=[],ro=null,io=null,mo=null,drag=null,pinch=null;
var lastCheckpointStates=new Map(),checkpointBursts=new Map();
var initialAttributes={tabindex:canvas.getAttribute('tabindex'),role:canvas.getAttribute('role'),label:canvas.getAttribute('aria-label'),touch:canvas.style.touchAction,cursor:canvas.style.cursor};
var tip=opts.tooltip||null,chip=opts.chip||null;
var mine=typeof opts.mine==='function'?opts.mine:opts.mine?function(b){return b.miner===opts.mine;}:function(){return false;};
function own(b){try{return !!mine(b);}catch(e){return false;}}
function emit(name){if(destroyed || typeof opts[name]!=='function')return;var args=[].slice.call(arguments,1);try{opts[name].apply(null,args);}catch(e){console.error('IgneumDag '+name+' callback:',e);}}
function on(target,name,handler,config){target.addEventListener(name,handler,config);remove.push(function(){target.removeEventListener(name,handler,config);});}
function theme(){
var s=getComputedStyle(doc.documentElement);
function c(n,f){return s.getPropertyValue(n).trim()||f;}
col={ember:c('--ember','#F2541B'),emberHi:c('--ember-hi','#FF6A2B'),molten:c('--molten','#FFB35C'),
bone:c('--bone','#F4F1EC'),ash:c('--ash','#9A9A9E'),line:c('--line','#2A2A30'),line2:c('--line-2','#3A3A42'),
bg:c('--row',c('--obsidian','#111114')),blue:c('--included','#3B7DD8'),red:c('--excluded','#B0362B'),
surface:c('--graphite','#16161A')};
redraw();
}
function rightTime(){
if(paused)return pausedRight;
if(!following)return fixedRight;
if(state!=='live' && staleRight!==null)return staleRight;
return reduced||!manualMotion?reducedRight:Date.now()-lag;
}
function xOf(ts,right){return padL+(W-padL-padR)*(1-(right-ts)/(windowS*1000));}
function eventXY(ev){var r=canvas.getBoundingClientRect();return {x:(ev.clientX-r.left)*W/(r.width||1),y:(ev.clientY-r.top)*H/(r.height||1)};}
function setState(s,r){
if(state===s && reason===r)return;
if(s!=='live' && state==='live')staleRight=rightTime();
state=s;reason=r||null;
if(s==='live')staleRight=null;
emit('onState',s,reason);redraw();
}
function layout(){
padL=compact||W<510?14:W<750?84:106;padR=compact?12:24;padT=compact?29:58;padB=compact?22:38;
var start=rightTime()-windowS*1000, end=rightTime()+lag+1000,counts=new Map(),mineKeys=new Set();
blocks.forEach(function(b){if(b.ts>=start && b.ts<=end){counts.set(b.miner,(counts.get(b.miner)||0)+1);if(own(b))mineKeys.add(b.miner);}});
var capacity=Math.max(2,Math.floor((H-padT-padB)/(compact?26:46))),ids=Array.from(counts.keys());
ids.sort(function(a,b){return (mineKeys.has(b)?1:0)-(mineKeys.has(a)?1:0)||(counts.get(b)-counts.get(a))||a.localeCompare(b);});
var chosen=ids.slice(0,ids.length>capacity?capacity-1:capacity);
// Preserve existing key order instead of re-ranking every poll. Always keep the user's lane visible.
var keep=lanes.filter(function(id){return chosen.includes(id);});
chosen.forEach(function(id){if(!keep.includes(id))keep.push(id);});
keep.sort(function(a,b){return (mineKeys.has(b)?1:0)-(mineKeys.has(a)?1:0);});
lanes=keep;if(ids.length>chosen.length)lanes.push('__others__');
laneOf=new Map();lanes.forEach(function(id,i){laneOf.set(id,i);});
var now=performance.now();
blocks.forEach(function(b){var i=laneOf.has(b.miner)?laneOf.get(b.miner):Math.max(0,lanes.length-1);
var y=padT+(i+.5)*(H-padT-padB)/Math.max(1,lanes.length);
if(!finite(b.y)){b.y=y;b.fromY=y;b.targetY=y;}
if(b.targetY!==y){b.fromY=b.y;b.targetY=y;b.laneAt=now;}
});
}
function size(){
if(destroyed)return;
var nextW=canvas.clientWidth,nextH=canvas.clientHeight,nextDpr=Math.min(root.devicePixelRatio||1,2);
if(W===nextW&&H===nextH&&dpr===nextDpr&&canvas.width===Math.round(nextW*nextDpr)){redraw();return;}
W=nextW;H=nextH;dpr=nextDpr;
canvas.width=Math.round(W*dpr);canvas.height=Math.round(H*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);layout();redraw();
}
function syncView(){
var now=performance.now(),first=view.size===0, next=new Map();
model.forEach(function(b,hash){var old=view.get(hash),v=Object.assign({},b);
v.born=old?old.born:first?now-3000:now;v.newArrival=old?old.newArrival:!first;v.visibleAt=old?old.visibleAt:null;v.changed=old && (old.proven!==b.proven||old.locked!==b.locked||old.color!==b.color||old.shards.map(function(s){return s.state;}).join(',')!==b.shards.map(function(s){return s.state;}).join(','))?now:old?old.changed:now-3000;
v.y=old?old.y:NaN;v.fromY=old?old.fromY:NaN;v.targetY=old?old.targetY:NaN;v.laneAt=old?old.laneAt:now;
next.set(hash,v);
});
view=next;blocks=Array.from(view.values()).sort(function(a,b){return a.ts-b.ts||a.hash.localeCompare(b.hash);});
cps=latestCps.map(function(c){return Object.assign({},c);});
cps.forEach(function(c){var prev=lastCheckpointStates.get(c.index);if(prev==='pending'&&c.state==='locked')checkpointBursts.set(c.index,now);lastCheckpointStates.set(c.index,c.state);});
var cpKeys=new Set(cps.map(function(c){return c.index;}));lastCheckpointStates.forEach(function(_,key){if(!cpKeys.has(key)){lastCheckpointStates.delete(key);checkpointBursts.delete(key);}});
layout();
if(selected && !view.has(selected)){selected=null;emit('onSelect',null,'expired');}
else if(selected)emit('onSelect',cloneBlock(view.get(selected)),'update');
if(hover&&!view.has(hover))hover=null;
queuedCount=0;
}
function push(d){
if(destroyed)return false;
if(!d||d.ok!==true||!d.state||typeof d.state!=='object'||!Array.isArray(d.blocks)){
setState('failed','Invalid observer reply; expected {ok, state, blocks}.');return false;
}
var now=Date.now(), incoming=[], bad=0;
d.blocks.forEach(function(b){var n=normalize(b);if(n)incoming.push(n);else bad++;});
if(d.blocks.length && !incoming.length){setState('failed','Observer reply contains no valid block records.');return false;}
invalid=bad;lastGood=now;reducedRight=now-lag;failures=0;lastData=d;
incoming.forEach(function(b){if(!model.has(b.hash))queuedCount++;model.set(b.hash,b);});
var newest=0;model.forEach(function(b){newest=Math.max(newest,b.ts);});
// Bounded retention uses the newest observed header, not a local wall-clock guess.
var cut=newest-340000;model.forEach(function(b,h){if(b.ts<cut)model.delete(h);});
omitted=0;
if(model.size>maxBlocks){var ordered=Array.from(model.values()).sort(function(a,b){return b.ts-a.ts;});omitted=model.size-maxBlocks;model=new Map(ordered.slice(0,maxBlocks).map(function(b){return[b.hash,b];}));}
latestCps=d.finality&&Array.isArray(d.finality.checkpoints)?d.finality.checkpoints.filter(function(c){return c&&finite(c.blue_score)&&['pending','locked'].includes(c.state);}).slice(-128).map(function(c){return {index:c.index,blue_score:c.blue_score,state:c.state,fraction_total:finite(c.fraction_total)&&c.fraction_total>=0&&c.fraction_total<=1?c.fraction_total:null};}):[];
if(d.state.stale){if(staleRight===null)staleRight=Math.min(now-lag,newest?newest+lag:now-lag);setState('stale','Observer stale'+(finite(d.state.age_s)?'; last update '+Math.round(d.state.age_s)+' s ago':''));}
else setState('live',null);
if(!paused)syncView();
emit('onData',d);emit('onStats',stats());if(!paused)redraw();return true;
}
function stats(){
var r=rightTime(),v=blocks.filter(function(b){return b.ts>=r-windowS*1000&&b.ts<=r;});
return {blocks:v.length,included:v.filter(function(b){return b.color==='blue';}).length,excluded:v.filter(function(b){return b.color==='red';}).length,
selectedChain:v.filter(function(b){return b.chain;}).length,proven:v.filter(function(b){return b.proven;}).length,
own:v.filter(own).length,minerKeys:new Set(v.map(function(b){return b.miner;})).size,
lockedCheckpoints:cps.filter(function(c){return c.state==='locked';}).length,paused:paused,following:following,queued:paused?queuedCount:0,
invalidRecords:invalid,omittedRecords:omitted,rendered:blocks.length,frames:frameCount,window:windowS,reducedMotion:reduced||!manualMotion,state:state};
}
function poll(){
if(destroyed||opts.poll===false||doc.hidden)return;
if(inflight){pollAgain=true;return;}
clearTimeout(pollTimer);inflight=true;controller=new AbortController();var signal=controller.signal;
timeout=setTimeout(function(){if(controller)controller.abort();},finite(opts.timeoutMs)?opts.timeoutMs:8000);
var url;
try{url=new URL(opts.url||'/api/live',doc.baseURI);if(!['https:','http:'].includes(url.protocol))throw new Error('Use an HTTP(S) observer URL.');url.searchParams.set('window',String(Math.round(windowS)));}
catch(e){finishError(e);return;}
fetch(url.href,{cache:'no-store',signal:signal,credentials:'same-origin'}).then(function(r){if(!r.ok)throw new Error('HTTP '+r.status);return r.json();}).then(function(d){if(destroyed)return;if(!push(d))throw new Error('Invalid observer reply');}).catch(finishError).finally(finish);
function finishError(e){if(destroyed||doc.hidden)return;failures++;setState(failures>=2?'failed':'stale','Observer unavailable; retrying'+(e&&e.name==='AbortError'?' (timeout)':''));if(!url)finish();}
function finish(){clearTimeout(timeout);timeout=0;controller=null;inflight=false;if(destroyed||opts.poll===false||doc.hidden)return;var again=pollAgain;pollAgain=false;pollTimer=setTimeout(poll,again?0:Math.min(30000,2000*Math.pow(2,Math.min(failures,4))));}
}
function passes(b){return filter==='all'||filter==='chain'&&b.chain||filter==='mine'&&own(b);}
function curve(b,p){var mx=(b.x+p.x)/2;ctx.beginPath();ctx.moveTo(p.x,p.y);ctx.bezierCurveTo(mx,p.y,mx,b.y,b.x,b.y);}
function pointOn(b,p,t){var u=1-t,mx=(b.x+p.x)/2;return {x:u*u*u*p.x+3*u*u*t*mx+3*u*t*t*mx+t*t*t*b.x,y:u*u*u*p.y+3*u*u*t*p.y+3*u*t*t*b.y+t*t*t*b.y};}
function lockIcon(x,y,size,color){ctx.strokeStyle=color;ctx.lineWidth=1.25;rounded(ctx,x-size*.42,y,size*.84,size*.65,1.5);ctx.stroke();ctx.beginPath();ctx.arc(x,y,size*.25,Math.PI,0);ctx.stroke();}
function draw(t){
if(destroyed||!W||!H)return;
frameCount++;var rt=rightTime(),left=rt-windowS*1000,animate=!reduced&&manualMotion&&!paused&&state==='live';var clock=paused?freezeT:t;
ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,W,H);ctx.fillStyle=col.bg;ctx.fillRect(0,0,W,H);
var plotW=W-padL-padR, plotH=H-padT-padB,lh=plotH/Math.max(1,lanes.length);
// Sparse time ticks; true horizontal coordinate is header time, not DAA spacing.
ctx.lineWidth=1;ctx.setLineDash([2,7]);
for(var g=0;g<=6;g++){var gx=padL+plotW*g/6;ctx.strokeStyle=rgba(col.line,.52);ctx.beginPath();ctx.moveTo(gx,padT-10);ctx.lineTo(gx,H-padB);ctx.stroke();}
ctx.setLineDash([]);
lanes.forEach(function(lane,i){var y=padT+(i+.5)*lh, ownLane=blocks.some(function(b){return b.miner===lane&&own(b);});
if(ownLane){var grad=ctx.createLinearGradient(padL,0,W,0);grad.addColorStop(0,rgba(col.molten,.085));grad.addColorStop(1,rgba(col.molten,.005));ctx.fillStyle=grad;rounded(ctx,padL-5,y-lh*.4,plotW+5,lh*.8,7);ctx.fill();}
ctx.strokeStyle=rgba(ownLane?col.molten:col.line,ownLane?.20:.65);ctx.beginPath();ctx.moveTo(padL,y);ctx.lineTo(W-padR,y);ctx.stroke();
if(padL>50){ctx.font='500 10px ui-monospace, SFMono-Regular, Consolas, monospace';ctx.textBaseline='middle';ctx.textAlign='left';ctx.fillStyle=ownLane?col.molten:col.ash;
ctx.fillText(ownLane?'YOUR KEY':lane==='__others__'?'OTHERS':lane.slice(0,8),12,y-5);
ctx.font='8px ui-monospace, monospace';ctx.fillStyle=rgba(col.ash,.78);ctx.fillText(ownLane?'YOUR BLOCKS':lane==='__others__'?'GROUPED KEYS':'MINER KEY',12,y+10);
}
});
var cpLabels=[];
cps.forEach(function(c){
// Exact score only. Never pin a checkpoint to an unrelated nearest block.
var candidates=blocks.filter(function(b){return b.blue_score===c.blue_score;});
var ref=candidates.find(function(b){return b.locked;})||candidates.find(function(b){return b.chain;})||(candidates.length===1?candidates[0]:null);
if(!ref||ref.ts<left||ref.ts>rt)return;
var x=xOf(ref.ts,rt),locked=c.state==='locked',burstAt=checkpointBursts.get(c.index),burst=animate&&burstAt?Math.max(0,1-(t-burstAt)/1400):0;
var band=ctx.createLinearGradient(x-20,0,x+36,0);band.addColorStop(0,rgba(col.ember,0));band.addColorStop(.38,rgba(col.ember,locked?.09:.035));band.addColorStop(1,rgba(col.ember,0));ctx.fillStyle=band;ctx.fillRect(x-20,padT-12,56,plotH+12);
ctx.strokeStyle=rgba(col.ember,locked?.6:.32);ctx.lineWidth=locked?1.5:1;ctx.setLineDash(locked?[]:[3,6]);ctx.beginPath();ctx.moveTo(x,padT-14);ctx.lineTo(x,H-padB);ctx.stroke();ctx.setLineDash([]);
if(burst>0){ctx.strokeStyle=rgba(col.molten,burst*.5);ctx.lineWidth=1;ctx.strokeRect(x-(1-burst)*42,padT-12,(1-burst)*84,plotH+12);}
if(!compact && W>510)cpLabels.push({x:x,cp:c,locked:locked});
});
cpLabels.sort(function(a,b){return b.x-a.x;});var cpLast=Infinity;
cpLabels.forEach(function(p){var label=(p.locked?'LOCKED':'PENDING')+(p.cp.fraction_total===null?'':' '+Math.round(p.cp.fraction_total*100)+'%'),w=label.length*5.5+22,x=clamp(p.x-w/2,padL,W-padR-w);if(x+w+8>cpLast)return;cpLast=x;
ctx.fillStyle=col.bg;ctx.strokeStyle=rgba(col.ember,p.locked?.45:.23);rounded(ctx,x,17,w,22,4);ctx.fill();ctx.stroke();ctx.font='9px ui-monospace, monospace';ctx.fillStyle=p.locked?col.ember:col.ash;ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillText(label,x+w/2,28);
});
// Animate lane transitions without changing their underlying key identity.
blocks.forEach(function(b){b.x=xOf(b.ts,rt);var progress=animate?clamp((clock-b.laneAt)/450,0,1):1;progress=1-Math.pow(1-progress,3);b.y=b.fromY+(b.targetY-b.fromY)*progress;b.on=b.ts>=left&&b.ts<=rt&&passes(b);if(b.on&&b.newArrival&&b.visibleAt===null){b.visibleAt=t;b.born=t;}});
ctx.save();ctx.beginPath();ctx.rect(padL-1,padT-15,plotW+2,plotH+25);ctx.clip();
blocks.forEach(function(b){if(!b.on)return;b.parents.forEach(function(hash){var p=view.get(hash);if(!p||!passes(p)||p.x>W-padR||p.x<padL-plotW*.2)return;
var path=b.chain&&p.chain,selectedEdge=selected&&(b.hash===selected||p.hash===selected);
ctx.strokeStyle=rgba(path?col.ember:selectedEdge?col.bone:col.ash,path?.1:selectedEdge?.10:.04);ctx.lineWidth=path?8:selectedEdge?4:2;curve(b,p);ctx.stroke();
ctx.strokeStyle=rgba(path?col.ember:selectedEdge?col.bone:b.color==='red'?col.red:col.ash,path?.68:selectedEdge?.7:b.color==='red'?.3:.23);ctx.lineWidth=path?1.65:selectedEdge?1.4:1;curve(b,p);ctx.stroke();
var age=t-b.born;if(animate&&age>=0&&age<1400){var f=clamp(age/1150,0,1),pt=pointOn(b,p,f);ctx.shadowBlur=10;ctx.shadowColor=path?col.ember:col.blue;ctx.fillStyle=path?col.molten:col.blue;ctx.beginPath();ctx.arc(pt.x,pt.y,path?2.2:1.65,0,TAU);ctx.fill();ctx.shadowBlur=0;}
});});
var density=blocks.filter(function(b){return b.on;}).length/Math.max(plotW,1), S=compact?6.5:density>.16?7:9.5;
blocks.forEach(function(b){if(!b.on)return;
var age=t-b.born,newness=animate?Math.max(clamp(1-age/1700,0,1),clamp(1-(t-b.changed)/900,0,1)*.65):0;
var ink=b.color==='red'?col.red:b.chain?col.ember:b.color==='blue'?col.blue:col.ash;
var isOwn=own(b),isSelected=b.hash===selected||b.hash===hover,sz=S*(1+(newness*.15));
if(newness>0||isOwn||isSelected){var glow=ctx.createRadialGradient(b.x,b.y,0,b.x,b.y,sz*3.8);glow.addColorStop(0,rgba(isOwn?col.molten:ink,newness*.26+(isSelected?.15:isOwn?.06:0)));glow.addColorStop(1,rgba(ink,0));ctx.fillStyle=glow;ctx.fillRect(b.x-sz*4,b.y-sz*4,sz*8,sz*8);}
ctx.globalAlpha=b.color==='red'?.78:1;
// Inset tiles: inclusion is the outline; proof is the core, never the same status.
ctx.fillStyle=col.bg;ctx.strokeStyle=rgba(ink,b.chain?.95:.8);ctx.lineWidth=b.chain?1.75:1.25;rounded(ctx,b.x-sz,b.y-sz,2*sz,2*sz,3);ctx.fill();ctx.stroke();
var tile=ctx.createLinearGradient(b.x,b.y-sz,b.x,b.y+sz);tile.addColorStop(0,rgba(ink,b.proven?.5:.16));tile.addColorStop(1,rgba(ink,b.proven?.22:.025));ctx.fillStyle=tile;rounded(ctx,b.x-sz+2,b.y-sz+2,2*sz-4,2*sz-4,2);ctx.fill();
if(b.proven){ctx.strokeStyle=rgba(b.color==='red'?col.red:col.bone,.9);ctx.lineWidth=1.3;ctx.beginPath();ctx.moveTo(b.x-3,b.y);ctx.lineTo(b.x-1,b.y+2);ctx.lineTo(b.x+3,b.y-2);ctx.stroke();}
else if(b.color==='red'){ctx.strokeStyle=ink;ctx.lineWidth=1.1;ctx.beginPath();ctx.moveTo(b.x-2,b.y-2);ctx.lineTo(b.x+2,b.y+2);ctx.moveTo(b.x+2,b.y-2);ctx.lineTo(b.x-2,b.y+2);ctx.stroke();}
else{ctx.fillStyle=rgba(ink,.65);ctx.fillRect(b.x-1.3,b.y-1.3,2.6,2.6);}
if(!compact && b.shards.length){var total=b.shards.length,shown=Math.min(total,8),gap=1.6,barW=2*sz,seg=(barW-gap*(shown-1))/shown;
for(var k=0;k<shown;k++){var shard=b.shards[k].state,alpha=shard==='proving'?(animate?.5+.4*Math.sin(t/300+k):.8):1;
ctx.fillStyle=rgba(shard==='paid'?col.molten:shard==='verified'?col.bone:shard==='proving'?col.ember:col.line2,alpha);ctx.fillRect(b.x-sz+k*(seg+gap),b.y+sz+4,Math.max(1,seg),2);
}
}
if(isOwn){ctx.strokeStyle=col.molten;ctx.lineWidth=1.2;var q=sz+4;ctx.beginPath();[[1,1],[1,-1],[-1,1],[-1,-1]].forEach(function(v){ctx.moveTo(b.x+v[0]*(q-4),b.y+v[1]*q);ctx.lineTo(b.x+v[0]*q,b.y+v[1]*q);ctx.lineTo(b.x+v[0]*q,b.y+v[1]*(q-4));});ctx.stroke();}
if(b.locked){ctx.strokeStyle=rgba(col.ember,.6);ctx.lineWidth=1;ctx.beginPath();ctx.arc(b.x,b.y,sz+8,0,TAU);ctx.stroke();}
if(b.final){ctx.fillStyle=rgba(col.ember,.75);ctx.beginPath();ctx.arc(b.x+sz-1,b.y-sz-2,2,0,TAU);ctx.fill();}
if(isSelected){ctx.strokeStyle=col.bone;ctx.lineWidth=1;ctx.setLineDash([3,3]);rounded(ctx,b.x-sz-8,b.y-sz-8,2*sz+16,2*sz+16,6);ctx.stroke();ctx.setLineDash([]);}
ctx.globalAlpha=1;
});ctx.restore();
// Timeline and feed edge. No fabricated DAA ticks or estimated checkpoint positions.
ctx.font=(compact?'8':'9')+'px ui-monospace, monospace';ctx.fillStyle=col.ash;ctx.textBaseline='middle';
var ticks=W<420?2:4;
for(var q=0;q<=ticks;q++){var tx=padL+plotW*q/ticks;ctx.textAlign=q===0?'left':q===ticks?'right':'center';var label=q===ticks?(paused?'PAUSED':!following?'HISTORY':state==='live'?'OBSERVED NOW':state.toUpperCase()):'-'+Math.round(windowS*(1-q/ticks))+'s';ctx.fillText(label,tx,H-(compact?8:13));}
if(!compact){var visibleBlocks=blocks.filter(function(b){return b.on;});ctx.font='8px ui-monospace, monospace';ctx.textAlign='left';ctx.fillStyle=rgba(col.ash,.8);if(padL>50)ctx.fillText('HEADER TIME',12,H-13);
if(visibleBlocks.length && W>600){var last=visibleBlocks[visibleBlocks.length-1];ctx.textAlign='right';ctx.fillText('DAA '+fmt(last.daa),W-padR,8);}
}
if(state==='live'&&following&&!paused){ctx.strokeStyle=rgba(col.ember,.3);ctx.beginPath();ctx.moveTo(W-padR,padT-9);ctx.lineTo(W-padR,H-padB);ctx.stroke();ctx.fillStyle=col.ember;ctx.beginPath();ctx.arc(W-padR,padT-12,2.3,0,TAU);ctx.fill();}
if(!blocks.some(function(b){return b.on;})){
ctx.textAlign='center';ctx.fillStyle=col.ash;ctx.font='12px system-ui, sans-serif';ctx.fillText(state==='connecting'?'Waiting for observer data':filter==='mine'?'No blocks for your key in this window':filter==='chain'?'No selected-chain blocks in this window':'No blocks in this time window',padL+plotW/2,H/2);
}
var active=selected?view.get(selected):hover?view.get(hover):null;if(active&&active.on)showTip(active);else if(tip)tip.hidden=true;
lastT=t;
}
function continuous(){return !destroyed&&!doc.hidden&&visible&&!paused&&!reduced&&manualMotion&&state==='live'&&blocks.length>0;}
function frame(t){raf=0;if(destroyed||doc.hidden||!visible)return;if(t-lastPaint>=minFrame-.5){draw(t);lastPaint=t;}if(continuous())raf=requestAnimationFrame(frame);}
function redraw(){if(destroyed||raf||doc.hidden||!visible)return;lastPaint=-Infinity;raf=requestAnimationFrame(frame);}
function stopFrame(){if(raf)cancelAnimationFrame(raf);raf=0;}
function words(b){
if(!b)return {title:'No block selected',lines:[]};
var inclusion=b.color==='blue'?'included':b.color==='red'?'excluded':'pending';
var bits=[inclusion];if(b.chain)bits.push('selected chain');if(b.proven)bits.push('proven');if(b.locked)bits.push('locked checkpoint');if(b.final)bits.push('final');
return {title:b.hash+' '+bits.join(' / '),lines:[(own(b)?'Your block / ':'')+'miner key '+b.miner,
'Blue score '+fmt(b.blue_score===undefined?b.blue:b.blue_score)+' / DAA '+fmt(b.daa),b.parents.length+' parent'+(b.parents.length===1?'':'s'),
'Shards: '+(b.shards.length?b.shards.map(function(s){return s.state;}).join(', '):'no shard plan yet')]};
}
function showTip(b){
if(!tip)return;
if(!b){tip.hidden=true;return;}
var w=words(b),key=w.title+'|'+w.lines.join('|');
if(tip.dataset.igneumKey!==key){tip.replaceChildren();var title=doc.createElement('b');title.textContent=w.title;tip.appendChild(title);w.lines.forEach(function(line){var p=doc.createElement('span');p.textContent=line;p.style.display='block';tip.appendChild(p);});tip.dataset.igneumKey=key;}
tip.hidden=false;tip.style.pointerEvents='none';tip.style.position='absolute';tip.style.left='0px';tip.style.top='0px';tip.style.maxWidth=Math.max(160,W-24)+'px';tip.style.whiteSpace='normal';tip.style.overflowWrap='anywhere';
var host=tip.offsetParent||canvas.parentElement,cr=canvas.getBoundingClientRect(),pr=host.getBoundingClientRect();
var tw=tip.offsetWidth,th=tip.offsetHeight;var x=clamp(b.x+16,8,Math.max(8,W-tw-8)),y=b.y-th-18;if(y<8)y=Math.min(H-th-8,b.y+22);
tip.style.transform='translate('+Math.round(cr.left-pr.left-host.clientLeft+x)+'px,'+Math.round(cr.top-pr.top-host.clientTop+Math.max(8,y))+'px)';
}
function hit(ev){var p=eventXY(ev),best=null,dist=22*22;blocks.forEach(function(b){if(!b.on)return;var d=(b.x-p.x)**2+(b.y-p.y)**2;if(d<dist){dist=d;best=b;}});return best;}
function select(hash,why){if(hash!==null&&!view.has(hash))return false;selected=hash;showTip(hash?view.get(hash):null);emit('onSelect',hash?cloneBlock(view.get(hash)):null,why||'select');redraw();return true;}
function engage(onValue){var value=!!onValue;if(engaged===value)return;engaged=value;canvas.classList.toggle('engaged',value);if(chip)chip.hidden=!value;emit('onEngage',value);}
function setWindow(seconds){var n=Math.round(clamp(seconds,30,300));if(!finite(n)||n===windowS)return;windowS=n;layout();emit('onWindow',n);emit('onStats',stats());poll();redraw();}
function zoom(f){if(!finite(f)||f<=0)return;setWindow(windowS*f);}
function follow(){following=true;fixedRight=0;if(paused)pausedRight=Date.now()-lag;layout();emit('onFollow',true);redraw();}
function setPaused(p){p=!!p;if(paused===p)return;
if(p){pausedRight=rightTime();freezeT=performance.now();paused=true;stopFrame();}
else{paused=false;syncView();}
if(opts.pauseButton){opts.pauseButton.setAttribute('aria-pressed',String(paused));opts.pauseButton.textContent=paused?'Resume':'Pause';}
emit('onPause',paused);emit('onStats',stats());redraw();
}
function setFilter(value){if(!['all','chain','mine'].includes(value))return;filter=value;select(null,'filter');layout();redraw();}
function setMotion(value){manualMotion=!!value;reducedRight=Date.now()-lag;stopFrame();redraw();}
function panTo(right){following=false;fixedRight=Math.min(right,Date.now()-lag);if(paused)pausedRight=fixedRight;emit('onFollow',false);layout();redraw();}
canvas.setAttribute('tabindex',canvas.getAttribute('tabindex')||'0');canvas.setAttribute('role','img');
if(!canvas.getAttribute('aria-label'))canvas.setAttribute('aria-label','Interactive block graph. Arrow keys inspect blocks. Plus and minus zoom. Space pauses, F follows, Escape releases.');
canvas.style.touchAction='pan-y';
on(canvas,'pointerdown',function(ev){if(ev.pointerType==='touch'||ev.button!==0)return;canvas.focus({preventScroll:true});engage(true);drag={id:ev.pointerId,x:ev.clientX,start:rightTime(),moved:false};canvas.setPointerCapture(ev.pointerId);});
on(canvas,'pointermove',function(ev){
if(drag&&drag.id===ev.pointerId){var dx=ev.clientX-drag.x;if(Math.abs(dx)>4)drag.moved=true;if(drag.moved){panTo(drag.start-dx*windowS*1000/Math.max(1,W-padL-padR));canvas.style.cursor='grabbing';return;}}
var b=hit(ev);hover=b?b.hash:null;canvas.style.cursor=b?'pointer':'grab';if(!selected)showTip(b);if(!continuous())redraw();
});
on(canvas,'pointerup',function(ev){if(ev.pointerType==='touch')return;if(!drag||drag.id!==ev.pointerId)return;var moved=drag.moved;drag=null;if(canvas.hasPointerCapture(ev.pointerId))canvas.releasePointerCapture(ev.pointerId);canvas.style.cursor='grab';if(!moved){var b=hit(ev);select(b&&selected!==b.hash?b.hash:null,'pointer');}});
on(canvas,'pointercancel',function(){drag=null;});
on(canvas,'pointerleave',function(){hover=null;if(!drag)engage(false);if(!selected)showTip(null);redraw();});
on(canvas,'wheel',function(ev){if(!ev.ctrlKey&&!ev.metaKey&&!engaged)return;if(Math.abs(ev.deltaY)<1)return;ev.preventDefault();zoom(ev.deltaY>0?1.15:1/1.15);},{passive:false});
var touchStart=null;
function dist(touches){return Math.hypot(touches[0].clientX-touches[1].clientX,touches[0].clientY-touches[1].clientY);}
on(canvas,'touchstart',function(ev){if(ev.touches.length===2){pinch={distance:dist(ev.touches),window:windowS};touchStart=null;}else if(ev.touches.length===1){touchStart={x:ev.touches[0].clientX,y:ev.touches[0].clientY};}},{passive:true});
on(canvas,'touchmove',function(ev){if(ev.touches.length===2&&pinch){ev.preventDefault();setWindow(pinch.window*pinch.distance/Math.max(1,dist(ev.touches)));}else if(touchStart&&ev.touches.length){if(Math.hypot(ev.touches[0].clientX-touchStart.x,ev.touches[0].clientY-touchStart.y)>8)touchStart=null;}},{passive:false});
on(canvas,'touchend',function(ev){if(pinch){if(ev.touches.length<2)pinch=null;touchStart=null;return;}if(touchStart&&ev.changedTouches.length){var b=hit(ev.changedTouches[0]);select(b&&selected!==b.hash?b.hash:null,'touch');touchStart=null;}},{passive:true});
on(canvas,'keydown',function(ev){
var key=ev.key;if(['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','Home','End'].includes(key)){
ev.preventDefault();var list=blocks.filter(function(b){return b.on;}),i=list.findIndex(function(b){return b.hash===selected;}),next;
if(!list.length)return;if(key==='Home')next=0;else if(key==='End')next=list.length-1;else next=i<0?list.length-1:clamp(i+(['ArrowRight','ArrowDown'].includes(key)?1:-1),0,list.length-1);select(list[next].hash,'keyboard');
}else if(key===' '){ev.preventDefault();setPaused(!paused);}else if(key==='+'||key==='='){ev.preventDefault();zoom(1/1.2);}else if(key==='-'){ev.preventDefault();zoom(1.2);}else if(key.toLowerCase()==='f'){ev.preventDefault();follow();}
});
on(doc,'keydown',function(ev){if(ev.key==='Escape'&&(engaged||doc.activeElement===canvas)){engage(false);select(null,'escape');}});
on(doc,'pointerdown',function(ev){if(engaged&&ev.target!==canvas&&!(chip&&chip.contains(ev.target)))engage(false);});
if(opts.pauseButton){opts.pauseButton.setAttribute('aria-pressed','false');on(opts.pauseButton,'click',function(){setPaused(!paused);});}
on(root,'resize',size);
if('ResizeObserver'in root){ro=new ResizeObserver(size);ro.observe(canvas);}
if('IntersectionObserver'in root){io=new IntersectionObserver(function(es){visible=es[0].isIntersecting;if(visible)redraw();else stopFrame();},{threshold:0});io.observe(canvas);}
if('MutationObserver'in root){mo=new MutationObserver(theme);mo.observe(doc.documentElement,{attributes:true,attributeFilter:['data-theme','class','style']});}
if(mq&&mq.addEventListener)on(mq,'change',function(e){reduced=e.matches;reducedRight=Date.now()-lag;stopFrame();redraw();});
if(mediaTheme&&mediaTheme.addEventListener)on(mediaTheme,'change',theme);
on(doc,'visibilitychange',function(){if(doc.hidden){stopFrame();clearTimeout(pollTimer);if(controller)controller.abort();}else{poll();redraw();}});
healthTimer=setInterval(function(){if(destroyed||doc.hidden||!lastGood)return;if(state==='live'&&Date.now()-lastGood>(finite(opts.staleAfterMs)?opts.staleAfterMs:15000))setState('stale','No observer update for '+Math.round((Date.now()-lastGood)/1000)+' s');},1000);
function destroy(){if(destroyed)return;destroyed=true;stopFrame();clearTimeout(pollTimer);clearTimeout(timeout);clearInterval(healthTimer);if(controller)controller.abort();remove.forEach(function(fn){fn();});if(ro)ro.disconnect();if(io)io.disconnect();if(mo)mo.disconnect();if(tip)tip.hidden=true;canvas.classList.remove('engaged');
[['tabindex',initialAttributes.tabindex],['role',initialAttributes.role],['aria-label',initialAttributes.label]].forEach(function(pair){if(pair[1]===null)canvas.removeAttribute(pair[0]);else canvas.setAttribute(pair[0],pair[1]);});canvas.style.touchAction=initialAttributes.touch;canvas.style.cursor=initialAttributes.cursor;instances.delete(canvas);model.clear();view.clear();blocks=[];
}
var api={push:push,setPaused:setPaused,zoom:zoom,engage:engage,getWindow:function(){return windowS;},getState:function(){return {state:state,reason:reason};},redraw:redraw,words:words,
destroy:destroy,follow:follow,setWindow:setWindow,setFilter:setFilter,setMotion:setMotion,select:select,getStats:stats,
getViewRange:function(){return {start:rightTime()-windowS*1000,end:rightTime(),filter:filter};},getBlocks:function(){return blocks.map(cloneBlock);},getCheckpoints:function(){return cps.map(function(c){return Object.assign({},c);});},
getSnapshot:function(){return {ok:true,state:{stale:state!=='live'},blocks:blocks.map(cloneBlock),finality:{checkpoints:cps.map(function(c){return Object.assign({},c);})}};}};
instances.set(canvas,api);if(chip)chip.hidden=true;theme();size();emit('onState',state,reason);if(opts.poll!==false)poll();redraw();return api;
}
root.IgneumDag={mount:mount,version:'2.0.0'};
})(window);