igneum/site/live.html
igneum-labs 6a93297dfe Share card: compact 256 px coin so chat apps show the small card
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 21:46:04 +00:00

356 lines
28 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>Igneum live devnet</title>
<meta name="description" content="The Igneum devnet as it runs: blocks, miners, difficulty and peers, read from a node every two seconds.">
<link rel="canonical" href="https://igneum.network/live">
<meta name="theme-color" content="#0C0C0E">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Igneum">
<meta property="og:title" content="Igneum live devnet">
<meta property="og:description" content="The Igneum devnet as it runs: blocks, miners, difficulty and peers, read from a node every two seconds.">
<meta property="og:url" content="https://igneum.network/live">
<meta property="og:image" content="https://igneum.network/og-small.png">
<meta property="og:image:width" content="256">
<meta property="og:image:height" content="256">
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="Igneum live devnet">
<meta name="twitter:description" content="The Igneum devnet as it runs: blocks, miners, difficulty and peers, read from a node every two seconds.">
<meta name="twitter:image" content="https://igneum.network/og-small.png">
<meta name="twitter:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Ccircle cx='50' cy='50' r='48' fill='%230C0C0E'/%3E%3Ccircle cx='50' cy='50' r='45' fill='none' stroke='%23F2541B' stroke-width='6'/%3E%3Cg transform='translate(24 24) scale(0.52)'%3E%3Cpolygon points='50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34' fill='%23F2541B'/%3E%3Cpolygon points='50,42 59,58 50,82 41,58' fill='%230C0C0E'/%3E%3C/g%3E%3C/svg%3E" type="image/svg+xml" type="image/svg+xml">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192">
<link rel="icon" href="/icon-512.png" type="image/png" sizes="512x512">
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<link rel="manifest" href="/site.webmanifest">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Unbounded:wght@500;700;900&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap">
<style>
:root{--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E;
--fs-hero:clamp(32px,5.5vw,56px);--gutter:clamp(16px,4vw,32px);--max:1200px}
*{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;background:var(--obsidian);color:var(--bone);font-family:'IBM Plex Sans',system-ui,-apple-system,sans-serif;font-size:17px;line-height:1.55;-webkit-font-smoothing:antialiased;overflow-x:hidden}
a{color:inherit;text-decoration:none}
a:hover{color:var(--molten)}
a:focus-visible,button:focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px}
img,svg,canvas{max-width:100%}
.wrap{max-width:var(--max);margin:0 auto;padding-inline:var(--gutter)}
.mono{font-family:'IBM Plex Mono',monospace}
.eyebrow{font-family:'IBM Plex Mono',monospace;font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--ash)}
h1,h2,h3{font-family:'Unbounded',sans-serif;margin:0;line-height:1.08;text-wrap:balance}
h1{font-weight:900;font-size:var(--fs-hero);letter-spacing:-.01em}
h2{font-weight:700;font-size:clamp(18px,2vw,22px)}
p{margin:0}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:48px;padding:12px 22px;border-radius:10px;font-weight:600;font-size:16px;border:1px solid var(--line-2);color:var(--bone);background:transparent;cursor:pointer}
.btn.primary{background:var(--ember);color:var(--ember-ink);border-color:var(--ember)}
.btn.primary:hover{background:#FF6A2B;border-color:#FF6A2B;color:var(--ember-ink)}
.card{background:var(--graphite);border:1px solid var(--line);border-radius:18px;padding:clamp(16px,3vw,28px);min-width:0}
/* nav, same as the homepage */
.nav{position:sticky;top:0;z-index:20;background:rgba(12,12,14,.82);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-bottom:1px solid rgba(42,42,48,.6)}
.nav .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:68px}
.brand{display:flex;align-items:center;gap:10px}
.brand .word{font-family:'Unbounded',sans-serif;font-weight:900;font-size:20px;letter-spacing:.06em}
.links{display:flex;align-items:center;gap:26px;font-size:15px;font-weight:500}
.links .btn{min-height:42px;padding:10px 16px;font-size:15px}
.links a.here{color:var(--molten)}
.burger{display:none;background:none;border:1px solid var(--line-2);color:var(--bone);border-radius:10px;width:44px;height:44px;align-items:center;justify-content:center;cursor:pointer}
@media (max-width:900px){
.links{display:none;position:absolute;left:0;right:0;top:68px;background:var(--obsidian);border-bottom:1px solid var(--line);padding:16px var(--gutter) 24px;flex-direction:column;align-items:stretch;gap:6px}
.links a{padding:12px 6px;border-radius:8px}
.links .btn{width:100%;margin-top:6px}
.links.open{display:flex}
.burger{display:inline-flex}
}
/* page */
.head{padding-block:clamp(36px,6vw,64px) clamp(20px,3vw,28px);display:flex;flex-direction:column;gap:14px}
.head p{color:var(--ink-2);max-width:60ch;font-size:16px}
.strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,150px),1fr));gap:12px;margin-bottom:24px}
.cell{background:var(--graphite);border:1px solid var(--line);border-radius:14px;padding:14px 16px;display:flex;flex-direction:column;gap:6px;min-width:0}
.cell .k{font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
.cell .v{font-family:'Unbounded',sans-serif;font-weight:700;font-size:clamp(18px,2.2vw,24px);line-height:1.1;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cell .s{font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--ash)}
.cell.status .v{display:flex;align-items:center;gap:10px}
.dot{width:10px;height:10px;border-radius:50%;background:var(--molten);display:inline-block;flex:0 0 10px;animation:pulse 2s ease-in-out infinite}
.off .dot{background:var(--ash);animation:none}
.off .cell.status .v{color:var(--ash)}
.on .cell.status .v{color:var(--molten)}
@keyframes pulse{0%,100%{box-shadow:0 0 0 0 rgba(255,179,92,.5)}50%{box-shadow:0 0 0 8px rgba(255,179,92,0)}}
@media (prefers-reduced-motion:reduce){.dot{animation:none}}
.viz-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px 20px;margin-bottom:12px}
.viz-stats{display:flex;flex-wrap:wrap;gap:14px 18px;font-size:12px;color:var(--ash)}
.viz-stats b{color:var(--bone);font-weight:500;font-variant-numeric:tabular-nums}
#dag{display:block;width:100%;height:clamp(240px,38vw,380px);border-radius:12px;background:var(--obsidian)}
.legend{display:flex;flex-wrap:wrap;gap:16px 20px;margin-top:16px;font-size:13px;color:var(--ink-2)}
.legend span{display:inline-flex;align-items:center;gap:8px}
.sw{width:14px;height:14px;border-radius:4px;display:inline-block}
.two{display:grid;gap:24px;grid-template-columns:1fr;margin-top:24px}
@media (min-width:900px){.two{grid-template-columns:1.1fr .9fr}}
.tbl{overflow-x:auto;margin-top:12px;-webkit-overflow-scrolling:touch}
table{border-collapse:collapse;width:100%;font-size:14px}
th,td{padding:10px 10px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}
th{font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);font-weight:500}
td.n{font-variant-numeric:tabular-nums;text-align:right}th.n{text-align:right}
tr:last-child td{border-bottom:0}
.id{font-family:'IBM Plex Mono',monospace;color:var(--molten)}
.bar{height:6px;border-radius:3px;background:var(--line);overflow:hidden;min-width:60px}
.bar i{display:block;height:100%;background:var(--ember)}
.feed{display:flex;flex-direction:column;margin-top:12px;font-family:'IBM Plex Mono',monospace;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:last-child{border-bottom:0}
.feed .t{color:var(--ash);flex:0 0 auto;font-size:12px}
.feed .k{color:var(--molten)}
.empty{color:var(--ash);font-size:14px;padding:12px 0}
#spark{display:block;width:100%;height:120px;border-radius:12px;background:var(--obsidian);margin-top:12px}
.note{font-size:13px;color:var(--ash);margin-top:14px}
main{padding-bottom:clamp(56px,8vw,96px)}
/* footer, same as the homepage */
footer{border-top:1px solid var(--line);padding-block:48px 32px}
.foot-grid{display:grid;gap:32px;grid-template-columns:1fr;margin-bottom:32px}
@media (min-width:720px){.foot-grid{grid-template-columns:1.4fr 1fr 1fr}}
.foot-brand p{color:var(--ash);font-size:15px;margin-top:12px;max-width:40ch}
.foot-col{display:flex;flex-direction:column;gap:10px;font-size:15px}
.foot-col .eyebrow{margin-bottom:6px}
.foot-col a{color:var(--ink-2)}
.foot-base{display:flex;flex-wrap:wrap;justify-content:space-between;gap:12px;border-top:1px solid var(--line);padding-top:24px;font-size:13px;color:var(--ash)}
</style>
</head>
<body class="off">
<nav class="nav" aria-label="Main">
<div class="wrap">
<a href="/" class="brand" aria-label="Igneum home">
<svg viewBox="0 0 100 100" width="30" height="30" aria-hidden="true"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></svg>
<span class="word">IGNEUM</span>
</a>
<button class="burger" id="burger" aria-expanded="false" aria-controls="links" aria-label="Menu"><svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M4 7h16M4 12h16M4 17h16"></path></svg></button>
<div class="links" id="links">
<a href="/#mine">Mine</a>
<a href="/#prove">Prove</a>
<a href="/#build">Build</a>
<a href="/#randomx">vs RandomX</a>
<a href="/#journey">Journey</a>
<a href="/#economics">Economics</a>
<a href="/litepaper">Litepaper</a>
<a href="/bench">Log</a>
<a href="/live" class="here" aria-current="page">Live</a>
<a href="/#mine" class="btn primary">Get the miner</a>
</div>
</div>
</nav>
<main class="wrap">
<div class="head">
<div class="eyebrow">devnet v0</div>
<h1>Live devnet</h1>
<p>A node is read every two seconds. Every block below is real: its parents, its miner, whether it sits on the selected chain, and whether it is a locked checkpoint. A checkpoint locks when the finality rule's quorum signs it. Proving is not in the devnet yet.</p>
</div>
<div class="strip" aria-live="polite">
<div class="cell status"><div class="k">Status</div><div class="v"><span class="dot"></span><span id="st-status">OFFLINE</span></div><div class="s" id="st-age">no observer update yet</div></div>
<div class="cell"><div class="k">Network</div><div class="v mono" id="st-network" style="font-family:'IBM Plex Mono',monospace;font-size:16px;font-weight:500">igneum-devnet</div><div class="s" id="st-node">node</div></div>
<div class="cell"><div class="k">Blocks / s</div><div class="v" id="st-bps">0.00</div><div class="s">over 60 s</div></div>
<div class="cell"><div class="k">Blue score</div><div class="v" id="st-blue">0</div><div class="s" id="st-count">0 blocks</div></div>
<div class="cell"><div class="k">Difficulty</div><div class="v" id="st-diff">0</div><div class="s">target per block</div></div>
<div class="cell"><div class="k">Hash rate</div><div class="v" id="st-hps">0</div><div class="s">network estimate</div></div>
<div class="cell"><div class="k">Miners</div><div class="v" id="st-miners">0</div><div class="s">active in 10 min</div></div>
<div class="cell"><div class="k">Peers</div><div class="v" id="st-peers">0</div><div class="s" id="st-mempool">mempool 0</div></div>
</div>
<div class="card" aria-label="Block DAG, live">
<div class="viz-head">
<div class="eyebrow"><span class="dot" style="margin-right:8px"></span>block 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>side <b id="c-side">0</b></span><span>newest blue <b id="c-newest">0</b></span><span>locked <b id="c-locked">none</b></span></div>
</div>
<canvas id="dag" aria-hidden="true"></canvas>
<div class="legend">
<span><i class="sw" style="background:var(--ember)"></i>chain block</span>
<span><i class="sw" style="border:2px solid var(--ash)"></i>side block (blue or red, off the selected chain)</span>
<span><i class="sw" style="border:3px solid var(--ember);border-radius:50%;background:transparent"></i>locked checkpoint, final</span>
<span><i class="sw" style="border:2px solid var(--molten);box-shadow:0 0 8px rgba(255,179,92,.7)"></i>just arrived</span>
<span><i class="sw" style="width:22px;height:0;border-top:1px solid var(--ash)"></i>parent edge</span>
<span class="mono" style="color:var(--molten)">2ea5116e</span><span style="margin-left:-12px">miner short id, one lane each</span>
</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>
<div class="card">
<div class="viz-head"><h2>Events</h2><div class="eyebrow">newest first</div></div>
<div class="feed" id="events"><div class="empty">No events yet.</div></div>
</div>
</div>
<div class="card" style="margin-top:24px">
<div class="viz-head"><h2>Blocks per minute</h2><div class="eyebrow" id="spark-range">last hour</div></div>
<canvas id="spark" aria-hidden="true"></canvas>
<p class="note" id="spark-note">Counted by the observer as blocks arrive.</p>
</div>
</main>
<footer>
<div class="wrap">
<div class="foot-grid">
<div class="foot-brand">
<div class="brand"><svg viewBox="0 0 100 100" width="26" height="26" aria-hidden="true"><polygon points="50,4 74,34 67,58 80,54 61,96 39,96 20,54 33,58 26,34" fill="#F2541B"></polygon><polygon points="50,42 59,58 50,82 41,58" fill="#0C0C0E"></polygon></svg><span class="word" style="font-size:16px;color:var(--bone)">IGNEUM</span></div>
<p>Mined by GPUs. Proven by fire.</p>
</div>
<nav class="foot-col" aria-label="Read">
<div class="eyebrow">Read</div>
<a href="/litepaper">Litepaper</a>
<a href="/litepaper#limits">What Igneum does not claim</a>
<a href="/litepaper#randomx">Igneum vs RandomX</a>
<a href="/bench">Engineering log</a>
</nav>
<nav class="foot-col" aria-label="Follow">
<div class="eyebrow">Follow</div>
<a href="/#journey">Journey</a>
<a href="/live">Live devnet</a>
<a href="https://github.com/igneum-network" rel="noopener" style="display:inline-flex;align-items:center;gap:6px"><svg viewBox="0 0 24 24" width="15" height="15" fill="currentColor" aria-hidden="true"><path d="M12 .5C5.7.5.5 5.7.5 12c0 5.1 3.3 9.4 7.9 10.9.6.1.8-.3.8-.6v-2.1c-3.2.7-3.9-1.4-3.9-1.4-.5-1.3-1.3-1.7-1.3-1.7-1-.7.1-.7.1-.7 1.2.1 1.8 1.2 1.8 1.2 1 1.8 2.7 1.3 3.4 1 .1-.8.4-1.3.7-1.6-2.6-.3-5.3-1.3-5.3-5.7 0-1.3.5-2.3 1.2-3.1-.1-.3-.5-1.5.1-3.1 0 0 1-.3 3.2 1.2.9-.3 1.9-.4 2.9-.4s2 .1 2.9.4c2.2-1.5 3.2-1.2 3.2-1.2.6 1.6.2 2.8.1 3.1.8.8 1.2 1.8 1.2 3.1 0 4.4-2.7 5.4-5.3 5.7.4.4.8 1.1.8 2.2v3.2c0 .3.2.7.8.6 4.6-1.5 7.9-5.8 7.9-10.9C23.5 5.7 18.3.5 12 .5z"/></svg>GitHub</a>
<a href="/#mine">Get the miner</a>
</nav>
</div>
<div class="foot-base">
<span>© 2026 Igneum. Nothing on this page is an offer to sell anything.</span>
<span class="mono">igneum.network</span>
</div>
</div>
</footer>
<script>
(function(){
var reduce=window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var dpr=Math.min(2,window.devicePixelRatio||1);
// nav
var burger=document.getElementById('burger'),links=document.getElementById('links');
burger.addEventListener('click',function(){var open=links.classList.toggle('open');burger.setAttribute('aria-expanded',open?'true':'false');});
// formatting
function $(id){return document.getElementById(id);}
function compact(n){if(n===null||n===undefined||isNaN(n))return '0';var a=Math.abs(n);if(a>=1e12)return (n/1e12).toFixed(2)+'T';if(a>=1e9)return (n/1e9).toFixed(2)+'G';if(a>=1e6)return (n/1e6).toFixed(2)+'M';if(a>=1e3)return (n/1e3).toFixed(1)+'k';return String(Math.round(n));}
function hashrate(n){if(n===null||n===undefined)return 'n/a';var u=['H/s','kH/s','MH/s','GH/s','TH/s'],i=0;while(n>=1000&&i<u.length-1){n/=1000;i++;}return (i?n.toFixed(2):Math.round(n))+' '+u[i];}
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 '+(m%60)+' min ago';return Math.floor(h/24)+' d ago';}
function esc(s){return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');}
// ---------- DAG scene ----------
var c=$('dag'),x=c.getContext('2d'),W,H,S,blocks=[],byHash={},lanesOf={},laneOrder=[],maxLanes=6,pxPerSec=40,targetPx=40;
var live=false,serverNow=0,serverAt=0,frozenAt=null,note='',bps=0;
function size(){W=c.clientWidth;H=c.clientHeight;c.width=W*dpr;c.height=H*dpr;x.setTransform(dpr,0,0,dpr,0,0);S=W<600?16:22;maxLanes=H<260?4:6;}
size();window.addEventListener('resize',function(){size();draw(performance.now());});
function laneCount(){return Math.max(1,Math.min(maxLanes,laneOrder.length));}
function laneFor(m){m=m||'?';if(!(m in lanesOf)){lanesOf[m]=laneOrder.length;laneOrder.push(m);}return lanesOf[m]%laneCount();}
function laneY(l){var n=laneCount();if(n===1)return H*0.5;return H*0.16+l*(H*0.62/(n-1));}
function jitter(h){var v=0;for(var i=0;i<8;i++)v=(v*31+h.charCodeAt(i))%1000;return (v/1000-0.5)*S*0.5;}
function vnow(){if(frozenAt!==null)return frozenAt;return serverNow+(performance.now()-serverAt);}
function ingest(d){
var now=performance.now();serverNow=new Date(d.now).getTime();serverAt=now;
var seen={};
d.blocks.forEach(function(b){seen[b.hash]=1;var e=byHash[b.hash];
if(e){e.chain=b.chain;e.locked=!!b.locked;e.final=!!b.final;return;}
laneFor(b.miner);
e={hash:b.hash,ts:b.ts,rx:b.rx,blue_score:b.blue_score,parents:b.parents,chain:b.chain,miner:b.miner||'?',born:now,j:jitter(b.hash),locked:!!b.locked,final:!!b.final};
byHash[b.hash]=e;blocks.push(e);});
// drop what fell off the left edge long ago
var cut=vnow()-(W/pxPerSec)*1000-20000;
blocks=blocks.filter(function(b){var keep=b.ts>cut;if(!keep)delete byHash[b.hash];return keep;});
blocks.sort(function(a,b){return a.ts-b.ts;});
bps=d.state.blocks_per_second_60s||0;
targetPx=Math.max(S*0.4,Math.min(420,bps*S*1.7));
var chain=0;blocks.forEach(function(b){if(b.chain)chain++;});
$('c-screen').textContent=blocks.length;$('c-chain').textContent=chain;$('c-side').textContent=blocks.length-chain;
$('c-newest').textContent=blocks.length?compact(blocks[blocks.length-1].blue_score):'0';
var f=d.finality;$('c-locked').textContent=f&&f.latest_locked_index?('#'+f.latest_locked_index+' at blue '+compact(f.latest_locked_blue_score)):(f&&f.supported?'none yet':'n/a');
}
function rrect(px,py,w,h,r){x.beginPath();x.moveTo(px+r,py);x.lineTo(px+w-r,py);x.quadraticCurveTo(px+w,py,px+w,py+r);x.lineTo(px+w,py+h-r);x.quadraticCurveTo(px+w,py+h,px+w-r,py+h);x.lineTo(px+r,py+h);x.quadraticCurveTo(px,py+h,px,py+h-r);x.lineTo(px,py+r);x.quadraticCurveTo(px,py,px+r,py);x.closePath();}
function xOf(b,t){return Math.min(W-S,W-S*1.2-(t-b.ts)*pxPerSec/1000);}
function draw(now){
pxPerSec+=(targetPx-pxPerSec)*0.05;
var t=vnow();
x.clearRect(0,0,W,H);
// lane rails and miner labels
var n=laneCount();x.font='500 10px IBM Plex Mono, monospace';x.textAlign='left';
for(var l=0;l<n;l++){var y=laneY(l);x.strokeStyle='rgba(42,42,48,0.7)';x.lineWidth=1;x.beginPath();x.moveTo(0,y);x.lineTo(W,y);x.stroke();
var names=laneOrder.filter(function(m,i){return i%n===l;});if(names.length){x.fillStyle='rgba(154,154,158,0.75)';x.fillText(names.slice(0,2).join(' ')+(names.length>2?' +'+(names.length-2):''),8,y-S*0.75);}}
var pos={};blocks.forEach(function(b){pos[b.hash]={x:xOf(b,t),y:laneY(laneFor(b.miner))+b.j};});
// edges
blocks.forEach(function(b){var p=pos[b.hash];b.parents.forEach(function(h){var q=pos[h];if(!q)return;var both=b.chain&&byHash[h]&&byHash[h].chain;
x.strokeStyle=both?'rgba(242,84,27,0.75)':'rgba(201,199,194,0.45)';x.lineWidth=both?2:1.25;x.beginPath();x.moveTo(p.x-S/2,p.y);var mx=(p.x+q.x)/2;x.bezierCurveTo(mx,p.y,mx,q.y,q.x+S/2,q.y);x.stroke();});});
// lock line: everything left of the newest locked checkpoint is final (mirrors the homepage simulation)
var lk=null;blocks.forEach(function(b){if(b.locked&&(!lk||b.ts>lk.ts))lk=b;});
if(lk){var lp=pos[lk.hash];x.strokeStyle='rgba(242,84,27,0.55)';x.lineWidth=1.5;x.setLineDash([5,6]);x.beginPath();x.moveTo(lp.x,6);x.lineTo(lp.x,H-6);x.stroke();x.setLineDash([]);x.fillStyle='rgba(154,154,158,0.9)';x.font='500 10px IBM Plex Mono, monospace';x.textAlign='right';x.fillText('final',lp.x-8,14);}
// blocks
blocks.forEach(function(b){var p=pos[b.hash],h=S/2,age=now-b.born,fl=Math.max(0,1-age/900);
if(fl>0){var g=x.createRadialGradient(p.x,p.y,0,p.x,p.y,S*1.8);g.addColorStop(0,'rgba(255,179,92,'+(0.55*fl)+')');g.addColorStop(1,'rgba(255,179,92,0)');x.fillStyle=g;x.beginPath();x.arc(p.x,p.y,S*1.8,0,Math.PI*2);x.fill();}
if(b.chain){x.fillStyle='#F2541B';rrect(p.x-h,p.y-h,S,S,S*0.24);x.fill();}
else{x.fillStyle='#0C0C0E';rrect(p.x-h,p.y-h,S,S,S*0.24);x.fill();x.strokeStyle=fl>0?'rgba(255,179,92,0.95)':'rgba(154,154,158,0.9)';x.lineWidth=2;rrect(p.x-h,p.y-h,S,S,S*0.24);x.stroke();}
if(fl>0&&b.chain){x.strokeStyle='rgba(255,179,92,'+fl+')';x.lineWidth=2;rrect(p.x-h-2,p.y-h-2,S+4,S+4,S*0.3);x.stroke();}
if(b.locked){x.strokeStyle='#F2541B';x.lineWidth=3;x.beginPath();x.arc(p.x,p.y,S*0.95,0,Math.PI*2);x.stroke();}
x.fillStyle=b.chain?'rgba(244,241,236,0.9)':'rgba(154,154,158,0.9)';x.font='500 '+(S<20?8:9)+'px IBM Plex Mono, monospace';x.textAlign='center';x.fillText(b.miner,p.x,p.y+h+11);});
// tip marker
if(blocks.length){var tip=blocks[blocks.length-1],tp=pos[tip.hash];x.fillStyle='rgba(154,154,158,0.9)';x.font='500 10px IBM Plex Mono, monospace';x.textAlign='right';x.fillText('tip',Math.min(W-6,tp.x+S*0.5),Math.max(12,tp.y-S*0.75));}
if(!blocks.length){x.fillStyle='rgba(154,154,158,0.9)';x.font='500 13px IBM Plex Mono, monospace';x.textAlign='center';x.fillText(live?'waiting for the first block':'no observer data',W/2,H/2);}
if(note){x.fillStyle='rgba(12,12,14,0.72)';x.fillRect(0,H/2-22,W,44);x.fillStyle='#F4F1EC';x.font='500 13px IBM Plex Mono, monospace';x.textAlign='center';x.fillText(note,W/2,H/2+5);}
}
function frame(now){draw(now);if(!reduce)requestAnimationFrame(frame);}
requestAnimationFrame(frame);
// ---------- sparkline ----------
var sc=$('spark'),sx=sc.getContext('2d');
function spark(pairs){var w=sc.clientWidth,h=sc.clientHeight;sc.width=w*dpr;sc.height=h*dpr;sx.setTransform(dpr,0,0,dpr,0,0);sx.clearRect(0,0,w,h);
if(!pairs||!pairs.length){sx.fillStyle='rgba(154,154,158,0.9)';sx.font='500 12px IBM Plex Mono, monospace';sx.textAlign='center';sx.fillText('no data yet',w/2,h/2);return;}
var vals=pairs.map(function(p){return p[1];}),max=Math.max(1,Math.max.apply(null,vals)),pad=10,n=vals.length;
function px(i){return pad+i*(w-2*pad)/Math.max(1,n-1);}function py(v){return h-pad-(v/max)*(h-2*pad-12);}
sx.strokeStyle='rgba(42,42,48,0.9)';sx.lineWidth=1;sx.beginPath();sx.moveTo(pad,py(0));sx.lineTo(w-pad,py(0));sx.stroke();
sx.beginPath();sx.moveTo(px(0),py(0));vals.forEach(function(v,i){sx.lineTo(px(i),py(v));});sx.lineTo(px(n-1),py(0));sx.closePath();sx.fillStyle='rgba(242,84,27,0.14)';sx.fill();
sx.beginPath();vals.forEach(function(v,i){i?sx.lineTo(px(i),py(v)):sx.moveTo(px(i),py(v));});sx.strokeStyle='#F2541B';sx.lineWidth=2;sx.lineJoin='round';sx.stroke();
var li=n-1;sx.fillStyle='#FFB35C';sx.beginPath();sx.arc(px(li),py(vals[li]),3,0,Math.PI*2);sx.fill();
sx.fillStyle='rgba(154,154,158,0.9)';sx.font='500 10px IBM Plex Mono, monospace';sx.textAlign='left';sx.fillText('max '+max+' / min',pad,12);sx.textAlign='right';sx.fillText('now',w-pad,12);
var total=vals.reduce(function(a,b){return a+b;},0);$('spark-note').textContent=total+' blocks in the last hour, '+(total/60).toFixed(1)+' per minute on average.';}
// ---------- polling ----------
var minersEl=$('miners'),eventsEl=$('events'),failures=0;
function setStatus(on,ageText){document.body.className=on?'on':'off';$('st-status').textContent=on?'LIVE':'OFFLINE';$('st-age').textContent=ageText;}
function render(d){
var s=d.state,on=!s.stale;
setStatus(on,s.age_s===null?'no observer update yet':'observer updated '+rel(s.age_s*1000));
$('st-network').textContent=s.network||'igneum-devnet';$('st-node').textContent=s.node_version?'node '+s.node_version:'node';
$('st-bps').textContent=(s.blocks_per_second_60s||0).toFixed(2);
$('st-blue').textContent=compact(s.blue_score);$('st-count').textContent=compact(s.block_count)+' blocks, '+compact(s.header_count)+' headers';
$('st-diff').textContent=compact(s.difficulty);$('st-hps').textContent=hashrate(s.hashes_per_second_estimate);
$('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('');}
else eventsEl.innerHTML='<div class="empty">No events yet.</div>';
spark(s.blocks_per_minute);
live=on;
if(on){frozenAt=null;note='';ingest(d);}
else{if(frozenAt===null)frozenAt=vnow()||now;note='observer offline, last update '+(s.age_s===null?'unknown':rel(s.age_s*1000));}
}
function poll(){fetch('/api/live',{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;render(d);}).catch(function(){failures++;if(failures>=3){setStatus(false,'api unreachable');if(frozenAt===null)frozenAt=vnow();note='api unreachable, scene frozen';}});}
poll();setInterval(poll,2000);
})();
</script>
</body>
</html>