Conflicts resolved keeping both: engine.rs carries the sweep state (miner-eff) and the node watchdog (reliability); the STATUS line goes through watchdog::parse_status and still feeds the sweep's rate sample; main.rs declares both modules; bench-log.md keeps both entries. site/build.mjs keeps master's partial-injected pages and adds the /miners bench table through the same page() with active: 'miners'; the Miners link is in site/partials/nav.html; sitemap gains /miners; every generated page rebuilt with node site/build.mjs.
595 lines
55 KiB
HTML
595 lines
55 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, five links and the miner button, a menu under 901 px */
|
|
.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;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:900px){
|
|
.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="/miners" data-nav="miners">Miners</a>
|
|
<a href="/evidence" data-nav="evidence">Evidence</a>
|
|
<a href="https://github.com/igneum-network/spec" rel="noopener">GitHub</a>
|
|
<a href="/#mine" 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 900 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:901px)');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>
|
|
</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 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 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 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: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="Follow">
|
|
<div class="eyebrow">Follow</div>
|
|
<a href="/live">Live devnet</a>
|
|
<a href="/#journey">Journey</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="/#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>
|
|
<!-- 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,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)';
|
|
// 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(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);}
|
|
}
|
|
// 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||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();});
|
|
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);
|
|
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>
|