site-ui-4: /live as the network observatory (prototype, not deployed)

The reference page's composition in our tokens and words, every number
from the observer: the header row (eyebrow, the title with its ember
full stop, one sentence, two buttons, the state word), six tiles in one
hairline row each with a definition, the block graph panel (our
live-dag.js in the graph slot, the block-count chip, selected chain
only, pause and follow, the inspector column with hash, lane, time, blue
score, DAA, chain, colour word, shards, checkpoint and lock state, the
drag-to-explore line, the zoom control, the legend row and the blue
score range), the three panels (block activity with per-minute bars,
mining identities with share bars and the reader's own key marked when
set, the event stream), the blocks table, our footer. live-dag.js gains
an onSelect hook on click (additive; the app's copy unchanged).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 08:30:23 +00:00
parent 25a1a310e9
commit 4ce0c34827
12 changed files with 277 additions and 169 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

View file

@ -9,7 +9,7 @@
outside; opts.chip is the "scroll to zoom, Esc to release" element); window 30 to 300 s (the API's ?window=); a pause button; 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. hover and tap show a block's details.
Usage: var dag = IgneumDag.mount(canvas, { compact, window, url, poll, mine, onState, onData, onWindow, onEngage, pauseButton, tooltip, chip }) Usage: var dag = IgneumDag.mount(canvas, { compact, window, url, poll, mine, onState, onData, onWindow, onEngage, onSelect, 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 - 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}) 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). whenever it has new data (every poll or every new block, either is fine; blocks already on screen are updated in place).
@ -198,7 +198,7 @@
} }
canvas.addEventListener('mousemove', function (ev) { hover = hit(ev); canvas.style.cursor = hover ? 'pointer' : 'default'; if (!pinned) showTip(hover, ev); kick(); }); 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('mouseleave', function () { hover = null; if (!pinned) showTip(null); kick(); });
canvas.addEventListener('click', function (ev) { engage(true); var b = hit(ev); pinned = (pinned && b && pinned.hash === b.hash) ? null : b; showTip(pinned || hover); kick(); }); canvas.addEventListener('click', function (ev) { engage(true); var b = hit(ev); pinned = (pinned && b && pinned.hash === b.hash) ? null : b; showTip(pinned || hover); if (opts.onSelect) opts.onSelect(pinned); kick(); });
// the wheel is never captured by default (6 Oct 2026, the owner: scrolling past the scene zoomed it): the page scrolls through the // 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 // 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 // "scroll to zoom, Esc to release"); Escape, a click outside or leaving the scene releases it

View file

