Merge remote-tracking branch 'origin/master' into ui-overlap
# Conflicts: # tools/ci/pre-push.sh
BIN
docs/plans/site-ui-5-shots/404__1440-dark.png
Normal file
|
After Width: | Height: | Size: 224 KiB |
BIN
docs/plans/site-ui-5-shots/404__1440-light.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
docs/plans/site-ui-5-shots/404__390-dark.png
Normal file
|
After Width: | Height: | Size: 238 KiB |
BIN
docs/plans/site-ui-5-shots/404__390-light.png
Normal file
|
After Width: | Height: | Size: 240 KiB |
BIN
docs/plans/site-ui-5-shots/app__1440-dark.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
docs/plans/site-ui-5-shots/app__1440-light.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
docs/plans/site-ui-5-shots/app__390-dark.png
Normal file
|
After Width: | Height: | Size: 586 KiB |
BIN
docs/plans/site-ui-5-shots/app__390-light.png
Normal file
|
After Width: | Height: | Size: 597 KiB |
BIN
docs/plans/site-ui-5-shots/bench__1440-dark.png
Normal file
|
After Width: | Height: | Size: 478 KiB |
BIN
docs/plans/site-ui-5-shots/bench__1440-light.png
Normal file
|
After Width: | Height: | Size: 478 KiB |
BIN
docs/plans/site-ui-5-shots/bench__390-dark.png
Normal file
|
After Width: | Height: | Size: 367 KiB |
BIN
docs/plans/site-ui-5-shots/bench__390-light.png
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
docs/plans/site-ui-5-shots/claims__1440-dark.png
Normal file
|
After Width: | Height: | Size: 549 KiB |
BIN
docs/plans/site-ui-5-shots/claims__1440-light.png
Normal file
|
After Width: | Height: | Size: 548 KiB |
BIN
docs/plans/site-ui-5-shots/claims__390-dark.png
Normal file
|
After Width: | Height: | Size: 549 KiB |
BIN
docs/plans/site-ui-5-shots/claims__390-light.png
Normal file
|
After Width: | Height: | Size: 547 KiB |
BIN
docs/plans/site-ui-5-shots/dev-fee__1440-dark.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
docs/plans/site-ui-5-shots/dev-fee__1440-light.png
Normal file
|
After Width: | Height: | Size: 332 KiB |
BIN
docs/plans/site-ui-5-shots/dev-fee__390-dark.png
Normal file
|
After Width: | Height: | Size: 277 KiB |
BIN
docs/plans/site-ui-5-shots/dev-fee__390-light.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
docs/plans/site-ui-5-shots/download__1440-dark.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
BIN
docs/plans/site-ui-5-shots/download__1440-light.png
Normal file
|
After Width: | Height: | Size: 605 KiB |
BIN
docs/plans/site-ui-5-shots/download__390-dark.png
Normal file
|
After Width: | Height: | Size: 518 KiB |
BIN
docs/plans/site-ui-5-shots/download__390-light.png
Normal file
|
After Width: | Height: | Size: 528 KiB |
BIN
docs/plans/site-ui-5-shots/evidence__1440-dark.png
Normal file
|
After Width: | Height: | Size: 517 KiB |
BIN
docs/plans/site-ui-5-shots/evidence__1440-light.png
Normal file
|
After Width: | Height: | Size: 516 KiB |
BIN
docs/plans/site-ui-5-shots/evidence__390-dark.png
Normal file
|
After Width: | Height: | Size: 309 KiB |
BIN
docs/plans/site-ui-5-shots/evidence__390-light.png
Normal file
|
After Width: | Height: | Size: 311 KiB |
BIN
docs/plans/site-ui-5-shots/explorer__1440-dark.png
Normal file
|
After Width: | Height: | Size: 381 KiB |
BIN
docs/plans/site-ui-5-shots/explorer__1440-light.png
Normal file
|
After Width: | Height: | Size: 385 KiB |
BIN
docs/plans/site-ui-5-shots/explorer__390-dark.png
Normal file
|
After Width: | Height: | Size: 263 KiB |
BIN
docs/plans/site-ui-5-shots/explorer__390-light.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
docs/plans/site-ui-5-shots/faucet__1440-dark.png
Normal file
|
After Width: | Height: | Size: 335 KiB |
BIN
docs/plans/site-ui-5-shots/faucet__1440-light.png
Normal file
|
After Width: | Height: | Size: 335 KiB |
BIN
docs/plans/site-ui-5-shots/faucet__390-dark.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
BIN
docs/plans/site-ui-5-shots/faucet__390-light.png
Normal file
|
After Width: | Height: | Size: 276 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.6 MiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 565 KiB After Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 600 KiB After Width: | Height: | Size: 586 KiB |
BIN
docs/plans/site-ui-5-shots/journey__1440-dark.png
Normal file
|
After Width: | Height: | Size: 328 KiB |
BIN
docs/plans/site-ui-5-shots/journey__1440-light.png
Normal file
|
After Width: | Height: | Size: 336 KiB |
BIN
docs/plans/site-ui-5-shots/journey__390-dark.png
Normal file
|
After Width: | Height: | Size: 327 KiB |
BIN
docs/plans/site-ui-5-shots/journey__390-light.png
Normal file
|
After Width: | Height: | Size: 337 KiB |
BIN
docs/plans/site-ui-5-shots/ledger__1440-dark.png
Normal file
|
After Width: | Height: | Size: 345 KiB |
BIN
docs/plans/site-ui-5-shots/ledger__1440-light.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
docs/plans/site-ui-5-shots/ledger__390-dark.png
Normal file
|
After Width: | Height: | Size: 374 KiB |
BIN
docs/plans/site-ui-5-shots/ledger__390-light.png
Normal file
|
After Width: | Height: | Size: 372 KiB |
BIN
docs/plans/site-ui-5-shots/litepaper__1440-dark.png
Normal file
|
After Width: | Height: | Size: 488 KiB |
BIN
docs/plans/site-ui-5-shots/litepaper__1440-light.png
Normal file
|
After Width: | Height: | Size: 489 KiB |
BIN
docs/plans/site-ui-5-shots/litepaper__390-dark.png
Normal file
|
After Width: | Height: | Size: 396 KiB |
BIN
docs/plans/site-ui-5-shots/litepaper__390-light.png
Normal file
|
After Width: | Height: | Size: 398 KiB |
BIN
docs/plans/site-ui-5-shots/live__1440-dark.png
Normal file
|
After Width: | Height: | Size: 613 KiB |
BIN
docs/plans/site-ui-5-shots/live__1440-light.png
Normal file
|
After Width: | Height: | Size: 634 KiB |
BIN
docs/plans/site-ui-5-shots/live__390-dark.png
Normal file
|
After Width: | Height: | Size: 393 KiB |
BIN
docs/plans/site-ui-5-shots/live__390-light.png
Normal file
|
After Width: | Height: | Size: 416 KiB |
BIN
docs/plans/site-ui-5-shots/metamask__1440-dark.png
Normal file
|
After Width: | Height: | Size: 407 KiB |
BIN
docs/plans/site-ui-5-shots/metamask__1440-light.png
Normal file
|
After Width: | Height: | Size: 407 KiB |
BIN
docs/plans/site-ui-5-shots/metamask__390-dark.png
Normal file
|
After Width: | Height: | Size: 350 KiB |
BIN
docs/plans/site-ui-5-shots/metamask__390-light.png
Normal file
|
After Width: | Height: | Size: 351 KiB |
BIN
docs/plans/site-ui-5-shots/miner__1440-dark.png
Normal file
|
After Width: | Height: | Size: 677 KiB |
BIN
docs/plans/site-ui-5-shots/miner__1440-light.png
Normal file
|
After Width: | Height: | Size: 682 KiB |
BIN
docs/plans/site-ui-5-shots/miner__390-dark.png
Normal file
|
After Width: | Height: | Size: 477 KiB |
BIN
docs/plans/site-ui-5-shots/miner__390-light.png
Normal file
|
After Width: | Height: | Size: 285 KiB |
BIN
docs/plans/site-ui-5-shots/miners__1440-dark.png
Normal file
|
After Width: | Height: | Size: 225 KiB |
BIN
docs/plans/site-ui-5-shots/miners__1440-light.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
BIN
docs/plans/site-ui-5-shots/miners__390-dark.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
docs/plans/site-ui-5-shots/miners__390-light.png
Normal file
|
After Width: | Height: | Size: 242 KiB |
BIN
docs/plans/site-ui-5-shots/provenance__1440-dark.png
Normal file
|
After Width: | Height: | Size: 426 KiB |
BIN
docs/plans/site-ui-5-shots/provenance__1440-light.png
Normal file
|
After Width: | Height: | Size: 425 KiB |
BIN
docs/plans/site-ui-5-shots/provenance__390-dark.png
Normal file
|
After Width: | Height: | Size: 333 KiB |
BIN
docs/plans/site-ui-5-shots/provenance__390-light.png
Normal file
|
After Width: | Height: | Size: 331 KiB |
BIN
docs/plans/site-ui-5-shots/randomx__1440-dark.png
Normal file
|
After Width: | Height: | Size: 396 KiB |
BIN
docs/plans/site-ui-5-shots/randomx__1440-light.png
Normal file
|
After Width: | Height: | Size: 393 KiB |
BIN
docs/plans/site-ui-5-shots/randomx__390-dark.png
Normal file
|
After Width: | Height: | Size: 396 KiB |
BIN
docs/plans/site-ui-5-shots/randomx__390-light.png
Normal file
|
After Width: | Height: | Size: 392 KiB |
BIN
docs/plans/site-ui-5-shots/wallet__1440-dark.png
Normal file
|
After Width: | Height: | Size: 571 KiB |
BIN
docs/plans/site-ui-5-shots/wallet__1440-light.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
docs/plans/site-ui-5-shots/wallet__390-dark.png
Normal file
|
After Width: | Height: | Size: 427 KiB |
BIN
docs/plans/site-ui-5-shots/wallet__390-light.png
Normal file
|
After Width: | Height: | Size: 451 KiB |
|
|
@ -5,7 +5,7 @@ are ONE renderer fed ONE JSON shape. This folder is the source; everything else
|
|||
|
||||
| File | What it is | Copies |
|
||||
|---|---|---|
|
||||
| `live-dag.js` | EMBER 02 `IgneumDag` 2.0.3: lanes per vote key, parent curves, the selected chain, checkpoint bands, proof glow | `site/live-dag.js`, `app/igneum-app/ui/live-dag.js` |
|
||||
| `live-dag.js` | EMBER 02 `IgneumDag` 2.0.4: lanes per vote key, parent curves, the selected chain, checkpoint bands, proof glow; the box height follows the lanes (`onSize`, `autoHeight`) | `site/live-dag.js`, `app/igneum-app/ui/live-dag.js` |
|
||||
| `proof-core.js` | `IgneumProof` 2.0.0: the selected block's shards | `site/proof-core.js`, `app/igneum-app/ui/proof-core.js` |
|
||||
| `tokens.css` | The fourteen palette tokens the two scripts read from `:root`, dark and light, the brand package's values | the `scene-tokens` block in `site/site.css` and `app/igneum-app/ui/app.css` |
|
||||
| `feed-contract.md`, `feed-contract.json` | The feed shape, in words and as key lists | read by `tools/scene/feed-contract.mjs` and the app's `src/live.rs` test |
|
||||
|
|
@ -25,6 +25,14 @@ option names this machine's vote keys, so its own blocks glow and its lane reads
|
|||
picture, never a second renderer, never a rewritten feed. The phone rule (30 s window, four lanes, bigger nodes) keys on the
|
||||
viewport width under 720 px, not on the canvas width: a 640 px hero on a laptop is not a phone.
|
||||
|
||||
The box's height follows the lanes (2.0.4): wanted height = top padding + axis padding + lanes shown times `laneHeight`
|
||||
(46 px, 40 on a phone, 26 compact; at most `maxLanes` 7, `narrowLanes` 4 on a phone, never under 2). The renderer calls
|
||||
`onSize(heightPx, {lanes, laneHeight, narrow, compact})` whenever that changes and exposes `getWantedHeight()`; a page that
|
||||
lays out its own box sets its height from the callback. With `autoHeight` (on by default when the host set no CSS height on
|
||||
the canvas, forced either way by the option, never in compact mode) the renderer sets `canvas.style.height` itself and shows
|
||||
every lane up to the cap. Every current host sets a height (/live 420 px, the hero 500 px, the app's card 220 px and its
|
||||
Inspect view 420 px), so until a page opts in the frames are unchanged.
|
||||
|
||||
## The tests (all in `tools/ci/pre-push.sh`)
|
||||
|
||||
| Line | What it proves |
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
/* Igneum DAG / Ember 2.0 (the EMBER 02 renderer pack, 7 Oct 2026). THE SHARED SOURCE: scene/live-dag.js; site/live-dag.js and
|
||||
* app/igneum-app/ui/live-dag.js are byte-equal copies written by tools/scene/sync.mjs (the gate refuses a drifted copy). Edit here.
|
||||
* 2.0.4 (7 Oct 2026): the box's height follows the lanes. laneHeight (default 46 px, 40 on a phone, 26 compact) times the lanes
|
||||
* shown (at most maxLanes, narrowLanes on a phone, never under 2) plus the axis and padding is the scene's wanted height;
|
||||
* onSize(heightPx, {lanes, laneHeight, narrow, compact}) reports it whenever it changes, so a page can size the box, and with
|
||||
* autoHeight (true by default when the host set no CSS height on the canvas, forced either way by the option; never in
|
||||
* compact mode, the app's card keeps its own height) the renderer sets canvas.style.height itself and lets every lane through.
|
||||
* 2.0.3 (scene parity, 7 Oct 2026): (1) every push, size and theme change paints the current picture at once; only the motion
|
||||
* loop waits for a visible document and an intersecting canvas (a page that loads with document.hidden true, or whose embedder
|
||||
* never fires visibilitychange, showed a blank canvas while reporting live). (2) The narrow (phone) rule keys on the viewport
|
||||
|
|
@ -61,6 +66,10 @@
|
|||
// narrowLanes lanes, nodes no smaller than narrowMinNode, hairlines off except the selected chain and the selected block,
|
||||
// checkpoint labels as the percent alone. The owner's call of 7 Oct 2026: faster drift, more gap, on a phone.
|
||||
var narrow=false,narrowBreak=finite(opts.narrowBreak)?opts.narrowBreak:720,narrowWindow=clamp(finite(opts.narrowWindow)?opts.narrowWindow:30,30,300),narrowLanes=clamp(finite(opts.narrowLanes)?opts.narrowLanes:4,2,12),narrowMinNode=finite(opts.narrowMinNode)?opts.narrowMinNode:11,baseWindow=0,windowTouched=false;
|
||||
// autoHeight: the host set no height on the canvas (its computed height is still the attribute's, 150 by default) and this
|
||||
// is not the compact card; read before size() touches the attribute
|
||||
var hostHeightSet=(function(){try{var cs=getComputedStyle(canvas),attr=canvas.getAttribute('height'),h=parseFloat(cs.height);return canvas.style.height!==''||!(finite(h)&&Math.round(h)===(attr===null?150:Number(attr)));}catch(e){return true;}})();
|
||||
var autoHeight=opts.autoHeight===true||(opts.autoHeight!==false&&!compact&&!hostHeightSet),laneHeightOpt=finite(opts.laneHeight)?opts.laneHeight:0,wantH=0,laneHNow=46;
|
||||
var maxLanes=clamp(finite(opts.maxLanes)?opts.maxLanes:7,2,12), minNode=finite(opts.minNode)?opts.minNode:9.5, hairAlpha=finite(opts.hairlineAlpha)?opts.hairlineAlpha:.12, laneReach=finite(opts.laneReach)?opts.laneReach:2, othersScale=finite(opts.othersScale)?opts.othersScale:.75, provenGlow=opts.provenGlow!==false;
|
||||
var mq=root.matchMedia?root.matchMedia('(prefers-reduced-motion: reduce)'):null;
|
||||
var reduced=mq?mq.matches:false, manualMotion=true;
|
||||
|
|
@ -112,7 +121,8 @@
|
|||
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.min(narrow?narrowLanes:maxLanes,Math.max(2,Math.floor((H-padT-padB)/(compact?26:narrow?40:46)))),ids=Array.from(counts.keys());
|
||||
var laneH=laneHeightOpt||(compact?26:narrow?40:46),cap=narrow?narrowLanes:maxLanes;laneHNow=laneH;
|
||||
var capacity=autoHeight?cap:Math.min(cap,Math.max(2,Math.floor((H-padT-padB)/laneH))),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.
|
||||
|
|
@ -121,6 +131,9 @@
|
|||
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);});
|
||||
// the wanted box height for these lanes (2.0.4): reported on change; applied here only under autoHeight
|
||||
var nextH=padT+padB+Math.max(2,lanes.length)*laneH;
|
||||
if(nextH!==wantH){wantH=nextH;emit('onSize',wantH,{lanes:lanes.length,laneHeight:laneH,narrow:narrow,compact:compact});if(autoHeight&&canvas.style.height!==wantH+'px')canvas.style.height=wantH+'px';}
|
||||
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);
|
||||
|
|
@ -179,7 +192,9 @@
|
|||
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,narrow:narrow,lanes:lanes.length,reducedMotion:reduced||!manualMotion,state:state};
|
||||
invalidRecords:invalid,omittedRecords:omitted,rendered:blocks.length,frames:frameCount,window:windowS,narrow:narrow,lanes:lanes.length,reducedMotion:reduced||!manualMotion,state:state,
|
||||
// the lane geometry (2.0.4), so a page that sizes its own box reads no constants
|
||||
laneHeight:laneHNow,padT:padT,padB:padB,capacity:narrow?narrowLanes:maxLanes,wantedHeight:wantH,autoHeight:autoHeight};
|
||||
}
|
||||
function poll(){
|
||||
if(destroyed||opts.poll===false||doc.hidden)return;
|
||||
|
|
@ -356,11 +371,11 @@
|
|||
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,
|
||||
var api={push:push,setPaused:setPaused,zoom:zoom,engage:engage,getWindow:function(){return windowS;},getWantedHeight:function(){return wantH;},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.3'};
|
||||
root.IgneumDag={mount:mount,version:'2.0.4'};
|
||||
})(window);
|
||||
|
|
|
|||
|
|
@ -210,7 +210,7 @@
|
|||
<h1>The app you <span class="accent">mine with.</span></h1>
|
||||
<p class="lead">Your rate, what it costs a day, and the chain drawn live with your own blocks ringed. One window, four pages, every card one row.</p>
|
||||
<div class="cta"><a href="/miner#get" class="btn primary">Download the miner</a><a href="#chain" class="btn">The chain on your screen</a></div>
|
||||
<div class="os"><span>Windows</span><span>macOS</span><span>Linux · HiveOS</span><span>light and dark</span></div>
|
||||
<div class="os"><span><span class="osmark mini" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span>Windows</span><span><span class="osmark mini" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span>macOS</span><span><span class="osmark mini" data-os="linux" title="Linux"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg></span>Linux</span><span><span class="osmark mini" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span>HiveOS</span><span>light and dark</span></div>
|
||||
</div>
|
||||
<figure class="frame glow">
|
||||
<div class="bar"><i></i><i></i><i></i><b>Overview</b></div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
/* Igneum DAG / Ember 2.0 (the EMBER 02 renderer pack, 7 Oct 2026). THE SHARED SOURCE: scene/live-dag.js; site/live-dag.js and
|
||||
* app/igneum-app/ui/live-dag.js are byte-equal copies written by tools/scene/sync.mjs (the gate refuses a drifted copy). Edit here.
|
||||
* 2.0.4 (7 Oct 2026): the box's height follows the lanes. laneHeight (default 46 px, 40 on a phone, 26 compact) times the lanes
|
||||
* shown (at most maxLanes, narrowLanes on a phone, never under 2) plus the axis and padding is the scene's wanted height;
|
||||
* onSize(heightPx, {lanes, laneHeight, narrow, compact}) reports it whenever it changes, so a page can size the box, and with
|
||||
* autoHeight (true by default when the host set no CSS height on the canvas, forced either way by the option; never in
|
||||
* compact mode, the app's card keeps its own height) the renderer sets canvas.style.height itself and lets every lane through.
|
||||
* 2.0.3 (scene parity, 7 Oct 2026): (1) every push, size and theme change paints the current picture at once; only the motion
|
||||
* loop waits for a visible document and an intersecting canvas (a page that loads with document.hidden true, or whose embedder
|
||||
* never fires visibilitychange, showed a blank canvas while reporting live). (2) The narrow (phone) rule keys on the viewport
|
||||
|
|
@ -61,6 +66,10 @@
|
|||
// narrowLanes lanes, nodes no smaller than narrowMinNode, hairlines off except the selected chain and the selected block,
|
||||
// checkpoint labels as the percent alone. The owner's call of 7 Oct 2026: faster drift, more gap, on a phone.
|
||||
var narrow=false,narrowBreak=finite(opts.narrowBreak)?opts.narrowBreak:720,narrowWindow=clamp(finite(opts.narrowWindow)?opts.narrowWindow:30,30,300),narrowLanes=clamp(finite(opts.narrowLanes)?opts.narrowLanes:4,2,12),narrowMinNode=finite(opts.narrowMinNode)?opts.narrowMinNode:11,baseWindow=0,windowTouched=false;
|
||||
// autoHeight: the host set no height on the canvas (its computed height is still the attribute's, 150 by default) and this
|
||||
// is not the compact card; read before size() touches the attribute
|
||||
var hostHeightSet=(function(){try{var cs=getComputedStyle(canvas),attr=canvas.getAttribute('height'),h=parseFloat(cs.height);return canvas.style.height!==''||!(finite(h)&&Math.round(h)===(attr===null?150:Number(attr)));}catch(e){return true;}})();
|
||||
var autoHeight=opts.autoHeight===true||(opts.autoHeight!==false&&!compact&&!hostHeightSet),laneHeightOpt=finite(opts.laneHeight)?opts.laneHeight:0,wantH=0,laneHNow=46;
|
||||
var maxLanes=clamp(finite(opts.maxLanes)?opts.maxLanes:7,2,12), minNode=finite(opts.minNode)?opts.minNode:9.5, hairAlpha=finite(opts.hairlineAlpha)?opts.hairlineAlpha:.12, laneReach=finite(opts.laneReach)?opts.laneReach:2, othersScale=finite(opts.othersScale)?opts.othersScale:.75, provenGlow=opts.provenGlow!==false;
|
||||
var mq=root.matchMedia?root.matchMedia('(prefers-reduced-motion: reduce)'):null;
|
||||
var reduced=mq?mq.matches:false, manualMotion=true;
|
||||
|
|
@ -112,7 +121,8 @@
|
|||
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.min(narrow?narrowLanes:maxLanes,Math.max(2,Math.floor((H-padT-padB)/(compact?26:narrow?40:46)))),ids=Array.from(counts.keys());
|
||||
var laneH=laneHeightOpt||(compact?26:narrow?40:46),cap=narrow?narrowLanes:maxLanes;laneHNow=laneH;
|
||||
var capacity=autoHeight?cap:Math.min(cap,Math.max(2,Math.floor((H-padT-padB)/laneH))),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.
|
||||
|
|
@ -121,6 +131,9 @@
|
|||
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);});
|
||||
// the wanted box height for these lanes (2.0.4): reported on change; applied here only under autoHeight
|
||||
var nextH=padT+padB+Math.max(2,lanes.length)*laneH;
|
||||
if(nextH!==wantH){wantH=nextH;emit('onSize',wantH,{lanes:lanes.length,laneHeight:laneH,narrow:narrow,compact:compact});if(autoHeight&&canvas.style.height!==wantH+'px')canvas.style.height=wantH+'px';}
|
||||
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);
|
||||
|
|
@ -179,7 +192,9 @@
|
|||
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,narrow:narrow,lanes:lanes.length,reducedMotion:reduced||!manualMotion,state:state};
|
||||
invalidRecords:invalid,omittedRecords:omitted,rendered:blocks.length,frames:frameCount,window:windowS,narrow:narrow,lanes:lanes.length,reducedMotion:reduced||!manualMotion,state:state,
|
||||
// the lane geometry (2.0.4), so a page that sizes its own box reads no constants
|
||||
laneHeight:laneHNow,padT:padT,padB:padB,capacity:narrow?narrowLanes:maxLanes,wantedHeight:wantH,autoHeight:autoHeight};
|
||||
}
|
||||
function poll(){
|
||||
if(destroyed||opts.poll===false||doc.hidden)return;
|
||||
|
|
@ -356,11 +371,11 @@
|
|||
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,
|
||||
var api={push:push,setPaused:setPaused,zoom:zoom,engage:engage,getWindow:function(){return windowS;},getWantedHeight:function(){return wantH;},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.3'};
|
||||
root.IgneumDag={mount:mount,version:'2.0.4'};
|
||||
})(window);
|
||||
|
|
|
|||
|
|
@ -86,13 +86,20 @@
|
|||
.tb[aria-pressed="true"]{color:var(--bone);border-color:var(--line-2);background:var(--graphite)}
|
||||
.tb.on{color:var(--ember-text);border-color:var(--ember-40)}
|
||||
.sep{width:1px;height:18px;background:var(--line)}
|
||||
.graph{display:grid;grid-template-columns:minmax(0,1fr) 295px}
|
||||
.graph{display:grid;grid-template-columns:minmax(0,1fr) 295px;align-items:start}
|
||||
@media (max-width:1000px){.graph{grid-template-columns:1fr}}
|
||||
.gcol{padding:0;min-width:0;border-right:0;position:relative}
|
||||
@media (max-width:1000px){.gcol{border-right:0;border-bottom:var(--hair)}}
|
||||
.gcol .state{position:absolute;left:25px;top:14px;z-index:3;font-size:10px}
|
||||
.scene{position:relative}
|
||||
#dag{display:block;width:100%;height:420px;background:var(--row);touch-action:pan-y}
|
||||
#dag{display:block;width:100%;height:418px;background:var(--row);touch-action:pan-y}
|
||||
.gtop{display:flex;flex-wrap:wrap;align-items:center;gap:8px 14px;padding:10px 25px;border-bottom:var(--hair);font:10px/1.7 var(--mono);color:var(--ash)}
|
||||
.gtop .hint{white-space:nowrap}
|
||||
.gtop .lg{display:flex;flex-wrap:wrap;gap:6px 10px;flex:1 1 auto;justify-content:center}
|
||||
.gtop .lg span{display:inline-flex;align-items:center;gap:6px;color:var(--ink-2);font-family:var(--f-sans);font-size:11px;white-space:nowrap}
|
||||
.gtop .zoom{margin-left:4px}
|
||||
.gunder{padding:8px 25px 10px;border-top:var(--hair);font:10px/1.7 var(--mono);color:var(--ash)}
|
||||
@media (max-width:700px){.gtop{padding:10px 16px;gap:8px 12px}.gtop .lg{flex-basis:100%;justify-content:flex-start}.gtop .zoom{margin-left:auto}.gunder{padding:8px 16px 10px}}
|
||||
.tip{position:absolute;left:0;top:0;pointer-events:none;background:var(--graphite);border:1px solid var(--line-2);border-radius:6px;padding:10px 12px;font:400 12px/1.5 var(--f-mono);color:var(--ink-2);white-space:nowrap;z-index:5}
|
||||
.tip[hidden]{display:none}
|
||||
.gfoot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:12px;padding:17px 25px;border-top:var(--hair);font:10px/1.7 var(--mono);color:var(--ash)}
|
||||
|
|
@ -100,14 +107,14 @@
|
|||
.zoom button{background:transparent;border:0;color:var(--ink-2);font:400 13px/1 var(--f-mono);padding:8px 12px;cursor:pointer}
|
||||
.zoom button:hover{background:var(--hover)}
|
||||
.zoom span{font:400 11px/1 var(--f-mono);color:var(--ash);padding:0 6px;min-width:44px;text-align:center}
|
||||
.icol{padding:22px;min-width:0;font-size:12px;border-left:var(--hair)}
|
||||
.icol .ih{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:14px}
|
||||
.icol{padding:16px 18px;min-width:0;font-size:12px;border-left:var(--hair)}
|
||||
.icol .ih{display:flex;align-items:center;gap:8px;margin-bottom:10px}.icol .ih .chip{margin-left:auto}
|
||||
.icol .ih .eyebrow{margin:0}
|
||||
.icol .num{font:500 22px/1.2 var(--f-sans);color:var(--bone);margin:6px 0 4px;overflow-wrap:anywhere}
|
||||
.icol .hash{font:400 11px/1.5 var(--f-mono);color:var(--ash);overflow-wrap:anywhere;margin-bottom:14px}
|
||||
.insp{display:grid;grid-template-columns:auto minmax(0,1fr);gap:0 10px;margin:0;border-top:var(--hair);padding-top:8px}
|
||||
.insp dt{font:11px var(--mono);color:var(--ash);padding:10px 0;border-bottom:var(--hair)}
|
||||
.insp dd{margin:0;font:11px var(--mono);color:var(--bone);font-weight:500;text-align:right;padding:10px 0;border-bottom:var(--hair);overflow-wrap:anywhere;min-width:0}
|
||||
.insp dt{font:11px var(--mono);color:var(--ash);padding:7px 0;border-bottom:var(--hair)}
|
||||
.insp dd{margin:0;font:11px var(--mono);color:var(--bone);font-weight:500;text-align:right;padding:7px 0;border-bottom:var(--hair);overflow-wrap:anywhere;min-width:0}
|
||||
.insp dd.hot{color:var(--molten-text)}
|
||||
.icol .note{font-size:12px;margin-top:14px;padding-top:12px;border-top:var(--hair)}
|
||||
.insp-empty{color:var(--ash);font-size:13px;line-height:1.5;padding:6px 0 2px}
|
||||
|
|
@ -125,7 +132,7 @@
|
|||
.tab{background:transparent;border:0;color:var(--ash);font:400 12px/1.2 var(--f-sans);padding:6px 10px;border-radius:4px;cursor:pointer}
|
||||
.tab.on{background:var(--row);color:var(--bone)}
|
||||
.ititle{font:500 22px/1.2 var(--f-sans);color:var(--bone);margin:6px 0 4px;letter-spacing:-.01em}
|
||||
.proof-scene{height:200px;border:var(--hair);border-radius:6px;background:var(--row);margin:12px 0;overflow:hidden}
|
||||
.proof-scene{height:170px;border:var(--hair);border-radius:6px;background:var(--row);margin:12px 0;overflow:hidden}
|
||||
.proof-scene canvas{display:block;width:100%;height:100%}
|
||||
.proof-head{display:flex;justify-content:space-between;align-items:baseline;font:500 13px/1.3 var(--f-sans);color:var(--bone);margin-bottom:8px}
|
||||
.track{display:flex;gap:3px;height:3px;margin-bottom:10px}
|
||||
|
|
@ -383,15 +390,23 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
</div>
|
||||
<div class="graph">
|
||||
<div class="gcol">
|
||||
<div class="state" id="dag-state"><span class="dot off"></span>connecting to the observer</div>
|
||||
<div class="scene"><canvas id="dag" aria-label="The devnet's block graph, live: time-aligned miner lanes, every parent connection, the selected chain as one path, checkpoint bands, your blocks framed"></canvas><div class="tip" id="dag-tip" hidden></div><div class="chip-zoom" id="dag-chip" hidden>scroll to zoom · Esc to release</div></div>
|
||||
<div class="gfoot">
|
||||
<span>Click to inspect · Drag to explore</span>
|
||||
<div class="gtop">
|
||||
<span class="hint">Click to inspect · Drag to explore</span>
|
||||
<div class="lg" aria-label="Key">
|
||||
<span><i class="lgi" style="border:1.5px solid var(--included)"></i>Included</span>
|
||||
<span><i class="lgi" style="border:1.5px solid var(--excluded)"></i>Excluded</span>
|
||||
<span><i class="lgi" style="border:1.5px solid var(--ember);background:var(--ember-12)"></i>Selected chain</span>
|
||||
<span><i class="lgi" style="border:1.5px dashed var(--molten)"></i>Your block</span>
|
||||
<span><i class="lgi tick"></i>Proven</span>
|
||||
<span><i class="lgi" style="border:1.5px solid var(--ember);border-radius:50%"></i>Locked checkpoint</span>
|
||||
</div>
|
||||
<span class="zoom"><button type="button" id="z-out" aria-label="Wider window">−</button><span id="z-pct">60 s</span><button type="button" id="z-in" aria-label="Narrower window">+</button></span>
|
||||
</div>
|
||||
<div class="scene"><div class="state" id="dag-state"><span class="dot off"></span>connecting to the observer</div><canvas id="dag" aria-label="The devnet's block graph, live: time-aligned miner lanes, every parent connection, the selected chain as one path, checkpoint bands, your blocks framed"></canvas><div class="tip" id="dag-tip" hidden></div><div class="chip-zoom" id="dag-chip" hidden>scroll to zoom · Esc to release</div></div>
|
||||
<div class="gunder"><span id="blue-range">blue score pending</span></div>
|
||||
</div>
|
||||
<aside class="icol" id="inspector" aria-live="polite">
|
||||
<div class="ih"><div class="eyebrow">Block inspector</div><span class="chip" id="i-kind">select a block</span></div>
|
||||
<div class="ih"><div class="eyebrow">Block inspector</div><span class="chip" id="i-kind">select a block</span><button type="button" class="btn small" id="insp-close" hidden>Clear</button></div>
|
||||
<h3 class="ititle" id="i-title">Select a block</h3>
|
||||
<div class="hash" id="i-hash">Click the graph, or a row of the table below.</div>
|
||||
<div class="proof-scene"><canvas id="proof" aria-label="Select a block to see its shards"></canvas></div>
|
||||
|
|
@ -409,20 +424,8 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
<dt>Finality</dt><dd id="i-lock">not set</dd>
|
||||
</dl>
|
||||
<p class="note">Proof, chain selection and finality are separate states. None is inferred from a block’s age; a lock is drawn only when the observer reports one.</p>
|
||||
<p style="margin:10px 0 0"><button type="button" class="linkish" id="insp-close" hidden>Clear</button></p>
|
||||
</aside>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<div class="lg">
|
||||
<span><i class="lgi" style="border:1.5px solid var(--included)"></i>Included</span>
|
||||
<span><i class="lgi" style="border:1.5px solid var(--excluded)"></i>Excluded</span>
|
||||
<span><i class="lgi" style="border:1.5px solid var(--ember);background:var(--ember-12)"></i>Selected chain</span>
|
||||
<span><i class="lgi" style="border:1.5px dashed var(--molten)"></i>Your block</span>
|
||||
<span><i class="lgi tick"></i>Proven</span>
|
||||
<span><i class="lgi" style="border:1.5px solid var(--ember);border-radius:50%"></i>Locked checkpoint</span>
|
||||
</div>
|
||||
<span id="blue-range">blue score pending</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="motion">
|
||||
|
|
@ -623,6 +626,10 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
function uninspect(){sel=null;$('insp-close').hidden=true;$('i-kind').textContent='select a block';$('i-title').textContent='Select a block';$('i-hash').textContent='Click the graph, or a row of the table below.';$('i-shard-count').textContent='not set';$('i-track').innerHTML='';$('i-shards').innerHTML='<li class="muted">No block selected.</li>';['i-incl','i-miner','i-blue','i-daa','i-parents','i-time','i-cp','i-lock'].forEach(function(id){$(id).textContent='not set';});if(proof)proof.setBlock(null);}
|
||||
$('insp-close').addEventListener('click',function(){if(dag)dag.select(null,'clear');uninspect();});
|
||||
// the four stats above the graph, from the module's own window
|
||||
// the scene box fits its lanes: the renderer places lanes at 46 px (40 on phones) under a 58 px top band and over a 38 px axis
|
||||
// (scene/live-dag.js 2.0.3 layout()); one lane per key in view plus the others lane, capped at the renderer's 7 (4 on phones)
|
||||
var fitH=0;
|
||||
function fit(st){var c=$('dag');if(!c||!st)return;var cap=st.narrow?4:7,laneH=st.narrow?40:46,keys=Math.max(2,st.minerKeys||0),lanes=Math.min(cap,keys>cap?cap:keys+(keys>cap?1:0));var h=58+lanes*laneH+38;if(h!==fitH){fitH=h;c.style.height=h+'px';}}
|
||||
function miner(st){if(!dag)return;var cps=dag.getCheckpoints(),cp=cps.length?cps[cps.length-1]:null;
|
||||
$('m-blocks').textContent=fmtN(st.blocks);$('m-win').textContent='last '+st.window+' s';$('m-keys').textContent=st.minerKeys+' miner keys · not a card count';$('lane-caption').textContent=st.minerKeys+' keys';
|
||||
$('m-mine').textContent=mine?fmtN(st.own):'0';$('m-mine').classList.toggle('dim',!mine);$('m-key').textContent=mine||'not set';
|
||||
|
|
@ -635,7 +642,7 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
onState:function(st,reason){var el=$('dag-state');el.className='state '+(st==='live'?'live':st==='failed'?'bad':'');el.innerHTML='<span class="dot '+(st==='live'?'live':st==='failed'?'bad':'off')+'"></span>'+(st==='live'?'live devnet':esc(reason||st));if(proof)proof.setPaused(st!=='live'||dag.getStats().paused);},
|
||||
onPause:function(p){$('dag-follow').classList.toggle('on',!p);if(proof)proof.setPaused(p||dag.getState().state!=='live');},
|
||||
onFollow:function(f){$('dag-follow').classList.toggle('on',f);},
|
||||
onStats:miner,
|
||||
onStats:function(st){miner(st);fit(st);},
|
||||
onSelect:function(b,why){if(b){var full=lastD&&(lastD.blocks||[]).filter(function(x){return x.hash===b.hash;})[0];inspect(Object.assign({},b,full?{number:full.number}:{}));}else uninspect();}});
|
||||
if(window.IgneumProof&&!proof)proof=IgneumProof.mount($('proof'));
|
||||
if(lastD)feed(lastD);}
|
||||
|
|
|
|||
|
|
@ -251,19 +251,19 @@ pre b{color:var(--molten-text);font-weight:500}
|
|||
<button type="button" role="tab" data-platform="hive" id="tab-hive" aria-controls="panel-hive" aria-selected="false" tabindex="-1">Linux / HiveOS</button>
|
||||
</div>
|
||||
<div class="platform-body" id="panel-windows" role="tabpanel" aria-labelledby="tab-windows" data-platform-panel="windows">
|
||||
<div class="download-platform"><svg class="osmark" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M3 5.5l7.5-1v7H3zM11.5 4.3L21 3v8.5h-9.5zM3 12.5h7.5v7L3 18.5zM11.5 12.5H21V21l-9.5-1.3z"/></svg><div><h3>Ember for Windows</h3><p>the installer, from the signed manifest</p></div></div>
|
||||
<div class="download-platform"><span class="osmark" data-os="windows" title="Windows"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M3 5.6l7.3-1v7.1H3zM11.4 4.4L21 3v8.7h-9.6zM3 12.3h7.3v7.1L3 18.4zM11.4 12.3H21V21l-9.6-1.4z"/></svg></span><div><h3>Ember for Windows</h3><p>the installer, from the signed manifest</p></div></div>
|
||||
<p class="download-note">Run the installer, press Start. NVIDIA and AMD cards both mine. Setting an NVIDIA power cap asks for administrator rights once.</p>
|
||||
<a data-dl="miner-windows" href="https://dl.igneum.network/public/igneum-miner-windows.exe" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download for Windows <span data-dl-meta="miner-windows" style="font-weight:400;opacity:.85">v0.3.14 · 45.4 MB</span></a>
|
||||
<div class="download-detail"><span>from dl.igneum.network</span><span>sha256 <span data-dl-sha="miner-windows">08e75effc732f88bca11006b3b7cdf2bc533576f56819f79509fee47a8f5dd59</span></span></div>
|
||||
</div>
|
||||
<div class="platform-body" id="panel-mac" role="tabpanel" aria-labelledby="tab-mac" data-platform-panel="mac" hidden>
|
||||
<div class="download-platform"><svg class="osmark" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.3 1.9-3.4 2-3.5-1.1-1.6-2.8-1.8-3.4-1.8-1.4-.1-2.8.8-3.5.8-.7 0-1.8-.8-3-.8-1.5 0-3 .9-3.8 2.3-1.6 2.8-.4 7 1.2 9.3.8 1.1 1.7 2.4 2.9 2.3 1.2 0 1.6-.7 3-.7s1.8.7 3 .7c1.3 0 2-1.1 2.8-2.3.9-1.3 1.2-2.6 1.3-2.6-.1 0-2.5-.9-2.5-3.7zM14.1 5.8c.6-.8 1.1-1.9.9-3-.9 0-2 .6-2.7 1.4-.6.7-1.1 1.8-1 2.9 1.1.1 2.1-.5 2.8-1.3z"/></svg><div><h3>Ember for macOS</h3><p>a disk image, Apple silicon, Metal</p></div></div>
|
||||
<div class="download-platform"><span class="osmark" data-os="mac" title="macOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="currentColor"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg></span><div><h3>Ember for macOS</h3><p>a disk image, Apple silicon, Metal</p></div></div>
|
||||
<p class="download-note">Open the disk image, drag the app across, press Start. A Mac mines on its GPU at about a fifth of a flagship card and proves on its CPU, slowly.</p>
|
||||
<a data-dl="miner-mac" href="https://dl.igneum.network/public/igneum-miner-mac.dmg" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download for macOS <span data-dl-meta="miner-mac" style="font-weight:400;opacity:.85">v0.3.14 · 41.9 MB</span></a>
|
||||
<div class="download-detail"><span>from dl.igneum.network</span><span>sha256 <span data-dl-sha="miner-mac">e9e4c24db81a262a36b6bac11609ce6d24ed03220973da4142df935d41fc4e40</span></span></div>
|
||||
</div>
|
||||
<div class="platform-body" id="panel-hive" role="tabpanel" aria-labelledby="tab-hive" data-platform-panel="hive" hidden>
|
||||
<div class="download-platform"><svg class="osmark" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2c-2.4 0-4 1.9-4 4.6 0 1.2.2 2 0 2.8-.6 1.4-2.1 2.9-2.6 4.9-.3 1.1-.1 2 .3 2.6-.6.4-1.3 1-1.1 1.7.3 1 2.1 1.2 3.2 1.8.7.4 1.5.6 2.1.1.6.2 1.3.3 2.1.3s1.5-.1 2.1-.3c.6.5 1.4.3 2.1-.1 1.1-.6 2.9-.8 3.2-1.8.2-.7-.5-1.3-1.1-1.7.4-.6.6-1.5.3-2.6-.5-2-2-3.5-2.6-4.9-.2-.8 0-1.6 0-2.8C16 3.9 14.4 2 12 2zm-1.4 4.2c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zm2.8 0c.5 0 .8.5.8 1.2s-.3 1.2-.8 1.2-.8-.5-.8-1.2.3-1.2.8-1.2zM12 9.3c.9 0 1.9.5 1.9 1s-1 1.2-1.9 1.2-1.9-.7-1.9-1.2 1-1 1.9-1zm0 3.4c2.2 0 3.6 2.6 3.6 4.4 0 1.5-1.6 2.3-3.6 2.3s-3.6-.8-3.6-2.3c0-1.8 1.4-4.4 3.6-4.4z"/></svg><div><h3>Ember for Linux and HiveOS</h3><p>a tarball for rigs and Hive flight sheets</p></div></div>
|
||||
<div class="download-platform"><span class="osmark" data-os="hive" title="HiveOS"><svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round" stroke-linecap="round"><path d="M12 2.6 20.2 7.3v9.4L12 21.4 3.8 16.7V7.3z"/><path d="M12 7.4 16 9.7v4.6L12 16.6 8 14.3V9.7z"/><path d="M12 7.4V2.6M16 9.7l4.2-2.4M16 14.3l4.2 2.4M12 16.6v4.8M8 14.3l-4.2 2.4M8 9.7 3.8 7.3"/></svg></span><div><h3>Ember for Linux and HiveOS</h3><p>a tarball for rigs and Hive flight sheets</p></div></div>
|
||||
<p class="download-note">For the rig people. One tarball, the miner and the node inside, the same signed manifest as the desktop apps.</p>
|
||||
<a data-dl="miner-hive" href="https://dl.igneum.network/public/igneum-miner-hive.tar.gz" class="btn primary"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M5 16v4h14v-4"/></svg>Download the tarball <span data-dl-meta="miner-hive" style="font-weight:400;opacity:.85">v0.3.14 · 24.7 MB</span></a>
|
||||
<div class="hive-sheet" id="hive"><b>HiveOS Flight Sheet.</b> Miner: <b>Custom</b>. Installation URL: <code data-dl-url="miner-hive">https://dl.igneum.network/dl/public/igneum-hive-0.3.14.tar.gz</code>. Miner name <code>igneum</code>, wallet and worker <code>0x<your 40-hex payout address>.%WORKER_NAME%</code>. Hive itself is untested on our side: tell us what breaks.</div>
|
||||
|
|
|
|||
|
|
@ -403,7 +403,7 @@ html.nav-locked,html.nav-locked body{overflow:hidden}
|
|||
.badge[data-mark].apple{color:var(--mark-apple);background:var(--mark-apple-well);border-color:var(--mark-apple-ring)}
|
||||
.badge[data-mark].gpu{color:var(--mark-gpu);background:var(--mark-gpu-well);border-color:var(--mark-gpu-ring)}
|
||||
tr:hover .badge[data-mark],a:hover .badge[data-mark],.gpu-row:hover .badge[data-mark],.gpu-row:focus-within .badge[data-mark]{border-color:currentColor;box-shadow:0 0 0 3px var(--mark-gpu-well)}
|
||||
.gen{font-family:var(--mono);font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);line-height:1.3;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
/* the app's .gen series line is not drawn on the site: the build pages own .gen for their footnote */
|
||||
td .badge[data-mark].mini{margin-right:10px}
|
||||
.cardname{display:inline-flex;align-items:center;gap:0;white-space:nowrap}
|
||||
@media (prefers-reduced-motion:reduce){.badge[data-mark],.osmark{transition:none}}
|
||||
|
|
@ -489,7 +489,15 @@ pre,code{overflow-wrap:anywhere}
|
|||
pre{max-width:100%;overflow:auto}
|
||||
.tbl,.table-wrap{max-width:100%;overflow-x:auto}
|
||||
/* the explorer's block and address titles are hashes: they wrap, never clip (overlap sweep, 7 Oct 2026) */
|
||||
main .head h1,h1#title,.hash,.mono.long{overflow-wrap:anywhere;word-break:break-all}
|
||||
main .head h1,h1#title,.hash,.mono.long{overflow-wrap:anywhere}
|
||||
/* the ledger's status row wraps its long badges at phone width (overlap sweep, 7 Oct 2026) */
|
||||
article .status{white-space:normal;flex-wrap:wrap;overflow:visible}
|
||||
article .status .badge:not([data-mark]){white-space:normal;overflow-wrap:anywhere;line-height:1.5;max-width:100%}
|
||||
/* data tables size to their content and scroll inside their frame, never squeeze a column to a sliver (site-ui-5) */
|
||||
.tbl table,.table-wrap table{width:100%;min-width:720px;table-layout:auto}
|
||||
.tbl td,.table-wrap td{white-space:normal;overflow-wrap:anywhere;vertical-align:top;min-width:9ch}
|
||||
.tbl td .cardname,.table-wrap td .cardname{white-space:nowrap}
|
||||
.phero .os span{display:inline-flex;align-items:center}
|
||||
.download-platform{display:flex;align-items:center;gap:14px}
|
||||
/* the evidence table's status pills wrap inside their cell (overlap sweep, 7 Oct 2026) */
|
||||
table#claims .st{white-space:normal;display:inline-block;line-height:1.4;max-width:100%}
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ tree_checks() {
|
|||
run "chain scene: the site's and the app's copies are scene/ byte for byte, the palette tokens live once (self-test, then the tree)" bash -c 'node tools/scene/sync.mjs --self-test && node tools/scene/sync.mjs --check'
|
||||
run "chain scene: a push paints with the document hidden and no animation frame (the blank /live of 7 Oct 2026; known-failed first)" node tools/scene/paint-test.cjs
|
||||
run "chain scene: the live feed contract (the recorded reply validates; a rewritten miner, a float now, a stray key refused)" node --test tools/scene/feed-contract.test.mjs
|
||||
run "chain scene parity: one recorded feed through the home fold, /live and the app's Inspect view on build-2, three frames each pixel-equal apart from the app's own-key overlay (a changed token fails first; skipped with no box and no Playwright)" bash tools/scene/parity-remote.sh
|
||||
run "no text overlaps: every served page at 390 to 1600 px, light and dark, the hero at each step (self-test first; IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs)" overlap_sweep
|
||||
run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/api/public-stats.test.mjs
|
||||
run "ship tool self-test" node tools/ship-app.mjs --self-test
|
||||
|
|
|
|||
36
tools/scene/parity-remote.sh
Executable file
|
|
@ -0,0 +1,36 @@
|
|||
#!/usr/bin/env bash
|
||||
# The scene parity test on a build box (the Mac runs no headless browser: standing rule, 7 October 2026). Carries the files the
|
||||
# test needs to the box under this lane's prefix, runs tools/scene/parity.mjs there and prints its report; the box's own
|
||||
# Playwright build and Chromium (root installed the libraries on 7 October 2026, 14:5x UK) do the rendering.
|
||||
#
|
||||
# tools/scene/parity-remote.sh [--box N] [--out <dir>] default box 2; --out fetches the frames and the report back
|
||||
#
|
||||
# Where it runs: a box named by ~/.config/igneum/build-server[-N] (build@<ip>); on a machine with no box file but a local
|
||||
# Playwright (a CI runner that is itself a box) it runs parity.mjs in place; with neither it prints a skip line and exits 0,
|
||||
# so a plain GitHub runner stays green while every box-side and Mac-side gate renders for real.
|
||||
set -uo pipefail
|
||||
cd "$(git rev-parse --show-toplevel)" || exit 1
|
||||
BOX=2; OUT=""
|
||||
while [ $# -gt 0 ]; do case "$1" in --box) BOX="$2"; shift 2 ;; --out) OUT="$2"; shift 2 ;; *) echo "unknown argument $1" >&2; exit 2 ;; esac; done
|
||||
HOSTFILE="$HOME/.config/igneum/build-server"; [ "$BOX" = 1 ] || HOSTFILE="$HOSTFILE-$BOX"
|
||||
if [ ! -s "$HOSTFILE" ]; then
|
||||
if [ -n "${PLAYWRIGHT_CORE:-}" ] || [ -f /srv/builds/_bin/overlap/node_modules/playwright-core/package.json ]; then exec node tools/scene/parity.mjs ${OUT:+--out "$OUT"}; fi
|
||||
echo "scene-parity: skipped, no build box file at $HOSTFILE and no Playwright here"; exit 0
|
||||
fi
|
||||
HOST="$(cat "$HOSTFILE")"; KEY="$HOME/.ssh/igneum_ed25519"
|
||||
SSH=(ssh -i "$KEY" -o BatchMode=yes -o ConnectTimeout=10 "$HOST")
|
||||
WT="$(basename "$(pwd)")"; DEST="/srv/builds/scene-parity/$WT"
|
||||
"${SSH[@]}" "mkdir -p $DEST/site $DEST/tools/ui-mock $DEST/tools/scene $DEST/scene $DEST/app/igneum-app/ui" || { echo "scene-parity: box $HOST unreachable" >&2; exit 1; }
|
||||
RS=(rsync -a --delete -e "ssh -i $KEY -o BatchMode=yes")
|
||||
"${RS[@]}" --exclude img --exclude verify --exclude '*.png' --exclude '*.ico' site/ "$HOST:$DEST/site/"
|
||||
"${RS[@]}" scene/ "$HOST:$DEST/scene/"
|
||||
"${RS[@]}" tools/scene/ "$HOST:$DEST/tools/scene/"
|
||||
"${RS[@]}" tools/ui-mock/ "$HOST:$DEST/tools/ui-mock/"
|
||||
"${RS[@]}" tools/site-serve.mjs "$HOST:$DEST/tools/site-serve.mjs"
|
||||
if [ -f app/igneum-app/ui/live-dag.js ]; then "${RS[@]}" --exclude '*.woff2' app/igneum-app/ui/ "$HOST:$DEST/app/igneum-app/ui/"; else "${SSH[@]}" "rm -rf $DEST/app"; fi
|
||||
[ -f brand/igneum-coin-1024.png ] && "${SSH[@]}" "mkdir -p $DEST/brand" && "${RS[@]}" brand/igneum-coin-1024.png "$HOST:$DEST/brand/igneum-coin-1024.png"
|
||||
# the copied-sources rule: re-stamped on the far side before anything runs (touch), so no stale mtime decides anything there
|
||||
"${SSH[@]}" "cd $DEST && find . -type f -exec touch {} + && git init -q . 2>/dev/null; [ -f /srv/builds/_bin/overlap/env.sh ] && . /srv/builds/_bin/overlap/env.sh; cd $DEST && timeout 240 nice -n 10 node tools/scene/parity.mjs --out $DEST/_out"
|
||||
RC=$?
|
||||
if [ -n "$OUT" ]; then mkdir -p "$OUT"; rsync -a -e "ssh -i $KEY -o BatchMode=yes" "$HOST:$DEST/_out/" "$OUT/" 2>/dev/null; fi
|
||||
exit $RC
|
||||
184
tools/scene/parity.mjs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
#!/usr/bin/env node
|
||||
// Scene parity: the same recorded feed through the site's home fold, the site's /live and the miner app's Inspect view, three
|
||||
// frames each at the same three instants of a fake clock, compared pixel for pixel. Runs on a build box (tools/scene/parity-remote.sh
|
||||
// carries the tree there and runs this; never a headless browser on the Mac).
|
||||
//
|
||||
// node tools/scene/parity.mjs [--out <dir>] the report on stdout, one line per comparison; exit 1 on any difference
|
||||
//
|
||||
// What it does, in order (the known-failed case first, as the watcher-trust rule asks):
|
||||
// 1. serves site/ through tools/site-serve.mjs and app/igneum-app/ui through tools/ui-mock/server.mjs on private ports;
|
||||
// every page's /api/live is answered from scene/fixtures/live-2026-10-07.json by a Playwright route, the clock is frozen
|
||||
// at the fixture's `now` (Date, timers, requestAnimationFrame), the viewport is 1280 by 900 at DPR 1, dark;
|
||||
// 2. KNOWN-FAILED: the app page with one palette token changed (--included) must differ from /live at every instant;
|
||||
// 3. base: no own key anywhere; home fold, /live and the app's Inspect scene byte-equal at T, T+2 s, T+5 s;
|
||||
// 4. overlay: the same vote key named on every surface (localStorage on the site, the mock's card ids on the app); the
|
||||
// three surfaces still byte-equal, and the app's overlay frame differs from its base frame (the overlay is drawn);
|
||||
// 5. the app's compact card (its own mode, no site counterpart) is rendered once and reported, not compared.
|
||||
// Every scene canvas is forced to 900 by 420 CSS px so the comparison is of the renderer, the tokens, the options and the
|
||||
// feed, not of the box the page gives the scene. A surface whose files are absent (master carries no app UI scene) is
|
||||
// reported as skipped and the remaining surfaces are compared.
|
||||
import { createRequire } from 'node:module';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { readFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
||||
import { join, dirname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { homedir } from 'node:os';
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url)), repo = join(here, '..', '..');
|
||||
const out = process.argv.includes('--out') ? process.argv[process.argv.indexOf('--out') + 1] : '';
|
||||
if (out) mkdirSync(out, { recursive: true });
|
||||
const PW = [process.env.PLAYWRIGHT_CORE, '/srv/builds/_bin/overlap/node_modules/playwright-core/package.json', '/srv/builds/scene-parity/pw/node_modules/playwright-core/package.json', join(homedir(), '.cache/igneum-pw/node_modules/playwright-core/package.json')].filter(Boolean).find(p => existsSync(p));
|
||||
const CHROME = [process.env.PLAYWRIGHT_CHROMIUM, join(homedir(), '.cache/ms-playwright/chromium_headless_shell-1194/chrome-linux/headless_shell'), join(homedir(), '.cache/ms-playwright/chromium-1194/chrome-linux/chrome')].filter(Boolean).find(p => existsSync(p));
|
||||
if (!PW || !CHROME) { console.log('scene-parity: skipped, no Playwright Chromium here (PLAYWRIGHT_CORE, PLAYWRIGHT_CHROMIUM)'); process.exit(0); }
|
||||
const { chromium } = createRequire(PW)('playwright-core');
|
||||
|
||||
const fixture = JSON.parse(readFileSync(join(repo, 'scene/fixtures/live-2026-10-07.json'), 'utf8'));
|
||||
const T0 = new Date(fixture.now).getTime();
|
||||
const KEY = fixture.miners[2].id; // a real key of the recording with a few blocks: the overlay case names it on every surface
|
||||
const SIZE = { w: 900, h: 420 }, INSTANTS = [0, 2000, 4000]; // the first push and the next two polls (every surface polls every 2 s)
|
||||
const hasApp = existsSync(join(repo, 'app/igneum-app/ui/live-dag.js')) && existsSync(join(repo, 'tools/ui-mock/server.mjs'));
|
||||
const problems = [], lines = [];
|
||||
const say = s => { lines.push(s); console.log(s); };
|
||||
|
||||
// ---- servers ----
|
||||
function serve(cmd, args, env, ready) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const p = spawn(process.execPath, [cmd, ...args], { env: { ...process.env, ...env }, stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
let log = ''; const t = setTimeout(() => reject(new Error('server did not start: ' + cmd + '\n' + log)), 15000);
|
||||
const probe = async () => { try { const r = await fetch(ready); if (r.ok || r.status === 404) { clearTimeout(t); resolve(p); return; } } catch { } setTimeout(probe, 200); };
|
||||
p.stdout.on('data', d => { log += d; }); p.stderr.on('data', d => { log += d; });
|
||||
p.on('exit', c => { clearTimeout(t); reject(new Error(cmd + ' exited ' + c + '\n' + log)); });
|
||||
setTimeout(probe, 300);
|
||||
});
|
||||
}
|
||||
const port = base => base + Math.floor(Math.random() * 200);
|
||||
const sitePort = port(4600), appPort = port(4900);
|
||||
const servers = [];
|
||||
servers.push(await serve(join(repo, 'tools/site-serve.mjs'), [], { PORT: String(sitePort), DATABASE_URL: 'postgres://none' }, `http://127.0.0.1:${sitePort}/live`));
|
||||
if (hasApp) servers.push(await serve(join(repo, 'tools/ui-mock/server.mjs'), [String(appPort)], {}, `http://127.0.0.1:${appPort}/t/mock/api/state`));
|
||||
const stop = () => { for (const s of servers) try { s.kill(); } catch { } };
|
||||
process.on('exit', stop);
|
||||
|
||||
// ---- the browser ----
|
||||
const browser = await chromium.launch({ executablePath: CHROME, args: ['--renderer-process-limit=1', '--font-render-hinting=none', '--disable-lcd-text'] });
|
||||
// the mount shim: every mounted scene is listed with a push counter, so a capture can wait for a push to have landed
|
||||
const capture = () => { let real; Object.defineProperty(window, 'IgneumDag', { configurable: true, get() { return real; }, set(v) { real = Object.assign({}, v, { mount(c, o) { const api = v.mount(c, o); const entry = { canvas: c, api, opts: o, pushes: 0 }; const push = api.push; api.push = function (d) { const ok = push(d); if (ok) entry.pushes++; return ok; }; (window.__dags = window.__dags || []).push(entry); return api; } }); } }); };
|
||||
|
||||
/** One surface: opens the page with the fixture as its feed and the clock frozen at T0; returns frame hashes at the instants. */
|
||||
async function surface(name, url, { ownKey, mutate, selectCanvas, prepare } = {}) {
|
||||
const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 }, deviceScaleFactor: 1, colorScheme: 'dark', reducedMotion: 'no-preference' });
|
||||
const page = await ctx.newPage();
|
||||
const errors = []; page.on('pageerror', e => errors.push(String(e.message)));
|
||||
await ctx.clock.install({ time: T0 });
|
||||
await ctx.clock.pauseAt(T0 + 1); // frozen: only runFor moves it, so every surface is read at the same three instants
|
||||
await page.addInitScript(capture);
|
||||
// the scene's box before any script runs (lane order keeps the history of earlier layouts, so the first layout must already
|
||||
// be at the compared size): a style appended the moment <head> exists; no border or padding inside the canvas box
|
||||
await page.addInitScript(({ sel, w, h, extra }) => { const css = sel + '{width:' + w + 'px!important;height:' + h + 'px!important;max-width:none!important;border:0!important;padding:0!important;box-sizing:content-box!important}' + extra; const add = () => { if (!document.head) return false; const st = document.createElement('style'); st.textContent = css; document.head.appendChild(st); return true; }; if (!add()) new MutationObserver((m, o) => { if (add()) o.disconnect(); }).observe(document, { childList: true, subtree: true }); }, { sel: selectCanvas, w: SIZE.w, h: SIZE.h, extra: mutate || '' });
|
||||
if (ownKey) await page.addInitScript(k => { try { localStorage.setItem('igneum-my-key', k); localStorage.setItem('igneum-theme', 'dark'); } catch { } }, ownKey);
|
||||
else await page.addInitScript(() => { try { localStorage.removeItem('igneum-my-key'); localStorage.setItem('igneum-theme', 'dark'); } catch { } });
|
||||
await page.route('**/api/live*', r => r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(fixture) }));
|
||||
// the app's own keys come from its cards: none in the base case (the mock's recorded card is a real devnet key that is in the
|
||||
// fixture), exactly the named key in the overlay case
|
||||
if (name.startsWith('app')) await page.route('**/api/state*', async r => { const res = await r.fetch(); const s = await res.json(); if (s.mining && s.mining.cards) s.mining.cards.forEach((c, i) => { c.ids = ownKey && i === 0 ? [ownKey] : []; }); r.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(s) }); });
|
||||
await page.goto(url, { waitUntil: 'load' });
|
||||
// the mount, then the first push (the page's own poll runs at load; the fetch resolves in real time)
|
||||
if (prepare) await prepare(page);
|
||||
await page.waitForFunction(sel => !!(window.__dags || []).find(x => x.canvas.matches(sel)), selectCanvas, { timeout: 15000 }).catch(() => { });
|
||||
// the canvas in view, so the IntersectionObserver reports it and the motion loop runs on every surface alike (a canvas below
|
||||
// the fold paints on push alone since 2.0.3, which is right for a viewer but not a comparison of the loop)
|
||||
await page.evaluate(sel => { const c = document.querySelector(sel); if (c) c.scrollIntoView({ block: 'center' }); }, selectCanvas);
|
||||
await page.waitForTimeout(200);
|
||||
// the capture protocol, the same on every surface: advance the frozen clock exactly to the next poll, wait (in real time)
|
||||
// for that push to land, read the canvas as the push's own synchronous paint left it. No animation frame runs between the
|
||||
// push and the read, so the frame depends on the fake time alone, never on how long the fetch took.
|
||||
const pushes = () => page.evaluate(sel => { const d = (window.__dags || []).find(x => x.canvas.matches(sel)); return d ? d.pushes : -1; }, selectCanvas);
|
||||
const frames = [];
|
||||
let at = 0, want = 1;
|
||||
for (const ms of INSTANTS) {
|
||||
if (ms > at) { await ctx.clock.runFor(ms - at); at = ms; }
|
||||
const t0 = Date.now(); while (Date.now() - t0 < 8000 && (await pushes()) < want) { await page.waitForTimeout(50); if ((await pushes()) < want && Date.now() - t0 > 1500) { await ctx.clock.runFor(1); at += 1; } }
|
||||
if ((await pushes()) < want) say(' push ' + want + ' never landed on ' + name + ' (' + (await pushes()) + ' so far)');
|
||||
want++;
|
||||
const f = await page.evaluate(sel => {
|
||||
const d = (window.__dags || []).find(x => x.canvas.matches(sel)); if (!d) return { error: 'no scene on ' + sel };
|
||||
const c = d.canvas, g = c.getContext('2d'), st = d.api.getStats();
|
||||
const px = g.getImageData(0, 0, c.width, c.height).data; let h = 0, lit = 0;
|
||||
for (let i = 0; i < px.length; i += 4) { if (px[i] + px[i + 1] + px[i + 2] > 90) lit++; }
|
||||
return { w: c.width, h: c.height, lit, blocks: st.blocks, lanes: st.lanes, window: st.window, narrow: st.narrow, state: st.state, frames: st.frames, png: c.toDataURL('image/png') };
|
||||
}, selectCanvas);
|
||||
if (f.error) { frames.push(f); continue; }
|
||||
f.hash = createHash('sha256').update(f.png).digest('hex').slice(0, 16);
|
||||
if (out) writeFileSync(join(out, `${name}-${ms}.png`), Buffer.from(f.png.split(',')[1], 'base64'));
|
||||
f.pngData = f.png; delete f.png; frames.push(f);
|
||||
}
|
||||
await ctx.close();
|
||||
return { name, frames, errors };
|
||||
}
|
||||
const describe = s => s.frames.map((f, i) => f.error ? f.error : `T+${INSTANTS[i] / 1000}s ${f.hash} ${f.w}x${f.h} lit ${f.lit} blocks ${f.blocks} lanes ${f.lanes} win ${f.window}${f.narrow ? ' narrow' : ''} ${f.state}`).join('; ');
|
||||
async function compare(label, a, b, mustDiffer) {
|
||||
const diffs = [];
|
||||
for (let i = 0; i < INSTANTS.length; i++) {
|
||||
const fa = a.frames[i], fb = b.frames[i];
|
||||
if (fa.error || fb.error) { diffs.push('absent'); continue; }
|
||||
if (fa.hash === fb.hash) { diffs.push('equal'); continue; }
|
||||
const d = (fa.w === fb.w && fa.h === fb.h) ? await pixelDiff(fa.pngData, fb.pngData) : { n: -1 };
|
||||
diffs.push('DIFFERS' + (d.n >= 0 ? ' (' + d.n + ' of ' + d.total + ' px, box ' + d.box.join(',') + ')' : ' (sizes differ)'));
|
||||
}
|
||||
const bad = mustDiffer ? diffs.some(d => !d.startsWith('DIFFERS')) : diffs.some(d => d !== 'equal');
|
||||
say(` ${bad ? 'RED ' : 'ok '} ${label}: ${diffs.map((d, i) => `T+${INSTANTS[i] / 1000}s ${d}`).join(', ')}`);
|
||||
if (bad) problems.push(label);
|
||||
}
|
||||
const dims = (a, b) => a.frames[0].w === b.frames[0].w && a.frames[0].h === b.frames[0].h;
|
||||
async function pixelDiff(aPng, bPng) {
|
||||
const ctx = await browser.newContext(); const page = await ctx.newPage();
|
||||
const r = await page.evaluate(async ([a, b]) => {
|
||||
const load = src => new Promise(res => { const i = new Image(); i.onload = () => res(i); i.src = src; });
|
||||
const [ia, ib] = await Promise.all([load(a), load(b)]);
|
||||
const c = document.createElement('canvas'); c.width = ia.width; c.height = ia.height; const g = c.getContext('2d');
|
||||
g.drawImage(ia, 0, 0); const pa = g.getImageData(0, 0, c.width, c.height).data; g.clearRect(0, 0, c.width, c.height); g.drawImage(ib, 0, 0); const pb = g.getImageData(0, 0, c.width, c.height).data;
|
||||
let n = 0, x0 = 1e9, y0 = 1e9, x1 = -1, y1 = -1;
|
||||
for (let i = 0; i < pa.length; i += 4) { if (pa[i] !== pb[i] || pa[i + 1] !== pb[i + 1] || pa[i + 2] !== pb[i + 2] || pa[i + 3] !== pb[i + 3]) { n++; const p = i / 4, x = p % c.width, y = (p / c.width) | 0; if (x < x0) x0 = x; if (y < y0) y0 = y; if (x > x1) x1 = x; if (y > y1) y1 = y; } }
|
||||
return { n, box: n ? [x0, y0, x1 - x0 + 1, y1 - y0 + 1] : null, total: c.width * c.height };
|
||||
}, [aPng, bPng]);
|
||||
await ctx.close(); return r;
|
||||
}
|
||||
|
||||
const SITE_LIVE = `http://127.0.0.1:${sitePort}/live`, SITE_HOME = `http://127.0.0.1:${sitePort}/`, APP = `http://127.0.0.1:${appPort}/t/mock/?scenario=live&theme=dark`;
|
||||
const openInspect = async page => { await page.waitForSelector('#dag-inspect', { timeout: 15000 }); await page.waitForFunction(() => (window.__dags || []).length > 0, null, { timeout: 15000 }).catch(() => { }); await page.waitForTimeout(300); await page.click('#dag-inspect'); await page.waitForTimeout(300); };
|
||||
try {
|
||||
say(`scene-parity: fixture ${fixture.now}, ${fixture.blocks.length} blocks, key ${KEY} for the overlay case, canvases ${SIZE.w}x${SIZE.h}, instants ${INSTANTS.join('/')} ms`);
|
||||
const live = await surface('live', SITE_LIVE, { selectCanvas: '#dag' });
|
||||
say(' /live base: ' + describe(live));
|
||||
const home = await surface('home', SITE_HOME, { selectCanvas: '#dag' });
|
||||
say(' home fold base: ' + describe(home));
|
||||
for (const s of [live, home]) for (const e of s.errors) say(' page error on ' + s.name + ': ' + e);
|
||||
if (live.frames.some(f => !f.lit)) problems.push('/live drew nothing');
|
||||
await compare('home fold = /live (same renderer, same feed, same options)', home, live, false);
|
||||
if (hasApp) {
|
||||
// known-failed first: one token changed on the app side must fail
|
||||
const mutated = await surface('app-mutated', APP, { selectCanvas: '#dag-full', prepare: openInspect, mutate: ':root{--included:#3B7DD9!important}' });
|
||||
say(' app Inspect with --included changed by one unit: ' + describe(mutated));
|
||||
await compare('KNOWN-FAILED: app with a changed palette token differs from /live', mutated, live, true);
|
||||
const app = await surface('app', APP, { selectCanvas: '#dag-full', prepare: openInspect });
|
||||
say(' app Inspect base: ' + describe(app));
|
||||
for (const e of app.errors) say(' page error on app: ' + e);
|
||||
if (!dims(app, live)) problems.push('canvas buffers differ in size');
|
||||
await compare('app Inspect = /live', app, live, false);
|
||||
// the overlay: the same key on every surface
|
||||
const liveK = await surface('live-key', SITE_LIVE, { selectCanvas: '#dag', ownKey: KEY });
|
||||
const appK = await surface('app-key', APP, { selectCanvas: '#dag-full', prepare: openInspect, ownKey: KEY });
|
||||
say(' /live with the key: ' + describe(liveK));
|
||||
say(' app Inspect with the key: ' + describe(appK));
|
||||
await compare('app Inspect with this machine\'s key = /live with the same key (the overlay is the same picture)', appK, liveK, false);
|
||||
await compare('the app\'s overlay frame differs from its base frame (the overlay is drawn)', appK, app, true);
|
||||
const compact = await surface('app-compact', APP, { selectCanvas: '#dag-live', prepare: async page => { await page.waitForFunction(() => (window.__dags || []).length > 0, null, { timeout: 15000 }).catch(() => { }); } });
|
||||
say(' app compact card (its own mode, reported, not compared): ' + describe(compact));
|
||||
} else {
|
||||
say(' app surface: skipped (no app/igneum-app/ui/live-dag.js in this tree; the app lives on the release branches)');
|
||||
}
|
||||
} finally { await browser.close(); stop(); }
|
||||
say(problems.length ? `scene-parity: RED, ${problems.length} problem(s): ${problems.join('; ')}` : 'scene-parity: ok');
|
||||
if (out) writeFileSync(join(out, 'report.txt'), lines.join('\n') + '\n');
|
||||
process.exit(problems.length ? 1 : 0);
|
||||