Merge site-ui-3 e9dac36: the step view returns to the home page on live blocks, each transition captioned; the /scenes orphan fixed
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
575abef4e1
8 changed files with 137 additions and 17 deletions
BIN
docs/plans/site-ui-3-shots/after/index-steps-1440-dark-clip.jpg
Normal file
BIN
docs/plans/site-ui-3-shots/after/index-steps-1440-dark-clip.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
docs/plans/site-ui-3-shots/after/index-steps-1440-dark.webm
Normal file
BIN
docs/plans/site-ui-3-shots/after/index-steps-1440-dark.webm
Normal file
Binary file not shown.
BIN
docs/plans/site-ui-3-shots/after/index-steps-390-dark-clip.jpg
Normal file
BIN
docs/plans/site-ui-3-shots/after/index-steps-390-dark-clip.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
|
|
@ -45,7 +45,7 @@ export default async function handler(req, res) {
|
|||
(SELECT row_to_json(s) FROM ${T}live_state s WHERE s.id = 1) AS state,
|
||||
(SELECT json_agg(e) FROM (SELECT ts, kind, text FROM ${T}live_events ORDER BY ts DESC LIMIT 30) e) AS events,
|
||||
(SELECT max(number) FROM ${T}live_blocks WHERE number IS NOT NULL AND received_at > now() - interval '10 minutes') AS height`),
|
||||
sql(`SELECT hash, blue_score, daa_score, timestamp_ms, parent_hashes, is_chain_block, vote_key_hash, received_at, color
|
||||
sql(`SELECT hash, blue_score, daa_score, timestamp_ms, parent_hashes, is_chain_block, vote_key_hash, received_at, color, number
|
||||
FROM ${T}live_blocks
|
||||
WHERE received_at > now() - ($1 || ' seconds')::interval
|
||||
ORDER BY received_at DESC
|
||||
|
|
@ -140,6 +140,8 @@ export default async function handler(req, res) {
|
|||
|
||||
const blocks = blockRows.reverse().map(b => ({
|
||||
hash: short(b.hash),
|
||||
// the chain block number (the EVM block number) once the observer has planned the block; null off the chain or before the plan
|
||||
number: num(b.number),
|
||||
blue_score: num(b.blue_score),
|
||||
daa: num(b.daa_score),
|
||||
ts: num(b.timestamp_ms),
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
115
site/live-steps.js
Normal file
115
site/live-steps.js
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/* Igneum live steps (6 October 2026, the owner: "I liked it when we could see each step of the algo working").
|
||||
The original step view with real blocks: a lane chart where every square is a block the observer stored, released at a
|
||||
calm pace (the feed is sampled, the counters elsewhere read the chain), and every square shows its step: mined (an outline,
|
||||
grey while pending, ember once included), shards being proven (molten quarter cells fill as shards are verified or paid),
|
||||
proven (ember fill), locked checkpoint (a ring; a dashed "final" line marks that everything to its left is final).
|
||||
Each transition is animated as it happens (a glow and a ring pulse) and captioned in one line for the newest block that
|
||||
changed. The wheel is never touched: the page scrolls through the scene. Light and dark from the CSS tokens; a still frame
|
||||
under reduced motion. Needs /scenes/feed.js (IgneumFeed) on the page.
|
||||
Usage: var steps = IgneumSteps.mount(canvas, { caption: el, window: 300 }); then IgneumFeed.start({ subs: [steps], window: 300 }). */
|
||||
(function (root) {
|
||||
'use strict';
|
||||
var F = root.IgneumFeed;
|
||||
function pad(n) { return String(n).padStart(2, '0'); }
|
||||
function hms(ms) { var d = new Date(ms); return pad(d.getHours()) + ':' + pad(d.getMinutes()) + ':' + pad(d.getSeconds()); }
|
||||
function fmt(n) { return n === null || n === undefined ? null : Number(n).toLocaleString('en-GB'); }
|
||||
function stepOf(b) {
|
||||
var sh = b.shards || [], done = function (x) { return x.state === 'verified' || x.state === 'paid'; };
|
||||
if (b.proven || (sh.length && sh.every(done))) return 'proven';
|
||||
if (sh.some(function (x) { return x.state === 'proving' || done(x); })) return 'proving';
|
||||
return 'mined';
|
||||
}
|
||||
function mount(canvas, opts) {
|
||||
opts = opts || {};
|
||||
var ctx = canvas.getContext('2d'), size = F.sizer(canvas, ctx), dims = size(), T = F.theme();
|
||||
var lanes = 5, S, GAP, speed, blocks = [], byHash = {}, queue = [], laneOf = {}, laneN = 0, last = 0, prevT = 0;
|
||||
// one block released every 2.5 s and a crossing of about 34 s at 1440: the chain merges a pending block in about 10 s, so a
|
||||
// square is still on screen when its step changes (the original pace of 10 s lost most transitions off the left edge)
|
||||
var RELEASE_MS = 2500;
|
||||
var caption = opts.caption || null, lastChange = 0, latest = null, finality = null, proving = null, serverNow = 0;
|
||||
function metrics() { S = dims.W < 600 ? 18 : 26; GAP = dims.W < 600 ? S * 2.5 : S * 3.1; speed = GAP / RELEASE_MS; }
|
||||
metrics();
|
||||
root.addEventListener('resize', function () { dims = size(); metrics(); L.kick(); });
|
||||
function laneY(l) { return dims.H * 0.14 + l * (dims.H * 0.72 / (lanes - 1)); }
|
||||
function laneFor(m) { if (!(m in laneOf)) laneOf[m] = laneN++ % lanes; return laneOf[m]; }
|
||||
function cpIndexOf(hash) { var cps = (finality && finality.checkpoints) || []; for (var i = cps.length - 1; i >= 0; i--) if (cps[i].hash === hash) return cps[i].index; return null; }
|
||||
// the record of a block for the caption: its words with their times
|
||||
function describe(e) {
|
||||
var b = e.src, parts = [], name = b.number !== null && b.number !== undefined ? 'block ' + fmt(b.number) : 'block ' + b.hash.slice(0, 8);
|
||||
parts.push('mined ' + hms(b.ts) + (b.color === 'red' ? ', excluded' : b.chain ? ', on the selected chain' : b.color === 'blue' ? ', included' : ', pending'));
|
||||
var sh = b.shards || [];
|
||||
if (sh.length) {
|
||||
var proving = sh.filter(function (x) { return x.state === 'proving'; }), done = sh.filter(function (x) { return x.state === 'verified' || x.state === 'paid'; });
|
||||
var provers = {}; sh.forEach(function (x) { if (x.prover) provers[x.prover] = 1; }); var np = Object.keys(provers).length;
|
||||
if (e.step === 'proven') { var lag = Math.max.apply(null, sh.map(function (x) { return x.lag || 0; })); parts.push(sh.length + (sh.length === 1 ? ' shard' : ' shards') + (np ? ' on ' + np + (np === 1 ? ' card' : ' cards') : '')); parts.push('proven in ' + (lag > 0 ? lag : Math.max(1, Math.round((Date.now() - b.ts) / 1000))) + ' s'); }
|
||||
else if (proving.length || done.length) parts.push((done.length ? done.length + ' of ' + sh.length + ' shards proven' : sh.length + (sh.length === 1 ? ' shard' : ' shards') + ' proving') + (np ? ' on ' + np + (np === 1 ? ' card' : ' cards') : ''));
|
||||
else parts.push(sh.length + (sh.length === 1 ? ' shard planned' : ' shards planned'));
|
||||
}
|
||||
if (b.locked) { var idx = cpIndexOf(b.hash); parts.push('locked at checkpoint' + (idx !== null ? ' ' + fmt(idx) : '')); }
|
||||
else if (b.final) parts.push('final under the newest lock');
|
||||
return name + ': ' + parts.join('; ');
|
||||
}
|
||||
function setCaption(text, muted) { if (!caption) return; caption.textContent = text; caption.classList.toggle('muted', !!muted); }
|
||||
function apply(e, src, now, announce) {
|
||||
var before = e.step + '|' + e.src.color + '|' + e.src.locked + '|' + e.src.final + '|' + (e.src.chain ? 1 : 0);
|
||||
e.src = src; e.step = stepOf(src);
|
||||
var after = e.step + '|' + src.color + '|' + src.locked + '|' + src.final + '|' + (src.chain ? 1 : 0);
|
||||
if (announce && after !== before) { e.glow = 1; if (src.locked && !e.ringed) { e.ringed = true; e.ripple = 1; } lastChange = now; latest = e; setCaption(describe(e)); if (opts.onChange) opts.onChange(e, before, after); }
|
||||
}
|
||||
var seeded = false;
|
||||
function push(d) {
|
||||
var now = performance.now(); finality = d.finality || null; proving = d.proving || null; serverNow = new Date(d.now).getTime();
|
||||
if (!seeded) {
|
||||
// the first reply fills the scene: the newest blocks placed across the width, oldest at the left, so nothing starts empty
|
||||
seeded = true; var n = Math.ceil(dims.W / GAP) + 1, recent = d.blocks.slice(-n);
|
||||
recent.forEach(function (b, i) { var e = { hash: b.hash, src: b, step: stepOf(b), x: dims.W - S - (recent.length - 1 - i) * GAP, y: laneY(laneFor(b.miner || '?')), glow: 0, ripple: 0, ringed: !!b.locked, born: now }; byHash[b.hash] = e; blocks.push(e); });
|
||||
}
|
||||
d.blocks.forEach(function (b) {
|
||||
var e = byHash[b.hash];
|
||||
if (e) { apply(e, b, now, true); return; }
|
||||
e = { hash: b.hash, src: b, step: stepOf(b), x: null, y: null, glow: 0, ripple: 0, ringed: !!b.locked, born: 0 };
|
||||
byHash[b.hash] = e; queue.push(e); if (queue.length > 6) { var dropped = queue.shift(); delete byHash[dropped.hash]; }
|
||||
});
|
||||
var seen = {}; d.blocks.forEach(function (b) { seen[b.hash] = 1; });
|
||||
// a block the feed no longer carries keeps its last words on screen until it leaves at the left
|
||||
if (!latest || now - lastChange > 60000) {
|
||||
var why = finality && finality.supported && !finality.active ? 'finality paused: under two thirds of the weight is signing, so no checkpoint locks' : proving && proving.supported && proving.active && !(proving.shards_paid_10m > 0) ? 'no proof landed in the last 10 minutes; proofs land about ' + (proving.median_proof_lag_s ? Math.round(proving.median_proof_lag_s) : 380) + ' s behind the tip' : 'no block changed step in the last minute';
|
||||
setCaption('Blocks arrive every second. ' + why.charAt(0).toUpperCase() + why.slice(1) + '.', true);
|
||||
}
|
||||
L.kick();
|
||||
}
|
||||
function rr(x, y, w, h, r) { ctx.beginPath(); ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }
|
||||
function spawn(e, t) { e.x = dims.W + S; e.y = laneY(laneFor(e.src.miner || '?')); e.born = t; blocks.push(e); if (blocks.length > 80) { var old = blocks.shift(); if (!queue.some(function (q) { return q === old; })) delete byHash[old.hash]; } }
|
||||
function draw(t) {
|
||||
var W = dims.W, H = dims.H; T = F.theme();
|
||||
var dt = prevT ? Math.min(50, t - prevT) : 16; prevT = t;
|
||||
if (queue.length && t - last > RELEASE_MS) { spawn(queue.pop(), t); queue.length = 0; last = t; } // the newest waiting block; the rest are sampled out
|
||||
blocks.forEach(function (b) { b.x -= speed * dt; if (b.glow > 0) b.glow -= 0.02; if (b.ripple > 0) b.ripple -= 0.012; });
|
||||
blocks = blocks.filter(function (b) { if (b.x < -S * 2) { if (byHash[b.hash] === b && queue.indexOf(b) < 0) delete byHash[b.hash]; return false; } return true; });
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.strokeStyle = F.rgba(T.line, 0.9); ctx.lineWidth = 1; for (var l = 0; l < lanes; l++) { ctx.beginPath(); ctx.moveTo(0, laneY(l)); ctx.lineTo(W, laneY(l)); ctx.stroke(); }
|
||||
// parent edges between blocks on screen
|
||||
blocks.forEach(function (b) { (b.src.parents || []).forEach(function (h) { var q = byHash[h]; if (!q || q.x === null || blocks.indexOf(q) < 0) return; var both = b.step === 'proven' && q.step === 'proven'; ctx.strokeStyle = both ? F.rgba(T.ember, 0.45) : F.rgba(T.ash, 0.22); ctx.lineWidth = both ? 1.5 : 1; ctx.beginPath(); ctx.moveTo(b.x, b.y); var mx = (b.x + q.x) / 2; ctx.bezierCurveTo(mx, b.y, mx, q.y, q.x, q.y); ctx.stroke(); }); });
|
||||
// the lock line: everything left of the newest locked block is final
|
||||
var lk = null; blocks.forEach(function (b) { if (b.src.locked && (!lk || b.x > lk.x)) lk = b; });
|
||||
if (lk) { ctx.strokeStyle = F.rgba(T.ember, 0.55); ctx.lineWidth = 1.5; ctx.setLineDash([5, 6]); ctx.beginPath(); ctx.moveTo(lk.x, 6); ctx.lineTo(lk.x, H - 6); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = T.ash; ctx.font = '500 ' + Math.max(10, Math.round(S * 0.42)) + 'px IBM Plex Mono, monospace'; ctx.textAlign = 'right'; ctx.textBaseline = 'alphabetic'; ctx.fillText('final', lk.x - 8, 14); }
|
||||
blocks.forEach(function (b) {
|
||||
var h = S / 2, w = b.step, src = b.src, ex = src.color === 'red';
|
||||
if (b.glow > 0) { var g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, S * 1.6); g.addColorStop(0, F.rgba(T.ember, 0.45 * b.glow)); g.addColorStop(1, F.rgba(T.ember, 0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, S * 1.6, 0, Math.PI * 2); ctx.fill(); }
|
||||
if (ex) ctx.globalAlpha = 0.45;
|
||||
if (w === 'proven') { ctx.fillStyle = T.ember; rr(b.x - h, b.y - h, S, S, S * 0.24); ctx.fill(); }
|
||||
else {
|
||||
ctx.fillStyle = T.row; rr(b.x - h, b.y - h, S, S, S * 0.24); ctx.fill();
|
||||
ctx.strokeStyle = w === 'proving' ? F.rgba(T.molten, 0.9) : (src.chain || src.color === 'blue') ? F.rgba(T.ember, 0.8) : F.rgba(T.ash, 0.7); ctx.lineWidth = 2; rr(b.x - h, b.y - h, S, S, S * 0.24); ctx.stroke();
|
||||
if (w === 'proving') { var sh = src.shards || [], n = Math.min(4, sh.length), q = S / 2 - 3; for (var i = 0; i < n; i++) { var st = sh[i].state; if (st === 'verified' || st === 'paid' || st === 'proving') { ctx.fillStyle = st === 'proving' ? F.rgba(T.molten, 0.45) : T.molten; var qx = b.x - h + 3 + (i % 2) * q, qy = b.y - h + 3 + Math.floor(i / 2) * q; rr(qx, qy, q - 1, q - 1, 2); ctx.fill(); } } }
|
||||
}
|
||||
if (src.locked) { var pulse = 0.8 + 0.2 * Math.sin(t / 350); ctx.strokeStyle = F.rgba(T.ember, pulse); ctx.lineWidth = 3; ctx.beginPath(); ctx.arc(b.x, b.y, S * 1.15, 0, Math.PI * 2); ctx.stroke(); ctx.fillStyle = T.bone; ctx.beginPath(); ctx.arc(b.x, b.y, S * 0.16, 0, Math.PI * 2); ctx.fill(); }
|
||||
if (b.ripple > 0) { ctx.strokeStyle = F.rgba(T.ember, b.ripple); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(b.x, b.y, S * (1.3 + (1 - b.ripple) * 2), 0, Math.PI * 2); ctx.stroke(); }
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
}
|
||||
var L = F.loop(draw, canvas); L.kick();
|
||||
return { push: push, redraw: L.kick };
|
||||
}
|
||||
root.IgneumSteps = { mount: mount };
|
||||
})(window);
|
||||
|
|
@ -41,7 +41,8 @@
|
|||
.scenes.only{grid-template-columns:1fr}
|
||||
.scene-card{display:flex;flex-direction:column;gap:var(--s-3)}
|
||||
.scene-card .head{display:flex;justify-content:space-between;align-items:baseline;gap:var(--s-3)}
|
||||
.scene-card .head .eyebrow{margin:0}
|
||||
.scene-card .head .eyebrow{margin:0;text-align:right}
|
||||
.scene-card .head h3{white-space:nowrap}
|
||||
.scene-card canvas{width:100%;height:420px;border-radius:var(--r-row);background:var(--row);border:1px solid var(--line);display:block}
|
||||
.scenes.only .scene-card canvas{height:560px}
|
||||
.scene-card p{margin:0;color:var(--ink-2);font-size:var(--t-base)}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
if (!d || !d.ok || !d.state || !d.blocks) throw new Error('bad reply');
|
||||
failures = 0;
|
||||
if (d.state.stale) set('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown')); else set('live', null);
|
||||
subs.forEach(function (s) { s.push(d); });
|
||||
subs.forEach(function (s) { s.push(d); }); if (opts.onData) opts.onData(d);
|
||||
}).catch(function () { failures++; if (failures >= 2) set('failed', 'live feed unavailable'); });
|
||||
}
|
||||
poll(); var t = setInterval(poll, 2000);
|
||||
|
|
|
|||
Loading…
Reference in a new issue