igneum/pool/web/index.html

199 lines
18 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>__POOL_NAME__</title>
<meta name="description" content="An Igneum mining pool: hash rate, miners, blocks, payouts, and how to connect igneum-miner.">
<meta name="theme-color" content="#0C0C0E">
<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">
<style>
/* the site's tokens (site/partials/head.html and site/explorer.html), self-contained: the pool runs on its own host */
:root{--f-sans:'IBM Plex Sans',system-ui,-apple-system,sans-serif;--f-display:'Unbounded','Arial Black',sans-serif;--f-mono:'IBM Plex Mono',ui-monospace,Menlo,monospace;
--obsidian:#0C0C0E;--graphite:#16161A;--line:#2A2A30;--line-2:#3A3A42;--ember:#F2541B;--molten:#FFB35C;--bone:#F4F1EC;--ash:#9A9A9E;--ink-2:#C9C7C2;--ember-ink:#0C0C0E;--green:#3FD39A;
--max:1200px;--gutter:clamp(16px,4vw,32px);--gap:24px;--gap-tile:12px;--card-pad:clamp(18px,3vw,28px);--card-r:18px;--tile-r:14px;--fs-h1:clamp(32px,5.5vw,56px);--fs-h3:clamp(18px,2vw,22px);--fs-tile:clamp(20px,2.2vw,26px)}
*{box-sizing:border-box}
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)}
.wrap{max-width:var(--max);margin:0 auto;padding-inline:var(--gutter)}
.nav{position:sticky;top:0;z-index:20;background:rgba(12,12,14,.84);backdrop-filter:blur(12px);border-bottom:1px solid var(--line)}
.nav .wrap{display:flex;align-items:center;justify-content:space-between;gap:16px;min-height:68px}
.brand{display:inline-flex;align-items:center;gap:10px}
.brand .word{font-family:var(--f-display);font-weight:900;font-size:20px;letter-spacing:.06em}
.nav .links{display:flex;gap:clamp(14px,2vw,26px);font-size:15px;font-weight:500;color:var(--ink-2)}
.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{font-family:var(--f-display);margin:0;line-height:1.08}
h1{font-weight:900;font-size:var(--fs-h1);letter-spacing:-.01em;overflow-wrap:anywhere}
h2{font-weight:700;font-size:var(--fs-h3)}
p{margin:0}
.head{padding-block:clamp(40px,6vw,64px) var(--gap);display:flex;flex-direction:column;gap:14px}
.head p{color:var(--ink-2);max-width:64ch;font-size:16px}
.card{background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:var(--card-pad);min-width:0;margin-top:var(--gap)}
.strip{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--gap-tile)}
@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:18px 20px;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}
.cell .s{font-family:var(--f-mono);font-size:12px;color:var(--ash)}
.viz-head{display:flex;flex-wrap:wrap;justify-content:space-between;align-items:center;gap:10px 20px;margin-bottom:12px}
.tbl{overflow-x:auto;margin-top:12px}
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;vertical-align:top}
th{font-family:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);font-weight:500}
td.n,th.n{text-align:right;font-variant-numeric:tabular-nums}
tr:last-child td{border-bottom:0}
.hash{font-family:var(--f-mono)}
.chip{display:inline-block;font-family:var(--f-mono);font-size:11px;letter-spacing:.08em;text-transform:uppercase;padding:3px 8px;border-radius:999px;border:1px solid var(--line-2);color:var(--ink-2)}
.chip.ok{border-color:var(--green);color:var(--green)}.chip.hot{border-color:var(--molten);color:var(--molten)}.chip.bad{border-color:var(--ember);color:var(--ember)}
.note{font-size:13px;color:var(--ash);margin-top:14px;max-width:80ch}
pre{background:var(--obsidian);border:1px solid var(--line);border-radius:10px;padding:14px 16px;font:500 13px/1.5 var(--f-mono);overflow-x:auto;margin:12px 0 0;color:var(--bone)}
.search{display:flex;gap:10px;max-width:760px;margin-top:4px}
.search input{flex:1;min-width:0;min-height:48px;padding:10px 14px;border-radius:10px;border:1px solid var(--line-2);background:var(--graphite);color:var(--bone);font:500 15px/1.3 var(--f-mono)}
.btn{display:inline-flex;align-items:center;justify-content:center;min-height:48px;padding:12px 22px;border-radius:10px;font-weight:600;font-size:16px;border:1px solid var(--ember);color:var(--ember-ink);background:var(--ember);cursor:pointer}
.btn:hover{background:#FF6A2B}
.msg{font-family:var(--f-mono);font-size:13px;color:var(--molten);min-height:18px}
dl{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:8px 20px;margin:12px 0 0;font-size:14px}
dt{font-family:var(--f-mono);font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--ash);padding-top:3px}
dd{margin:0;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}
.two{display:grid;gap:var(--gap);grid-template-columns:1fr;margin-top:var(--gap)}
@media (min-width:900px){.two{grid-template-columns:1fr 1fr}.two .card{margin-top:0}}
main{padding-bottom:clamp(56px,8vw,96px)}
.foot{border-top:1px solid var(--line);padding:28px 0;font-size:13px;color:var(--ash)}
</style>
</head>
<body>
<nav class="nav" aria-label="Main">
<div class="wrap">
<a href="https://igneum.network/" class="brand" aria-label="Igneum home">
<svg 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>
<div class="links"><a href="#connect">Connect</a><a href="#blocks">Blocks</a><a href="#payments">Payments</a><a href="#lookup">Your address</a><a href="/api/stats">API</a><a href="https://igneum.network/miner">The miner</a></div>
</div>
</nav>
<main id="main" class="wrap">
<div class="head">
<div class="eyebrow">__NETWORK__ · PPLNS · fee __FEE__% · minimum payout __MIN_PAYOUT__ IGN</div>
<h1>__POOL_NAME__</h1>
<p>Your vote key stays yours: the pool names your key in every header it gives you, so your finality weight follows you (spec 09). The pool pays 80% of each block it finds across the last shares; the 20% proving share goes to provers by the chain's own rule and never passes through here.</p>
</div>
<div class="strip" aria-live="polite">
<div class="cell"><div class="k">Pool hash rate</div><div class="v" id="p-hr">0</div><div class="s" id="p-hr1h">10 min / 1 h</div></div>
<div class="cell"><div class="k">Miners / workers</div><div class="v" id="p-miners">0 / 0</div><div class="s">online now</div></div>
<div class="cell"><div class="k">Blocks 24 h</div><div class="v" id="p-blocks">0</div><div class="s" id="p-blocks-s">confirmed / orphaned</div></div>
<div class="cell"><div class="k">Last block</div><div class="v" id="p-last">none</div><div class="s" id="p-last-s"></div></div>
<div class="cell"><div class="k">Network hash rate</div><div class="v" id="n-hr">0</div><div class="s" id="n-diff">difficulty</div></div>
<div class="cell"><div class="k">Block reward</div><div class="v" id="n-reward">0<small>IGN</small></div><div class="s">80% to the miner, 20% to provers</div></div>
<div class="cell"><div class="k">Effort</div><div class="v" id="p-effort">0%</div><div class="s" id="p-luck">luck 24 h</div></div>
<div class="cell"><div class="k">Paid 24 h</div><div class="v" id="p-paid">0<small>IGN</small></div><div class="s" id="p-paid-s">pending</div></div>
</div>
<div class="card" id="connect">
<div class="viz-head"><h2>Connect</h2><div class="eyebrow">igneum-miner 0.3.6 or later</div></div>
<p class="note" style="margin-top:0">One command per card. The payout address is an EVM address you hold the key for; the worker name labels the card on this page. Your own node is optional: with one, the miner checks the pool's seeds and templates against it, votes on finality with your key, and submits found blocks there too. Without one it hashes and does not vote.</p>
<pre>igneum-miner mine grpc://127.0.0.1:26610 1 100000000 rig1 --pool __POOL_URL__ --evm-address 0xYOURADDRESS --worker-name rig1-gpu0 \
--worker ./igneum-worker-cuda --worker-args "--device 0 --pack packs/devnet"
# without a node of your own (hashes, does not vote):
igneum-miner mine none 1 100000000 rig1 --pool __POOL_URL__ --evm-address 0xYOURADDRESS --worker-name rig1-gpu0 --worker ./igneum-worker-cuda</pre>
<dl>
<dt>HiveOS</dt><dd>Flight Sheet pool URL <span class="mono">pool://__POOL_URL__</span>, wallet <span class="mono">0xYOURADDRESS.%WORKER_NAME%</span>; the rest as on igneum.network/miner</dd>
<dt>Shares</dt><dd>a share is a lane hash at or below your share target; the pool sets it so you send about one every 10 s and checks every one on the CPU exactly as a node checks a block</dd>
<dt>Dev fee</dt><dd>in pool mode the miner software takes no dev fee: the pool's fee (__FEE__%) is the only fee, and it is in the welcome message every miner prints at start</dd>
<dt>Pool address</dt><dd class="hash">__POOL_ADDRESS__</dd>
</dl>
</div>
<div class="card" id="lookup">
<div class="viz-head"><h2>Your address</h2><div class="eyebrow">shares, hash rate, balance, payments</div></div>
<form class="search" id="search"><input id="q" type="search" autocomplete="off" spellcheck="false" placeholder="0x your payout address" aria-label="Payout address"><button class="btn" type="submit">Look up</button></form>
<div class="msg" id="msg" aria-live="polite"></div>
<div id="miner" hidden>
<dl>
<dt>Hash rate</dt><dd id="m-hr"></dd>
<dt>Shares</dt><dd id="m-shares"></dd>
<dt>Balance</dt><dd id="m-balance"></dd>
<dt>Paid</dt><dd id="m-paid"></dd>
<dt>Blocks found</dt><dd id="m-blocks"></dd>
</dl>
<div class="tbl"><table><thead><tr><th>Worker</th><th class="n">Hash rate</th><th class="n">Accepted</th><th class="n">Stale</th><th class="n">Rejected</th><th>Shift</th><th>Proving</th><th>Status</th></tr></thead><tbody id="m-workers"></tbody></table></div>
<p class="note">A worker that also proves shards gives about 4% of its hash rate to the prover (measured on an RTX 5090, bench-log "proving v1"); proving income is paid to your own key by the chain and does not appear here. The Proving column says when the miner reports it.</p>
</div>
</div>
<div class="two">
<div class="card" id="blocks" style="margin-top:0">
<div class="viz-head"><h2>Blocks</h2><div class="eyebrow">newest first, every 5 s</div></div>
<div class="tbl"><table><thead><tr><th>Block</th><th class="n">DAA</th><th>Finder</th><th class="n">Reward</th><th class="n">Effort</th><th>Status</th><th>Time</th></tr></thead><tbody id="blocks-body"><tr><td colspan="7">Loading.</td></tr></tbody></table></div>
</div>
<div class="card" id="payments" style="margin-top:0">
<div class="viz-head"><h2>Payments</h2><div class="eyebrow">EVM transfers from the pool address</div></div>
<div class="tbl"><table><thead><tr><th>Time</th><th>Address</th><th class="n">IGN</th><th>Tx</th><th>Status</th></tr></thead><tbody id="pay-body"><tr><td colspan="5">Loading.</td></tr></tbody></table></div>
</div>
</div>
<div class="card">
<div class="viz-head"><h2>API</h2><div class="eyebrow">JSON, GET, CORS open</div></div>
<dl>
<dt>/api/stats</dt><dd><a class="hash" href="/api/stats">pool hash rate, miners, workers, blocks 24 h, last block, fee, minimum payout, luck, and the network's difficulty, hash rate and reward</a></dd>
<dt>/api/blocks</dt><dd><a class="hash" href="/api/blocks">blocks found with DAA score, time, finder, status and the PPLNS split</a></dd>
<dt>/api/miners/&lt;address&gt;</dt><dd class="hash">hash rate, shares, workers, balance, paid, payments</dd>
<dt>/api/payments</dt><dd><a class="hash" href="/api/payments">every payout with its transaction hash</a></dd>
<dt>/api/pool-stats</dt><dd><a class="hash" href="/api/pool-stats">the flat camelCase object pool dashboards poll</a></dd>
</dl>
</div>
</main>
<footer class="foot"><div class="wrap">Igneum. Mined by GPUs. Proven by fire. · <a href="https://igneum.network/">igneum.network</a> · Nothing on this page is an offer to sell anything.</div></footer>
<script>
const $ = id => document.getElementById(id);
const esc = s => String(s ?? '').replace(/&/g,'&amp;').replace(/</g,'&lt;');
const hr = h => { h = Number(h) || 0; const u = ['H/s','kH/s','MH/s','GH/s','TH/s']; let i = 0; while (h >= 1000 && i < u.length - 1) { h /= 1000; i++; } return h.toFixed(i ? 2 : 0) + ' ' + u[i]; };
const short = h => h ? h.slice(0, 10) + '…' : '';
const rel = ms => { const d = (Date.now() - ms) / 1000; return d < 60 ? Math.round(d) + ' s ago' : d < 3600 ? Math.round(d / 60) + ' min ago' : Math.round(d / 3600) + ' h ago'; };
const chip = s => `<span class="chip ${s === 'confirmed' ? 'ok' : s === 'orphan' || s === 'failed' ? 'bad' : 'hot'}">${esc(s)}</span>`;
async function stats() {
try {
const r = await fetch('/api/stats'); const s = await r.json(); const p = s.pool, n = s.network;
$('p-hr').textContent = hr(p.hashrate); $('p-hr1h').textContent = hr(p.hashrate_1h) + ' over 1 h';
$('p-miners').textContent = p.miners + ' / ' + p.workers;
$('p-blocks').textContent = p.blocks_24h; $('p-blocks-s').textContent = p.blocks_confirmed_24h + ' confirmed / ' + p.blocks_orphaned_24h + ' orphaned, ' + p.blocks_total + ' total';
if (p.last_block) { $('p-last').textContent = short(p.last_block.hash); $('p-last-s').textContent = rel(p.last_block.ts_ms) + ', ' + p.last_block.status; }
$('n-hr').textContent = n.hashrate == null ? 'n/a' : hr(n.hashrate); $('n-diff').textContent = 'difficulty ' + Math.round(n.difficulty).toLocaleString() + ', DAA ' + n.daa_score;
$('n-reward').innerHTML = esc(n.miner_reward_ign.toFixed(4)) + '<small>IGN</small>';
$('p-effort').textContent = Math.round(p.effort_current * 100) + '%'; $('p-luck').textContent = 'luck 24 h ' + (p.luck_24h ? Math.round(p.luck_24h * 100) + '%' : 'n/a');
$('p-paid').innerHTML = esc(p.paid_24h_ign.toFixed(4)) + '<small>IGN</small>'; $('p-paid-s').textContent = p.pending_balance_ign.toFixed(4) + ' IGN pending' + (p.dry_run ? ' (dry run)' : '');
} catch (e) { $('p-last-s').textContent = 'stats unavailable'; }
}
async function blocks() {
try {
const r = await fetch('/api/blocks?limit=25'); const j = await r.json();
$('blocks-body').innerHTML = j.blocks.length ? j.blocks.map(b => `<tr><td class="hash">${short(b.hash)}</td><td class="n">${b.daa_score}</td><td class="hash">${short(b.finder)} ${esc(b.worker)}</td><td class="n">${b.reward_ign.toFixed(4)}</td><td class="n">${Math.round(b.effort * 100)}%</td><td>${chip(b.status)}</td><td>${rel(b.ts_ms)}</td></tr>`).join('') : '<tr><td colspan="7">No block yet.</td></tr>';
} catch (e) {}
}
async function payments() {
try {
const r = await fetch('/api/payments?limit=25'); const j = await r.json();
$('pay-body').innerHTML = j.payments.length ? j.payments.map(p => `<tr><td>${rel(p.ts_ms)}</td><td class="hash">${short(p.address)}</td><td class="n">${p.amount_ign.toFixed(6)}</td><td class="hash">${p.tx_hash ? short(p.tx_hash) : ''}</td><td>${chip(p.status)}</td></tr>`).join('') : '<tr><td colspan="5">No payment yet.</td></tr>';
} catch (e) {}
}
$('search').addEventListener('submit', async e => {
e.preventDefault(); const a = $('q').value.trim();
if (!/^0x[0-9a-fA-F]{40}$/.test(a)) { $('msg').textContent = 'An address is 0x followed by 40 hex characters.'; return; }
$('msg').textContent = 'Looking.';
const r = await fetch('/api/miners/' + a); const m = await r.json();
if (!m.ok) { $('msg').textContent = m.error; return; }
$('msg').textContent = m.online ? 'Online.' : (m.first_seen_ms ? 'Not connected now.' : 'No shares from this address yet.');
$('miner').hidden = false;
$('m-hr').textContent = hr(m.hashrate) + ' (10 min), ' + hr(m.hashrate_1h) + ' (1 h)';
$('m-shares').textContent = m.shares.accepted + ' accepted, ' + m.shares.stale + ' stale, ' + m.shares.rejected + ' rejected';
$('m-balance').textContent = m.balance_ign.toFixed(6) + ' IGN'; $('m-paid').textContent = m.paid_ign.toFixed(6) + ' IGN'; $('m-blocks').textContent = m.blocks;
$('m-workers').innerHTML = m.workers.length ? m.workers.map(w => `<tr><td>${esc(w.name)}</td><td class="n">${hr(w.hashrate)}</td><td class="n">${w.accepted}</td><td class="n">${w.stale}</td><td class="n">${w.rejected}</td><td>${w.shift ?? ''}</td><td>${w.proving ? 'yes' : 'no'}</td><td>${chip(w.online ? 'online' : 'offline')}</td></tr>`).join('') : '<tr><td colspan="8">No worker yet.</td></tr>';
history.replaceState(null, '', '#lookup');
});
stats(); blocks(); payments();
setInterval(stats, 5000); setInterval(blocks, 5000); setInterval(payments, 15000);
</script>
</body>
</html>