Merge site-ui-3: the DAG scene never captures the page scroll; the miners strip and five-line events on /live
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
bda9c644b9
6 changed files with 54 additions and 25 deletions
Binary file not shown.
|
Before Width: | Height: | Size: 542 KiB After Width: | Height: | Size: 218 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 354 KiB After Width: | Height: | Size: 138 KiB |
File diff suppressed because one or more lines are too long
|
|
@ -4,10 +4,12 @@
|
|||
are drawn; the edge between two selected-chain blocks is heavier and the chain glows as one path. Colour words are the
|
||||
ledger's: pending, included (blue), excluded (red), proven (every shard verified or paid), locked (a checkpoint block).
|
||||
Checkpoints are vertical bands at their blue score with the lock weight; a locked band is solid, a pending one dashed.
|
||||
The view scrolls at chain speed, new blocks arrive from the right with a brief glow; wheel and pinch zoom the window
|
||||
(30 to 300 s, the API's ?window=); a pause button; hover and tap show a block's details.
|
||||
The view scrolls at chain speed, new blocks arrive from the right with a brief glow. The page's scroll passes through the
|
||||
scene; zoom takes ctrl or cmd with the wheel, a pinch, or a click into the scene (engaged, released by Escape or a click
|
||||
outside; opts.chip is the "scroll to zoom, Esc to release" element); window 30 to 300 s (the API's ?window=); a pause button;
|
||||
hover and tap show a block's details.
|
||||
|
||||
Usage: var dag = IgneumDag.mount(canvas, { compact, window, url, poll, mine, onState, onData, onWindow, pauseButton, tooltip })
|
||||
Usage: var dag = IgneumDag.mount(canvas, { compact, window, url, poll, mine, onState, onData, onWindow, onEngage, pauseButton, tooltip, chip })
|
||||
- url: the /api/live endpoint (default '/api/live'); poll: false stops the module's own 2 s fetch, the host then calls
|
||||
dag.push(reply) with a reply of the same shape ({ok, state:{stale, age_s, height, ...}, blocks:[...], finality:{checkpoints:[...]}, proving})
|
||||
whenever it has new data (every poll or every new block, either is fine; blocks already on screen are updated in place).
|
||||
|
|
@ -196,8 +198,20 @@
|
|||
}
|
||||
canvas.addEventListener('mousemove', function (ev) { hover = hit(ev); canvas.style.cursor = hover ? 'pointer' : 'default'; if (!pinned) showTip(hover, ev); kick(); });
|
||||
canvas.addEventListener('mouseleave', function () { hover = null; if (!pinned) showTip(null); kick(); });
|
||||
canvas.addEventListener('click', function (ev) { var b = hit(ev); pinned = (pinned && b && pinned.hash === b.hash) ? null : b; showTip(pinned || hover); kick(); });
|
||||
canvas.addEventListener('wheel', function (ev) { if (!ev.ctrlKey && Math.abs(ev.deltaY) < 1) return; ev.preventDefault(); zoom(ev.deltaY > 0 ? 1.15 : 1 / 1.15); }, { passive: false });
|
||||
canvas.addEventListener('click', function (ev) { engage(true); var b = hit(ev); pinned = (pinned && b && pinned.hash === b.hash) ? null : b; showTip(pinned || hover); kick(); });
|
||||
// the wheel is never captured by default (6 Oct 2026, the owner: scrolling past the scene zoomed it): the page scrolls through the
|
||||
// scene; zoom takes ctrl or cmd with the wheel, a pinch, or a click into the scene, which engages it (opts.chip shows
|
||||
// "scroll to zoom, Esc to release"); Escape, a click outside or leaving the scene releases it
|
||||
var engaged = false, chip = opts.chip || null;
|
||||
function engage(on) { if (on === engaged) return; engaged = on; canvas.classList.toggle('engaged', on); if (chip) chip.hidden = !on; if (opts.onEngage) opts.onEngage(on); }
|
||||
canvas.addEventListener('wheel', function (ev) {
|
||||
var zoomKey = ev.ctrlKey || ev.metaKey;
|
||||
if (!zoomKey && !engaged) return; // a plain wheel or a two-finger scroll passes through to the page
|
||||
if (Math.abs(ev.deltaY) < 1) return;
|
||||
ev.preventDefault(); zoom(ev.deltaY > 0 ? 1.15 : 1 / 1.15);
|
||||
}, { passive: false });
|
||||
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape' && engaged) engage(false); });
|
||||
document.addEventListener('pointerdown', function (ev) { if (engaged && !canvas.contains(ev.target) && !(chip && chip.contains(ev.target))) engage(false); });
|
||||
var pinch = null;
|
||||
canvas.addEventListener('touchstart', function (ev) { if (ev.touches.length === 2) pinch = dist(ev.touches); }, { passive: true });
|
||||
canvas.addEventListener('touchmove', function (ev) { if (ev.touches.length === 2 && pinch) { var d = dist(ev.touches); if (Math.abs(d - pinch) > 12) { zoom(d > pinch ? 1 / 1.1 : 1.1); pinch = d; } } }, { passive: true });
|
||||
|
|
@ -216,7 +230,7 @@
|
|||
new MutationObserver(function () { readTheme(); kick(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
|
||||
readTheme(); size(); if (opts.poll !== false) { poll(); setInterval(poll, 2000); } kick();
|
||||
return { push: push, setPaused: setPaused, zoom: zoom, getWindow: function () { return windowS; }, getState: function () { return { state: state, reason: reason }; }, redraw: kick, words: words };
|
||||
return { push: push, setPaused: setPaused, zoom: zoom, engage: engage, getWindow: function () { return windowS; }, getState: function () { return { state: state, reason: reason }; }, redraw: kick, words: words };
|
||||
}
|
||||
root.IgneumDag = { mount: mount };
|
||||
})(window);
|
||||
|
|
|
|||
|
|
@ -89,6 +89,7 @@ h1,h2,h3{font-family:var(--f-display);margin:0;line-height:1.08;text-wrap:balanc
|
|||
.legend .lk{font-family:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);min-width:64px}
|
||||
.legend span.txt{white-space:normal;display:inline;color:var(--ash)}
|
||||
.two{display:grid;gap:var(--gap);grid-template-columns:1fr;margin-top:var(--gap)}
|
||||
@media (min-width:900px){.two{grid-template-columns:1fr 1fr}}
|
||||
@media (min-width:900px){.two{grid-template-columns:1.1fr .9fr}}
|
||||
td.n{font-variant-numeric:tabular-nums;text-align:right}th.n{text-align:right}
|
||||
.id{font-family:var(--f-mono);color:var(--molten)}
|
||||
|
|
@ -96,6 +97,12 @@ td.n{font-variant-numeric:tabular-nums;text-align:right}th.n{text-align:right}
|
|||
.bar i{display:block;height:100%;background:var(--ember)}
|
||||
.feed{display:flex;flex-direction:column;margin-top:12px;font-family:var(--f-mono);font-size:13px;color:var(--ink-2)}
|
||||
.feed div{display:flex;justify-content:space-between;gap:12px;border-bottom:1px solid var(--line);padding:9px 0;align-items:baseline}
|
||||
.feed div>span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}
|
||||
.lanes{display:flex;flex-direction:column;margin-top:12px}
|
||||
.lane{display:grid;grid-template-columns:84px minmax(0,1fr) 72px 64px;gap:12px;align-items:center;padding:8px 0;border-bottom:1px solid var(--line);font-family:var(--f-mono);font-size:13px;color:var(--ink-2)}
|
||||
.lane:last-child{border-bottom:0}
|
||||
.lane .n{text-align:right;white-space:nowrap}.lane .t{color:var(--ash);font-size:12px;text-align:right;white-space:nowrap}
|
||||
@media (max-width:480px){.lane{grid-template-columns:76px minmax(0,1fr) 60px}.lane .t{display:none}}
|
||||
.feed div:last-child{border-bottom:0}
|
||||
.feed .t{color:var(--ash);flex:0 0 auto;font-size:12px}
|
||||
.feed .k{color:var(--molten)}
|
||||
|
|
@ -175,7 +182,7 @@ main{padding-bottom:var(--sec)}
|
|||
<div class="state" id="dag-state"><span class="dot off"></span>connecting to the devnet observer</div>
|
||||
<div class="viz-stats mono"><span>on screen <b id="c-screen">0</b></span><span>chain <b id="c-chain">0</b></span><span>identities <b id="c-idents">0</b></span><span>last lock <b id="c-locked">pending</b></span><span>proven <b id="c-proven">pending</b></span></div>
|
||||
</div>
|
||||
<div class="scene"><canvas id="dag" aria-label="The devnet's block DAG, live: blocks by time and miner lane, parent edges, the selected chain, checkpoint bands with their lock weight, proofs filling blocks as segments are paid"></canvas><div class="tip" id="dag-tip" hidden></div></div>
|
||||
<div class="scene"><canvas id="dag" aria-label="The devnet's block DAG, live: blocks by time and miner lane, parent edges, the selected chain, checkpoint bands with their lock weight, proofs filling blocks as segments are paid"></canvas><div class="tip" id="dag-tip" hidden></div><div class="chip-zoom" id="dag-chip" hidden><span class="dot live"></span>scroll to zoom, Esc to release</div></div>
|
||||
<div class="legend">
|
||||
<span><i class="sw" style="border:2px solid var(--line-2)"></i>pending</span>
|
||||
<span><i class="sw" style="border:2px solid var(--bone)"></i>included</span><span><i class="sw" style="border:2px solid var(--ember)"></i>selected chain</span>
|
||||
|
|
@ -185,23 +192,19 @@ main{padding-bottom:var(--sec)}
|
|||
<span><i class="sw" style="width:0;height:12px;border-left:2px solid var(--ember);border-radius:0"></i>finality band, locked, with its weight</span>
|
||||
<span><i class="sw" style="width:0;height:12px;border-left:2px dashed var(--ember);border-radius:0;opacity:.5"></i>finality band, pending</span>
|
||||
</div>
|
||||
<div class="scene-foot"><p class="note" style="margin:0"><span id="c-window">120 s of blocks on screen</span>. One lane per miner, the busiest first; the rest share the last lane. Hover or tap a block for its hash, miner, blue score, parents and proof state. Scroll or pinch to change the window; the page's <code>?window=</code> fixes it.</p><button type="button" class="btn small" id="dag-pause" aria-pressed="false">Pause</button></div>
|
||||
<div class="scene-foot"><p class="note" style="margin:0"><span id="c-window">120 s of blocks on screen</span>. One lane per miner, the busiest first; the rest share the last lane. Hover or tap a block for its hash, miner, blue score, parents and proof state. Ctrl or cmd and scroll, a pinch, or a click into the scene then scroll, changes the window; the page's <code>?window=</code> fixes it.</p><button type="button" class="btn small" id="dag-pause" aria-pressed="false">Pause</button></div>
|
||||
</div>
|
||||
|
||||
<div class="two">
|
||||
<div class="card">
|
||||
<div class="viz-head"><h2>Miners</h2><div class="eyebrow">last 10 min</div></div>
|
||||
<div class="tbl">
|
||||
<table>
|
||||
<thead><tr><th>Miner</th><th class="n">Blocks</th><th>Share</th><th>Last seen</th><th>Engine</th></tr></thead>
|
||||
<tbody id="miners"><tr><td colspan="5" class="empty">No blocks in the last 10 minutes.</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="note">The short id is the first 8 hex characters of the vote key hash in each header. The engine is what the miner writes into the coinbase; the devnet miner writes nothing yet.</p>
|
||||
<div class="viz-head"><h2>Miners</h2><div class="eyebrow" id="miners-count">last 10 min</div></div>
|
||||
<div class="lanes" id="miners"><div class="empty">No blocks in the last 10 minutes.</div></div>
|
||||
<p class="note" id="miners-more">Every vote key with a block in the last 10 minutes; a card runs several. The short id is the first 8 hex characters of the vote key hash in each header.</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="viz-head"><h2>Events</h2><div class="eyebrow">newest first</div></div>
|
||||
<div class="viz-head"><h2>Events</h2><div class="eyebrow">the last five</div></div>
|
||||
<div class="feed" id="events"><div class="empty">No events yet.</div></div>
|
||||
<p class="note" id="events-more">Checkpoint locks, miners going quiet and coming back, provers seen, as the observer records them.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -293,7 +296,7 @@ main{padding-bottom:var(--sec)}
|
|||
var dagCanvas=$('dag'),dag=null,lastD=null;
|
||||
function windowWord(w){return w+' s of blocks on screen';}
|
||||
function mountDag(){if(dag||!dagCanvas||!window.IgneumDag)return;
|
||||
dag=IgneumDag.mount(dagCanvas,{poll:false,window:fixedWindow||120,tooltip:$('dag-tip'),pauseButton:$('dag-pause'),
|
||||
dag=IgneumDag.mount(dagCanvas,{poll:false,window:fixedWindow||120,tooltip:$('dag-tip'),chip:$('dag-chip'),pauseButton:$('dag-pause'),
|
||||
onWindow:function(w){$('c-window').textContent=windowWord(w);},
|
||||
onState:function(st,reason){var el=$('dag-state');if(!el)return;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':st==='connecting'?'connecting to the devnet observer':esc(reason||'live feed unavailable'));}});
|
||||
$('c-window').textContent=windowWord(dag.getWindow());if(lastD)dag.push(lastD);}
|
||||
|
|
@ -337,9 +340,15 @@ main{padding-bottom:var(--sec)}
|
|||
var hr=hashrate(s.hashes_per_second_estimate).split(' ');$('st-hps').innerHTML=hr.length>1?esc(hr[0])+'<small>'+esc(hr[1])+'</small>':esc(hr[0]);
|
||||
$('st-miners').textContent=s.miners_10m;$('st-peers').textContent=s.peers===null?'0':s.peers;$('st-mempool').textContent='mempool '+(s.mempool||0);
|
||||
var now=new Date(d.now).getTime();
|
||||
if(d.miners.length){minersEl.innerHTML=d.miners.map(function(m){return '<tr><td><span class="id">'+esc(m.id)+'</span></td><td class="n">'+m.blocks+'</td><td><div style="display:flex;align-items:center;gap:8px"><div class="bar" style="width:70px"><i style="width:'+m.share+'%"></i></div><span class="mono" style="font-size:12px">'+m.share+'%</span></div></td><td>'+rel(now-new Date(m.last_seen).getTime())+'</td><td>'+(m.engine?esc(m.engine):'<span style="color:var(--ash)">not reported</span>')+'</td></tr>';}).join('');}
|
||||
else minersEl.innerHTML='<tr><td colspan="5" class="empty">No blocks in the last 10 minutes.</td></tr>';
|
||||
if(d.events.length){eventsEl.innerHTML=d.events.map(function(e){return '<div><span><span class="k">'+esc(e.kind)+'</span> '+esc(e.text)+'</span><span class="t">'+rel(now-new Date(e.ts).getTime())+'</span></div>';}).join('');}
|
||||
// miners: one compact strip, the top five lanes by blocks with a bar each, the rest as a count (the full table is the bench page's job)
|
||||
if(d.miners.length){var top=d.miners.slice(0,5),max=Math.max(1,top[0].blocks),rest=d.miners.length-top.length;
|
||||
$('miners-count').textContent=d.miners.length+' vote keys in 10 min';
|
||||
minersEl.innerHTML=top.map(function(m){return '<div class="lane"><span class="id">'+esc(m.id)+'</span><div class="bar"><i style="width:'+Math.round(m.blocks/max*100)+'%"></i></div><span class="n">'+m.blocks+' blocks</span><span class="t">'+esc(rel(now-new Date(m.last_seen.replace(' ','T')).getTime()))+'</span></div>';}).join('');
|
||||
$('miners-more').innerHTML=(rest>0?'and '+rest+' more. ':'')+'Every vote key with a block in the last 10 minutes; a card runs several. <a href="/miners">The GPU bench table</a> has the measured rates per card.';}
|
||||
else{minersEl.innerHTML='<div class="empty">No blocks in the last 10 minutes.</div>';$('miners-count').textContent='last 10 min';}
|
||||
// events: the last five, one line each
|
||||
if(d.events.length){var ev5=d.events.slice(0,5);eventsEl.innerHTML=ev5.map(function(e){return '<div><span><span class="k">'+esc(e.kind.replace(/_/g,' '))+'</span> '+esc(e.text)+'</span><span class="t">'+rel(now-new Date(e.ts).getTime())+'</span></div>';}).join('');
|
||||
$('events-more').textContent=(d.events.length>5?(d.events.length-5)+' more in the observer\'s last 30. ':'')+'Checkpoint locks, miners going quiet and coming back, provers seen, as the observer records them.';}
|
||||
else eventsEl.innerHTML='<div class="empty">No events yet.</div>';
|
||||
spark(s.blocks_per_minute);
|
||||
lastD=d;sceneCounters(d);if(dag)dag.push(d);
|
||||
|
|
|
|||
|
|
@ -210,6 +210,11 @@ input:focus,select:focus,textarea:focus{border-color:var(--line-2)}
|
|||
.legend span{display:inline-flex;align-items:center;gap:8px}
|
||||
.sw{width:12px;height:12px;border-radius:3px;display:inline-block;flex:none}
|
||||
|
||||
/* the engaged scene: a chip in the corner while the wheel zooms */
|
||||
.scene canvas.engaged{outline:2px solid var(--ember);outline-offset:-2px}
|
||||
.chip-zoom{position:absolute;right:12px;top:12px;display:inline-flex;align-items:center;gap:8px;background:var(--graphite);border:1px solid var(--line-2);border-radius:var(--r-pill);padding:5px 12px;font:500 var(--t-sm)/1.4 var(--f-mono);color:var(--ink-2);pointer-events:none}
|
||||
.chip-zoom[hidden]{display:none}
|
||||
|
||||
/* ---------- 8. print ---------- */
|
||||
@media print{
|
||||
:root{--obsidian:#fff;--graphite:#fff;--row:#f3f3f3;--line:#bbb;--line-2:#999;--bone:#000;--ink-2:#111;--ash:#444;--ember:#B8390C;--ember-text:#B8390C;--molten-text:#8F5810}
|
||||
|
|
|
|||
Loading…
Reference in a new issue