igneum/site/live.html
igneum-josh 37bfd8faf7 site: search and share metadata on every page, with the logo
Titles, descriptions, canonical URLs, Open Graph and Twitter cards on index, litepaper, live and bench (bench through page() in build.mjs). og.png 1200x630 from the brand wordmark and tagline, PNG icons, apple-touch-icon, favicon.ico, site.webmanifest, robots.txt and sitemap.xml. Organization JSON-LD on the homepage only. Rebuilt bench.html.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 19:41:45 +00:00

350 lines
26 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.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Igneum. Mined by GPUs. Proven by fire.">
<meta name="twitter:card" content="summary_large_image">
<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.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%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/svg%3E" 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 and whether it sits on the selected chain. Proving and checkpoints are 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></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: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/igneum" 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;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)};
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';
}
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();});});
// 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();}
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>