igneum/site/live.html

532 lines
47 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?v=2">
<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?v=2">
<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 1024 1024'%3E%3Crect width='1024' height='1024' fill='%230C0C0E'/%3E%3Cg transform='translate(166.95 166.95) scale(6.901)'%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">
<link rel="icon" href="/favicon.ico" sizes="48x48">
<link rel="icon" href="/favicon-32.png" type="image/png" sizes="32x32">
<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;
/* one system for every page: container, gutter, section rhythm, card and tile boxes, heading scale (same tokens as the homepage) */
--max:1200px;--gutter:clamp(16px,4vw,32px);--sec:clamp(56px,8vw,96px);--head:clamp(40px,6vw,64px);
--card-pad:clamp(18px,3vw,28px);--card-r:18px;--tile-pad:18px 20px;--tile-r:14px;--gap:24px;--gap-tile:12px;
--fs-hero:clamp(40px,7vw,76px);--fs-h1:clamp(32px,5.5vw,56px);--fs-h2:clamp(28px,4.2vw,44px);--fs-h3:clamp(18px,2vw,22px);--fs-tile:clamp(20px,2.2vw,26px)}
*{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-h1);letter-spacing:-.01em}
h2{font-weight:700;font-size:var(--fs-h3)}
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:var(--card-r);padding:var(--card-pad);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:var(--head) var(--gap);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(4,minmax(0,1fr));gap:var(--gap-tile);margin-bottom:var(--gap)}
@media (max-width:700px){.strip{grid-template-columns:repeat(2,minmax(0,1fr))}}
.cell{background:var(--graphite);border:1px solid var(--line);border-radius:var(--tile-r);padding:var(--tile-pad);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:var(--fs-tile);line-height:1.1;font-variant-numeric:tabular-nums;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cell .v.name{font-family:'IBM Plex Mono',monospace;font-size:15px;font-weight:500}
.cell .s{font-family:'IBM Plex Mono',monospace;font-size:12px;color:var(--ash);text-wrap:balance}
.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:calc(clamp(240px,38vw,380px) + 18px + 64px);border-radius:12px;background:var(--obsidian);touch-action:pan-y}
@media (max-width:640px){#dag{height:calc(clamp(240px,38vw,380px) + 18px + 50px)}}
.legend{display:flex;flex-direction:column;gap:8px;margin-top:16px;font-size:13px;color:var(--ink-2)}
.legend div{display:flex;flex-wrap:wrap;align-items:center;gap:6px 10px}
.legend .lk{font-family:'IBM Plex Mono',monospace;font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);min-width:64px}
.legend span{display:inline-flex;align-items:center;gap:6px;white-space:nowrap}
.sw{width:14px;height:14px;border-radius:4px;display:inline-block}
.two{display:grid;gap:var(--gap);grid-template-columns:1fr;margin-top:var(--gap)}
@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:var(--sec)}
/* 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)}
.foot-base span{text-wrap:balance}
</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. Once the proving layer is activated, every chain block's shards show as they are planned, proven and paid.</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 name" id="st-network">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">Identities</div><div class="v" id="st-miners">0</div><div class="s">vote keys active in 10 min, a card runs several</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, finality and proving, live">
<div class="viz-head">
<div class="eyebrow"><span class="dot" style="margin-right:8px"></span>blocks, finality, proving</div>
<div class="viz-stats mono"><span>on screen <b id="c-screen">0</b></span><span>chain <b id="c-chain">0</b></span><span>identities <b id="c-idents">0</b></span><span>last lock <b id="c-locked">none</b></span><span>proven <b id="c-proven">n/a</b></span></div>
</div>
<canvas id="dag" aria-hidden="true"></canvas>
<div class="legend">
<div><span class="lk">blocks</span><span><i class="sw" style="background:var(--ember);border:1.5px solid #5CB8FF"></i>chain</span><span><i class="sw" style="background:rgba(92,184,255,.7);border:1.5px solid #5CB8FF"></i>included, paid</span><span><i class="sw" style="border:1.5px solid var(--ash)"></i>pending</span><span><i class="sw" style="border:1.5px solid rgba(154,154,158,.6);background:linear-gradient(45deg,transparent 44%,var(--ash) 44%,var(--ash) 56%,transparent 56%) center/8px 8px no-repeat"></i>excluded</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:2px solid var(--ember)"></i>selected chain</span><span>one lane per miner, colour from its id; hover or tap a block</span></div>
<div><span class="lk">finality</span><span><i class="sw" style="width:22px;height:10px;background:rgba(242,84,27,.22);border-radius:2px"></i>final</span><span><i class="sw" style="width:3px;height:12px;background:var(--molten);border-radius:1px"></i>locked checkpoint</span><span><i class="sw" style="width:22px;height:10px;background:rgba(154,154,158,.10);border:1px solid rgba(154,154,158,.3);border-radius:2px"></i>proposed</span><span>reads "paused" whenever under two thirds of the weight is signing</span></div>
<div><span class="lk">proving</span><span><i class="sw" style="width:12px;height:9px;border:1px solid rgba(154,154,158,.55);border-radius:2px"></i>shard planned</span><span><i class="sw" style="width:12px;height:9px;border:1px solid var(--molten);background:rgba(255,179,92,.25);border-radius:2px"></i>proving</span><span><i class="sw" style="width:12px;height:9px;background:#3FD39A;border-radius:2px"></i>verified, prover colour</span><span><i class="sw" style="width:12px;height:9px;background:#3FD39A;border-radius:2px;position:relative"><i style="position:absolute;left:3px;top:1px;width:3px;height:5px;border:solid #0C0C0E;border-width:0 1.5px 1.5px 0;transform:rotate(45deg)"></i></i>paid</span><span><i class="sw" style="width:0;height:12px;border-left:1px dashed var(--molten)"></i>where proofs land behind the tip</span><span>hover or tap a cell</span></div>
</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:var(--gap)">
<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/spec" 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'),navLinks=document.getElementById('links');
burger.addEventListener('click',function(){var open=navLinks.classList.toggle('open');burger.setAttribute('aria-expanded',open?'true':'false');});
document.addEventListener('click',function(e){if(navLinks.classList.contains('open')&&!navLinks.contains(e.target)&&!burger.contains(e.target)){navLinks.classList.remove('open');burger.setAttribute('aria-expanded','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(1):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;');}
// ---------- three strips on one time axis ----------
// One canvas, one requestAnimationFrame loop, newest at the right. BLOCKS (lanes: one per miner in the visible window,
// most blocks on top, the least active share an "others" lane; labels in a left gutter; edges under blocks), then a
// FINALITY bar, then a PROVING strip with one cell per shard of every chain block. All three share px-per-second.
var c=$('dag'),x=c.getContext('2d'),W=0,H=0,BH=0,S=22,G=78,narrow=false,level=0,maxLanes=8,FIN_H=18,PROV_H=64;
var blocks=[],byHash={},miners={},lanes=[],laneN=1,othersN=0,fin=null,prov=null,idents=0;
var LV=[{S:22,s:14,pps:40,npps:14},{S:16,s:12,pps:26,npps:10},{S:12,s:10,pps:18,npps:8}];
var pxPerSec=40,targetPx=40,bps=0,live=false,serverNow=0,serverAt=0,frozenAt=null,note='';
// ?window=N (30 to 300 s): a fixed long view for diagnosis, the API returns the same span; the default stays the adaptive scale
var fixedWindow=Math.max(0,Math.min(300,Number(new URLSearchParams(location.search).get('window'))||0));if(fixedWindow&&fixedWindow<30)fixedWindow=30;
var running=false,needSize=true,inView=true,ptrX=-1,ptrY=-1,pin=null,laneLast=[],laneRun=[],lagNote='';
var TOP=20,BOT=12;
// 12 hues that sit beside ember on obsidian; the index comes from the short id, so a miner (or a prover) keeps its colour across reloads
var PAL=['#5CB8FF','#3FD39A','#F5C542','#C49BFF','#FF7AA8','#8FD15F','#FFB08A','#6F8CFF','#D9E25A','#FF8FD6','#4FD8D8','#E0A96D'];
function colour(id){var v=0;for(var i=0;i<id.length;i++)v=(v*33+id.charCodeAt(i))>>>0;return PAL[v%PAL.length];}
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;}
function vnow(){if(frozenAt!==null)return frozenAt;return serverNow+(performance.now()-serverAt);}
function laneY(l){var u=BH-TOP-BOT,p=u/Math.max(1,laneN);return TOP+(l+0.5)*p;}
function size(){
needSize=false;W=c.clientWidth;H=c.clientHeight;if(!W||!H)return;
c.width=Math.round(W*dpr);c.height=Math.round(H*dpr);x.setTransform(dpr,0,0,dpr,0,0);
narrow=W<560;G=narrow?36:80;PROV_H=narrow?50:64;BH=H-FIN_H-PROV_H;applyLevel();
for(var id in miners)miners[id].y=laneY(miners[id].lane); // a resize is a layout change: lanes snap, nothing eases from a stale position
}
function applyLevel(){
var L=LV[level];S=narrow?L.s:L.S;targetPx=narrow?L.npps:L.pps;
if(fixedWindow){targetPx=(W-G)/fixedWindow;S=fixedWindow>120?(narrow?8:10):(narrow?L.s:LV[1].S);}
maxLanes=Math.max(2,Math.min(14,Math.floor((BH-TOP-BOT)/(S*1.7))));
layoutLanes();
}
function pickLevel(){
// projected blocks on screen at the base scale drives the step, with hysteresis so it never flickers
var proj=bps*(W-G)/(narrow?LV[0].npps:LV[0].pps),up=[48,110],down=[36,85],next=level;
if(level<2&&proj>up[level])next=level+1;else if(level>0&&proj<down[level-1])next=level-1;
if(fixedWindow)next=fixedWindow>120?2:1;
if(next!==level){level=next;applyLevel();}
}
function layoutLanes(){
// rank miners by blocks in the window; one lane each, the overflow shares the last lane with a count
var ids=Object.keys(miners);
ids.sort(function(a,b){return miners[b].n-miners[a].n||(a<b?-1:1);});
laneN=Math.max(1,Math.min(maxLanes,ids.length));
othersN=ids.length>laneN?ids.length-(laneN-1):0;
lanes=[];
ids.forEach(function(id,i){var m=miners[id],l=othersN&&i>=laneN-1?laneN-1:i;m.lane=l;if(m.y===undefined&&BH>0)m.y=laneY(l);if(!othersN||l<laneN-1)lanes[l]=m;});
while(laneLast.length<laneN){laneLast.push(0);laneRun.push(0);}
}
function ingest(d){
if(needSize)size(); // the first poll can land before the first frame; the counts below need the canvas width
var now=performance.now();serverNow=new Date(d.now).getTime();serverAt=now;
fin=d.finality||null;prov=d.proving||null;idents=d.state.miners_10m||0;
d.blocks.forEach(function(b){var e=byHash[b.hash];
if(e){e.chain=b.chain;e.locked=!!b.locked;e.final=!!b.final;e.color=b.color||'pending';e.shards=b.shards||[];e.proven=!!b.proven;return;}
e={hash:b.hash,ts:b.ts,blue_score:b.blue_score,daa:b.daa,parents:b.parents||[],chain:b.chain,color:b.color||'pending',miner:b.miner||'?',born:now,j:jitter(b.hash),locked:!!b.locked,final:!!b.final,shards:b.shards||[],proven:!!b.proven,px:0,py:0,on:false};
byHash[b.hash]=e;blocks.push(e);});
// keep what the target scale will show (the scale eases toward it), plus 20 s so a scale change never drops a block early
var cut=vnow()-((W-G)/Math.max(2,Math.min(pxPerSec,targetPx)))*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;});
for(var id in miners)miners[id].n=0;
blocks.forEach(function(b){var m=miners[b.miner];if(!m){m=miners[b.miner]={id:b.miner,n:0,col:colour(b.miner),tag:b.miner.slice(0,3),lane:0};}m.n++;});
for(id in miners)if(!miners[id].n)delete miners[id];
bps=d.state.blocks_per_second_60s||0;
pickLevel();layoutLanes();
var vis=vnow()-((W-G)/Math.max(2,targetPx))*1000,on=0,chain=0,planned=0,proven=0;
blocks.forEach(function(b){if(b.ts>vis){on++;if(b.chain){chain++;if(b.shards.length){planned++;if(b.proven)proven++;}}}});
$('c-screen').textContent=on;$('c-chain').textContent=chain;$('c-idents').textContent=idents;
var f=fin;$('c-locked').textContent=f&&f.latest_locked_index?('#'+f.latest_locked_index+(f.checkpoints&&f.checkpoints.length?', '+lockAge(f):'')):(f&&f.supported?'none yet':'n/a');
$('c-proven').textContent=prov&&prov.supported&&prov.active?(proven+'/'+planned):(prov&&prov.supported?'not active':'n/a');
if(!running&&!document.hidden)kick();
}
function lockAge(f){var cps=f.checkpoints||[],lk=null;for(var i=cps.length-1;i>=0;i--)if(cps[i].state==='locked'){lk=cps[i];break;}
if(!lk||!lk.locked_at)return '';return rel(vnow()-new Date(lk.locked_at).getTime());}
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();}
// a cubic with horizontal tangents; the control offset is clamped so a link across many lanes reads as a gentle S, never a loop
function link(a,b){var d=Math.max(S*0.6,Math.min(48,Math.abs(b.px-a.px)*0.5));x.moveTo(a.px,a.py);x.bezierCurveTo(a.px+d,a.py,b.px-d,b.py,b.px,b.py);}
// shard cells: a column under each chain block, shard 0 at the top; sizes from the zoom level, never wider than the block
function cellGeom(n){var avail=PROV_H-12,gap=2,ch=Math.min(narrow?7:9,Math.floor((avail-(n-1)*gap)/Math.max(1,n))),cw=Math.max(3,Math.min(narrow?10:12,Math.round(S*0.7),Math.round(pxPerSec*0.85)));return {ch:ch,cw:cw,gap:gap,top:BH+FIN_H+6+Math.max(0,Math.floor((avail-(n*ch+(n-1)*gap))/2))};}
function hit(){
// returns {b, i}: a block (i = -1) in the blocks strip, or a shard cell (i = shard index) in the proving strip
var qx=ptrX,qy=ptrY;
if(pin){var p=byHash[pin.h];return p&&p.on?{b:p,i:pin.i}:null;}
if(qx<0)return null;
if(qy<BH){var best=null,bd=S*0.9;
for(var i=0;i<blocks.length;i++){var b=blocks[i];if(!b.on)continue;var dx=Math.abs(b.px-qx),dy=Math.abs(b.py-qy);if(dx<bd&&dy<bd&&dx+dy<bd*1.5){best=b;bd=Math.max(dx,dy);}}
return best?{b:best,i:-1}:null;}
if(qy>=BH+FIN_H&&provOn()){var bb=null,bdx=1e9;
for(i=0;i<blocks.length;i++){b=blocks[i];if(!b.on||!b.chain||!b.shards.length)continue;var ddx=Math.abs(b.px-qx);if(ddx<bdx){bdx=ddx;bb=b;}}
if(!bb)return null;var g=cellGeom(bb.shards.length);if(bdx>Math.max(g.cw,S*0.5))return null;
var row=Math.floor((qy-g.top)/(g.ch+g.gap));if(row<0||row>=bb.shards.length)return null;return {b:bb,i:row};}
return null;
}
function provOn(){return !!(prov&&prov.supported&&prov.active);}
function draw(now){
if(needSize)size();if(!W||!H)return;
pxPerSec+=(targetPx-pxPerSec)*0.06;
var t=vnow(),xr=W-S,i,b,m,n=blocks.length,id;
for(id in miners){m=miners[id];var ty=laneY(m.lane);if(m.y===undefined)m.y=ty;m.y+=(ty-m.y)*0.12;if(Math.abs(ty-m.y)<0.05)m.y=ty;}
for(i=0;i<laneN;i++){laneLast[i]=-1e9;laneRun[i]=0;}
// positions: time on x, lane on y; same-lane neighbours closer than a block are nudged up and down in a fixed sequence
for(i=0;i<n;i++){b=blocks[i];m=miners[b.miner];if(!m)continue;var l=m.lane;b.px=Math.min(xr,xr-(t-b.ts)*pxPerSec/1000);
if(b.px-laneLast[l]<S*1.25)laneRun[l]++;else laneRun[l]=0;laneLast[l]=b.px;
var k=laneRun[l]%3,dy=k===0?0:(k===1?1:-1)*S*0.62;b.py=m.y+dy;b.on=b.px>G-S;}
x.clearRect(0,0,W,H);
// the newest locked checkpoint on screen: its x bounds the final region of the finality bar
var lk=null,fx=-1;
for(i=0;i<n;i++){b=blocks[i];if(b.locked&&(!lk||b.ts>lk.ts))lk=b;if(b.final&&b.px>fx)fx=b.px;}
if(lk)fx=lk.px;
// lane rails
x.strokeStyle='rgba(42,42,48,0.75)';x.lineWidth=1;x.beginPath();for(i=0;i<laneN;i++){var y=Math.round(laneY(i))+0.5;x.moveTo(G,y);x.lineTo(W,y);}x.stroke();
x.save();x.beginPath();x.rect(G,0,W-G,BH);x.clip();
// side links: each side block to its first parent on screen, faint and short
x.strokeStyle='rgba(154,154,158,0.3)';x.lineWidth=1;x.beginPath();
for(i=0;i<n;i++){b=blocks[i];if(b.chain||!b.on)continue;var q=byHash[b.parents[0]];if(q&&q.px>G-S*2)link(q,b);}
x.stroke();
// the selected chain as one path through the chain blocks
x.strokeStyle=level?'rgba(242,84,27,0.6)':'rgba(242,84,27,0.85)';x.lineWidth=narrow||level?1.5:2;x.beginPath();var prev=null;
for(i=0;i<n;i++){b=blocks[i];if(!b.chain)continue;if(prev&&b.px>G-S*2)link(prev,b);prev=b;}
x.stroke();
// hovered block: all of its parent edges, bright
var hv=hit(),hb=hv?hv.b:null;
if(hb&&hv.i<0){x.strokeStyle='rgba(255,179,92,0.9)';x.lineWidth=1.5;x.beginPath();for(i=0;i<hb.parents.length;i++){var pq=byHash[hb.parents[i]];if(pq)link(pq,hb);}x.stroke();}
// blocks
var h=S/2,r=S*0.24,tip=null;
for(i=0;i<n;i++){b=blocks[i];if(!b.on)continue;m=miners[b.miner];var age=now-b.born,fl=age<900?1-age/900:0;
if(fl>0){var g=x.createRadialGradient(b.px,b.py,0,b.px,b.py,S*1.6);g.addColorStop(0,'rgba(255,179,92,'+(0.5*fl)+')');g.addColorStop(1,'rgba(255,179,92,0)');x.fillStyle=g;x.beginPath();x.arc(b.px,b.py,S*1.6,0,Math.PI*2);x.fill();}
if(b.chain){x.fillStyle='#F2541B';rrect(b.px-h,b.py-h,S,S,r);x.fill();x.strokeStyle=m?m.col:'#9A9A9E';x.lineWidth=1.5;rrect(b.px-h-1,b.py-h-1,S+2,S+2,r+1);x.stroke();tip=b;}
else if(b.color==='blue'){var mc=m?m.col:'#9A9A9E';x.fillStyle=mc+'B3';rrect(b.px-h,b.py-h,S,S,r);x.fill();x.strokeStyle=fl>0?'rgba(255,179,92,0.95)':mc;x.lineWidth=1.5;rrect(b.px-h,b.py-h,S,S,r);x.stroke();}
else{x.fillStyle='#0C0C0E';rrect(b.px-h,b.py-h,S,S,r);x.fill();x.strokeStyle=fl>0?'rgba(255,179,92,0.95)':(b.color==='red'?'rgba(154,154,158,0.6)':'rgba(154,154,158,0.85)');x.lineWidth=1.5;rrect(b.px-h,b.py-h,S,S,r);x.stroke();
if(b.color==='red'){x.strokeStyle='rgba(154,154,158,0.9)';x.lineWidth=1.5;x.beginPath();x.moveTo(b.px-h*0.5,b.py+h*0.5);x.lineTo(b.px+h*0.5,b.py-h*0.5);x.stroke();}}
if(fl>0&&b.chain){x.strokeStyle='rgba(255,179,92,'+fl+')';x.lineWidth=2;rrect(b.px-h-3,b.py-h-3,S+6,S+6,r+2);x.stroke();}
if(hb===b){x.strokeStyle='#F4F1EC';x.lineWidth=1.5;rrect(b.px-h-3,b.py-h-3,S+6,S+6,r+2);x.stroke();}}
x.font='500 10px IBM Plex Mono, monospace';
// the chain tip carries its short id; nothing else is labelled unless hovered or pinned
if(tip){var lt=tip.hash.slice(0,8),lw=x.measureText(lt).width,lh=11,pad=3;
// candidate label boxes around the tip: above-right, above-left, below-right, below-left, left, right; the first that
// stays inside the strip and overlaps no other on-screen block wins, so the id is never drawn over a neighbour
var cands=[[tip.px+h+2-lw,tip.py-h-6-lh],[tip.px-h-2-lw,tip.py-h-6-lh],[tip.px+h+2-lw,tip.py+h+4],[tip.px-h-2-lw,tip.py+h+4],[tip.px-h-6-lw,tip.py-lh/2],[tip.px+h+6,tip.py-lh/2]];
var pick=null,ci;for(ci=0;ci<cands.length&&!pick;ci++){var cx=cands[ci][0],cy=cands[ci][1];if(cx<G+2||cx+lw>W-2||cy<2||cy+lh>BH-2)continue;var clash=false;
for(var bi=0;bi<n&&!clash;bi++){var ob=blocks[bi];if(ob===tip||!ob.on)continue;if(cx-pad<ob.px+h&&cx+lw+pad>ob.px-h&&cy-pad<ob.py+h&&cy+lh+pad>ob.py-h)clash=true;}
if(!clash)pick=cands[ci];}
if(!pick)pick=cands[0];x.fillStyle='rgba(244,241,236,0.92)';x.textAlign='left';x.textBaseline='top';x.fillText(lt,Math.max(G+2,Math.min(W-2-lw,pick[0])),Math.max(2,pick[1]));x.textBaseline='alphabetic';}
x.restore();
// ---- FINALITY bar: regions along the same axis. Paused finality shows only the pause (R4.6.3: never 'final' while paused)
var y0=BH,yb=y0+3,hb2=FIN_H-6;
x.strokeStyle='rgba(42,42,48,0.9)';x.lineWidth=1;x.beginPath();x.moveTo(G,y0+0.5);x.lineTo(W,y0+0.5);x.moveTo(G,y0+FIN_H+0.5);x.lineTo(W,y0+FIN_H+0.5);x.stroke();
x.save();x.beginPath();x.rect(G,y0+1,W-G,FIN_H-1);x.clip();
x.font='500 10px IBM Plex Mono, monospace';x.textAlign='left';x.textBaseline='middle';
var ftxt=null,fcol='rgba(154,154,158,0.9)';
if(!fin||!fin.supported){ftxt='no finality data';}
else if(!fin.active){var wts=fin.weights,silent=wts&&wts.total_weight?Math.max(0,Math.round(100*(1-(wts.active_weight||0)/wts.total_weight))):null;
x.fillStyle='rgba(154,154,158,0.10)';x.fillRect(G,yb,W-G,hb2);ftxt='finality paused: '+(silent?silent+'% of weight silent':'not enough weight signing yet');fcol='#FFB35C';}
else{
// final: everything up to the newest locked checkpoint on screen
if(fx>G){x.fillStyle='rgba(242,84,27,0.22)';x.fillRect(G,yb,fx-G,hb2);}
// proposed: from the newest lock to the newest proposed checkpoint on screen, faint; every checkpoint a tick
var cps=fin.checkpoints||[],px2=-1,ci2,cb;
for(ci2=0;ci2<cps.length;ci2++){cb=byHash[cps[ci2].hash];if(!cb||!cb.on)continue;if(cps[ci2].state!=='locked'&&cb.px>px2)px2=cb.px;}
if(px2>Math.max(G,fx)){x.fillStyle='rgba(154,154,158,0.10)';x.fillRect(Math.max(G,fx),yb,px2-Math.max(G,fx),hb2);}
for(ci2=0;ci2<cps.length;ci2++){cb=byHash[cps[ci2].hash];if(!cb||!cb.on)continue;var lkd=cps[ci2].state==='locked';
x.fillStyle=lkd?'#FFB35C':'rgba(154,154,158,0.55)';x.fillRect(Math.round(cb.px)-(lkd?1:0.5),yb,lkd?2:1,hb2);}
if(fin.latest_locked_index){ftxt='locked #'+fin.latest_locked_index+(lockAge(fin)?', '+lockAge(fin):'');fcol='#FFB35C';}
else ftxt='no lock yet';
// the label sits beside the newest lock while that block is on screen, else at the left edge of the bar
if(ftxt&&lk&&fx>G){var tw0=x.measureText(ftxt).width,lx0=fx+8+tw0<W-4?fx+8:fx-8-tw0;x.fillStyle='rgba(12,12,14,0.82)';x.fillRect(lx0-3,yb,tw0+6,hb2);x.fillStyle=fcol;x.fillText(ftxt,lx0,y0+FIN_H/2);ftxt=null;}
}
if(ftxt){x.fillStyle=fcol;x.fillText(ftxt,G+8,y0+FIN_H/2);}
x.textBaseline='alphabetic';x.restore();
// ---- PROVING strip: one cell per shard under its chain block, and where proofs land behind the tip
var y1=BH+FIN_H;
x.save();x.beginPath();x.rect(G,y1+1,W-G,H-y1-1);x.clip();
x.font='500 10px IBM Plex Mono, monospace';x.textAlign='left';x.textBaseline='middle';
if(!provOn()){var ptxt=!prov||!prov.supported?'Proving layer: '+(prov&&prov.reason?prov.reason:'node without proving'):'Proving layer: not yet activated on this devnet; activation '+(prov.activation_daa!==null&&prov.activation_daa!==undefined?'at DAA '+prov.activation_daa.toLocaleString('en-GB')+(prov.tip_daa?' (tip '+prov.tip_daa.toLocaleString('en-GB')+')':''):'not set');
if(narrow)ptxt=ptxt.replace('Proving layer: ','').replace(' on this devnet','').replace(/ \(tip [^)]*\)/,'');
x.fillStyle='rgba(154,154,158,0.9)';x.fillText(ptxt,G+8,y1+PROV_H/2);}
else{
// the lag line: proofs land about median_proof_lag_s behind the tip
var lag=prov.median_proof_lag_s;
if(lag!==null&&lag!==undefined){var lx=Math.round(xr-lag*pxPerSec)+0.5,lagTxt=lag<1?'under 1 s':lag<120?Math.round(lag)+' s':Math.floor(lag/60)+' min '+Math.round(lag%60)+' s';
if(lx>G){x.strokeStyle='rgba(255,179,92,0.7)';x.lineWidth=1;x.setLineDash([3,4]);x.beginPath();x.moveTo(lx,y1+3);x.lineTo(lx,H-3);x.stroke();x.setLineDash([]);
var ltx=narrow?'lag '+lagTxt:'proofs land '+lagTxt+' behind the tip',ltw=x.measureText(ltx).width;x.fillStyle='#FFB35C';x.textAlign=lx+6+ltw<W-4?'left':'right';x.fillText(ltx,lx+(x.textAlign==='left'?6:-6),y1+9);x.textAlign='left';}
else{x.fillStyle='#FFB35C';x.fillText(narrow?'lag '+lagTxt+', off screen':'proofs land '+lagTxt+' behind the tip, left of this window',G+8,y1+9);}}
for(i=0;i<n;i++){b=blocks[i];if(!b.on||!b.chain||!b.shards.length)continue;var sh=b.shards,ng=cellGeom(sh.length);
for(var si=0;si<sh.length;si++){var sc=sh[si],cx0=Math.round(b.px-ng.cw/2),cy0=ng.top+si*(ng.ch+ng.gap),st=sc.state,pc=sc.prover?colour(sc.prover):'#9A9A9E';
if(st==='verified'||st==='paid'){x.fillStyle=pc;rrect(cx0,cy0,ng.cw,ng.ch,2);x.fill();
if(st==='paid'){x.strokeStyle='#0C0C0E';x.lineWidth=1.5;x.beginPath();x.moveTo(cx0+ng.cw*0.25,cy0+ng.ch*0.55);x.lineTo(cx0+ng.cw*0.45,cy0+ng.ch*0.8);x.lineTo(cx0+ng.cw*0.78,cy0+ng.ch*0.25);x.stroke();}}
else if(st==='proving'){x.fillStyle='rgba(255,179,92,0.25)';rrect(cx0,cy0,ng.cw,ng.ch,2);x.fill();x.strokeStyle='rgba(255,179,92,0.9)';x.lineWidth=1;rrect(cx0+0.5,cy0+0.5,ng.cw-1,ng.ch-1,2);x.stroke();}
else{x.strokeStyle='rgba(154,154,158,0.55)';x.lineWidth=1;rrect(cx0+0.5,cy0+0.5,ng.cw-1,ng.ch-1,2);x.stroke();}
if(hb===b&&hv.i===si){x.strokeStyle='#F4F1EC';x.lineWidth=1;rrect(cx0-1.5,cy0-1.5,ng.cw+3,ng.ch+3,3);x.stroke();}}}
}
x.textBaseline='alphabetic';x.restore();
// tooltips on the canvas, no DOM: a block, or one shard cell
x.save();x.beginPath();x.rect(G,0,W-G,H);x.clip();x.font='500 10px IBM Plex Mono, monospace';
if(hb){var l1,l2,l3,ay,tcol=miners[hb.miner]?miners[hb.miner].col:'#3A3A42';
if(hv.i<0){l1=hb.hash.slice(0,8)+' '+(hb.locked?'locked':hb.chain?'chain':hb.color==='blue'?'included, paid':hb.color==='red'?'excluded':'pending')+(hb.chain&&hb.shards.length?(hb.proven?', proven':', '+hb.shards.length+(hb.shards.length===1?' shard':' shards')):'');l2='blue '+compact(hb.blue_score)+' daa '+compact(hb.daa);l3=hb.parents.length+(hb.parents.length===1?' parent':' parents')+' '+hb.miner;ay=hb.py;}
else{var sd=hb.shards[hv.i];l1=hb.hash.slice(0,8)+' shard '+(hv.i+1)+' of '+hb.shards.length+' '+sd.state;l2='prover '+(sd.prover||'none yet')+' lag '+(sd.lag!==null&&sd.lag!==undefined?sd.lag+' s':'n/a');l3=(sd.payout!==null&&sd.payout!==undefined?sd.payout+' IGN':'unpaid')+(sd.pgas?' '+compact(sd.pgas)+' pgas':'');ay=BH+FIN_H+PROV_H/2;if(sd.prover)tcol=colour(sd.prover);}
var tw=Math.max(x.measureText(l1).width,x.measureText(l2).width,x.measureText(l3).width)+20,th=52,tx=hb.px+h+10,tty=ay-th/2;
if(tx+tw>W-4)tx=hb.px-h-10-tw;if(tx<G+4)tx=G+4;tty=Math.max(4,Math.min(H-th-4,tty));
x.fillStyle='rgba(22,22,26,0.96)';rrect(tx,tty,tw,th,6);x.fill();x.strokeStyle=tcol;x.lineWidth=1;rrect(tx+0.5,tty+0.5,tw-1,th-1,6);x.stroke();
x.textAlign='left';x.fillStyle='#F4F1EC';x.fillText(l1,tx+10,tty+16);x.fillStyle='#C9C7C2';x.fillText(l2,tx+10,tty+31);x.fillStyle='#9A9A9E';x.fillText(l3,tx+10,tty+46);}
x.restore();
// gutter: lane labels in the miner's colour, strip names, over anything that scrolled under it
x.fillStyle='#0C0C0E';x.fillRect(0,0,G,H);x.strokeStyle='rgba(42,42,48,0.9)';x.lineWidth=1;x.beginPath();x.moveTo(G-0.5,0);x.lineTo(G-0.5,H);x.stroke();
x.textAlign='left';x.font='500 '+(narrow?10:11)+'px IBM Plex Mono, monospace';
for(i=0;i<laneN;i++){var ly2=laneY(i)+4,lm=lanes[i];
if(lm){x.fillStyle=lm.col;x.fillText(narrow?lm.tag:lm.id,8,ly2);}
else{x.fillStyle='rgba(154,154,158,0.9)';x.fillText(narrow?'+'+othersN:'+'+othersN+' more',8,ly2);}}
x.font='500 '+(narrow?9:10)+'px IBM Plex Mono, monospace';x.fillStyle='rgba(154,154,158,0.9)';x.textBaseline='middle';
x.fillText(narrow?'fin':'finality',8,BH+FIN_H/2);x.fillText(narrow?'prf':'proving',8,BH+FIN_H+(provOn()?12:PROV_H/2));x.textBaseline='alphabetic';
if(lagNote){x.font='500 10px IBM Plex Mono, monospace';x.textAlign='right';x.fillStyle='#FFB35C';x.fillText(lagNote,W-6,12);}
if(!n){x.fillStyle='rgba(154,154,158,0.9)';x.font='500 13px IBM Plex Mono, monospace';x.textAlign='center';x.fillText(lagNote?'no block has reached the observer for '+lagNote.replace('observer ','').replace(' behind',''):(live?'waiting for the first block':'no observer data'),G+(W-G)/2,BH/2);}
if(note){x.fillStyle='rgba(12,12,14,0.72)';x.fillRect(0,BH/2-22,W,44);x.fillStyle='#F4F1EC';x.font='500 13px IBM Plex Mono, monospace';x.textAlign='center';x.fillText(note,W/2,BH/2+5);}
}
function frame(now){if(!running)return;draw(now);requestAnimationFrame(frame);}
function start(){if(running||reduce)return;if(document.hidden||!inView)return;running=true;requestAnimationFrame(frame);}
function stop(){running=false;}
function kick(){if(!running)requestAnimationFrame(function(now){draw(now);});}
document.addEventListener('visibilitychange',function(){if(document.hidden)stop();else start();});
if('IntersectionObserver' in window){new IntersectionObserver(function(es){inView=es[0].isIntersecting;if(inView)start();else stop();},{threshold:0}).observe(c);}
if('ResizeObserver' in window){new ResizeObserver(function(){needSize=true;kick();}).observe(c);}
else window.addEventListener('resize',function(){needSize=true;kick();});
c.addEventListener('pointermove',function(e){if(e.pointerType==='touch')return;ptrX=e.offsetX;ptrY=e.offsetY;pin=null;if(reduce)kick();});
c.addEventListener('pointerleave',function(){ptrX=-1;ptrY=-1;if(reduce)kick();});
c.addEventListener('pointerdown',function(e){if(e.pointerType!=='touch')return;ptrX=e.offsetX;ptrY=e.offsetY;var old=pin;pin=null;var t2=hit();pin=t2&&!(old&&old.h===t2.b.hash&&old.i===t2.i)?{h:t2.b.hash,i:t2.i}:null;ptrX=-1;ptrY=-1;if(reduce)kick();});
start();
// ---------- 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;
var lag=s.observer_lag_s;lagNote=on&&lag!==null&&lag>30?'observer '+(lag<120?Math.round(lag)+' s':Math.round(lag/60)+' min')+' behind':'';
setStatus(on,s.age_s===null?'no observer update yet':(lagNote?lagNote+(s.queue_depth?', '+s.queue_depth+' queued':''):'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';
$('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'+(fixedWindow?'?window='+fixedWindow:''),{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>