@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Igneum live devnet</title> <title>Igneum live devnet</title>
<meta name="description" content="The Igneum devnet as it happens: every block, the miners, the provers paid and the finality rule, read from a node every two seconds."> <meta name="description" content="The Igneum network observatory: every block and its connections, the miners, the provers paid and the finality rule, read from a node every two seconds.">
<link rel="canonical" href="https://igneum.network/live"> <link rel="canonical" href="https://igneum.network/live">
<meta name="theme-color" content="#0C0C0E"> <meta name="theme-color" content="#0C0C0E">
<meta property="og:type" content="website"> <meta property="og:type" content="website">
@ -53,54 +53,116 @@
</script> </script>
<!-- head:end --> <!-- head:end -->
<style> <style>
/* live page block (7 Oct 2026: redrawn to show the system working); tokens and components are in /site.css */ /* live page block (7 Oct 2026, site-ui-4: the observatory composition, every number read from the observer); tokens and components are in /site.css */
.lhead{padding:var(--s-6) 0 var(--s-5)} .obs{--pr:8px;--hair:1px solid var(--line)}
.lhead .wrap{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:var(--s-3) var(--s-5)} .obs-head{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;gap:var(--s-4) var(--s-5);padding:var(--s-6) 0 var(--s-5)}
.lhead h1{font-size:var(--t-h1);margin:var(--s-2) 0 0} .obs-head .eyebrow{display:flex;align-items:center;gap:8px;margin-bottom:var(--s-3)}
.lhead{padding-bottom:var(--s-4)} .obs-head .eyebrow i{width:7px;height:7px;background:var(--ember);transform:rotate(45deg);display:inline-block}
.lhead p{margin:var(--s-3) 0 0;color:var(--ink-2);max-width:60ch} .obs-head h1{font-size:clamp(34px,4.6vw,56px);font-weight:700;letter-spacing:-.02em;margin:0}
.bleed{width:100%;padding:0 var(--gutter)} .obs-head h1 b{color:var(--ember-text)}
.bleed canvas{width:100%;height:min(54vh,520px);min-height:300px;display:block;border-radius:var(--r-card);background:var(--row);border:1px solid var(--line)} .obs-head p{margin:var(--s-3) 0 0;color:var(--ink-2);max-width:60ch}
.caption{margin:var(--s-3) auto 0;font:400 13px/1.5 var(--f-mono);color:var(--bone);min-height:1.5em;max-width:var(--max);padding:0 var(--gutter)} .obs-head .side{display:flex;flex-direction:column;align-items:flex-end;gap:var(--s-2)}
.caption.muted{color:var(--ash)} .obs-head .side .state{font-size:var(--t-xs)}
.strip{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:var(--s-3);margin-top:var(--s-6)} .tiles6{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));border:var(--hair);border-radius:var(--pr);background:var(--graphite);overflow:hidden}
@media (max-width:1000px){.strip{grid-template-columns:repeat(3,minmax(0,1fr))}} .tiles6>div{padding:18px 19px 19px;border-right:var(--hair);min-width:0}
@media (max-width:600px){.strip{grid-template-columns:repeat(2,minmax(0,1fr))}} .tiles6>div:last-child{border-right:0}
.cell{background:var(--graphite);border:1px solid var(--line);border-radius:var(--r-row);padding:var(--s-4);min-width:0} .tiles6 .k{display:flex;justify-content:space-between;align-items:center;font:400 13px/1.4 var(--f-sans);color:var(--ink-2)}
.cell .k{font:500 var(--t-xs)/1.4 var(--f-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--ash)} .tiles6 .k i{color:var(--ash);font:400 12px/1 var(--f-mono)}
.cell .v{font:700 clamp(20px,2.2vw,28px)/1.1 var(--f-display);color:var(--bone);margin-top:6px;overflow-wrap:anywhere} .tiles6 .v{font:500 clamp(22px,2vw,30px)/1.15 var(--f-sans);color:var(--bone);margin:14px 0 10px;letter-spacing:-.01em;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}
.cell .v.state{font:500 var(--t-base)/1.3 var(--f-mono);color:var(--ash)} .tiles6 .v.word{font:500 15px/1.3 var(--f-mono);color:var(--ash)}
.cell .s{font:400 var(--t-sm)/1.4 var(--f-mono);color:var(--ash);margin-top:4px} .tiles6 .d{font:400 11px/1.4 var(--f-mono);color:var(--ash)}
.fin{display:flex;align-items:center;gap:var(--s-3);margin:var(--s-5) 0 0;padding:var(--s-4) var(--s-5);border:1px solid var(--line);border-radius:var(--r-row);background:var(--row);color:var(--ink-2);font-size:var(--t-md)} @media (max-width:1100px){.tiles6{grid-template-columns:repeat(3,minmax(0,1fr))}.tiles6>div:nth-child(3){border-right:0}.tiles6>div:nth-child(-n+3){border-bottom:var(--hair)}}
.fin b{color:var(--bone)} @media (max-width:600px){.tiles6{grid-template-columns:repeat(2,minmax(0,1fr))}.tiles6>div{border-right:var(--hair)!important;border-bottom:var(--hair)}.tiles6>div:nth-child(2n){border-right:0!important}.tiles6>div:nth-last-child(-n+2){border-bottom:0}}
.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-4);margin-top:var(--s-5)} .panel{border:var(--hair);border-radius:var(--pr);background:var(--graphite);margin-top:var(--s-5);overflow:hidden}
@media (max-width:1000px){.three{grid-template-columns:1fr}} .toolbar{display:flex;flex-wrap:wrap;align-items:center;gap:12px;padding:15px 21px;border-bottom:var(--hair);background:var(--row)}
.three h2{font-size:var(--t-h3);margin-bottom:var(--s-3)} .toolbar .title{display:flex;align-items:center;gap:10px;font:500 var(--t-md)/1.3 var(--f-sans);color:var(--bone)}
.lanes{display:flex;flex-direction:column} .toolbar .title i{width:14px;height:14px;border:1.5px solid var(--ember);border-radius:50%;display:inline-block;position:relative}
.lane{display:grid;grid-template-columns:84px minmax(0,1fr) 72px;gap:12px;align-items:center;padding:8px 0;border-bottom:1px solid var(--line);font:400 13px/1.4 var(--f-mono);color:var(--ink-2)} .toolbar .title i::after{content:"";position:absolute;inset:4px;background:var(--ember);border-radius:50%}
.lane:last-child{border-bottom:0} .chip{font:400 11px/1.2 var(--f-mono);color:var(--ash);border:var(--hair);border-radius:4px;padding:5px 8px;white-space:nowrap}
.lane .id{color:var(--molten-text)}.lane .n{text-align:right;white-space:nowrap} .toolbar .right{margin-left:auto;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.bar{height:6px;border-radius:3px;background:var(--line);overflow:hidden;min-width:40px} .tb{background:transparent;border:1px solid transparent;color:var(--ash);font:400 13px/1.2 var(--f-sans);padding:7px 10px;border-radius:6px;cursor:pointer}
.bar i{display:block;height:100%;background:var(--ember)} .tb:hover{color:var(--bone)}
.kv2{display:grid;grid-template-columns:1fr auto;gap:6px 12px;font:400 13px/1.5 var(--f-mono);color:var(--ink-2)} .tb[aria-pressed="true"]{color:var(--bone);border-color:var(--line-2);background:var(--graphite)}
.kv2 b{color:var(--bone);font-weight:500;text-align:right;white-space:nowrap} .tb.on{color:var(--ember-text);border-color:var(--ember-40)}
.blocks{display:flex;flex-direction:column;font:400 13px/1.4 var(--f-mono);color:var(--ink-2)} .sep{width:1px;height:18px;background:var(--line)}
.blocks div{display:grid;grid-template-columns:minmax(72px,auto) minmax(0,1fr) auto;gap:10px;padding:7px 0;border-bottom:1px solid var(--line);align-items:baseline} .graph{display:grid;grid-template-columns:minmax(0,1fr) 250px}
.blocks div>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} @media (max-width:1000px){.graph{grid-template-columns:1fr}}
.blocks div:last-child{border-bottom:0} .gcol{padding:18px 21px 14px;min-width:0;border-right:var(--hair)}
.blocks .w{color:var(--ash);white-space:nowrap}.blocks .w.hot{color:var(--molten-text)}.blocks .num{color:var(--bone)} @media (max-width:1000px){.gcol{border-right:0;border-bottom:var(--hair)}}
.blocks div.new{animation:land .9s ease-out} .gcol .state{margin-bottom:10px;font-size:var(--t-xs)}
@keyframes land{0%{background:var(--ember-12)}100%{background:transparent}}
@media (prefers-reduced-motion:reduce){.blocks div.new{animation:none}}
.lc{display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:var(--s-2) var(--s-4);margin-top:var(--s-5);padding:var(--s-3) var(--s-4);border:1px solid var(--line);border-radius:var(--r-row);background:var(--row);font-size:var(--t-base);color:var(--ink-2)}
.lc .tick{width:28px;height:28px;border-radius:8px;background:var(--ember);display:grid;place-items:center;flex:none;color:var(--ember-ink)}
.lc [data-lc]{color:var(--bone)}
@media (max-width:700px){.lc{grid-template-columns:auto minmax(0,1fr)}.lc .state{grid-column:2}}
.scene{position:relative} .scene{position:relative}
#dag{display:block;width:100%;height:clamp(280px,38vw,420px);border-radius:var(--r-row);background:var(--row);border:1px solid var(--line);touch-action:pan-y} #dag{display:block;width:100%;height:clamp(300px,36vw,440px);border-radius:6px;background:var(--row);touch-action:pan-y}
.tip{position:absolute;left:0;top:0;pointer-events:none;background:var(--graphite);border:1px solid var(--line-2);border-radius:var(--r-row);padding:10px 12px;font:400 12px/1.5 var(--f-mono);color:var(--ink-2);white-space:nowrap;z-index:5} .tip{position:absolute;left:0;top:0;pointer-events:none;background:var(--graphite);border:1px solid var(--line-2);border-radius:6px;padding:10px 12px;font:400 12px/1.5 var(--f-mono);color:var(--ink-2);white-space:nowrap;z-index:5}
.tip[hidden]{display:none} .tip[hidden]{display:none}
.dl-row{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:var(--s-3);margin-top:var(--s-6);padding-top:var(--s-5);border-top:1px solid var(--line)} .gfoot{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px;margin-top:12px;font:400 11px/1.4 var(--f-mono);color:var(--ash)}
.zoom{display:inline-flex;align-items:center;border:var(--hair);border-radius:6px;overflow:hidden}
.zoom button{background:transparent;border:0;color:var(--ink-2);font:400 13px/1 var(--f-mono);padding:8px 12px;cursor:pointer}
.zoom button:hover{background:var(--hover)}
.zoom span{font:400 11px/1 var(--f-mono);color:var(--ash);padding:0 6px;min-width:44px;text-align:center}
.icol{padding:18px 21px;min-width:0;font-size:13px}
.icol .ih{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:14px}
.icol .ih .eyebrow{margin:0}
.icol .num{font:500 22px/1.2 var(--f-sans);color:var(--bone);margin:6px 0 4px;overflow-wrap:anywhere}
.icol .hash{font:400 11px/1.5 var(--f-mono);color:var(--ash);overflow-wrap:anywhere;margin-bottom:14px}
.insp{display:grid;grid-template-columns:auto minmax(0,1fr);gap:0 10px;margin:0;border-top:var(--hair);padding-top:8px}
.insp dt{font:400 11px/1.3 var(--f-mono);color:var(--ash);padding:6px 0}
.insp dd{margin:0;font:400 11px/1.3 var(--f-mono);color:var(--bone);text-align:right;padding:6px 0;overflow-wrap:anywhere;min-width:0}
.insp dd.hot{color:var(--molten-text)}
.icol .note{font-size:12px;margin-top:14px;padding-top:12px;border-top:var(--hair)}
.insp-empty{color:var(--ash);font-size:13px;line-height:1.5;padding:6px 0 2px}
.legend-row{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:8px 18px;padding:12px 21px;border-top:var(--hair);font:400 11px/1.4 var(--f-mono);color:var(--ash)}
.legend-row .lg{display:flex;flex-wrap:wrap;gap:8px 16px}
.legend-row .lg span{display:inline-flex;align-items:center;gap:7px;color:var(--ink-2);font-family:var(--f-sans);font-size:12px}
.sw{width:9px;height:9px;transform:rotate(45deg);display:inline-block;flex:none}
.three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-4);margin-top:var(--s-4)}
@media (max-width:1000px){.three{grid-template-columns:1fr}}
.dp{border:var(--hair);border-radius:7px;background:var(--graphite);padding:20px 21px 16px;min-width:0}
.dp .ph{display:flex;justify-content:space-between;align-items:baseline;gap:10px;margin-bottom:14px}
.dp h2{font:500 var(--t-md)/1.3 var(--f-sans);margin:0;color:var(--bone)}
.dp .tag{font:400 10px/1.2 var(--f-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
.big{display:flex;align-items:baseline;gap:10px;margin-bottom:8px}
.big b{font:500 30px/1 var(--f-sans);color:var(--bone)}
.big span{font:400 12px/1.4 var(--f-sans);color:var(--ash)}
.bars{display:flex;align-items:flex-end;gap:3px;height:80px;margin-top:10px}
.bars i{flex:1 1 0;background:linear-gradient(to top,var(--ember-40),var(--molten));border-radius:2px 2px 0 0;min-height:2px;opacity:.9}
.bars i.now{background:var(--molten)}
.axis{display:flex;justify-content:space-between;font:400 10px/1.4 var(--f-mono);color:var(--ash);margin-top:8px}
.ids{display:flex;flex-direction:column;gap:12px}
.id-row{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:4px 10px;font:400 12px/1.4 var(--f-mono);color:var(--ink-2);align-items:center}
.id-row .name{display:flex;align-items:center;gap:8px;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.id-row .name .sw{background:var(--line-2)}
.id-row.mine .name{color:var(--ember-text)}
.id-row.mine .name .sw{background:var(--ember)}
.id-row .pct{color:var(--bone);white-space:nowrap}
.id-row .bar{grid-column:1/-1;height:3px;background:var(--line);border-radius:2px;overflow:hidden}
.id-row .bar i{display:block;height:100%;background:var(--ember);opacity:.85}
.id-row.mine .bar i{background:var(--molten)}
.evs{display:flex;flex-direction:column;max-height:330px;overflow:auto}
.evs div{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:10px;align-items:baseline;padding:9px 0;border-bottom:var(--hair);font:400 12px/1.4 var(--f-mono);color:var(--ink-2)}
.evs div:last-child{border-bottom:0}
.evs .dot{width:6px;height:6px;background:var(--molten);border-radius:50%;animation:none}
.evs .dot.lock{background:var(--ember)}.evs .dot.quiet{background:var(--ash)}
.evs .what{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.evs .what b{font-weight:500;color:var(--bone)}
.evs .t{color:var(--ash);font-size:11px;white-space:nowrap}
.evs div.new{animation:land .9s ease-out}
@keyframes land{0%{background:var(--ember-12)}100%{background:transparent}}
@media (prefers-reduced-motion:reduce){.evs div.new{animation:none}}
.tablebar{margin-top:var(--s-4);border:var(--hair);border-radius:7px;background:var(--graphite)}
.tablebar summary{padding:16px 21px;font:500 var(--t-md)/1.3 var(--f-sans);color:var(--bone)}
.tablebar summary::after{content:"+";font:400 14px/1 var(--f-mono);color:var(--ash)}
.tablebar[open] summary::after{content:"\2212"}
.tablebar summary small{font:400 11px/1.4 var(--f-mono);color:var(--ash);margin-left:auto;margin-right:12px}
.tablebar .tbl{border:0;border-top:var(--hair);border-radius:0}
.tablebar table{font-size:12px;font-family:var(--f-mono)}
.tablebar td,.tablebar th{padding:8px 21px}
.tablebar tr[data-hash]{cursor:pointer}
.tablebar tr[data-hash]:hover td{background:var(--hover)}
.obs-foot{display:flex;flex-wrap:wrap;justify-content:space-between;gap:10px;margin-top:var(--s-5);font:400 10px/1.5 var(--f-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--ash)}
.linkish{background:none;border:0;padding:0;color:var(--ember-text);font:inherit;cursor:pointer}
.linkish:hover{text-decoration:underline}
details.tablebar{border-top:var(--hair);border-bottom:var(--hair)}
details.tablebar summary{display:flex;align-items:center}
</style> </style>
</head> </head>
<body> <body>
@ -151,90 +213,111 @@
</script> </script>
<!-- nav:end --> <!-- nav:end -->
<main id="main"> <main id="main" class="wrap obs">
<header class="lhead" id="top"> <header class="obs-head" id="top">
<div class="wrap">
<div> <div>
<div class="state" id="live-state"><span class="dot off"></span>connecting to the devnet observer</div> <div class="eyebrow"><i></i>Network observatory</div>
<h1>The chain, as it happens.</h1> <h1>Live devnet<b>.</b></h1>
<p>Every block has a story. Follow the connections.</p>
</div> </div>
<a href="/miner#get" class="btn primary">Download the miner</a> <div class="side">
<div class="cta"><a href="/miner#get" class="btn primary">Download the miner</a><a href="/explorer" class="btn">Open the explorer</a></div>
<div class="state" id="live-state"><span class="dot off"></span>connecting to the observer</div>
</div>
</header>
<div class="tiles6" aria-live="polite">
<div><div class="k">Blocks / second <i>&#8599;</i></div><div class="v tick" id="s-bps">pending</div><div class="d">counted over the node&rsquo;s last 60 s; the target is 1</div></div>
<div><div class="k">Blue score <i>#</i></div><div class="v tick" id="s-blue">pending</div><div class="d">the newest block&rsquo;s count of blue ancestors</div></div>
<div><div class="k">Network hash rate <i>&#8767;</i></div><div class="v tick" id="s-hash">pending</div><div class="d">the node&rsquo;s estimate from the last 1,000 blocks&rsquo; difficulty</div></div>
<div><div class="k">Active vote keys <i>&#9671;</i></div><div class="v tick" id="s-keys">pending</div><div class="d">keys with a block in 10 min; a card runs several</div></div>
<div><div class="k">Connected peers <i>&#8644;</i></div><div class="v tick" id="s-peers">pending</div><div class="d">peers of the observer&rsquo;s node</div></div>
<div><div class="k">Locked checkpoint <i>&#9679;</i></div><div class="v tick" id="s-lock">pending</div><div class="d" id="s-lock-d">signed by two thirds of all 30-day weight</div></div>
</div> </div>
</header>
<div class="bleed"> <section class="panel" aria-label="The block graph">
<canvas id="steps" aria-label="The live devnet, step by step: every square is a real block moving left as time passes; its outline, quarter cells, fill and ring show mined, shards being proven, proven and locked checkpoint"></canvas> <div class="toolbar">
</div> <div class="title"><i></i>The block graph</div>
<p class="caption mono" id="steps-caption" aria-live="polite">Connecting to the devnet observer.</p> <span class="chip" id="c-count">0 blocks</span>
<div class="right">
<section class="sec" style="padding-top:0"> <button type="button" class="tb" id="chain-only" aria-pressed="false">Selected chain only</button>
<div class="wrap"> <span class="sep"></span>
<div class="strip" aria-live="polite"> <button type="button" class="tb" id="dag-pause" aria-pressed="false">&#10074;&#10074; Pause view</button>
<div class="cell"><div class="k">Hash rate</div><div class="v tick" id="s-hash">pending</div><div class="s">network estimate</div></div> <button type="button" class="tb on" id="dag-follow">Follow</button>
<div class="cell"><div class="k">Blocks, 10 min</div><div class="v tick" id="s-b10">pending</div><div class="s">target 1 a second</div></div>
<div class="cell"><div class="k">Latest block</div><div class="v tick" id="s-last">pending</div><div class="s" id="s-last-s">pending</div></div>
<div class="cell"><div class="k">Shards proven, paid</div><div class="v tick" id="s-paid">pending</div><div class="s" id="s-paid-s">pending</div></div>
<div class="cell"><div class="k">Vote keys, 10 min</div><div class="v tick" id="s-keys">pending</div><div class="s">a card runs several</div></div>
<div class="cell"><div class="k">Last lock</div><div class="v tick" id="s-lock">pending</div><div class="s" id="s-lock-s">pending</div></div>
</div>
<p class="fin" id="fin"><span class="dot off"></span><span>Reading the finality rule.</span></p>
<p class="note">One node is read every 2 s; nothing here is a replay. Every square in the scene is a real block: its outline, fill and ring are its state. Fees since DAA 210,000: <a href="/litepaper#economics">the adopted fee table</a>.</p>
<div class="three">
<div class="card">
<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.</p>
</div> </div>
<div class="card"> </div>
<div class="viz-head"><h2>Provers paid</h2><div class="eyebrow" id="prov-state">pending</div></div> <div class="graph">
<div class="kv2" id="prov"> <div class="gcol">
<span>shards proven and paid, all time</span><b id="p-total">pending</b> <div class="state" id="dag-state"><span class="dot off"></span>live devnet</div>
<span>paid in the last 10 min</span><b id="p-10">pending</b> <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>scroll to zoom · Esc to release</div></div>
<span>blocks fully proven, 10 min</span><b id="p-full">pending</b> <div class="gfoot">
<span>proofs land behind the tip</span><b id="p-lag">pending</b> <span>Drag to explore &nbsp;&middot;&nbsp; Select a block to inspect</span>
<span class="zoom"><button type="button" id="z-out" aria-label="Zoom out">&minus;</button><span id="z-pct">100%</span><button type="button" id="z-in" aria-label="Zoom in">+</button></span>
</div> </div>
<p class="note">Every chain block is split into shards; a card proves one, posts it, and is paid on the chain.</p>
</div>
<div class="card">
<div class="viz-head"><h2>Recent blocks</h2><div class="eyebrow">the last eight</div></div>
<div class="blocks" id="blocks"><div class="empty">No blocks yet.</div></div>
<p class="note">Number, miner, state. A number arrives once the block is planned onto the chain.</p>
</div> </div>
<aside class="icol" id="inspector" aria-live="polite">
<div class="ih"><div class="eyebrow">Block inspector</div><span class="chip" id="i-kind">select a block</span></div>
<div class="insp-empty" id="insp-empty">Click a block in the graph, or a row of the table below. Hover shows the short form.</div>
<div id="insp" hidden>
<div class="num" id="i-number"></div>
<div class="hash" id="i-hash"></div>
<dl class="insp">
<dt>Vote key</dt><dd id="i-miner"></dd>
<dt>Parents</dt><dd id="i-parents"></dd>
<dt>Selected chain</dt><dd id="i-chain"></dd>
<dt>Colour</dt><dd id="i-color"></dd>
<dt>Blue score</dt><dd id="i-blue"></dd>
<dt>DAA score</dt><dd id="i-daa"></dd>
<dt>Shards</dt><dd id="i-shards"></dd>
<dt>Checkpoint</dt><dd id="i-cp"></dd>
<dt>Lock</dt><dd id="i-lock"></dd>
<dt>Header time</dt><dd id="i-time"></dd>
</dl>
<p class="note">A selected-chain block is not final by itself. Only a checkpoint lock, signed by two thirds of all 30-day weight, makes the blocks under it final.</p>
<p style="margin:10px 0 0"><button type="button" class="linkish" id="insp-close">Clear</button></p>
</div>
</aside>
</div> </div>
<div class="legend-row">
<div class="lc" id="light-client"> <div class="lg">
<span class="tick"><svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7"></path></svg></span> <span><i class="sw" style="background:var(--ember)"></i>Selected chain</span>
<span>This tab checks the chain itself: checkpoint <span data-lc="checkpoint">checking</span>, <span data-lc="tab">checking</span>. <span data-lc="line" class="muted"></span></span> <span><i class="sw" style="border:1.5px solid var(--bone)"></i>Included</span>
<span class="state" data-lc="badge"><span class="dot off"></span><span data-lc="badge-text">PREVIEW</span></span> <span><i class="sw" style="border:1.5px solid var(--line-2);opacity:.6"></i>Excluded</span>
<span hidden data-lc="proof"></span><span hidden data-lc="system"></span> <span><i class="sw" style="background:var(--molten);border-radius:50%;transform:none"></i>Locked checkpoint</span>
<script type="module" src="/verify/verify.js"></script> <span><i class="sw" style="width:14px;height:0;border-top:1px solid var(--ash);transform:none"></i>Parent reference</span>
</div>
<div class="card viz" style="margin-top:var(--s-5)" aria-label="Block DAG, finality and proving, live">
<div class="viz-head">
<div class="state" id="dag-state"><span class="dot off"></span>the DAG</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><span id="c-window">120 s</span> on screen</span><button type="button" class="btn small" id="dag-pause">Pause</button></div>
</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>scroll to zoom · Esc to release</div></div> <span id="blue-range">blue score pending</span>
<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>
<span><i class="sw" style="border:2px solid var(--line-2);opacity:.45"></i>excluded</span>
<span><i class="sw" style="background:var(--ember)"></i>proven</span>
<span><i class="sw" style="border:3px solid var(--ember);border-radius:50%;background:transparent"></i>locked checkpoint</span>
<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>
</div>
<p class="note" style="margin-top:var(--s-3)">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 zooms the window. <a href="/explorer">The explorer</a> has every block.</p>
</div> </div>
</section>
<div class="dl-row"> <div class="three">
<p class="yourcard" data-yourcard style="margin:0;flex:1 1 420px">Your card: pending</p> <div class="dp">
<a href="/miner#get" class="btn primary">Download the miner</a> <div class="ph"><h2>Block activity</h2><span class="tag">header times / window</span></div>
<div class="big"><b id="a-count">0</b><span>blocks in this view</span></div>
<div class="bars" id="a-bars" aria-label="Blocks per minute over the last hour, newest on the right"></div>
<div class="axis"><span>one bar a minute, the last 30 min</span><span>now</span></div>
<p class="note" id="a-note" style="margin-top:12px;font-size:12px">Counted by the observer as blocks arrive.</p>
</div>
<div class="dp">
<div class="ph"><h2>Mining identities</h2><span class="tag">displayed window</span></div>
<div class="ids" id="miners"><div class="empty">No blocks in the window.</div></div>
<p class="note" style="margin-top:14px;font-size:12px" id="miners-more">Vote-key share of the displayed blocks. Several keys may belong to one GPU. <button type="button" class="linkish" id="mine-btn">Mark my key</button></p>
</div>
<div class="dp">
<div class="ph"><h2>Event stream</h2><span class="tag" id="ev-count">live</span></div>
<div class="evs" id="events"><div class="empty">Waiting for the first read.</div></div>
</div> </div>
</div> </div>
</section>
<details class="tablebar" id="table">
<summary>Explore blocks as a table <small>keyboard-accessible view</small></summary>
<div class="tbl"><table>
<thead><tr><th>Blue score</th><th>Hash</th><th>Vote key</th><th>Parents</th><th>State</th><th>Chain block</th></tr></thead>
<tbody id="tbody"><tr><td colspan="6">Waiting for the first read.</td></tr></tbody>
</table></div>
</details>
<div class="obs-foot"><span>Igneum / observatory</span><span>One node read every 2 s. Nothing here is a replay.</span></div>
</main> </main>
<!-- footer:start --> <!-- footer:start -->
@ -298,64 +381,83 @@
</script> </script>
<!-- footer:end --> <!-- footer:end -->
<script src="/scenes/feed.js"></script>
<script src="/live-steps.js"></script>
<script src="/live-dag.js" defer></script> <script src="/live-dag.js" defer></script>
<script src="/yourcard.js" defer></script>
<script> <script>
(function(){ (function(){
function $(id){return document.getElementById(id);} function $(id){return document.getElementById(id);}
var fmtN=function(v){return v===null||v===undefined?'pending':Number(v).toLocaleString('en-GB');}; var fmtN=function(v){return v===null||v===undefined?'pending':Number(v).toLocaleString('en-GB');};
function fmtHash(h){if(h===null||h===undefined||!(h>0))return 'pending';return h>=1e9?(h/1e9).toFixed(1)+' GH/s':(h/1e6).toFixed(1)+' MH/s';} function fmtHash(h){if(h===null||h===undefined||!(h>0))return 'pending';return h>=1e9?(h/1e9).toFixed(1)+' GH/s':(h/1e6).toFixed(1)+' MH/s';}
function rel(ms){var s=Math.max(0,Math.round(ms/1000));if(s<60)return s+' s ago';var m=Math.floor(s/60);if(m<60)return m+' min ago';var h=Math.floor(m/60);if(h<24)return h+' h ago';return Math.floor(h/24)+' d ago';} function rel(ms){var s=Math.max(0,Math.round(ms/1000));if(s<60)return s+' s ago';var m=Math.floor(s/60);if(m<60)return m+' min ago';var h=Math.floor(m/60);if(h<24)return h+' h ago';return Math.floor(h/24)+' d ago';}
function hms(ms){var d=new Date(ms);return String(d.getUTCHours()).padStart(2,'0')+':'+String(d.getUTCMinutes()).padStart(2,'0')+':'+String(d.getUTCSeconds()).padStart(2,'0');}
function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');} function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
// a counter that changed glows for half a second: motion only from a real change // a number that changed glows for a frame: motion only from a real change
function set(id,text){var el=$(id);if(!el||el.textContent===text)return;el.textContent=text;el.classList.add('flash');setTimeout(function(){el.classList.remove('flash');},60);} function set(id,text){var el=$(id);if(!el||el.textContent===text)return;el.textContent=text;el.classList.add('flash');setTimeout(function(){el.classList.remove('flash');},60);}
var cap=$('steps-caption'),lastTs=null,seen={}; var seen={},shardState={},cpState={},evSeen={},evList=[],ready=false,sel=null,lastD=null,dag=null,chainOnly=false,zoomPct=100;
function onState(s,r){var el=$('live-state');el.className='state '+(s==='live'?'live':s==='failed'?'bad':'');el.innerHTML='<span class="dot '+(s==='live'?'live':s==='failed'?'bad':'off')+'"></span>'+(s==='live'?'live devnet':s==='connecting'?'connecting to the devnet observer':esc(r||s)); var mine=(function(){try{return localStorage.getItem('igneum-my-key')||'';}catch(e){return '';}})();
if(s==='live')return;cap.classList.add('muted');cap.textContent=s==='connecting'?'Connecting to the devnet observer.':s==='stale'?(r.charAt(0).toUpperCase()+r.slice(1))+'. The scene holds the last stored blocks.':'Live feed unavailable. The scene holds its last blocks.';} document.addEventListener('click',function(e){var t=e.target;if(t&&t.id==='mine-btn'){var v=prompt('Your vote key’s short id, the first 8 hex characters shown in the app or the explorer:',mine||'');if(v!==null){mine=v.trim().toLowerCase();try{if(mine)localStorage.setItem('igneum-my-key',mine);else localStorage.removeItem('igneum-my-key');}catch(x){}if(lastD)render(lastD);}}
function onData(d){var s=d.state,pv=d.proving,fn=d.finality,now=new Date(d.now).getTime(); var row=t&&t.closest?t.closest('tr[data-hash]'):null;if(row&&lastD){var b=(lastD.blocks||[]).filter(function(x){return x.hash===row.getAttribute('data-hash');})[0];if(b)inspect(b,lastD);}});
set('s-hash',fmtHash(s.hashes_per_second_estimate)); // the inspector: a clicked block's facts from the observer, kept fresh on every read
var bpm=s.blocks_per_minute||[],b10=bpm.slice(-10).reduce(function(a,p){return a+(p[1]||0);},0);set('s-b10',fmtN(b10)); function inspect(b,d){sel=b;$('insp-empty').hidden=true;$('insp').hidden=false;$('i-kind').textContent=b.locked?'checkpoint':b.chain?'chain block':b.color==='red'?'excluded':'side block';
var newest=null;(d.blocks||[]).forEach(function(b){if(!newest||b.ts>newest.ts)newest=b;}); $('i-number').textContent=b.number?'#'+fmtN(b.number):'blue '+fmtN(b.blue_score);$('i-hash').textContent=b.hash+(b.number?'':' · not yet planned onto the chain');
if(newest){lastTs=newest.ts;$('s-last-s').textContent=newest.number?'block '+fmtN(newest.number)+(newest.chain?', on the selected chain':''):'block '+newest.hash+', not yet planned';} $('i-miner').textContent=b.miner+(mine&&b.miner===mine?' (you)':'');$('i-parents').textContent=(b.parents||[]).length?(b.parents.length+(b.parents.length===1?' reference: ':' references: ')+b.parents.join(', ')):'none';
if(pv&&pv.supported&&pv.active){set('s-paid',fmtN(pv.paid_shards_total));$('s-paid-s').textContent=fmtN(pv.shards_paid_10m||0)+' in the last 10 min';$('prov-state').textContent='live'; $('i-chain').textContent=b.chain?'yes':'no';var col=$('i-color');col.textContent=b.color==='blue'?'blue, merged and paid':b.color==='red'?'red, excluded':'pending';col.className=b.color==='blue'?'hot':'';
$('p-total').textContent=fmtN(pv.paid_shards_total);$('p-10').textContent=fmtN(pv.shards_paid_10m||0);$('p-full').textContent=fmtN(pv.blocks_fully_proven_10m||0)+' of '+fmtN(pv.blocks_10m||0);$('p-lag').textContent=pv.median_proof_lag_s?'a median '+Math.round(pv.median_proof_lag_s)+' s':'pending';} $('i-blue').textContent=fmtN(b.blue_score);$('i-daa').textContent=fmtN(b.daa);
else{var w=pv&&pv.supported?'proving not active':'no proving layer';$('s-paid').textContent=w;$('s-paid').className='v state';$('s-paid-s').textContent='';$('prov-state').textContent=w;} var sh=b.shards||[];$('i-shards').textContent=sh.length?sh.map(function(s){return (s.i+1)+'/'+s.n+' '+s.state;}).join(', '):(b.chain?'no plan yet':'none');
set('s-keys',fmtN(s.miners_10m!==null&&s.miners_10m!==undefined?s.miners_10m:(d.miners||[]).length)); var fn=d.finality,cp=null;if(fn&&fn.checkpoints)fn.checkpoints.forEach(function(c){if(b.locked&&c.hash===b.hash)cp=c;});
// finality: the last lock, and the rule in plain words $('i-cp').textContent=cp?'this block, #'+fmtN(cp.index):(b.final?'under the newest lock':'not a checkpoint');var lk=$('i-lock');lk.textContent=cp?(cp.state==='locked'?'locked'+(cp.fraction_total?', '+Math.round(cp.fraction_total*100)+'% of weight':''):cp.state):(b.final?'final':'not final');lk.className=(cp&&cp.state==='locked')||b.final?'hot':'';
var fin=$('fin'),lock=null;if(fn&&fn.checkpoints)fn.checkpoints.forEach(function(c){if(c.state==='locked'&&(!lock||c.index>lock.index))lock=c;}); $('i-time').textContent=hms(b.ts)+' UTC';}
if(fn&&fn.supported&&!fn.active){set('s-lock',fn.latest_locked_index?'#'+fmtN(fn.latest_locked_index):'none');$('s-lock-s').textContent='finality paused'; function uninspect(){sel=null;$('insp').hidden=true;$('insp-empty').hidden=false;$('i-kind').textContent='select a block';}
fin.innerHTML='<span class="dot off"></span><span><b>Finality is paused.</b> Under two thirds of the 30-day weight is signing, so no checkpoint locks until it returns. The chain keeps growing; the last lock stands.</span>';} $('insp-close').addEventListener('click',function(){uninspect();if(dag)dag.engage(false);});
else if(lock){set('s-lock','#'+fmtN(lock.index));var t=lock.locked_at?new Date(String(lock.locked_at).replace(' ','T')).getTime():NaN;$('s-lock-s').textContent=isNaN(t)?'locked':'locked '+rel(now-t); function render(d){var s=d.state,pv=d.proving,fn=d.finality,now=new Date(d.now).getTime();lastD=d;
var pct=lock.fraction_total?Math.round(lock.fraction_total*100):(lock.signed&&lock.total?Math.round(lock.signed/lock.total*100):null); // the six tiles
fin.innerHTML='<span class="dot live"></span><span><b>Finality is live.</b> Checkpoint #'+fmtN(lock.index)+' locked'+(isNaN(t)?'':' '+rel(now-t))+(pct?' with '+pct+'% of all 30-day weight signing':'')+'. A checkpoint locks when two thirds of all weight signs it, every 30 seconds.</span>';} set('s-bps',s.blocks_per_second_60s!==null&&s.blocks_per_second_60s!==undefined?Number(s.blocks_per_second_60s).toFixed(2):'pending');
else{set('s-lock',fn&&fn.supported?'none yet':'no rule');$('s-lock-s').textContent=fn&&fn.supported?'window filling':'';fin.innerHTML='<span class="dot off"></span><span><b>No lock yet.</b> The 30-day weight window is filling; the first checkpoint locks once it is full.</span>';} set('s-blue',fmtN(s.blue_score));set('s-hash',fmtHash(s.hashes_per_second_estimate));
// miners: top five lanes with a bar each set('s-keys',fmtN(s.miners_10m!==null&&s.miners_10m!==undefined?s.miners_10m:(d.miners||[]).length));set('s-peers',fmtN(s.peers));
var mel=$('miners');if(d.miners&&d.miners.length){var top=d.miners.slice(0,5),max=Math.max(1,top[0].blocks),rest=d.miners.length-top.length; var lock=null;if(fn&&fn.checkpoints)fn.checkpoints.forEach(function(c){if(c.state==='locked'&&(!lock||c.index>lock.index))lock=c;});
$('miners-count').textContent=d.miners.length+' vote keys'; var le=$('s-lock');if(fn&&fn.supported&&!fn.active){set('s-lock',fn.latest_locked_index?'#'+fmtN(fn.latest_locked_index):'none');le.className='v tick';$('s-lock-d').textContent='finality paused: under two thirds of the weight is signing';}
mel.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></div>';}).join(''); else if(lock){set('s-lock','#'+fmtN(lock.index));le.className='v tick';var t=lock.locked_at?new Date(String(lock.locked_at).replace(' ','T')).getTime():NaN;$('s-lock-d').textContent=(isNaN(t)?'locked':'locked '+rel(now-t))+(lock.fraction_total?' with '+Math.round(lock.fraction_total*100)+'% of all 30-day weight':'');}
$('miners-more').textContent=(rest>0?'and '+rest+' more. ':'')+'Every vote key with a block in the last 10 minutes; a card runs several.';} else{set('s-lock',fn&&fn.supported?'none yet':'no rule');le.className='v tick word';$('s-lock-d').textContent=fn&&fn.supported?'the 30-day weight window is filling':'';}
else{mel.innerHTML='<div class="empty">No blocks in the last 10 minutes.</div>';} // the graph's count, the blue range, the chain-only view
// recent blocks: the last eight by time, newest first; a block seen for the first time lands with a glow var w=dag?dag.getWindow():120,cut=now-w*1000,on=[],lo=null,hi=null;(d.blocks||[]).forEach(function(b){if(b.ts>=cut&&(!chainOnly||b.chain)){on.push(b);if(lo===null||b.blue_score<lo)lo=b.blue_score;if(hi===null||b.blue_score>hi)hi=b.blue_score;}});
var list=(d.blocks||[]).slice().sort(function(a,b){return b.ts-a.ts;}).slice(0,8),bel=$('blocks'); $('c-count').textContent=on.length+' blocks';$('blue-range').textContent=lo===null?'blue score pending':'blue score '+fmtN(lo)+' to '+fmtN(hi);
if(list.length){bel.innerHTML=list.map(function(b){var word=b.locked?'locked':b.proven?'proven':b.color==='red'?'excluded':b.chain?'on the chain':b.color==='blue'?'included':'pending';var fresh=!seen[b.hash];seen[b.hash]=1; // block activity: the last 30 minutes, one bar a minute, from the observer's per-minute counts
return '<div'+(fresh&&Object.keys(seen).length>8?' class="new"':'')+'><span class="num">'+(b.number?fmtN(b.number):esc(b.hash))+'</span><span>'+esc(b.miner||'')+'</span><span class="w'+(word==='locked'||word==='proven'?' hot':'')+'">'+word+'</span></div>';}).join('');} var bpm=(s.blocks_per_minute||[]).slice(-30),max=Math.max(1,Math.max.apply(null,bpm.map(function(p){return p[1]||0;}).concat([1])));
if(window.IgneumYourCard)IgneumYourCard.update(s.hashes_per_second_estimate); $('a-count').textContent=on.length;$('a-bars').innerHTML=bpm.map(function(p,i){return '<i style="height:'+Math.max(2,Math.round((p[1]||0)/max*100))+'%" title="'+(p[1]||0)+' blocks"'+(i===bpm.length-1?' class="now"':'')+'></i>';}).join('');
lastD=d;sceneCounters(d);if(dag)dag.push(d); var tot30=bpm.reduce(function(a,p){return a+(p[1]||0);},0);$('a-note').textContent=fmtN(tot30)+' blocks in the last '+bpm.length+' min, '+(bpm.length?(tot30/bpm.length).toFixed(1):'0')+' a minute on average; the target is 60.';
// mining identities: share of the displayed blocks, the reader's own key marked when known
var share={},tot=0;on.forEach(function(b){share[b.miner]=(share[b.miner]||0)+1;tot++;});var ids=Object.keys(share).sort(function(a,b){return share[b]-share[a];}),mel=$('miners');
if(ids.length){var top=ids.slice(0,8),rest=ids.length-top.length;mel.innerHTML=top.map(function(id){var n=share[id],pc=n/tot*100;return '<div class="id-row'+(mine&&id===mine?' mine':'')+'"><span class="name"><i class="sw"></i>'+esc(id)+(mine&&id===mine?' (you)':'')+'</span><span class="pct">'+pc.toFixed(1)+'%</span><div class="bar"><i style="width:'+Math.round(pc)+'%"></i></div></div>';}).join('');
$('miners-more').innerHTML=(rest>0?'and '+rest+' more. ':'')+'Vote-key share of the displayed blocks. Several keys may belong to one GPU. <button type="button" class="linkish" id="mine-btn">'+(mine?'Change my key':'Mark my key')+'</button>';}
else mel.innerHTML='<div class="empty">No blocks in the window.</div>';
// the event stream: the observer's own events plus what changed between reads
var evs=[];(d.events||[]).forEach(function(e){evs.push({ts:new Date(e.ts).getTime(),k:e.kind.replace(/_/g,' '),cls:e.kind==='checkpoint_locked'?'lock':/quiet/.test(e.kind)?'quiet':'',text:e.text});});
(d.blocks||[]).forEach(function(b){if(!seen[b.hash]){seen[b.hash]=1;if(ready)evs.push({ts:b.ts,k:'Block observed',cls:'',text:(b.number?'#'+fmtN(b.number):b.hash)+' by '+b.miner});}
(b.shards||[]).forEach(function(sh){var key=b.hash+':'+sh.i,prev=shardState[key];shardState[key]=sh.state;if(ready&&prev!==sh.state&&(sh.state==='verified'||sh.state==='paid'))evs.push({ts:now,k:sh.state==='paid'?'Shard paid':'Shard proven',cls:'',text:(sh.i+1)+' of '+sh.n+' for '+(b.number?'#'+fmtN(b.number):b.hash)+(sh.prover?' by '+sh.prover:'')});});});
if(fn&&fn.checkpoints)fn.checkpoints.forEach(function(c){var prev=cpState[c.index];cpState[c.index]=c.state;if(ready&&prev!==c.state)evs.push({ts:c.state==='locked'&&c.locked_at?new Date(String(c.locked_at).replace(' ','T')).getTime():now,k:'Checkpoint '+c.state,cls:c.state==='locked'?'lock':'',text:'#'+fmtN(c.index)+(c.fraction_total?', '+Math.round(c.fraction_total*100)+'% of weight':'')});});
evs.forEach(function(e){var id=e.k+'|'+e.text+'|'+Math.round(e.ts/1000);if(!evSeen[id]){evSeen[id]=1;evList.unshift({id:id,fresh:ready,ts:e.ts,k:e.k,cls:e.cls,text:e.text});}});
evList.sort(function(a,b){return b.ts-a.ts;});evList=evList.slice(0,14);ready=true;
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>';
if(sel){var cur=(d.blocks||[]).filter(function(b){return b.hash===sel.hash;})[0];if(cur)inspect(cur,d);}
} }
// the latest-block cell ticks every second between reads // the graph: the shared module, fed by this page's own reads so the chain-only toggle can filter what it draws
setInterval(function(){if(lastTs){var el=$('s-last');el.textContent=rel(Date.now()-lastTs);}},1000); var failures=0;
var c=$('steps');
if(c&&window.IgneumFeed&&window.IgneumSteps){var steps=IgneumSteps.mount(c,{caption:cap});IgneumFeed.start({window:300,subs:[steps],onState:onState,onData:onData});}
// the DAG below, fed by the same reads
var dag=null,lastD=null;
function sceneCounters(d){var w=dag?dag.getWindow():120,cut=new Date(d.now).getTime()-w*1000,on=0,chain=0;(d.blocks||[]).forEach(function(b){if(b.ts>=cut){on++;if(b.chain)chain++;}});$('c-screen').textContent=on;$('c-chain').textContent=chain;}
function mountDag(){if(dag||!window.IgneumDag)return;dag=IgneumDag.mount($('dag'),{poll:false,window:120,tooltip:$('dag-tip'),chip:$('dag-chip'),pauseButton:$('dag-pause'), function mountDag(){if(dag||!window.IgneumDag)return;dag=IgneumDag.mount($('dag'),{poll:false,window:120,tooltip:$('dag-tip'),chip:$('dag-chip'),pauseButton:$('dag-pause'),
onWindow:function(w){$('c-window').textContent=w+' s';}, onWindow:function(w){zoomPct=Math.round(120/w*100);$('z-pct').textContent=zoomPct+'%';if(lastD)render(lastD);},
onState:function(st,reason){var el=$('dag-state');el.className='state '+(st==='live'?'live':st==='failed'?'bad':'');el.innerHTML='<span class="dot '+(st==='live'?'live':st==='failed'?'bad':'off')+'"></span>'+(st==='live'?'the DAG, live':st==='connecting'?'the DAG':esc(reason||st));}}); onState:function(st,reason){var el=$('dag-state');el.className='state '+(st==='live'?'live':st==='failed'?'bad':'');el.innerHTML='<span class="dot '+(st==='live'?'live':st==='failed'?'bad':'off')+'"></span>'+(st==='live'?'live devnet':esc(reason||st));},
if(lastD)dag.push(lastD);} onSelect:function(b){if(b&&lastD)inspect(b,lastD);else uninspect();}});
if(lastD)feed(lastD);}
function feed(d){if(!dag)return;if(!chainOnly){dag.push(d);return;}var c=Object.assign({},d);c.blocks=(d.blocks||[]).filter(function(b){return b.chain;});dag.push(c);}
if(window.IgneumDag)mountDag();else{var ms=document.querySelector('script[src="/live-dag.js"]');if(ms)ms.addEventListener('load',mountDag);} if(window.IgneumDag)mountDag();else{var ms=document.querySelector('script[src="/live-dag.js"]');if(ms)ms.addEventListener('load',mountDag);}
$('chain-only').addEventListener('click',function(){chainOnly=!chainOnly;this.setAttribute('aria-pressed',chainOnly?'true':'false');this.classList.toggle('on',chainOnly);if(lastD){feed(lastD);render(lastD);}});
$('dag-pause').addEventListener('click',function(){var p=this.getAttribute('aria-pressed')!=='true';this.setAttribute('aria-pressed',p?'true':'false');$('dag-follow').classList.toggle('on',!p);});
$('dag-follow').addEventListener('click',function(){if(dag)dag.setPaused(false);$('dag-pause').setAttribute('aria-pressed','false');this.classList.add('on');});
$('z-in').addEventListener('click',function(){if(dag)dag.zoom(0.75);});$('z-out').addEventListener('click',function(){if(dag)dag.zoom(1/0.75);});
function setLive(st,reason){var el=$('live-state');el.className='state '+(st==='live'?'live':st==='failed'?'bad':'');el.innerHTML='<span class="dot '+(st==='live'?'live':st==='failed'?'bad':'off')+'"></span>'+(st==='live'?'live devnet · one node read every 2 s':st==='connecting'?'connecting to the observer':esc(reason||st));}
function poll(){fetch('/api/live?window=300',{cache:'no-store'}).then(function(r){if(!r.ok)throw new Error(r.status);return r.json();}).then(function(d){if(!d.ok)throw new Error(d.error);failures=0;setLive(d.state.stale?'stale':'live',d.state.stale?'observer stale':'');render(d);feed(d);}).catch(function(){failures++;if(failures>=3)setLive('failed','live feed unavailable');});}
poll();setInterval(function(){if(!document.hidden)poll();},2000);
})(); })();
</script> </script>
</body> </body>