igneum-prove-core carries the node's fees.rs (PgasTable, FeeParams PROTOTYPE and CALIBRATED_V1, FeeSchedule::at); the shard input and every fixture carry the schedule and the block's DAA score; the executor reads the set at that score, raises the base fees to its floors and meters with its intrinsic, B_p and modexp entry, as the node's execute_segment does. The 328-byte statement is unchanged: the node's native veto pins the schedule (a new layout would be a consensus change for every node). Exporter: schedule and daaScore from the dump (gen.mjs writes them), per-segment switch on replay, S_p from the set. Fixtures from one simnet chain across the switch at DAA 800: fees-switch-prototype (block 51), fees-v1-shards2 (351), fees-v1-shards3 (355); 358 segments replayed, every state root the node's. Host tests on both sides. Guest re-pinned: shard 0x2b1a81cb..., aggregator 0x474678f3...; pinned-guests-check passes. Node fork 2b6d23ef unchanged (igneum-exec tests 11 passed). Digest for the override with fees_v1_activation_daa 210000: ab8847da538dead1dc10e046dfaadab3c1c35928e3748810c4e050d4a886087a. Runbook docs/plans/fee-switch-devnet.md; infra/devnet/restart-hand-nodes.sh and restart-seed.sh take the override object (the hand nodes and the seed run 20139145 today and must move to 2b6d23ef first). One line on the live page, spec 5.11, the testnet README, the floor analysis, the bench log and the journey. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
660 lines
61 KiB
HTML
660 lines
61 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=3">
|
|
<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=3">
|
|
<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">
|
|
<!-- head:start -->
|
|
<link rel="preload" href="/fonts/unbounded-900.woff2" as="font" type="font/woff2" crossorigin>
|
|
<link rel="preload" href="/fonts/plex-sans-400.woff2" as="font" type="font/woff2" crossorigin>
|
|
<style>
|
|
/* Shared by every page. Source: site/partials/head.html, injected by site/build.mjs between the head markers. Edit the partial, not the page. */
|
|
/* Fonts, self-hosted (latin subsets, OFL): Unbounded 500/700/900, IBM Plex Sans 400/500/600, IBM Plex Mono 400/500. Fallbacks carry size and ascent overrides so the swap does not move the layout. */
|
|
@font-face{font-family:'Unbounded';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/unbounded-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
@font-face{font-family:'Unbounded';font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/unbounded-700.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
@font-face{font-family:'Unbounded';font-style:normal;font-weight:900;font-display:swap;src:url(/fonts/unbounded-900.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-sans-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-sans-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
@font-face{font-family:'IBM Plex Sans';font-style:normal;font-weight:600;font-display:swap;src:url(/fonts/plex-sans-600.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/plex-mono-400.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/plex-mono-500.woff2) format('woff2');unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
|
|
@font-face{font-family:'Unbounded Fallback';src:local('Arial Black'),local('Arial-Black'),local('Impact');size-adjust:114%;ascent-override:87.5%;descent-override:21.5%;line-gap-override:0%}
|
|
@font-face{font-family:'Plex Sans Fallback';src:local('Arial'),local('Helvetica Neue'),local('Helvetica');size-adjust:100.5%;ascent-override:102%;descent-override:27.4%;line-gap-override:0%}
|
|
@font-face{font-family:'Plex Mono Fallback';src:local('Courier New'),local('Menlo');size-adjust:100%;ascent-override:102.5%;descent-override:27.5%;line-gap-override:0%}
|
|
:root{--f-sans:'IBM Plex Sans','Plex Sans Fallback',system-ui,-apple-system,sans-serif;--f-display:'Unbounded','Unbounded Fallback',sans-serif;--f-mono:'IBM Plex Mono','Plex Mono Fallback',ui-monospace,Menlo,monospace;
|
|
/* site chrome tokens (nav, footer, skip link). Dark by default; the litepaper sets light values and resets these in dark mode */
|
|
--nav-h:68px;--ui-bg:rgba(12,12,14,.84);--ui-menu:#0C0C0E;--ui-ink:#F4F1EC;--ui-ink-2:#C9C7C2;--ui-ash:#9A9A9E;--ui-line:#2A2A30;--ui-line-2:#3A3A42;--ui-accent:#F2541B;--ui-accent-ink:#0C0C0E;--ui-hot:#FFB35C;--ui-hover:#FF6A2B;--ui-tint:rgba(242,84,27,.12)}
|
|
html{-webkit-text-size-adjust:100%}
|
|
h1,h2,h3{text-wrap:balance}
|
|
p,li,dd,figcaption{text-wrap:pretty}
|
|
a:focus-visible,button:focus-visible,summary:focus-visible,[tabindex]:focus-visible{outline:2px solid var(--ui-accent);outline-offset:3px;border-radius:6px}
|
|
.skip{position:absolute;left:12px;top:-80px;z-index:50;background:var(--ui-accent);color:var(--ui-accent-ink);padding:10px 14px;border-radius:10px;font:600 15px/1.2 var(--f-sans);text-decoration:none}
|
|
.skip:focus{top:12px}
|
|
/* nav: one bar on every page, the mark in its black square, seven links and the miner button, a menu under 941 px (the eight items need 688 px beside the brand at 15 px, so the bar fits from 941 px up without a wrapped label) */
|
|
.nav{position:sticky;top:0;z-index:20;background:var(--ui-bg);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);border-bottom:1px solid var(--ui-line);color:var(--ui-ink)}
|
|
.nav .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:var(--nav-h)}
|
|
.nav a{color:inherit;text-decoration:none}
|
|
.brand{display:inline-flex;align-items:center;gap:10px;color:var(--ui-ink);text-decoration:none;flex:0 0 auto}
|
|
.brand .mark{display:block;flex:0 0 auto}
|
|
.brand .word{font-family:var(--f-display);font-weight:900;font-size:20px;letter-spacing:.06em;line-height:1}
|
|
.nav .links{display:flex;align-items:center;gap:clamp(14px,2vw,26px);font-size:15px;font-weight:500}
|
|
.nav .links a{color:var(--ui-ink-2);padding:4px 0;border-bottom:2px solid transparent;white-space:nowrap;transition:color .15s ease,border-color .15s ease}
|
|
.nav .links a:hover,.nav .brand:hover{color:var(--ui-hot);text-decoration:none}
|
|
.nav .brand:hover{color:var(--ui-ink)}
|
|
.nav .links a[aria-current="page"]{color:var(--ui-ink);border-bottom-color:var(--ui-accent)}
|
|
.nav .links a.cta{display:inline-flex;align-items:center;justify-content:center;min-height:40px;padding:8px 16px;border-radius:10px;font-weight:600;background:var(--ui-accent);color:var(--ui-accent-ink);border:1px solid var(--ui-accent);transition:background .15s ease,transform .15s ease}
|
|
.nav .links a.cta:hover{background:var(--ui-hover);border-color:var(--ui-hover);color:var(--ui-accent-ink);transform:translateY(-1px)}
|
|
.burger{display:none;background:none;border:1px solid var(--ui-line-2);color:var(--ui-ink);border-radius:10px;width:44px;height:44px;align-items:center;justify-content:center;cursor:pointer;padding:0;flex:0 0 auto}
|
|
.burger .x{display:none}.burger[aria-expanded="true"] .x{display:block}.burger[aria-expanded="true"] .bars{display:none}
|
|
@media (max-width:940px){
|
|
.nav .links{display:none;position:absolute;left:0;right:0;top:100%;background:var(--ui-menu);border-bottom:1px solid var(--ui-line);box-shadow:0 24px 40px rgba(0,0,0,.35);padding:10px var(--gutter,16px) 18px;flex-direction:column;align-items:stretch;gap:2px}
|
|
.nav .links a{padding:13px 10px;border-radius:10px;border-bottom:0;font-size:16px}
|
|
.nav .links a[aria-current="page"]{background:var(--ui-tint)}
|
|
.nav .links a.cta{margin-top:8px;min-height:48px}
|
|
.nav .links.open{display:flex}
|
|
.burger{display:inline-flex}
|
|
}
|
|
/* footer, the same on every page */
|
|
.foot{border-top:1px solid var(--ui-line);color:var(--ui-ink);margin-top:var(--sec,64px)}
|
|
.foot .wrap{padding-block:48px 32px}
|
|
.foot-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,200px),1fr));gap:32px}
|
|
.foot-brand{display:flex;flex-direction:column;gap:12px;max-width:34ch}
|
|
.foot-brand .brand{align-self:flex-start}.foot-brand .word{font-size:16px}
|
|
.foot-brand p{color:var(--ui-ash);font-size:14px;margin:0}
|
|
.foot-col{display:flex;flex-direction:column;gap:10px;font-size:15px}
|
|
.foot-col .eyebrow{font-family:var(--f-mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--ui-ash);margin-bottom:4px}
|
|
.foot-col a{color:var(--ui-ink-2);text-decoration:none;display:inline-flex;align-items:center;gap:6px;align-self:flex-start}
|
|
.foot-col a:hover{color:var(--ui-hot);text-decoration:none}
|
|
.foot-base{display:flex;flex-wrap:wrap;justify-content:space-between;gap:8px 24px;margin-top:36px;padding-top:20px;border-top:1px solid var(--ui-line);font-size:13px;color:var(--ui-ash)}
|
|
.foot-base span{text-wrap:balance}.foot-base .mono{font-family:var(--f-mono)}
|
|
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation-duration:.01ms!important;animation-iteration-count:1!important;transition-duration:.01ms!important;scroll-behavior:auto!important}}
|
|
</style>
|
|
<!-- head:end -->
|
|
<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:var(--f-sans);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:var(--f-mono)}
|
|
.eyebrow{font-family:var(--f-mono);font-size:12px;letter-spacing:.18em;text-transform:uppercase;color:var(--ash)}
|
|
h1,h2,h3{font-family:var(--f-display);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 and footer: shared, see site/partials */
|
|
|
|
/* 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:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash)}
|
|
.cell .v{font-family:var(--f-display);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 small{font-family:var(--f-mono);font-weight:500;font-size:13px;color:var(--ash);margin-left:4px;letter-spacing:.04em}
|
|
@media (max-width:700px){.cell .v{font-size:19px}}
|
|
.cell .v.name{font-family:var(--f-mono);font-size:15px;font-weight:500}
|
|
.cell .s{font-family:var(--f-mono);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:var(--f-mono);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}
|
|
.legend span.txt{white-space:normal;display:inline;color:var(--ash)}
|
|
.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:var(--f-mono);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:var(--f-mono);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:var(--f-mono);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)}
|
|
.foot{margin-top:0}
|
|
</style>
|
|
</head>
|
|
<body class="off">
|
|
|
|
<!-- nav:start -->
|
|
<a class="skip" href="#main">Skip to content</a>
|
|
<nav class="nav" aria-label="Main">
|
|
<div class="wrap">
|
|
<a href="/" class="brand" aria-label="Igneum home">
|
|
<svg class="mark" viewBox="0 0 1024 1024" width="36" height="36" aria-hidden="true"><rect width="1024" height="1024" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><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></g></svg>
|
|
<span class="word">IGNEUM</span>
|
|
</a>
|
|
<button class="burger" id="nav-burger" type="button" aria-expanded="false" aria-controls="nav-links" aria-label="Menu"><svg class="bars" 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><svg class="x" viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M6 6l12 12M18 6L6 18"></path></svg></button>
|
|
<div class="links" id="nav-links">
|
|
<a href="/litepaper" data-nav="litepaper">Litepaper</a>
|
|
<a href="/live" data-nav="live" aria-current="page">Live devnet</a>
|
|
<a href="/bench" data-nav="bench">Engineering log</a>
|
|
<a href="/miner" data-nav="miner">Miner</a>
|
|
<a href="/wallet" data-nav="wallet">Wallet</a>
|
|
<a href="/evidence" data-nav="evidence">Evidence</a>
|
|
<a href="https://github.com/igneum-network/spec" rel="noopener">GitHub</a>
|
|
<a href="/miner#get" class="cta">Get the miner</a>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
<script>
|
|
(function(){
|
|
// the menu: open on the button, closed by any link, a tap outside, Escape (focus back on the button) or a resize past 940 px
|
|
var b=document.getElementById('nav-burger'),l=document.getElementById('nav-links');if(!b||!l)return;
|
|
function set(o){l.classList.toggle('open',o);b.setAttribute('aria-expanded',o?'true':'false');}
|
|
b.addEventListener('click',function(){set(!l.classList.contains('open'));});
|
|
l.addEventListener('click',function(e){if(e.target.closest('a'))set(false);});
|
|
document.addEventListener('click',function(e){if(l.classList.contains('open')&&!l.contains(e.target)&&!b.contains(e.target))set(false);});
|
|
document.addEventListener('keydown',function(e){if(e.key==='Escape'&&l.classList.contains('open')){set(false);b.focus();}});
|
|
var mq=window.matchMedia('(min-width:941px)');if(mq.addEventListener)mq.addEventListener('change',function(){set(false);});
|
|
})();
|
|
</script>
|
|
<!-- nav:end -->
|
|
|
|
<main id="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>
|
|
<p>Fees: the devnet meters with its prototype table until DAA score 210,000; from there the adopted table applies (300 pgas per transaction, 30,000-pgas shards, floors of 100 gwei per gas and 10,000 gwei per pgas) and the provers' program carries both.</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 class="txt">one lane per miner, colour from its id; hover a block, or tap it to keep its details up</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 class="txt">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 class="txt">hover a cell, or tap it to keep its details up</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:start -->
|
|
<footer class="foot">
|
|
<div class="wrap">
|
|
<div class="foot-grid">
|
|
<div class="foot-brand">
|
|
<a href="/" class="brand" aria-label="Igneum home"><svg class="mark" viewBox="0 0 1024 1024" width="30" height="30" aria-hidden="true"><rect width="1024" height="1024" fill="#0C0C0E"></rect><g transform="translate(166.95 166.95) scale(6.901)"><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></g></svg><span class="word">IGNEUM</span></a>
|
|
<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="/litepaper#shoulders">Built on the shoulders</a>
|
|
<a href="/bench">Engineering log</a>
|
|
<a href="/evidence">Evidence</a>
|
|
</nav>
|
|
<nav class="foot-col" aria-label="Run">
|
|
<div class="eyebrow">Run</div>
|
|
<a href="/miner">The miner</a>
|
|
<a href="/wallet">The wallet</a>
|
|
<a href="/miners">GPU bench table</a>
|
|
<a href="/miner#fee">The dev fee</a>
|
|
</nav>
|
|
<nav class="foot-col" aria-label="Follow">
|
|
<div class="eyebrow">Follow</div>
|
|
<a href="/live">Live devnet</a>
|
|
<a href="/#journey">Journey</a>
|
|
<a href="/wallet">Add Igneum to MetaMask</a>
|
|
<a href="https://github.com/igneum-network/spec" rel="noopener"><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"></path></svg>GitHub, spec and vectors</a>
|
|
<a href="/miner#get">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>
|
|
<!-- footer:end -->
|
|
|
|
<script>
|
|
(function(){
|
|
var reduce=window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
// the DAG canvas is redrawn every frame, so its backing store is capped at 1.5x: text stays crisp on a phone and the fill
|
|
// cost on a 2x laptop display drops by 44%
|
|
var dpr=Math.min(1.5,window.devicePixelRatio||1);
|
|
|
|
// 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,'&').replace(/</g,'<').replace(/>/g,'>');}
|
|
|
|
// ---------- 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,laneLast=[],laneRun=[],lagNote='';
|
|
// pin: a tapped or clicked block (i = -1) or shard cell (i = shard index). It stays until a tap or click lands elsewhere,
|
|
// Escape, or the block leaves the strip (then pinFade holds it for a 200 ms fade). Hover never touches it.
|
|
var pin=null,pinFade=null,PIN_FADE=200,closeRect=null,panelRect=null,down=null,TAP_SLOP=10;
|
|
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 hitAt(qx,qy,touch){
|
|
// returns {b, i}: a block (i = -1) in the blocks strip, or a shard cell (i = shard index) in the proving strip.
|
|
// A finger is wider than a pointer: a touch gets a larger radius, the nearest target still wins; hover is unchanged
|
|
if(qx<0)return null;
|
|
if(qy<BH){var best=null,bd=S*(touch?1.3: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),ns=bb.shards.length;if(bdx>Math.max(g.cw,S*0.5)*(touch?1.8:1))return null;
|
|
var row=Math.floor((qy-g.top)/(g.ch+g.gap));
|
|
if(touch&&(row<0||row>=ns)&&qy>=g.top-8&&qy<=g.top+ns*(g.ch+g.gap)+8)row=Math.max(0,Math.min(ns-1,row));
|
|
if(row<0||row>=ns)return null;return {b:bb,i:row};}
|
|
return null;
|
|
}
|
|
function hover(){return ptrX<0?null:hitAt(ptrX,ptrY,false);}
|
|
// the pinned target while its block is still drawn; null once it has scrolled out of the strip
|
|
function pinned(){if(!pin)return null;var p=byHash[pin.h];return p&&p.on?{b:p,i:pin.i}:null;}
|
|
function same(a,b){return !!(a&&b&&a.b===b.b&&a.i===b.i);}
|
|
function inRect(r,qx,qy){return !!(r&&qx>=r[0]&&qx<r[0]+r[2]&&qy>=r[1]&&qy<r[1]+r[3]);}
|
|
function setPin(t){pin=t?{h:t.b.hash,i:t.i,b:t.b}:null;pinFade=null;if(reduce)kick();}
|
|
function clearPin(){if(!pin&&!pinFade)return;pin=null;pinFade=null;if(reduce)kick();}
|
|
// a tap or click at (qx, qy): the close control clears, the panel itself is inert, a block or cell pins (a second tap on the
|
|
// pinned one keeps it), anything else on the canvas clears
|
|
function tap(qx,qy,touch){
|
|
if(inRect(closeRect,qx,qy)){clearPin();return;}
|
|
if(inRect(panelRect,qx,qy))return;
|
|
var t=hitAt(qx,qy,touch);
|
|
if(t)setPin(t);else clearPin();
|
|
}
|
|
// the three panel lines for a target, its anchor y for the floating panel, and its border colour
|
|
function lines(t){var b=t.b,tcol=miners[b.miner]?miners[b.miner].col:'#3A3A42',l1,l2,l3,ay;
|
|
if(t.i<0){l1=b.hash.slice(0,8)+' '+(b.locked?'locked':b.chain?'chain':b.color==='blue'?'included, paid':b.color==='red'?'excluded':'pending')+(b.chain&&b.shards.length?(b.proven?', proven':', '+b.shards.length+(b.shards.length===1?' shard':' shards')):'');l2='blue '+compact(b.blue_score)+' daa '+compact(b.daa);l3=b.parents.length+(b.parents.length===1?' parent':' parents')+' '+b.miner;ay=b.py;}
|
|
else{var sd=b.shards[t.i]||{};l1=b.hash.slice(0,8)+' shard '+(t.i+1)+' of '+b.shards.length+' '+(sd.state||'gone');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);}
|
|
return [l1,l2,l3,ay,tcol];}
|
|
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();
|
|
// the pinned block scrolled out: let it go with a short fade instead of a snap (reduced motion: at once)
|
|
var pv=pinned();
|
|
if(pin&&!pv){pinFade=reduce?null:{b:pin.b,i:pin.i,at:now};pin=null;}
|
|
if(pinFade&&now-pinFade.at>=PIN_FADE)pinFade=null;
|
|
// hovered block (pointer devices only) and pinned block: all of their parent edges, bright
|
|
var hv=hover(),hb=hv?hv.b:null,pb=pv?pv.b:null;
|
|
if(pb&&pv.i<0){x.strokeStyle='rgba(255,179,92,0.7)';x.lineWidth=1.5;x.beginPath();for(i=0;i<pb.parents.length;i++){var pq0=byHash[pb.parents[i]];if(pq0)link(pq0,pb);}x.stroke();}
|
|
if(hb&&hv.i<0&&hb!==pb){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&&b!==pb){x.strokeStyle='#F4F1EC';x.lineWidth=1.5;rrect(b.px-h-3,b.py-h-3,S+6,S+6,r+2);x.stroke();}
|
|
// the pinned block: a molten ring with a glow, so the reader can find it as the strip moves
|
|
if(pb===b){x.save();x.shadowColor='rgba(255,179,92,0.8)';x.shadowBlur=8;x.strokeStyle='#FFB35C';x.lineWidth=2;rrect(b.px-h-5,b.py-h-5,S+10,S+10,r+3);x.stroke();x.restore();}}
|
|
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)';
|
|
// a line wider than the strip breaks at its semicolon into two lines, so the phone never clips the activation height
|
|
if(x.measureText(ptxt).width>W-G-16&&ptxt.indexOf('; ')>0){var parts=ptxt.split('; ');x.fillText(parts[0]+';',G+8,y1+PROV_H/2-7);x.fillText(parts.slice(1).join('; '),G+8,y1+PROV_H/2+7);}
|
|
else 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(pb===b&&pv.i===si){x.save();x.shadowColor='rgba(255,179,92,0.8)';x.shadowBlur=6;x.strokeStyle='#FFB35C';x.lineWidth=1.5;rrect(cx0-2.5,cy0-2.5,ng.cw+5,ng.ch+5,3);x.stroke();x.restore();}
|
|
else 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();
|
|
|
|
// the detail panel on the canvas, no DOM: a block, or one shard cell. A hovered target that is not the pinned one gets
|
|
// the floating panel beside it, as before. Otherwise the pinned target (or the one fading out) gets the fixed panel at
|
|
// the top left of the strip, where the oldest blocks are: it never moves as new blocks arrive.
|
|
x.save();x.beginPath();x.rect(G,0,W-G,H);x.clip();x.font='500 10px IBM Plex Mono, monospace';x.textAlign='left';
|
|
closeRect=null;panelRect=null;
|
|
var show=hv&&!same(hv,pv)?hv:(pv||pinFade),fixed=show&&show!==hv;
|
|
if(show){var L=lines(show),tw=Math.max(x.measureText(L[0]).width,x.measureText(L[1]).width,x.measureText(L[2]).width)+20,th,tx,tty;
|
|
if(!fixed){th=52;tx=show.b.px+h+10;tty=L[3]-th/2;
|
|
if(tx+tw>W-4)tx=show.b.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=L[4];x.lineWidth=1;rrect(tx+0.5,tty+0.5,tw-1,th-1,6);x.stroke();
|
|
x.fillStyle='#F4F1EC';x.fillText(L[0],tx+10,tty+16);x.fillStyle='#C9C7C2';x.fillText(L[1],tx+10,tty+31);x.fillStyle='#9A9A9E';x.fillText(L[2],tx+10,tty+46);}
|
|
else{th=72;tw=Math.max(tw,112);tx=G+6;tty=6;var CL=32,fade=show===pinFade?Math.max(0,1-(now-pinFade.at)/PIN_FADE):1;
|
|
x.globalAlpha=fade;
|
|
x.fillStyle='rgba(22,22,26,0.96)';rrect(tx,tty,tw,th,6);x.fill();x.strokeStyle=L[4];x.lineWidth=1;rrect(tx+0.5,tty+0.5,tw-1,th-1,6);x.stroke();
|
|
// header: a small pinned label and a 32 px close control for a thumb
|
|
x.font='500 9px IBM Plex Mono, monospace';x.fillStyle='#FFB35C';x.fillText('PINNED',tx+10,tty+14);
|
|
var cxm=tx+tw-CL/2,cym=tty+CL/2;x.strokeStyle='rgba(154,154,158,0.55)';x.lineWidth=1;x.beginPath();x.arc(cxm,cym,8.5,0,Math.PI*2);x.stroke();
|
|
x.strokeStyle='#F4F1EC';x.lineWidth=1.5;x.beginPath();x.moveTo(cxm-3.5,cym-3.5);x.lineTo(cxm+3.5,cym+3.5);x.moveTo(cxm+3.5,cym-3.5);x.lineTo(cxm-3.5,cym+3.5);x.stroke();
|
|
x.font='500 10px IBM Plex Mono, monospace';x.fillStyle='#F4F1EC';x.fillText(L[0],tx+10,tty+36);x.fillStyle='#C9C7C2';x.fillText(L[1],tx+10,tty+51);x.fillStyle='#9A9A9E';x.fillText(L[2],tx+10,tty+66);
|
|
x.globalAlpha=1;
|
|
if(show===pv){closeRect=[tx+tw-CL,tty,CL,CL];panelRect=[tx,tty,tw,th];}}}
|
|
x.restore();
|
|
// pointer devices: a hand over a block, a cell or the close control
|
|
var cur=hv||(ptrX>=0&&inRect(closeRect,ptrX,ptrY))?'pointer':'';if(c.style.cursor!==cur)c.style.cursor=cur;
|
|
// 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);}
|
|
}
|
|
// Idle, the scene moves at 14 to 40 px/s, so 30 frames a second is enough; a pointer or a pinned block gets every frame.
|
|
// ?perf=1 exposes window.__igneumPerf = {frames, drawMs, since} so the draw cost can be read from the console.
|
|
var PERF=/[?&]perf=1/.test(location.search)?(window.__igneumPerf={frames:0,drawMs:0,since:performance.now()}):null,lastDraw=0;
|
|
function frame(now){if(!running)return;
|
|
if(ptrX>=0||pin||pinFade||now-lastDraw>=31){lastDraw=now;var t0=PERF?performance.now():0;draw(now);if(PERF){PERF.frames++;PERF.drawMs+=performance.now()-t0;}}
|
|
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();});
|
|
// hover: pointer devices only, never a touch; it reads the strip and leaves the pin alone
|
|
c.addEventListener('pointermove',function(e){if(e.pointerType==='touch')return;ptrX=e.offsetX;ptrY=e.offsetY;if(reduce)kick();});
|
|
c.addEventListener('pointerleave',function(){ptrX=-1;ptrY=-1;if(reduce)kick();});
|
|
// tap or click: one pointer, down to up, under 10 px of travel, so a scroll on a phone never pins. Pointer events only;
|
|
// the canvas ignores click, so a touch tap fires once (its synthetic click is dropped below)
|
|
c.addEventListener('pointerdown',function(e){if(e.button&&e.button!==0)return;down=down?null:{id:e.pointerId,x:e.offsetX,y:e.offsetY};});
|
|
c.addEventListener('pointerup',function(e){if(!down||down.id!==e.pointerId){down=null;return;}
|
|
var dx=e.offsetX-down.x,dy=e.offsetY-down.y;down=null;if(dx*dx+dy*dy>TAP_SLOP*TAP_SLOP)return;tap(e.offsetX,e.offsetY,e.pointerType==='touch');});
|
|
c.addEventListener('pointercancel',function(){down=null;});
|
|
// anywhere else on the page clears the pin; Escape too
|
|
document.addEventListener('click',function(e){if(e.target!==c)clearPin();});
|
|
document.addEventListener('keydown',function(e){if(e.key==='Escape')clearPin();});
|
|
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);
|
|
var hr=hashrate(s.hashes_per_second_estimate).split(' ');$('st-hps').innerHTML=hr.length>1?esc(hr[0])+'<small>'+esc(hr[1])+'</small>':esc(hr[0]);
|
|
$('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>
|