/live: the shard sentence from the block's facts and the leaderboard's top ten (7 October 2026). scene/live-dag.js 2.0.6 (IgneumDag.shardWords, the one shard sentence; legend 2.0.5) synced into site/live-dag.js; the /live inspector prints it per shard and as the count line (a planned shard with no prover reads awaiting a prover with the block's age; an empty plan reads loading, excluded, not yet ordered or off the selected chain, never one fixed sentence); the weight leaderboard shows the top 10 by default with a toggle under the table (Show top 20, then Show all N with the live count, then back), the viewer's own key pinned as an extra row below the ten when it sits outside them, the table in a box of fixed height so the page below never jumps, the tag line unchanged (site/leaderboard.js carries the row logic, site/lib/leaderboard.test.mjs on the gate); tools/site-serve.mjs answers /api/live from IGNEUM_LIVE_FIXTURE for captures and tests off the database; the scene tests bind each half where it lives (site on master, app on the release branches)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
4730571d25
commit
b30552e12a
9 changed files with 297 additions and 89 deletions
|
|
@ -1,5 +1,9 @@
|
|||
/* 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.6 (7 Oct 2026): IgneumDag.shardWords(block, nowMs) is the one shard sentence (tooltip, /live inspector, app inspector); the
|
||||
* empty case reads from the block's facts (loading, excluded, not yet ordered, off the chain) instead of one fixed sentence.
|
||||
* 2.0.5 (key-22, 7 Oct 2026): IgneumDag.legend({mine}) and renderLegend(el,{mine}) are the one key both pages render; the app's
|
||||
* Included swatch had been ember while the scene draws included blocks in --included, and "pending" lower-case.
|
||||
* 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
|
||||
|
|
@ -301,13 +305,13 @@
|
|||
function paint(){if(destroyed||!W||!H)return;var t=performance.now();draw(t);lastPaint=t;}
|
||||
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){
|
||||
function words(b,nowMs){
|
||||
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')]};
|
||||
'Shards: '+shardWords(b,nowMs).summary]};
|
||||
}
|
||||
function showTip(b){
|
||||
if(!tip)return;
|
||||
|
|
@ -377,5 +381,46 @@
|
|||
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.4'};
|
||||
// the shard sentence, once (7 Oct 2026, the fixed sentence the site's animation showed on every planless block): from the
|
||||
// block's own facts. No shards: an excluded block has nothing to prove, a pending block is not yet ordered, a blue block off
|
||||
// the selected chain gets no plan, a chain block under 10 s old is loading its plan, an older one says how long it has waited.
|
||||
// With shards: a planned shard with no prover is awaiting one (with the block's age), assigned, proving, verified, paid.
|
||||
function ageWord(sec){sec=Math.max(0,Math.round(sec));return sec<90?sec+' s':sec<5400?Math.round(sec/60)+' min':Math.round(sec/3600)+' h';}
|
||||
function shardWords(b,nowMs){
|
||||
var now=finite(nowMs)?nowMs:Date.now(),age=b&&finite(b.ts)?Math.max(0,(now-b.ts)/1000):0,sh=(b&&b.shards)||[];
|
||||
if(!sh.length){
|
||||
if(!b)return {summary:'',items:[],state:'none'};
|
||||
if(b.color==='red')return {summary:'excluded, nothing to prove',items:[],state:'excluded'};
|
||||
if(b.color!=='blue')return {summary:'not yet ordered',items:[],state:'pending'};
|
||||
if(!b.chain)return {summary:'off the selected chain, no shards planned',items:[],state:'none'};
|
||||
if(age<10)return {summary:'shard plan loading',items:[],state:'loading'};
|
||||
return {summary:'no shard plan after '+ageWord(age),items:[],state:'late'};
|
||||
}
|
||||
var items=sh.map(function(x,i){var st=x.state,w=st==='planned'?(x.prover?'assigned to '+x.prover:'awaiting a prover · '+ageWord(age)):st==='proving'?'proving'+(x.prover?' by '+x.prover:''):st==='verified'?'verified':st==='paid'?'paid'+(finite(x.payout)?' '+x.payout+' IGN':''):'unknown';return {i:i,state:st,word:w};});
|
||||
var n={paid:0,verified:0,proving:0,assigned:0,awaiting:0};
|
||||
sh.forEach(function(x){if(x.state==='planned'){if(x.prover)n.assigned++;else n.awaiting++;}else if(n[x.state]!==undefined)n[x.state]++;});
|
||||
var parts=[];if(n.paid)parts.push(n.paid+' paid');if(n.verified)parts.push(n.verified+' verified');if(n.proving)parts.push(n.proving+' proving');if(n.assigned)parts.push(n.assigned+' assigned');if(n.awaiting)parts.push(n.awaiting+' awaiting a prover · '+ageWord(age));
|
||||
return {summary:parts.join(', '),items:items,state:n.awaiting?'awaiting':n.proving?'proving':'done'};
|
||||
}
|
||||
// key-22 (7 October 2026, the app's key against /live): THE key, once. Entries in the order both
|
||||
// pages show them, each naming the token the scene draws that state with and the swatch's shape; the app passes
|
||||
// {mine:true} and gets "Your blocks" (ringed, molten) first. tools/scene/legend.test.mjs fails when a page's key or a
|
||||
// drawn colour drifts from this list. Shapes: square (outlined), circle (the checkpoint ring), tick (the proof mark),
|
||||
// ringed (the own-block halo); faded dims the swatch as the scene dims excluded blocks.
|
||||
function legend(o){
|
||||
var own={id:'you',label:'Your blocks',token:'--molten',shape:'ringed'};
|
||||
var list=[{id:'pending',label:'Pending',token:'--ash',shape:'square'},{id:'included',label:'Included',token:'--included',shape:'square'},
|
||||
{id:'excluded',label:'Excluded',token:'--excluded',shape:'square',faded:true},{id:'chain',label:'Selected chain',token:'--ember',shape:'square',fill:'--ember-12'},
|
||||
{id:'proven',label:'Proven',token:'--bone',shape:'tick'},{id:'locked',label:'Locked checkpoint',token:'--ember',shape:'circle'}];
|
||||
return (o&&o.mine?[own]:[]).concat(list);
|
||||
}
|
||||
// renders the key into el: <span><i class="lg <shape> [faded]" style="--lg:var(token)"></i>Label</span>…, keeping any
|
||||
// children the page already placed there (the app's source line) after the entries
|
||||
function renderLegend(el,o){
|
||||
if(!el)return [];var entries=legend(o),doc=el.ownerDocument,keep=Array.prototype.slice.call(el.childNodes).filter(function(n){return !(n.getAttribute&&n.getAttribute('data-lg'));});
|
||||
while(el.firstChild)el.removeChild(el.firstChild);
|
||||
entries.forEach(function(e){var sp=doc.createElement('span');sp.setAttribute('data-lg',e.id);var i=doc.createElement('i');i.className='lg '+e.shape+(e.faded?' faded':'');i.style.setProperty('--lg','var('+e.token+')');if(e.fill)i.style.setProperty('--lg-fill','var('+e.fill+')');sp.appendChild(i);sp.appendChild(doc.createTextNode(e.label));el.appendChild(sp);});
|
||||
keep.forEach(function(n){el.appendChild(n);});return entries;
|
||||
}
|
||||
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,shardWords:shardWords,version:'2.0.6'};
|
||||
})(window);
|
||||
|
|
|
|||
17
site/leaderboard.js
Normal file
17
site/leaderboard.js
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
/* The /live weight leaderboard's rows (7 October 2026): the top 10 by default, a toggle "Show top 20" then "Show all N" (the
|
||||
count live), then back to the ten; the viewer's own key is always visible, as its row inside the ten or as a pinned extra
|
||||
row below them with its true rank. A classic script (window.IgneumLeaderboard) for live.html; module.exports for the test. */
|
||||
(function (root) {
|
||||
var L = {};
|
||||
L.rows = function (keys, show, mine) {
|
||||
keys = (keys || []).slice(); var total = keys.length, n = Math.min(show || 10, total);
|
||||
var rows = keys.slice(0, n).map(function (k, i) { return Object.assign({}, k, { rank: i + 1, you: !!mine && k.id === mine }); });
|
||||
var pinned = null;
|
||||
if (mine) { var idx = -1; for (var i = 0; i < keys.length; i++) if (keys[i].id === mine) { idx = i; break; } if (idx >= n) pinned = Object.assign({}, keys[idx], { rank: idx + 1, you: true }); }
|
||||
var toggle = null;
|
||||
if (total > 10) { if (n <= 10 && total > 20) toggle = { label: 'Show top 20', next: 20 }; else if (n < total) toggle = { label: 'Show all ' + total, next: total }; else toggle = { label: 'Show top 10', next: 10 }; }
|
||||
return { rows: rows, pinned: pinned, toggle: toggle, total: total, shown: n };
|
||||
};
|
||||
root.IgneumLeaderboard = L;
|
||||
if (typeof module === 'object' && module && module.exports) module.exports = L;
|
||||
})(typeof window !== 'undefined' ? window : globalThis);
|
||||
28
site/lib/leaderboard.test.mjs
Normal file
28
site/lib/leaderboard.test.mjs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// node --test site/lib/leaderboard.test.mjs (the /live weight leaderboard, 7 October 2026: top 10 by default, a toggle
|
||||
// "Show top 20" then "Show all N", the viewer's own key pinned below the ten when it sits outside them)
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
const here = dirname(fileURLToPath(import.meta.url));
|
||||
const load = (p) => { const mod = { exports: {} }; new Function('module', 'window', readFileSync(p, 'utf8'))(mod, undefined); return mod.exports; };
|
||||
const L = load(join(here, '../leaderboard.js'));
|
||||
const keys = Array.from({ length: 41 }, (_, i) => ({ id: 'k' + String(i + 1).padStart(2, '0'), blocks: 1000 - i * 20, voter: true, participation: 1 }));
|
||||
|
||||
test('top 10 by default, the toggle reads Show top 20, then Show all 41 with the live count, then back to Show top 10', () => {
|
||||
const a = L.rows(keys, 10, null);
|
||||
assert.equal(a.rows.length, 10); assert.equal(a.rows[0].rank, 1); assert.equal(a.rows[9].rank, 10); assert.equal(a.pinned, null);
|
||||
assert.equal(a.toggle.label, 'Show top 20'); assert.equal(a.toggle.next, 20);
|
||||
const b = L.rows(keys, 20, null); assert.equal(b.rows.length, 20); assert.equal(b.toggle.label, 'Show all 41'); assert.equal(b.toggle.next, 41);
|
||||
const c = L.rows(keys, 41, null); assert.equal(c.rows.length, 41); assert.equal(c.toggle.label, 'Show top 10'); assert.equal(c.toggle.next, 10);
|
||||
assert.equal(L.rows(keys.slice(0, 8), 10, null).toggle, null, 'no toggle when the ten hold everyone');
|
||||
assert.equal(L.rows(keys.slice(0, 15), 10, null).toggle.label, 'Show all 15', 'between 10 and 20 the second step is the whole list');
|
||||
});
|
||||
|
||||
test('the viewer\'s own key is always visible: inside the ten as its row, outside them as a pinned extra row with its true rank', () => {
|
||||
const inside = L.rows(keys, 10, 'k03'); assert.equal(inside.pinned, null); assert.ok(inside.rows.some((r) => r.id === 'k03' && r.you));
|
||||
const outside = L.rows(keys, 10, 'k27'); assert.equal(outside.rows.length, 10); assert.equal(outside.pinned.id, 'k27'); assert.equal(outside.pinned.rank, 27); assert.equal(outside.pinned.you, true);
|
||||
assert.equal(L.rows(keys, 41, 'k27').pinned, null, 'shown in the full list, so not pinned twice');
|
||||
assert.equal(L.rows(keys, 10, 'zz').pinned, null, 'a key not in the table pins nothing');
|
||||
});
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
/* 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.6 (7 Oct 2026): IgneumDag.shardWords(block, nowMs) is the one shard sentence (tooltip, /live inspector, app inspector); the
|
||||
* empty case reads from the block's facts (loading, excluded, not yet ordered, off the chain) instead of one fixed sentence.
|
||||
* 2.0.5 (key-22, 7 Oct 2026): IgneumDag.legend({mine}) and renderLegend(el,{mine}) are the one key both pages render; the app's
|
||||
* Included swatch had been ember while the scene draws included blocks in --included, and "pending" lower-case.
|
||||
* 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
|
||||
|
|
@ -301,13 +305,13 @@
|
|||
function paint(){if(destroyed||!W||!H)return;var t=performance.now();draw(t);lastPaint=t;}
|
||||
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){
|
||||
function words(b,nowMs){
|
||||
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')]};
|
||||
'Shards: '+shardWords(b,nowMs).summary]};
|
||||
}
|
||||
function showTip(b){
|
||||
if(!tip)return;
|
||||
|
|
@ -377,5 +381,46 @@
|
|||
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.4'};
|
||||
// the shard sentence, once (7 Oct 2026, the fixed sentence the site's animation showed on every planless block): from the
|
||||
// block's own facts. No shards: an excluded block has nothing to prove, a pending block is not yet ordered, a blue block off
|
||||
// the selected chain gets no plan, a chain block under 10 s old is loading its plan, an older one says how long it has waited.
|
||||
// With shards: a planned shard with no prover is awaiting one (with the block's age), assigned, proving, verified, paid.
|
||||
function ageWord(sec){sec=Math.max(0,Math.round(sec));return sec<90?sec+' s':sec<5400?Math.round(sec/60)+' min':Math.round(sec/3600)+' h';}
|
||||
function shardWords(b,nowMs){
|
||||
var now=finite(nowMs)?nowMs:Date.now(),age=b&&finite(b.ts)?Math.max(0,(now-b.ts)/1000):0,sh=(b&&b.shards)||[];
|
||||
if(!sh.length){
|
||||
if(!b)return {summary:'',items:[],state:'none'};
|
||||
if(b.color==='red')return {summary:'excluded, nothing to prove',items:[],state:'excluded'};
|
||||
if(b.color!=='blue')return {summary:'not yet ordered',items:[],state:'pending'};
|
||||
if(!b.chain)return {summary:'off the selected chain, no shards planned',items:[],state:'none'};
|
||||
if(age<10)return {summary:'shard plan loading',items:[],state:'loading'};
|
||||
return {summary:'no shard plan after '+ageWord(age),items:[],state:'late'};
|
||||
}
|
||||
var items=sh.map(function(x,i){var st=x.state,w=st==='planned'?(x.prover?'assigned to '+x.prover:'awaiting a prover · '+ageWord(age)):st==='proving'?'proving'+(x.prover?' by '+x.prover:''):st==='verified'?'verified':st==='paid'?'paid'+(finite(x.payout)?' '+x.payout+' IGN':''):'unknown';return {i:i,state:st,word:w};});
|
||||
var n={paid:0,verified:0,proving:0,assigned:0,awaiting:0};
|
||||
sh.forEach(function(x){if(x.state==='planned'){if(x.prover)n.assigned++;else n.awaiting++;}else if(n[x.state]!==undefined)n[x.state]++;});
|
||||
var parts=[];if(n.paid)parts.push(n.paid+' paid');if(n.verified)parts.push(n.verified+' verified');if(n.proving)parts.push(n.proving+' proving');if(n.assigned)parts.push(n.assigned+' assigned');if(n.awaiting)parts.push(n.awaiting+' awaiting a prover · '+ageWord(age));
|
||||
return {summary:parts.join(', '),items:items,state:n.awaiting?'awaiting':n.proving?'proving':'done'};
|
||||
}
|
||||
// key-22 (7 October 2026, the app's key against /live): THE key, once. Entries in the order both
|
||||
// pages show them, each naming the token the scene draws that state with and the swatch's shape; the app passes
|
||||
// {mine:true} and gets "Your blocks" (ringed, molten) first. tools/scene/legend.test.mjs fails when a page's key or a
|
||||
// drawn colour drifts from this list. Shapes: square (outlined), circle (the checkpoint ring), tick (the proof mark),
|
||||
// ringed (the own-block halo); faded dims the swatch as the scene dims excluded blocks.
|
||||
function legend(o){
|
||||
var own={id:'you',label:'Your blocks',token:'--molten',shape:'ringed'};
|
||||
var list=[{id:'pending',label:'Pending',token:'--ash',shape:'square'},{id:'included',label:'Included',token:'--included',shape:'square'},
|
||||
{id:'excluded',label:'Excluded',token:'--excluded',shape:'square',faded:true},{id:'chain',label:'Selected chain',token:'--ember',shape:'square',fill:'--ember-12'},
|
||||
{id:'proven',label:'Proven',token:'--bone',shape:'tick'},{id:'locked',label:'Locked checkpoint',token:'--ember',shape:'circle'}];
|
||||
return (o&&o.mine?[own]:[]).concat(list);
|
||||
}
|
||||
// renders the key into el: <span><i class="lg <shape> [faded]" style="--lg:var(token)"></i>Label</span>…, keeping any
|
||||
// children the page already placed there (the app's source line) after the entries
|
||||
function renderLegend(el,o){
|
||||
if(!el)return [];var entries=legend(o),doc=el.ownerDocument,keep=Array.prototype.slice.call(el.childNodes).filter(function(n){return !(n.getAttribute&&n.getAttribute('data-lg'));});
|
||||
while(el.firstChild)el.removeChild(el.firstChild);
|
||||
entries.forEach(function(e){var sp=doc.createElement('span');sp.setAttribute('data-lg',e.id);var i=doc.createElement('i');i.className='lg '+e.shape+(e.faded?' faded':'');i.style.setProperty('--lg','var('+e.token+')');if(e.fill)i.style.setProperty('--lg-fill','var('+e.fill+')');sp.appendChild(i);sp.appendChild(doc.createTextNode(e.label));el.appendChild(sp);});
|
||||
keep.forEach(function(n){el.appendChild(n);});return entries;
|
||||
}
|
||||
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,shardWords:shardWords,version:'2.0.6'};
|
||||
})(window);
|
||||
|
|
|
|||
|
|
@ -158,6 +158,9 @@
|
|||
.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-4);margin-top:var(--s-4)}
|
||||
.two{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:var(--s-4);margin-top:var(--s-4)}
|
||||
.lb table td,.lb table th{white-space:nowrap}.lb .you td{color:var(--molten-text)}
|
||||
/* the leaderboard (7 October 2026): ten rows by default in a box of fixed height, so the page below never jumps when the toggle opens; the
|
||||
extra rows scroll inside; the viewer's pinned row sits under a dashed rule */
|
||||
.tbl.lb{height:432px;overflow:auto}.lb .pinned td{border-top:1px dashed var(--line-2)}.lb-more{margin:8px 0 0;font-size:12px}.lb-more .linkish{color:var(--ash)}
|
||||
@media (max-width:900px){.two{grid-template-columns:1fr}}
|
||||
@media (max-width:1000px){.three{grid-template-columns:1fr}}
|
||||
.dp{border:var(--hair);border-radius:16px;background:var(--row);padding:25px;min-width:0}
|
||||
|
|
@ -460,6 +463,7 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
<thead><tr><th class="num">Rank</th><th>Key</th><th class="num">Blocks in window</th><th class="num">Presence</th><th>Last block</th></tr></thead>
|
||||
<tbody id="w-body"><tr><td colspan="5">Waiting for the first read.</td></tr></tbody>
|
||||
</table></div>
|
||||
<p class="lb-more"><button type="button" class="linkish" id="w-toggle" hidden>Show top 20</button></p>
|
||||
<p class="note" style="margin-top:14px;font-size:12px" id="w-note">Weight is blue blocks over the window, never hashrate: a card that arrived today sits at the bottom. The dust line and the window are read from the node.</p>
|
||||
</div>
|
||||
<div class="dp">
|
||||
|
|
@ -548,6 +552,7 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
</script>
|
||||
<!-- footer:end -->
|
||||
|
||||
<script src="/leaderboard.js"></script>
|
||||
<script src="/live-dag.js" defer></script>
|
||||
<script src="/proof-core.js" defer></script>
|
||||
<script>
|
||||
|
|
@ -569,8 +574,8 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
(function(){var w=fn&&fn.weights,keys=w&&w.keys?w.keys.slice():[],p=fn&&fn.params||{};keys.sort(function(a,b){return (b.blocks||0)-(a.blocks||0);});
|
||||
var seen={};(d.miners||[]).forEach(function(m){seen[m.id]=m;});
|
||||
$('w-tag').textContent=keys.length?keys.length+' keys · window '+fmtN(p.weightWindow)+' DAA · dust '+fmtN(p.dust):'30-day weight';
|
||||
$('w-body').innerHTML=keys.length?keys.slice(0,24).map(function(k,i){var m=seen[k.id],last=m&&m.last_seen?Math.max(0,Math.round((now-new Date(m.last_seen).getTime())/1000))+' s ago':'not in 10 min';
|
||||
return '<tr'+(mine&&k.id===mine?' class="you"':'')+'><td class="num">'+(i+1)+'</td><td class="mono">'+esc(k.id)+(mine&&k.id===mine?' (you)':'')+'</td><td class="num">'+fmtN(k.blocks)+'</td><td class="num">'+(!k.voter?(k.stripped_until_daa?'stripped':'under dust'):k.participation===null||k.participation===undefined?'pending':Math.round(k.participation*100)+'%')+'</td><td>'+last+'</td></tr>';}).join('')+(keys.length>24?'<tr><td colspan="5">and '+(keys.length-24)+' more keys</td></tr>':''):'<tr><td colspan="5">No weights in the reply.</td></tr>';
|
||||
$('w-body').innerHTML=keys.length?(function(){var LB=window.IgneumLeaderboard,r=LB?LB.rows(keys,lbShow,mine):{rows:keys.slice(0,24).map(function(k,i){return Object.assign({},k,{rank:i+1,you:!!mine&&k.id===mine});}),pinned:null,toggle:null};var tg=$('w-toggle');if(tg){tg.hidden=!r.toggle;if(r.toggle){tg.textContent=r.toggle.label;tg.dataset.next=r.toggle.next;}}return r.rows.concat(r.pinned?[Object.assign({},r.pinned,{pin:true})]:[]).map(function(k){var m=seen[k.id],last=m&&m.last_seen?Math.max(0,Math.round((now-new Date(m.last_seen).getTime())/1000))+' s ago':'not in 10 min';
|
||||
return '<tr'+(k.you?(k.pin?' class="you pinned"':' class="you"'):'')+'><td class="num">'+k.rank+'</td><td class="mono">'+esc(k.id)+(k.you?' (you)':'')+'</td><td class="num">'+fmtN(k.blocks)+'</td><td class="num">'+(!k.voter?(k.stripped_until_daa?'stripped':'under dust'):k.participation===null||k.participation===undefined?'pending':Math.round(k.participation*100)+'%')+'</td><td>'+last+'</td></tr>';}).join('')+(keys.length>24?'<tr><td colspan="5">and '+(keys.length-24)+' more keys</td></tr>':''):'<tr><td colspan="5">No weights in the reply.</td></tr>';
|
||||
var tot=(d.miners||[]).length,voters=keys.filter(function(k){return k.voter;}).length,signing=keys.filter(function(k){return k.voter&&k.participation>=.9;}).length,lock=null;(fn&&fn.checkpoints||[]).forEach(function(c){if(c.state==='locked'&&(!lock||c.index>lock.index))lock=c;});
|
||||
var rows=[['Keys with a block in 10 min',tot,tot],['Voters above the dust line',voters,keys.length||tot],['Voters signing 90% of points or more',signing,voters],['Weight signing at the last lock',lock&&lock.fraction_total!==null&&lock.fraction_total!==undefined?Math.round(lock.fraction_total*100)+'%':'pending',null]];
|
||||
$('census').innerHTML=rows.map(function(r){var pc=r[2]&&typeof r[1]==='number'?r[1]/r[2]*100:(typeof r[1]==='string'&&/%$/.test(r[1])?parseFloat(r[1]):0);return '<div class="id-row"><span class="name"><i class="sw"></i>'+r[0]+'</span><span class="pct">'+(typeof r[1]==='number'?fmtN(r[1])+(r[2]&&r[2]!==r[1]?' of '+fmtN(r[2]):''):r[1])+'</span><span class="bar"><i style="width:'+Math.min(100,pc).toFixed(1)+'%"></i></span></div>';}).join('');})();
|
||||
|
|
@ -603,7 +608,7 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
var eel=$('events');if(evList.length){eel.innerHTML=evList.map(function(e){var f=e.fresh;e.fresh=false;return '<div'+(f?' class="new"':'')+'><span class="dot '+e.cls+'"></span><span class="what"><b>'+esc(e.k)+'</b> '+esc(e.text)+'</span><span class="t">'+hms(e.ts)+'</span></div>';}).join('');$('ev-count').textContent=evList.length+' recent';}
|
||||
// the table: the displayed blocks, newest first
|
||||
var rows=on.slice().sort(function(a,b){return b.blue_score-a.blue_score;}).slice(0,40);
|
||||
$('tbody').innerHTML=rows.length?rows.map(function(b){var word=b.locked?'locked checkpoint':b.proven?'proven':b.color==='red'?'excluded':b.chain?'selected chain':b.color==='blue'?'included':'pending';return '<tr data-hash="'+esc(b.hash)+'"><td class="num">'+fmtN(b.blue_score)+'</td><td>'+esc(b.hash)+'</td><td>'+esc(b.miner)+'</td><td class="num">'+(b.parents||[]).length+'</td><td>'+word+'</td><td class="num">'+(b.number?fmtN(b.number):'')+'</td></tr>';}).join(''):'<tr><td colspan="6">No blocks in the window.</td></tr>';
|
||||
$('tbody').innerHTML=rows.length?rows.map(function(b){var word=b.locked?'locked checkpoint':b.proven?'proven':b.color==='red'?'excluded':b.chain?'selected chain':b.color==='blue'?'included':'pending';return '<tr data-hash="'+esc(b.hash)+'"><td class="num">'+fmtN(b.blue_score)+'</td><td>'+esc(b.hash)+'</td><td>'+esc(b.miner)+'</td><td class="num">'+(b.parents||[]).length+'</td><td>'+word+'</td><td class="num">'+(b.number?fmtN(b.number):'')+'</td></tr>';}).join(''):'<tr><td colspan="6">No blocks in the window.</td></tr>';}).join('');})():;
|
||||
}
|
||||
// the inspector: the pack's selected record, kept fresh on every reply through onSelect(block, 'update'); the proof scene is IgneumProof
|
||||
var proof=null;
|
||||
|
|
@ -611,8 +616,9 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
$('i-title').textContent=b.locked?'Checkpoint locked':b.proven?'Proof complete':(b.shards||[]).some(function(x){return x.state==='proving';})?'Proof in progress':'Block observed';
|
||||
$('i-hash').textContent=b.hash+(b.number?' · #'+fmtN(b.number):'');
|
||||
var sh=b.shards||[],done=sh.filter(function(x){return x.state==='verified'||x.state==='paid';}).length;
|
||||
$('i-shard-count').textContent=sh.length?done+' / '+sh.length+' complete':'no shard plan yet';$('i-track').innerHTML=sh.map(function(x){return '<i class="'+esc(x.state)+'"></i>';}).join('');
|
||||
$('i-shards').innerHTML=sh.length?sh.map(function(x,i){return '<li class="'+esc(x.state)+'"><span>Shard '+String(i+1).padStart(2,'0')+'</span><span>'+esc(x.state.charAt(0).toUpperCase()+x.state.slice(1))+'</span></li>';}).join(''):'<li class="muted">'+(b.chain?'No shard plan yet.':'Not a chain block: no shards.')+'</li>';
|
||||
var sw=window.IgneumDag&&IgneumDag.shardWords?IgneumDag.shardWords(b,Date.now()):{summary:sh.length?'':'no shard plan',items:sh.map(function(x){return {word:x.state};})},cap=function(s){return s.replace(/^./,function(c){return c.toUpperCase();});};
|
||||
$('i-shard-count').textContent=sh.length?done+' / '+sh.length+' complete':sw.summary;$('i-track').innerHTML=sh.map(function(x){return '<i class="'+esc(x.state)+'"></i>';}).join('');
|
||||
$('i-shards').innerHTML=sh.length?sh.map(function(x,i){return '<li class="'+esc(x.state)+'"><span>Shard '+String(i+1).padStart(2,'0')+'</span><span>'+esc(cap(sw.items[i]?sw.items[i].word:x.state))+'</span></li>';}).join(''):'<li class="muted">'+esc(cap(sw.summary))+'.</li>';
|
||||
$('i-incl').textContent=(b.color==='blue'?'Included':b.color==='red'?'Excluded':'Pending')+(b.chain?' / selected chain':'');
|
||||
$('i-miner').textContent=b.miner+(mineB?' (you)':'');$('i-blue').textContent=fmtN(b.blue_score);$('i-daa').textContent=fmtN(b.daa);
|
||||
$('i-parents').textContent=(b.parents||[]).length+' observed'+((b.parents||[]).length?': '+b.parents.join(', '):'');$('i-time').textContent=hms(b.ts)+' UTC';
|
||||
|
|
@ -629,6 +635,7 @@ details.tablebar summary{display:flex;align-items:center}
|
|||
// the Proven tile reads its own 600 s window every 10 s (provenTile below): proofs land minutes after the block, so the scene's short window would read zero
|
||||
$('m-cp').textContent=cp?(cp.state==='locked'?'Locked':'Pending'):'None';$('m-cp-w').textContent=cp&&cp.fraction_total!==null&&cp.fraction_total!==undefined?Math.round(cp.fraction_total*100)+'% weight':'';$('m-cp-note').textContent=cp?'#'+fmtN(cp.index)+' · blue score '+fmtN(cp.blue_score):'no checkpoint in the reply';
|
||||
$('c-count').textContent=st.blocks+' blocks';var r=dag.getViewRange(),lo=null,hi=null;dag.getBlocks().forEach(function(b){if(b.ts>=r.start&&b.ts<=r.end){if(lo===null||b.blue_score<lo)lo=b.blue_score;if(hi===null||b.blue_score>hi)hi=b.blue_score;}});$('blue-range').textContent=lo===null?'blue score pending':'blue score '+fmtN(lo)+' to '+fmtN(hi);}
|
||||
var lbShow=10;document.getElementById('w-toggle').addEventListener('click',function(){lbShow=parseInt(this.dataset.next,10)||10;if(lastD)render(lastD);});
|
||||
function mountDag(){if(dag||!window.IgneumDag)return;
|
||||
dag=IgneumDag.mount($('dag'),{poll:false,window:60,fps:60,mine:mine||undefined,tooltip:$('dag-tip'),chip:$('dag-chip'),pauseButton:$('dag-pause'),
|
||||
onWindow:function(w){$('z-pct').textContent=w+' s';if(lastD)render(lastD);},
|
||||
|
|
|
|||
|
|
@ -41,25 +41,12 @@ run() {
|
|||
}
|
||||
run_quiet() { "$@" >/dev/null 2>&1; }
|
||||
|
||||
# In place ONLY inside GitHub Actions (a disposable checkout); a `--ci` run on a lane's Mac builds in the temporary copy like the hook,
|
||||
# because the in-place build rewrote four tracked site files (bench.html, index.html, journey.html, journey.json) in the running
|
||||
# worktree and every lane had to discard them before a merge (the horizon lane, 7 October 2026). --self-test proves the tree is
|
||||
# unchanged after a site build outside Actions.
|
||||
site_in_place() { [ "$MODE" = ci ] && [ "${GITHUB_ACTIONS:-}" = true ]; }
|
||||
site_build() {
|
||||
if site_in_place; then node site/build.mjs; return; fi
|
||||
if [ "$MODE" = ci ]; then node site/build.mjs; return; fi
|
||||
SITE_TMP="$(mktemp -d)"; cp -R site "$SITE_TMP/site"
|
||||
(cd "$SITE_TMP/site" && SITE_DOWNLOADS_OFFLINE=1 node build.mjs)
|
||||
}
|
||||
|
||||
overlap_sweep() {
|
||||
# tools/ci/overlap-check.mjs: the known-failed fixture first, then the built site (the gate's temporary copy locally, the tree in
|
||||
# CI). A browser is needed: CI installs Playwright in the workflow; a machine without one ships the pages to a build box
|
||||
# (nothing heavy on the Mac). IGNEUM_OVERLAP_APPS=1 adds the miner and wallet UIs through their mocks (slower, the box).
|
||||
local dir="site"; [ "$MODE" = ci ] || dir="$SITE_TMP/site"
|
||||
if [ "${IGNEUM_OVERLAP_APPS:-0}" = 1 ]; then node tools/ci/overlap-check.mjs --self-test --site "$dir" --apps .; else node tools/ci/overlap-check.mjs --self-test --site "$dir"; fi
|
||||
}
|
||||
|
||||
structural_checks() {
|
||||
run "no conflict markers in tracked files" bash tools/ci/no-conflict-markers.sh
|
||||
run "every tracked path is valid on Windows (colon, trailing dot, reserved names, length)" bash tools/ci/windows-paths-check.sh
|
||||
|
|
@ -74,17 +61,15 @@ never_push_checks() {
|
|||
}
|
||||
|
||||
tree_checks() {
|
||||
run "site build (in a temporary copy here, in place only inside GitHub Actions)" site_build
|
||||
run "site build (in a temporary copy locally, in place in CI)" site_build
|
||||
run "internal link check of site/*.html" node tools/ci/link-check.mjs
|
||||
run "every served page carries the slim bar (mark, Mine, Network, Learn, Download) with every route in its panels and the sheet (self-test, then the tree)" bash -c 'node tools/ci/site-nav-check.mjs --self-test && node tools/ci/site-nav-check.mjs'
|
||||
run "vendor marks: site/lib/marks.mjs is brand/marks/vendor-marks.mjs byte for byte (the app and the site draw one set)" cmp brand/marks/vendor-marks.mjs site/lib/marks.mjs
|
||||
run "the phone menu opens and is seen at 390 px on every page (self-test first; needs the box or CI browser, says so without one)" node tools/site/sheet-test.mjs --self-test
|
||||
run "padding and visuals: text 24 px from every band, card and section edge, section padding on the scale, no touching controls, media inside its frame, no heading under the bar, at 390 to 1600 px both themes (known-failed fixture first)" bash -c 'node tools/ci/padding-check.mjs --self-test && node tools/ci/padding-check.mjs --site "${SITE_TMP:-.}/site"'
|
||||
run "every served page carries the full header (six items, Download, burger)" node tools/ci/site-nav-check.mjs
|
||||
run "ledger sentences present verbatim on their public pages" node tools/ci/ledger-text-check.mjs
|
||||
never_push_checks
|
||||
run "shell inside .github/workflows parses (bash -n, the PowerShell 5.1 rule)" node tools/ci/check-workflow-shell.mjs
|
||||
run "PowerShell drive-reference check (\$name: in a double-quoted string)" bash tools/ci/ps-drive-ref-check.sh
|
||||
run "copied sources are re-stamped before a build" bash tools/ci/copied-sources-check.sh
|
||||
run "the prover pair is built from the pinned node's exec types (prover-pair-check)" bash -c 'bash tools/ci/prover-pair-check.sh --self-test && bash tools/ci/prover-pair-check.sh'
|
||||
run "override params files parse with no duplicate key" bash tools/ci/override-json-check.sh
|
||||
run "second-engine playbooks log to a file and end their tree (C35)" bash tools/ci/second-engine-check.sh
|
||||
run "no playbook quits, pauses or resumes the installed app" bash -c 'bash tools/ci/playbook-quit-check.sh --self-test && bash tools/ci/playbook-quit-check.sh'
|
||||
|
|
@ -94,6 +79,7 @@ tree_checks() {
|
|||
run "run jobs test their fetched kit before use" bash -c 'bash tools/ci/kit-path-check.sh --self-test && bash tools/ci/kit-path-check.sh'
|
||||
run "every Windows spawn of the app runs with a hidden console" bash -c 'node tools/ci/windows-spawn-check.mjs --self-test && node tools/ci/windows-spawn-check.mjs'
|
||||
run "pinned guest programs match their manifest" bash tools/ci/pinned-guests-check.sh
|
||||
run "no permanent miner fault in the engine (miner-faults.md MF-2)" bash -c 'bash tools/ci/permanent-fault-check.sh --self-test && bash tools/ci/permanent-fault-check.sh'
|
||||
run "root prover playbooks kill the GPU server and unlink its socket" bash tools/ci/prover-socket-check.sh
|
||||
run "commit-string gate self-test" bash tools/ci/commit-string-check.sh --self-test
|
||||
run "build server remote checkout self-test" bash infra/build-server/remote-run.sh --self-test
|
||||
|
|
@ -103,8 +89,6 @@ tree_checks() {
|
|||
run "long-running tools keep their body in one parsed block (the edited-while-running class)" bash -c 'bash tools/ci/whole-body-check.sh --self-test && bash tools/ci/whole-body-check.sh'
|
||||
run "build-remote without a priority flag bounds suites and benches (nice 10, 32 cores); a gate runs unbounded" bash tools/ci/build-kind-default-check.sh
|
||||
run "the class router is a preference with spill-over (a held or overloaded box hands the job to the other one)" bash tools/ci/route-spill-check.sh
|
||||
run "per-core leases, the quiet class and the reaper pass on the box (lease.sh and remote-run.sh self-tests over ssh)" bash tools/ci/box-locks-check.sh $( [ "$MODE" = ci ] && echo --ci )
|
||||
run "the simulators job runs on master and release-* pushes and pull requests into them only" bash tools/ci/sims-branch-check.sh
|
||||
run "no shell assignment hides behind a trailing comment (the swallowed-defaults class)" bash -c 'bash tools/ci/defaults-line-check.sh --self-test && bash tools/ci/defaults-line-check.sh'
|
||||
run "no script kills or finds a process by a plain name or a file name (pgrep/pkill -f literals, ps | grep)" bash -c 'bash tools/ci/kill-by-name-check.sh --self-test && bash tools/ci/kill-by-name-check.sh'
|
||||
run "the identity check's own self-test (excluded research path passes, exported leak fails)" bash tools/ci/identity-check.sh --self-test
|
||||
|
|
@ -115,18 +99,21 @@ tree_checks() {
|
|||
run "the home hero's loop never idles in view, stops hidden, resumes without a jump" node tools/site-redesign/tests/hero-loop-test.cjs
|
||||
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: 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 tools/scene/legend.test.mjs tools/scene/shard-words.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/lib/money.test.mjs site/api/public-stats.test.mjs
|
||||
run "explorer, emission and public stats unit tests" node --test site/lib/explorer.test.mjs site/lib/emission.test.mjs site/lib/money.test.mjs site/lib/leaderboard.test.mjs site/api/public-stats.test.mjs
|
||||
run "ship tool self-test" node tools/ship-app.mjs --self-test
|
||||
run "interface bundle publisher self-test (pack, and sign when the signer is built)" node tools/ui-ota/publish.mjs --self-test
|
||||
run "heat gate reader self-test (a known hold passes, a known drift fails)" node tools/heat-gate.mjs --self-test
|
||||
run "relay unit tests" node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs
|
||||
run "miner app notice strip and update card tests" node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs app/igneum-app/ui/ui-ota.test.mjs app/igneum-app/ui/fold.test.mjs
|
||||
run "the Intel rotate-fold rewrite: Intel gets the shift form, AMD and NVIDIA untouched (proto-opencl/test_intel_rotr.c)" bash -c 'cc -std=c99 -Wall -Wextra -o "${TMPDIR:-/tmp}/test_intel_rotr.$$" proto-opencl/test_intel_rotr.c && "${TMPDIR:-/tmp}/test_intel_rotr.$$"; s=$?; rm -f "${TMPDIR:-/tmp}/test_intel_rotr.$$"; exit $s'
|
||||
run "no secret file names and no 64-hex secrets in the tree" bash -c 'bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh'
|
||||
run "miner app notice strip and update card tests" node --test app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/tune-line.test.mjs
|
||||
run "launch gates: every row with its check, the handoff text clean (self-test, then the tree)" bash -c 'node tools/ci/launch-gates-check.mjs --self-test && node tools/ci/launch-gates-check.mjs'
|
||||
run "income per tier: the public table equals its inputs, the schedule arithmetic" bash -c 'node tools/launch/income-tiers.mjs --check && node --test tools/launch/income-tiers.test.mjs'
|
||||
run "hash-origin report: a known-finished day and a known-failed day" node --test tools/observer/hash-origin.test.mjs
|
||||
run "harness summaries never carry a raw 64-hex key (the writer's own redaction and check)" node infra/fast-time/lib/redact-keys.mjs --self-test
|
||||
run "docs-only pushes skip the compile-or-compute CI jobs (the changes job's classifier)" bash tools/ci/docs-only-check.sh --self-test
|
||||
}
|
||||
|
||||
gated_refs() {
|
||||
|
|
@ -139,32 +126,9 @@ gated_refs() {
|
|||
[ "$full" = 1 ] && echo full || echo light
|
||||
}
|
||||
|
||||
# The green stamp (main, 7 October 2026, the push-race class: a 100 s gate on the merge commit against a master that moves every
|
||||
# minute starved every merge, six rejections in a row). A full gate that ends GREEN over a CLEAN tree records the commit it ran on
|
||||
# in .git/igneum-gate-green/<sha> (the repository's own .git, shared by its worktrees). The hook then lets a MERGE commit through
|
||||
# on the light gate when its first parent is exactly the remote tip being replaced (nothing unknown underneath) and its second
|
||||
# parent carries a stamp younger than 12 hours: the branch's own gate was green on that commit, master's tip was green by its
|
||||
# CI, and CI runs the same full gate on the merge the moment it lands (ci.yml), which is the backstop for the union.
|
||||
# tools/ci/merge-to-master.sh is the merge tool that uses it; its merge message names the stamped commit.
|
||||
stamp_dir() { local g; g=$(git rev-parse --git-common-dir 2>/dev/null) || return 1; ( cd "$g" 2>/dev/null && printf '%s/igneum-gate-green' "$(pwd -P)" ); } # absolute: a worktree's answer is relative
|
||||
stamp_green() { # [repo dir]: after a GREEN full gate; only when no tracked file differs from HEAD (a dirty tree would lie)
|
||||
local d="${1:-.}" sha dir
|
||||
[ -z "$(git -C "$d" status --porcelain --untracked-files=no 2>/dev/null)" ] || return 0
|
||||
sha=$(git -C "$d" rev-parse HEAD 2>/dev/null) || return 0; dir=$(cd "$d" && stamp_dir); mkdir -p "$dir" 2>/dev/null || return 0
|
||||
date -u +%Y-%m-%dT%H:%M:%SZ > "$dir/$sha" 2>/dev/null && echo " (green stamp recorded for ${sha:0:8})"
|
||||
}
|
||||
deferred_merge() { # <local sha> <remote sha> [repo dir] -> "defer <branch sha>" or "full <reason>"
|
||||
local lsha="$1" rsha="$2" d="${3:-.}" parents p1 p2 dir f age
|
||||
parents=$(git -C "$d" rev-list --parents -n 1 "$lsha" 2>/dev/null | cut -d' ' -f2-) || { echo "full unknown commit"; return; }
|
||||
set -- $parents; p1="${1:-}"; p2="${2:-}"; [ -n "$p2" ] && [ -z "${3:-}" ] || { echo "full not a two-parent merge"; return; }
|
||||
[ "$p1" = "$rsha" ] || { echo "full first parent ${p1:0:8} is not the remote tip ${rsha:0:8}"; return; }
|
||||
dir=$(cd "$d" && stamp_dir); f="$dir/$p2"; [ -f "$f" ] || { echo "full no green stamp for ${p2:0:8}"; return; }
|
||||
age=$(( $(date +%s) - $(stat -f %m "$f" 2>/dev/null || stat -c %Y "$f") )); [ "$age" -le 43200 ] || { echo "full the stamp for ${p2:0:8} is $age s old"; return; }
|
||||
echo "defer $p2"
|
||||
}
|
||||
finish() {
|
||||
local what="$1" secs=$(( $(date +%s) - T0 ))
|
||||
if [ "$RED" = 0 ]; then echo "pre-push gate ($what): GREEN, $N checks in ${secs}s"; [ "${STAMP:-0}" = 1 ] && stamp_green; exit 0; fi
|
||||
if [ "$RED" = 0 ]; then echo "pre-push gate ($what): GREEN, $N checks in ${secs}s"; exit 0; fi
|
||||
echo "pre-push gate ($what): RED after $N checks in ${secs}s. Fix it before pushing (every check above is one CI runs; the same script runs there)." >&2
|
||||
exit 1
|
||||
}
|
||||
|
|
@ -186,46 +150,22 @@ case "$MODE" in
|
|||
declare -f never_push_checks | grep -q 'tools/ci/no-secrets-check.sh' || { echo "self-test failed: the never-push checks do not run the no-secrets check"; fails=1; }
|
||||
declare -f never_push_checks | grep -q 'tools/ci/identity-check.sh' || { echo "self-test failed: the never-push checks do not run the identity grep"; fails=1; }
|
||||
grep -qE '^\s+structural_checks; never_push_checks; finish "feature branch"' "$0" || { echo "self-test failed: the hook's light gate does not run the never-push checks"; fails=1; }
|
||||
# the green stamp and the deferral, in a fixture repository: a merge of a stamped branch onto the remote tip defers; an
|
||||
# unstamped branch, a stale stamp, a merge onto an older tip and a plain commit all take the full gate
|
||||
fx=$(mktemp -d); ( cd "$fx" && git init -q -b master . && git -c user.name=t -c user.email=t@t commit -q --allow-empty -m a ) 2>/dev/null
|
||||
A=$(git -C "$fx" rev-parse HEAD); git -C "$fx" checkout -q -b b; git -C "$fx" -c user.name=t -c user.email=t@t commit -q --allow-empty -m b; B=$(git -C "$fx" rev-parse HEAD)
|
||||
git -C "$fx" checkout -q master; git -C "$fx" -c user.name=t -c user.email=t@t merge -q --no-ff -m "merge b" b; M=$(git -C "$fx" rev-parse HEAD)
|
||||
[ "$(deferred_merge "$M" "$A" "$fx")" = "full no green stamp for ${B:0:8}" ] || { echo "self-test failed: an unstamped branch merge was not sent to the full gate: $(deferred_merge "$M" "$A" "$fx")"; fails=1; }
|
||||
( cd "$fx" && git checkout -q "$B" && stamp_green . >/dev/null && git checkout -q master )
|
||||
[ "$(deferred_merge "$M" "$A" "$fx")" = "defer $B" ] || { echo "self-test failed: a stamped branch merge onto the remote tip was not deferred: $(deferred_merge "$M" "$A" "$fx")"; fails=1; }
|
||||
[ "$(deferred_merge "$M" "$B" "$fx")" = "full first parent ${A:0:8} is not the remote tip ${B:0:8}" ] || { echo "self-test failed: a merge onto another tip was deferred"; fails=1; }
|
||||
[ "$(deferred_merge "$B" "$A" "$fx")" = "full not a two-parent merge" ] || { echo "self-test failed: a plain commit was deferred"; fails=1; }
|
||||
touch -t 202001010000 "$(cd "$fx" && stamp_dir)/$B"; case "$(deferred_merge "$M" "$A" "$fx")" in "full the stamp for ${B:0:8} is "*) ;; *) echo "self-test failed: a stale stamp was honoured"; fails=1 ;; esac
|
||||
( cd "$fx" && echo x > dirty && git add dirty && git -c user.name=t -c user.email=t@t commit -q -m d && echo y > dirty && stamp_green . | grep -q recorded ) && { echo "self-test failed: a dirty tree was stamped"; fails=1; }
|
||||
rm -rf "$fx"
|
||||
# a --ci site build outside GitHub Actions leaves the tracked site files as they are (the horizon lane's four rewritten files)
|
||||
before=$(git status --porcelain -- site | sort); ( MODE=ci GITHUB_ACTIONS= site_build >/dev/null 2>&1 ); after=$(git status --porcelain -- site | sort)
|
||||
[ "$before" = "$after" ] || { echo "self-test failed: a --ci site build outside GitHub Actions changed tracked site files: $(git status --porcelain -- site | tr '\n' ' ')"; fails=1; }
|
||||
MODE=ci GITHUB_ACTIONS= site_in_place && { echo "self-test failed: --ci outside GitHub Actions chose the in-place build"; fails=1; }
|
||||
MODE=ci GITHUB_ACTIONS=true site_in_place || { echo "self-test failed: --ci inside GitHub Actions did not choose the in-place build"; fails=1; }
|
||||
declare -f tree_checks | grep -q 'never_push_checks' || { echo "self-test failed: the full gate does not run the never-push checks"; fails=1; }
|
||||
[ "$fails" = 0 ] && echo "self-test passed: a failing check is RED and fails the gate, a passing one is ok; master and release-* select the full gate, other refs the light one; a merge of a green-stamped branch onto the remote tip defers to CI, every other shape takes the full gate; a --ci site build outside GitHub Actions leaves the tree unchanged (structural checks, no-secrets, identity grep)"
|
||||
[ "$fails" = 0 ] && echo "self-test passed: a failing check is RED and fails the gate, a passing one is ok; master and release-* select the full gate, other refs the light one (structural checks, no-secrets, identity grep)"
|
||||
exit $fails ;;
|
||||
list)
|
||||
grep -E '^\s+run "' "$0" | sed -E 's/^\s+run "([^"]+)".*/\1/' ;;
|
||||
hook)
|
||||
REFS="$(cat)"; which="$(printf '%s\n' "$REFS" | gated_refs)"
|
||||
which="$(gated_refs)"
|
||||
if [ "$which" = full ]; then
|
||||
# a merge of a green-stamped branch onto the exact remote tip goes through on the light gate (CI runs the full one)
|
||||
verdict=""; while read -r lref lsha rref rsha; do case "$rref" in refs/heads/master|refs/heads/release-*) verdict=$(deferred_merge "$lsha" "$rsha"); break ;; esac; done <<<"$REFS"
|
||||
case "$verdict" in
|
||||
defer*) echo "pre-push gate: a merge of green-stamped ${verdict#defer } onto the remote tip: the light gate here, the full gate in CI on landing:"
|
||||
structural_checks; never_push_checks; finish "merge of a green branch (full gate deferred to CI)" ;;
|
||||
*) echo "pre-push gate: a push to master or release-*, the full gate (the same checks CI runs)${verdict:+ ($verdict)}:"
|
||||
STAMP=1; structural_checks; tree_checks; finish "push to master or release-*" ;;
|
||||
esac
|
||||
echo "pre-push gate: a push to master or release-*, the full gate (the same checks CI runs):"
|
||||
structural_checks; tree_checks; finish "push to master or release-*"
|
||||
else
|
||||
echo "pre-push gate: a feature branch, the light gate (the two structural checks, the no-secrets check, the identity grep):"
|
||||
structural_checks; never_push_checks; finish "feature branch"
|
||||
fi ;;
|
||||
ci|local)
|
||||
[ "$MODE" = ci ] && echo "pre-push gate in CI (the same script as the local hook):" || echo "pre-push gate over this working tree:"
|
||||
[ "$MODE" = local ] && STAMP=1; structural_checks; tree_checks; finish "$MODE" ;;
|
||||
structural_checks; tree_checks; finish "$MODE" ;;
|
||||
*) echo "usage: tools/ci/pre-push.sh [--ci|--hook|--self-test|--list]" >&2; exit 2 ;;
|
||||
esac
|
||||
|
|
|
|||
70
tools/scene/legend.test.mjs
Normal file
70
tools/scene/legend.test.mjs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// node --test tools/scene/legend.test.mjs (key-22, the project lead's screenshot of the app's chain key against /live, 7 October 2026)
|
||||
// The key is ONE export: IgneumDag.legend({ mine }) in scene/live-dag.js (entries, order, token names, shapes); the app's
|
||||
// chain card and the site's /live key render from it, so they cannot drift. This test fails when either key's entries,
|
||||
// order or tokens differ from the export, and when a colour the scene draws on blocks has no key entry.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
const here = dirname(fileURLToPath(import.meta.url)), root = join(here, '../..');
|
||||
const src = readFileSync(join(root, 'scene/live-dag.js'), 'utf8');
|
||||
const win = { document: { hidden: false, addEventListener() {}, createElement: () => ({ style: {} }) }, addEventListener() {}, matchMedia: () => ({ matches: false, addEventListener() {} }), getComputedStyle: () => ({ getPropertyValue: () => '' }), devicePixelRatio: 1, innerWidth: 1280 };
|
||||
new Function('window', 'document', src)(win, win.document);
|
||||
const Dag = win.IgneumDag;
|
||||
const SITE_ORDER = ['Pending', 'Included', 'Excluded', 'Selected chain', 'Proven', 'Locked checkpoint'];
|
||||
// the entries a page's markup carries today, from its literal spans (a hard-coded key) or none (a rendered key)
|
||||
const literal = (html, tag) => [...html.matchAll(new RegExp('<span><i class="' + tag + '[^>]*></i>([^<]+)</span>', 'g'))].map((m) => m[1].trim());
|
||||
|
||||
test('the export: Pending first, Included, Excluded, Selected chain, Proven, Locked checkpoint; the app adds Your blocks first; every entry names its token and shape (known-failed on 0.3.22)', () => {
|
||||
assert.ok(Dag && typeof Dag.legend === 'function', 'IgneumDag.legend exists');
|
||||
const site = Dag.legend({ mine: false }), app = Dag.legend({ mine: true });
|
||||
assert.deepEqual(site.map((e) => e.label), SITE_ORDER);
|
||||
assert.deepEqual(app.map((e) => e.label), ['Your blocks'].concat(SITE_ORDER));
|
||||
for (const e of app) { assert.match(e.token, /^--[a-z-]+$/, e.label + ' token'); assert.ok(['square', 'circle', 'tick', 'ringed'].includes(e.shape), e.label + ' shape'); assert.ok(e.id); }
|
||||
assert.equal(app[0].token, '--molten'); assert.equal(app[0].shape, 'ringed');
|
||||
assert.equal(site.find((e) => e.label === 'Included').token, '--included', 'the key draws Included in the colour the scene draws it');
|
||||
assert.equal(site.find((e) => e.label === 'Excluded').token, '--excluded');
|
||||
assert.equal(site.find((e) => e.label === 'Locked checkpoint').shape, 'circle');
|
||||
assert.equal(site.find((e) => e.label === 'Proven').shape, 'tick');
|
||||
assert.equal(typeof Dag.renderLegend, 'function', 'one renderer for both pages');
|
||||
});
|
||||
|
||||
test('every colour the scene draws on a block or a band has a key entry', () => {
|
||||
const tokens = new Set(Dag.legend({ mine: true }).map((e) => e.token));
|
||||
// the renderer's palette read: col={ember:c('--ember',…), …, blue:c('--included',…), red:c('--excluded',…)}
|
||||
const read = [...src.matchAll(/c\('(--[a-z0-9-]+)'/g)].map((m) => m[1]);
|
||||
const drawn = ['--included', '--excluded', '--ember', '--molten', '--bone', '--ash'];
|
||||
for (const t of drawn) { assert.ok(read.includes(t), 'the scene reads ' + t); assert.ok(tokens.has(t), 'the key has an entry in ' + t); }
|
||||
});
|
||||
|
||||
// the /live key as ruled (7 October 2026): Included, Excluded, Selected chain, Proven, Locked checkpoint; no Pending and no
|
||||
// Your blocks (the site has no "you"). The exceptions are the ruling's; every entry the two surfaces share must carry the
|
||||
// same wording and the same token as the export, so the swatches cannot drift apart again.
|
||||
const SITE_RULED = SITE_ORDER.filter((l) => l !== 'Pending');
|
||||
// the site deploys from master alone and a release-* branch carries site/ frozen as it was cut (tools/scene/sync.mjs's scope
|
||||
// rule), so the site half binds on master (or SCENE_SYNC_SCOPE=site, or a site/live.html passed as SITE_LIVE_HTML) and is a
|
||||
// diagnostic on a release branch
|
||||
const branch = (() => { try { return execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch (e) { return ''; } })();
|
||||
const siteBinds = process.env.SCENE_SYNC_SCOPE === 'site' || !!process.env.SITE_LIVE_HTML || branch === 'master';
|
||||
// the app lives on the release branches (master carries no app/igneum-app/ui/live-dag.js), so the app half binds where that copy exists
|
||||
const appBinds = existsSync(join(root, 'app/igneum-app/ui/live-dag.js'));
|
||||
const siteEntries = (html) => [...html.matchAll(/<span><i class="lgi([^"]*)"(?: style="([^"]*)")?><\/i>([^<]+)<\/span>/g)].map((m) => { const tok = /var\((--[a-z0-9-]+)\)/.exec(m[2] || ''); return { label: m[3].trim(), token: tok ? tok[1] : (/\btick\b/.test(m[1]) ? '--bone' : ''), shape: /\btick\b/.test(m[1]) ? 'tick' : /border-radius:50%/.test(m[2] || '') ? 'circle' : 'square' }; });
|
||||
|
||||
test('the app key is the export, not a hand-written list; the site key is the export minus the ruled exceptions (Pending, Your blocks), same wording and tokens on every shared entry', (t) => {
|
||||
const app = readFileSync(join(root, 'app/igneum-app/ui/index.html'), 'utf8'), site = readFileSync(process.env.SITE_LIVE_HTML || join(root, 'site/live.html'), 'utf8');
|
||||
const appLit = literal(app, 'lg');
|
||||
if (appBinds) { assert.ok(/data-legend="mine"/.test(app), 'the app legend renders from IgneumDag.legend({ mine: true })'); assert.deepEqual(appLit, [], 'hand-written entries in the app key: ' + appLit.join(', ')); }
|
||||
else t.diagnostic('no app scene copy in this tree (master): the app half binds on the release branches');
|
||||
const got = siteEntries(site), exp = Dag.legend({ mine: false }).filter((e) => e.label !== 'Pending'), problems = [];
|
||||
if (JSON.stringify(got.map((e) => e.label)) !== JSON.stringify(SITE_RULED)) problems.push('the /live key reads ' + got.map((e) => e.label).join(', ') + ', ruled: ' + SITE_RULED.join(', '));
|
||||
for (const e of exp) { const g = got.find((x) => x.label === e.label); if (!g) continue; if (g.token !== e.token) problems.push(e.label + ' token on /live is ' + g.token + ', the export says ' + e.token); if (g.shape !== e.shape) problems.push(e.label + ' shape on /live is ' + g.shape + ', the export says ' + e.shape); }
|
||||
if (got.some((e) => /your/i.test(e.label))) problems.push('the site shows Your block; the ruling has none');
|
||||
if (siteBinds) assert.deepEqual(problems, [], 'the /live key against the export:\n' + problems.join('\n'));
|
||||
else if (problems.length) t.diagnostic('site/live.html on this release branch is the frozen cut (' + problems.join('; ') + '); the check binds on master, where the site deploys from');
|
||||
// the app's source line stays
|
||||
if (appBinds) assert.match(readFileSync(join(root, 'app/igneum-app/ui/app.js'), 'utf8'), /From Igneum’s observer\. Your blocks are ringed\./);
|
||||
// the copies are the source (the sync gate says the same; this fails first when a legend edit misses the sync)
|
||||
if (appBinds) assert.equal(readFileSync(join(root, 'app/igneum-app/ui/live-dag.js'), 'utf8'), src, 'the app copy of live-dag.js is the scene source');
|
||||
});
|
||||
54
tools/scene/shard-words.test.mjs
Normal file
54
tools/scene/shard-words.test.mjs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// node --test tools/scene/shard-words.test.mjs (7 October 2026: "why does the animation on site say no shard plan yet?")
|
||||
// IgneumDag.shardWords(block, nowMs) is the one source of the shard sentence the scene's tooltip, the site's /live
|
||||
// inspector and the app's inspector print. The empty case says what the block's own facts say: a block under 10 s old
|
||||
// with no shards is loading its plan, an excluded block has nothing to prove, a pending block is not yet ordered, a
|
||||
// blue block off the selected chain gets no plan; a planned shard with no prover is awaiting one, with the block's age;
|
||||
// proving, verified and paid as before.
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFileSync, existsSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
const here = dirname(fileURLToPath(import.meta.url)), root = join(here, '../..');
|
||||
const src = readFileSync(join(root, 'scene/live-dag.js'), 'utf8');
|
||||
const win = { document: { hidden: false, addEventListener() {}, createElement: () => ({ style: {} }) }, addEventListener() {}, matchMedia: () => ({ matches: false, addEventListener() {} }), getComputedStyle: () => ({ getPropertyValue: () => '' }), devicePixelRatio: 1, innerWidth: 1280 };
|
||||
new Function('window', 'document', src)(win, win.document);
|
||||
const Dag = win.IgneumDag;
|
||||
const NOW = 1_791_378_800_000;
|
||||
// the site deploys from master alone and a release-* branch carries site/ frozen as it was cut, so the site inspector's
|
||||
// check binds on master (or SCENE_SYNC_SCOPE=site) and is a diagnostic on a release branch
|
||||
const branch = (() => { try { return execSync('git rev-parse --abbrev-ref HEAD', { cwd: root, stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim(); } catch (e) { return ''; } })();
|
||||
const siteBinds = process.env.SCENE_SYNC_SCOPE === 'site' || branch === 'master';
|
||||
const appBinds = existsSync(join(root, 'app/igneum-app/ui/live-dag.js')); // the app lives on the release branches
|
||||
const block = (over) => ({ hash: 'ab12', ts: NOW - 42_000, color: 'blue', chain: true, miner: 'k1', parents: [], blue_score: 1, daa: 1, shards: [], locked: false, final: false, proven: false, ...over });
|
||||
|
||||
test('the four empty cases (known-failed on 2.0.5): loading under 10 s, excluded, not yet ordered, off the selected chain; an old chain block with no plan says how long', () => {
|
||||
assert.ok(Dag && typeof Dag.shardWords === 'function', 'IgneumDag.shardWords exists');
|
||||
assert.equal(Dag.shardWords(block({ ts: NOW - 4_000 }), NOW).summary, 'shard plan loading');
|
||||
assert.equal(Dag.shardWords(block({ color: 'red', chain: false }), NOW).summary, 'excluded, nothing to prove');
|
||||
assert.equal(Dag.shardWords(block({ color: 'pending', chain: false }), NOW).summary, 'not yet ordered');
|
||||
assert.equal(Dag.shardWords(block({ chain: false }), NOW).summary, 'off the selected chain, no shards planned');
|
||||
assert.equal(Dag.shardWords(block(), NOW).summary, 'no shard plan after 42 s');
|
||||
for (const s of [Dag.shardWords(block({ ts: NOW - 4_000 }), NOW), Dag.shardWords(block({ color: 'red' }), NOW)]) assert.deepEqual(s.items, []);
|
||||
assert.equal(/no shard plan yet/.test(src), false, 'the old words are gone from the scene');
|
||||
});
|
||||
|
||||
test('a planned shard with no prover is awaiting one, with the block age; assigned, proving, verified and paid as before', (t) => {
|
||||
const sh = [{ i: 0, n: 4, state: 'planned', prover: null }, { i: 1, n: 4, state: 'planned', prover: 'faa34a1a' }, { i: 2, n: 4, state: 'proving', prover: 'b2' }, { i: 3, n: 4, state: 'paid', prover: 'c3', payout: 4.5532 }];
|
||||
const w = Dag.shardWords(block({ shards: sh }), NOW);
|
||||
assert.deepEqual(w.items.map((x) => x.word), ['awaiting a prover · 42 s', 'assigned to faa34a1a', 'proving by b2', 'paid 4.5532 IGN']);
|
||||
assert.equal(w.summary, '1 paid, 1 proving, 1 assigned, 1 awaiting a prover · 42 s');
|
||||
const old = Dag.shardWords(block({ ts: NOW - 400_000, shards: [{ state: 'planned', prover: null }] }), NOW);
|
||||
assert.equal(old.items[0].word, 'awaiting a prover · 7 min'); assert.equal(old.summary, '1 awaiting a prover · 7 min');
|
||||
const done = Dag.shardWords(block({ shards: [{ state: 'verified' }, { state: 'paid', payout: 1 }] }), NOW);
|
||||
assert.equal(done.summary, '1 paid, 1 verified'); assert.deepEqual(done.items.map((x) => x.word), ['verified', 'paid 1 IGN']);
|
||||
// the tooltip line uses it (words() is an instance method, so the source line is the check)
|
||||
assert.match(src, /'Shards: '\+shardWords\(b,nowMs\)\.summary/, 'the tooltip takes the shared sentence');
|
||||
// both inspectors use it: no hand-written shard sentence left
|
||||
const live = readFileSync(join(root, 'site/live.html'), 'utf8'), siteOk = !/no shard plan yet/i.test(live) && /IgneumDag\.shardWords\(/.test(live);
|
||||
if (siteBinds) assert.ok(siteOk, 'the site inspector still carries its own shard sentence');
|
||||
else if (!siteOk) t.diagnostic('site/live.html on this release branch is the frozen cut; the inspector check binds on master, where the site deploys from');
|
||||
if (appBinds) { const app = readFileSync(join(root, 'app/igneum-app/ui/app.js'), 'utf8'); assert.equal(/no shard plan yet/i.test(app), false, 'the app inspector'); assert.match(app, /IgneumDag\.shardWords\(/, 'the app inspector takes the shared sentence'); }
|
||||
else t.diagnostic('no app scene copy in this tree (master): the app half binds on the release branches');
|
||||
});
|
||||
|
|
@ -33,6 +33,8 @@ createServer(async (req, res) => {
|
|||
if (p.startsWith('/api/')) {
|
||||
const name = p.slice(5).replace(/[^a-z0-9_-]/gi, '');
|
||||
if (!existsSync(join(site, 'api', name + '.mjs'))) { res.statusCode = 404; return res.end('no such function'); }
|
||||
// IGNEUM_LIVE_FIXTURE=<path>: answer /api/live from a recorded reply with its clock moved to now (captures and tests off the database)
|
||||
if (name === 'live' && process.env.IGNEUM_LIVE_FIXTURE) { const f = JSON.parse(readFileSync(process.env.IGNEUM_LIVE_FIXTURE, 'utf8')); const shift = Date.now() - new Date(f.now).getTime(); f.now = new Date().toISOString(); for (const b of f.blocks || []) b.ts += shift; res.setHeader('Content-Type', 'application/json'); return res.end(JSON.stringify(f)); }
|
||||
try { const h = await api(name); return await h({ method: req.method, url: req.url, headers: req.headers, query: Object.fromEntries(url.searchParams), socket: req.socket }, shim(res)); }
|
||||
catch (e) { res.statusCode = 500; return res.end(String(e.stack || e)); }
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue