From fa64025de58c19115f4a18514bbb57aeff82c6c0 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:05:52 +0000 Subject: [PATCH] miner-ui-5 (0.3.19): the ladder on every page, the count-up, the block card, Earnings in IGN first, EMBER 02 scene View: poisson, ignPerDay, ladderRungs (every threshold from the node's params: dust, weightWindow, presenceWindow), blockCard, earningsLines (IGN first, the typed price never fetched, the share and rank, IGN per kWh), cardIgnDay, timeline, profileWords; FIELDS names every number's RPC field for the title on hover. Overview: the Poisson count-up until the first block, the block card in place with Save the card (a canvas PNG, no network call), Copy the link and the explorer, the ladder strip. Earnings: the six lines, the ladder card, Your first hour. Prove: the shard card. Cards: IGN a day per card. Settings: Make my page public. The chain scene is the site lane's EMBER 02 pack (live-dag.js and proof-core.js byte-identical): the compact card fed by the page's own api/live read, Inspect opens the full scene with the block inspector in the site's words. ui-mock: scenarios firstwait, firstblock, ladder, api/ladder, api/card. view.test.mjs: a Devnet 2 key walks every rung; 47 UI tests. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit 32793b686a00279fc297c67140aac0ddca00b561) --- app/igneum-app/ui/app.css | 104 +++++- app/igneum-app/ui/app.js | 418 ++++++++++++++++++++++-- app/igneum-app/ui/index.html | 99 +++++- app/igneum-app/ui/live-dag.js | 542 +++++++++++++++++++------------- app/igneum-app/ui/proof-core.js | 72 +++++ app/igneum-app/ui/view.test.mjs | 196 ++++++++++++ tools/ui-mock/server.mjs | 38 ++- 7 files changed, 1219 insertions(+), 250 deletions(-) create mode 100644 app/igneum-app/ui/proof-core.js diff --git a/app/igneum-app/ui/app.css b/app/igneum-app/ui/app.css index 2d0dcdca..996ad537 100644 --- a/app/igneum-app/ui/app.css +++ b/app/igneum-app/ui/app.css @@ -24,13 +24,15 @@ --s-1:4px;--s-2:8px;--s-3:12px;--s-4:16px;--s-5:24px;--s-6:32px; --gutter:var(--s-6);--card-pad:var(--s-5);--card-r:16px;--row-r:12px;--gap:var(--s-4); --sans:'IBM Plex Sans',system-ui,-apple-system,sans-serif;--mono:'IBM Plex Mono',ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;--head:'Unbounded',sans-serif; - --top:60px;--bottom:0px;--drawer-h:260px;--rail:196px} + --top:60px;--bottom:0px;--drawer-h:260px;--rail:196px; + /* the chain scene's inclusion colours (EMBER 02 live-dag.js reads them from :root) */ + --included:#3B7DD8;--excluded:#B0362B} @media (prefers-color-scheme:light){:root:not([data-theme="dark"]){ --obsidian:#F4F1EC;--graphite:#FFFFFF;--row:#FAF8F5;--line:#E2DED8;--line-2:#CFCAC2;--ember:#E04A14;--ember-hi:#F2541B;--molten:#B8731F;--bone:#16161A;--ash:#6B6B70;--ink-2:#3C3C42;--ember-ink:#FFFFFF; - --ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--nvidia:#3C9B2C;--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--ok:#B8731F}} + --ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--nvidia:#3C9B2C;--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--ok:#B8731F;--included:#2A62B8;--excluded:#B0362B}} :root[data-theme="light"]{ --obsidian:#F4F1EC;--graphite:#FFFFFF;--row:#FAF8F5;--line:#E2DED8;--line-2:#CFCAC2;--ember:#E04A14;--ember-hi:#F2541B;--molten:#B8731F;--bone:#16161A;--ash:#6B6B70;--ink-2:#3C3C42;--ember-ink:#FFFFFF; - --ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--nvidia:#3C9B2C;--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--ok:#B8731F} + --ember-12:rgba(224,74,20,.1);--ember-40:rgba(224,74,20,.4);--molten-10:rgba(184,115,31,.1);--molten-40:rgba(184,115,31,.4);--nvidia:#3C9B2C;--rail-bg:#EFEBE4;--hover:rgba(0,0,0,.04);--top-bg:rgba(244,241,236,.88);--shadow:rgba(0,0,0,.18);--scrim:rgba(244,241,236,.72);--ok:#B8731F;--included:#2A62B8;--excluded:#B0362B} *{box-sizing:border-box} html,body{height:100%} body{margin:0;background:var(--obsidian);color:var(--bone);font-family:var(--sans);font-size:var(--t-md);line-height:1.5;-webkit-font-smoothing:antialiased;overflow:hidden;user-select:none;-webkit-user-select:none;font-variant-numeric:tabular-nums} @@ -367,6 +369,8 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)} .money-row:last-child{border-bottom:0} .money-row .v{font-family:var(--head);font-weight:700;font-size:var(--t-hero);line-height:1.1;white-space:nowrap;letter-spacing:-.01em;min-width:200px} .money-row .v.small{font-size:var(--t-num)} +.money-row .v.tiny{font-family:var(--sans);font-size:var(--t-lg);font-weight:600;white-space:normal;min-width:0;line-height:1.35} +@media (max-width:720px){.money-row .v{white-space:normal;min-width:0;font-size:var(--t-h2)}.money-row .v.small{font-size:var(--t-xl)}} .money-row .s{font-size:var(--t-base);color:var(--ash);min-width:0} /* prove */ @@ -692,3 +696,97 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} .legend{gap:6px 12px} .rail-foot .nav.small{padding:6px 4px} } + +/* ---- miner-ui-5: the miner's first month: the count-up, the block card, the ladder strip, the rungs, the timeline ---- */ +.wait-card{background:var(--row);padding:16px 20px} +.wait-row{display:flex;align-items:center;gap:var(--s-4)} +.wait-ring{position:relative;width:52px;height:52px;flex:0 0 52px;display:inline-flex;align-items:center;justify-content:center} +.wait-ring svg{position:absolute;inset:0;width:52px;height:52px;transform:rotate(-90deg)} +.wait-ring .track{fill:none;stroke:var(--line);stroke-width:3} +.wait-ring .arc{fill:none;stroke:var(--molten);stroke-width:3;stroke-linecap:round;stroke-dasharray:97.4;stroke-dashoffset:97.4;transition:stroke-dashoffset .6s ease} +.wait-ring .pct{font-size:var(--t-xs);color:var(--molten)} +.wait-text .t{font-size:var(--t-md);color:var(--bone)} +.wait-text .s{font-size:var(--t-sm);color:var(--ash);margin-top:2px} +.block-card{position:relative;background:linear-gradient(135deg,var(--graphite),var(--row));border-color:var(--line-2);box-shadow:inset 0 1px 0 var(--ember),0 18px 50px var(--shadow);user-select:text;-webkit-user-select:text} +.bc-head{display:flex;align-items:center;gap:var(--s-3);margin-bottom:var(--s-2)} +.bc-when{margin-left:auto;font-size:var(--t-xs);color:var(--ash)} +.bc-close{position:static;margin-left:4px} +.bc-title{font-size:var(--t-h1);letter-spacing:-.02em;margin-bottom:var(--s-2)} +.bc-title.small{font-size:var(--t-h2)} +.bc-line{font-size:var(--t-lg);color:var(--ink-2);margin:2px 0} +.bc-line.dim{color:var(--ash);font-size:var(--t-md)} +.bc-hash{font-size:var(--t-sm);color:var(--ash);margin:8px 0 12px;word-break:break-all} +.shard-card{box-shadow:inset 0 1px 0 var(--nvidia)} +.ladder-strip{padding:14px 20px} +.ls-rungs{display:flex;gap:4px;flex-wrap:wrap;margin-bottom:10px} +.ls-rung{display:inline-flex;align-items:center;gap:7px;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.04em;color:var(--ash);padding:4px 10px 4px 6px;border:1px solid var(--line);border-radius:999px;white-space:nowrap} +.ls-rung i{width:8px;height:8px;border-radius:50%;background:var(--line-2);display:inline-block} +.ls-rung.done{color:var(--molten);border-color:var(--molten-40)} +.ls-rung.done i{background:var(--molten)} +.ls-rung.now{color:var(--bone);border-color:var(--ember-40)} +.ls-rung.now i{background:var(--ember);box-shadow:0 0 0 3px var(--ember-12)} +.ls-rung.off{opacity:.55} +.ls-line{display:flex;align-items:baseline;gap:var(--s-3);flex-wrap:wrap} +.ls-line .t{font-size:var(--t-md);color:var(--bone);font-weight:500} +.ls-line .s{font-size:var(--t-sm);min-width:0} +.ls-line .btn{margin-left:auto} +.rungs{list-style:none;margin:var(--s-3) 0 0;padding:0;display:flex;flex-direction:column} +.rung{display:grid;grid-template-columns:22px 1fr;gap:var(--s-3);padding:10px 0;border-bottom:1px solid var(--line)} +.rung:last-child{border-bottom:0} +.rg-dot{width:12px;height:12px;border-radius:50%;background:var(--line-2);margin-top:5px;border:2px solid transparent;position:relative} +.rung.done .rg-dot{background:var(--molten)} +.rung.now .rg-dot{background:var(--ember);box-shadow:0 0 0 4px var(--ember-12)} +.rung.off .rg-dot{background:transparent;border-color:var(--line-2)} +.rg-body{display:flex;flex-direction:column;gap:2px;min-width:0} +.rg-name{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.12em;text-transform:uppercase;color:var(--ash)} +.rg-line{font-size:var(--t-lg);color:var(--bone)} +.rung.done .rg-line{color:var(--molten)} +.rung.next .rg-line,.rung.off .rg-line{color:var(--ash)} +.rg-sub{font-size:var(--t-sm);color:var(--ash);line-height:1.45} +.rung .bar{display:block;height:3px;border-radius:2px;background:var(--line);overflow:hidden;margin-top:6px;max-width:320px} +.rung .bar b{display:block;height:100%;background:var(--ember)} +.timeline{list-style:none;margin:0;padding:0;display:flex;flex-direction:column} +.timeline li{display:grid;grid-template-columns:140px auto 1fr;gap:var(--s-3);align-items:baseline;padding:6px 0;border-bottom:1px solid var(--line);color:var(--ash);font-size:var(--t-md)} +.timeline li:last-child{border-bottom:0} +.timeline li.done{color:var(--bone)} +.timeline .tl-at{font-size:var(--t-sm);color:var(--molten);white-space:nowrap} +.timeline .tl-note{font-size:var(--t-sm);min-width:0} +.money-row.price-row .row{gap:6px;align-items:center} +.money-row .s+.s::before{content:"\00b7";margin-right:var(--s-2);color:var(--line-2)} +.num.ign .v{color:var(--molten)} +@media (max-width:720px){.bc-title{font-size:var(--t-h2)}.timeline li{grid-template-columns:110px auto 1fr}.ls-line .btn{margin-left:0}} + +/* ---- the full chain scene with the block inspector (EMBER 02; the words of the site's /live) ---- */ +.chain-full{margin-top:var(--s-3);border-top:1px solid var(--line);padding-top:var(--s-3)} +.cf-bar{display:flex;justify-content:space-between;align-items:center;gap:var(--s-3);flex-wrap:wrap;margin-bottom:var(--s-3)} +.cf-right{display:inline-flex;align-items:center;gap:var(--s-2)} +.zoom{display:inline-flex;align-items:center;border:1px solid var(--line);border-radius:6px;overflow:hidden} +.zoom button{background:transparent;border:0;color:var(--ink-2);font:400 13px/1 var(--mono);padding:7px 11px;cursor:pointer} +.zoom button:hover{background:var(--hover)} +.zoom span{font-size:var(--t-xs);color:var(--ash);padding:0 6px;min-width:44px;text-align:center} +.cf-graph{display:grid;grid-template-columns:minmax(0,1fr) 250px;gap:var(--s-4)} +.cf-scene{position:relative;min-width:0} +.cf-scene canvas{display:block;width:100%;height:360px;border-radius:10px;background:var(--row);border:1px solid var(--line)} +.cf-foot{margin-top:8px;font-size:var(--t-xs);color:var(--ash)} +.inspector{min-width:0;font-size:var(--t-base);border-left:1px solid var(--line);padding-left:var(--s-4)} +.inspector .ih{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:10px} +.chip{font-family:var(--mono);font-size:var(--t-xs);color:var(--ash);border:1px solid var(--line);border-radius:4px;padding:4px 8px;white-space:nowrap} +.ititle{font-family:var(--sans);font-weight:500;font-size:20px;line-height:1.2;color:var(--bone);margin:6px 0 4px;letter-spacing:-.01em} +.ihash{font-size:var(--t-xs);color:var(--ash);overflow-wrap:anywhere;margin-bottom:12px;user-select:text;-webkit-user-select:text} +.proof-scene{height:180px;border:1px solid var(--line);border-radius:6px;background:var(--row);margin:10px 0;overflow:hidden} +.proof-scene canvas{display:block;width:100%;height:100%} +.proof-head{display:flex;justify-content:space-between;align-items:baseline;font-weight:500;color:var(--bone);margin-bottom:8px} +.track{display:flex;gap:3px;height:3px;margin-bottom:10px} +.track i{flex:1 1 0;background:var(--line);border-radius:2px} +.track i.proving{background:var(--ember)}.track i.verified{background:var(--bone)}.track i.paid{background:var(--molten)} +.shards{list-style:none;margin:0 0 12px;padding:0;font-size:var(--t-xs);line-height:1.7;max-height:120px;overflow:auto} +.shards li{display:flex;justify-content:space-between;gap:8px;color:var(--ink-2)} +.shards li::before{content:"\25CF";font-size:7px;margin-right:6px;color:var(--line-2)} +.shards li.proving,.shards li.proving::before{color:var(--ember)}.shards li.verified,.shards li.verified::before{color:var(--bone)}.shards li.paid,.shards li.paid::before{color:var(--molten)} +.shards li.muted{color:var(--ash)} +.insp{display:grid;grid-template-columns:auto minmax(0,1fr);gap:0 10px;margin:0;border-top:1px solid var(--line);padding-top:6px;font-size:var(--t-xs)} +.insp dt{color:var(--ash);padding:5px 0} +.insp dd{margin:0;color:var(--bone);text-align:right;padding:5px 0;overflow-wrap:anywhere;min-width:0} +.inspector .note{font-size:var(--t-sm);margin-top:12px;padding-top:10px;border-top:1px solid var(--line)} +.inspector .top-gap{margin-top:8px} +@media (max-width:900px){.cf-graph{grid-template-columns:1fr}.inspector{border-left:0;padding-left:0;border-top:1px solid var(--line);padding-top:var(--s-3)}} diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index b2603b4d..ed3997af 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -597,6 +597,11 @@ var View = (function () { [/^restarting the node with the new consensus parameters/, function () { return 'Restarting the node with the new rule settings'; }], [/^kernel tuning from the signed manifest/, function () { return 'Tuning starting points read from the team'; }], [/^block accepted by the node \((.+)\)/, function (m) { return 'Block found by ' + m[1]; }], + [/^first block: found by (.+) \(block card\)/, function (m) { return 'Your first block, found by ' + m[1]; }], + [/^block ([\d,]+) of this machine: found by (.+) \(block card\)/, function (m) { return 'Block ' + m[1] + ' of this machine, found by ' + m[2]; }], + [/^first block on (.+) \(block card\)/, function (m) { return 'First block on ' + m[1]; }], + [/^public profile on:/, function () { return 'Your address page is public'; }], + [/^public profile off:/, function () { return 'Your address page is unlisted'; }], [/^igneumd exited with code (\d+) after (\d+) s; restarting/, function (m) { return 'The node stopped (code ' + m[1] + '). Restarting'; }], [/^igneumd exited at once \(code (\d+)\)/, function (m) { return 'The node stopped at once (code ' + m[1] + ')'; }], [/^igneumd could not start/, function () { return 'The node could not start'; }], @@ -772,10 +777,173 @@ var View = (function () { if (n.source === 'none') return { text: 'no node', sub: n.port_note || n.mode_reason || 'the ports are taken by a node on other rules' }; return { text: '', sub: '' }; } + // ---- miner-ui-5 (7 October 2026): the miner's first month. The Poisson count-up before the first block, the block + // card, Earnings in IGN first with a typed price, the ladder rungs as chain facts, the first-hour timeline. + // Every number names its RPC field on hover: FIELDS holds the one sentence per number, the renderers put it in + // the title attribute. `chain` is GET api/ladder (src/chainfacts.rs): the node's getFinalityWeights table with + // this machine's keys ranked, the params, the locked index and the public /api/stats numbers. ---- + var FIELDS = { + hashrate: '/api/stats hashrate: the node’s estimateNetworkHashesPerSecond over 1,000 blocks (H/s)', + reward: '/api/stats block_reward.miner_ign: the 80 producer points of E(daa) (spec 2.5) at the newest DAA score, after the launch ramp', + ramp: '/api/stats block_reward.ramp_factor: the launch ramp, 10% on day one to 100% at day 30', + blocks: 'getFinalityWeights keys[].blocks: blue blocks with your key in the weight window (params.weightWindow)', + voter: 'getFinalityWeights keys[].voter: blocks at or above params.dust', + dust: 'getFinalityWeights params.dust: the blocks a key needs in the window to vote', + window: 'getFinalityWeights params.weightWindow: the window in DAA steps (one a second)', + participation: 'getFinalityWeights keys[].participation: the share of the last params.presenceWindow checkpoints your key signed', + rank: 'getFinalityWeights keys[] sorted by blocks, your key’s place; "of" = keys_listed (the observer relays at most 64)', + voters: 'getFinalityWeights voters: keys at or above dust', + total: 'getFinalityWeights totalWeight: every key’s blocks in the window', + locked: 'getFinalityCheckpoints latestLockedIndex: the newest checkpoint locked by two thirds of all weight', + signed: 'this app’s miner log: VOTE index=N lines, read as locked once a LOCK checkpoint line reaches N; the per-key signer list is owed from the node (getFinalityCheckpoints checkpoints[].signers)', + shards: 'igneum_getProofRecords(block).paid[shard] with your payout address (src/prover.rs)', + found: 'this app: the miner’s ACCEPTED block lines, each matched to the node’s PoW accepted line by nonce (ladder.json)', + found_hour: 'this app: mining.found, the accepted blocks of the last hour', + days: 'this app: distinct UTC days with a block in the last 30 days (ladder.json); per-key days are owed from the node (getFinalityWeights keys[].daysMined)', + streak: 'this app: unbroken VOTE lines inside the presence window (ladder.json); the chain fact is keys[].participation', + price: 'your figure: typed on this machine, remembered here, never fetched; no exchange lists IGN', + watts: 'the cards’ power draw now (nvidia-smi, the AMD telemetry, or the engine’s mining.watts_total)', + share: 'your rate over /api/stats hashrate' + }; + function fmtIgn(v) { if (v === null || v === undefined || isNaN(v)) return ''; var a = Math.abs(v); return (a >= 1000 ? withCommas(v) : a >= 100 ? v.toFixed(1) : a >= 1 ? v.toFixed(2) : v.toFixed(4)) + ' IGN'; } + function pct(v, digits) { if (!(v >= 0) || !isFinite(v)) return ''; var p = v * 100; return (p >= 10 ? p.toFixed(1) : p >= 1 ? p.toFixed(2) : p >= 0.01 ? p.toFixed(3) : p.toPrecision(2)) + '%'; } + // "16 minutes" / "1.6 hours" / "16 hours" / "6.8 days" for an expected gap + function gapWords(s) { + if (!(s > 0) || !isFinite(s)) return ''; + if (s < 90) return Math.max(1, Math.round(s)) + ' seconds'; + if (s < 5400) return Math.round(s / 60) + ' minutes'; + if (s < 36 * 3600) { var h = s / 3600; return (h < 10 ? h.toFixed(1) : Math.round(h)) + ' hours'; } + var d = s / 86400; return (d < 10 ? d.toFixed(1) : Math.round(d)) + ' days'; + } + // "41 days" / "3 h 12 min" / "48 min" for a streak or an age + function spanWords(s) { s = Math.max(0, Math.round(s || 0)); if (s < 60) return s + ' s'; if (s < 3600) return Math.floor(s / 60) + ' min'; if (s < 86400) return Math.floor(s / 3600) + ' h ' + Math.floor(s % 3600 / 60) + ' min'; var d = Math.floor(s / 86400); return d + ' day' + (d === 1 ? '' : 's'); } + // the chain's numbers the lines need, with nulls where the reply has none: c = GET api/ladder + function chainNums(c) { + var ok = !!(c && c.ok), n = (ok && c.network) || {}, p = (ok && c.params) || {}; + return { ok: ok, hps: n.hashrate_hps > 0 ? n.hashrate_hps : null, minerIgn: n.miner_ign_per_block > 0 ? n.miner_ign_per_block : null, ramp: n.ramp_factor > 0 ? n.ramp_factor : null, bps: n.bps > 0 ? n.bps : 1, dust: p.dust > 0 ? p.dust : null, windowS: p.weight_window > 0 ? p.weight_window : null, presence: p.presence_window > 0 ? p.presence_window : null, interval: p.checkpoint_interval > 0 ? p.checkpoint_interval : 30, best: ok && c.best ? c.best : null, mine: (ok && c.mine) || [], listed: ok ? (c.keys_listed || 0) : 0, cap: ok ? (c.keys_cap || 0) : 0, voters: ok ? (c.voters || 0) : 0, locked: ok ? (c.latest_locked_index || 0) : 0, active: ok ? !!c.finality_active : false, source: ok ? c.source : '', stale: !ok || !!n.stale, error: !ok && c ? (c.error || '') : '' }; + } + // the Poisson count-up: a card at `mhs` on a network at `hps` finds a block every hps / (mhs * 1e6 * bps) seconds on + // average; after `elapsed` seconds the chance of at least one is 1 - exp(-elapsed / gap). {gap_s, gapText, chance, + // chanceText, line, pool, share} + function poisson(mhs, hps, elapsed, bps, fleet) { + if (!(mhs > 0) || !(hps > 0)) return null; + bps = bps > 0 ? bps : 1; + var share = mhs * 1e6 / hps, gap = 1 / (share * bps), chance = 1 - Math.exp(-Math.max(0, elapsed || 0) / gap); + var who = fleet ? 'Your cards find' : 'A card like yours finds'; + var line = who + ' a block about every ' + gapWords(gap) + ' on today’s network. Chance so far: ' + Math.round(chance * 100) + '%.'; + var pool = gap > 8 * 3600 ? 'Above an 8-hour gap a pool pays by the share, every minute. The pool setting arrives with pool-0.' : ''; + return { gap_s: gap, gapText: gapWords(gap), chance: chance, chanceText: Math.round(chance * 100) + '%', line: line, pool: pool, share: share }; + } + // IGN a day for a rate on a network: 86,400 blocks a day at one a second, your share of them, the producer's points of each + function ignPerDay(mhs, hps, minerIgn, bps) { if (!(mhs > 0) || !(hps > 0) || !(minerIgn > 0)) return null; bps = bps > 0 ? bps : 1; return 86400 * bps * (mhs * 1e6 / hps) * minerIgn; } + // the window in the user's words: "30 days" on mainnet, "2 hours" on the devnet (params.weightWindow in seconds) + function windowWords(windowS) { if (!(windowS > 0)) return { n: 30, unit: 'days', text: '30 days' }; if (windowS >= 86400) { var d = Math.round(windowS / 86400); return { n: d, unit: 'days', text: d + ' day' + (d === 1 ? '' : 's') }; } var h = Math.max(1, Math.round(windowS / 3600)); return { n: h, unit: 'hours', text: h + ' hour' + (h === 1 ? '' : 's') }; } + // the days (or hours on the devnet) this key has mined inside the window: the ladder's day ring, or the hours + // spanned by its blocks when the window is under a day + function daysIn(ladder, windowS, now) { + var w = windowWords(windowS); ladder = ladder || {}; + if (w.unit === 'days') return { have: Math.min(w.n, ladder.days_mined_30 || 0), of: w.n, unit: 'days' }; + var hours = 0; + if (ladder.first_block_at > 0 && now) hours = Math.min(w.n, Math.floor((now - ladder.first_block_at) / 3600) + ((ladder.blocks_today || 0) > 0 || (ladder.blocks_30d || 0) > 0 ? 1 : 0)); + return { have: Math.max(0, hours), of: w.n, unit: 'hours' }; + } + // the ladder's rungs for a state: s = state, chain = api/ladder, now = unix s. Each rung: {id, name, state (done | now | + // next | off), line, sub, field, progress (-1 or 0..1)}. The gate (mission 2.6): a key walks every rung on screen. + function ladderRungs(s, chain, now) { + s = s || {}; var m = s.mining || {}, ld = s.ladder || {}, pv = s.proving || {}, c = chainNums(chain), rungs = []; + var best = c.best, inTable = !!best, blocks = inTable ? best.blocks : 0, found = m.accepted_total || 0; + // 0. first block + var first = found > 0 || blocks > 0; + rungs.push({ id: 'first', name: 'First block', state: first ? 'done' : 'now', line: first ? 'First block' + (ld.first_block_at > 0 && now ? ' ' + ago(now - ld.first_block_at) : '') + (ld.first_block_card ? ', ' + cardName(s, ld.first_block_card) : '') + '.' : 'Waiting for your first block.', sub: inTable ? withCommas(blocks) + ' block' + (blocks === 1 ? '' : 's') + ' with your key in the window' : (found ? withCommas(found) + ' found by this machine' : ''), field: inTable ? FIELDS.blocks : FIELDS.found, progress: -1 }); + // 1. the vote + var dust = c.dust, voter = inTable && best.voter; + rungs.push({ id: 'vote', name: 'Your key has a vote', state: voter ? 'done' : first ? 'now' : 'next', line: voter ? 'Your key has a vote.' : (dust ? withCommas(blocks) + ' of ' + withCommas(dust) + ' blocks to a vote.' : 'A vote needs the dust line’s blocks in the window.'), sub: voter ? withCommas(blocks) + ' blocks in the window, the line is ' + withCommas(dust || 0) : (inTable ? 'your key is in the table' : c.ok ? 'your key is not in the table yet' : (c.error || 'the chain facts are not read yet')), field: FIELDS.voter + ' · ' + FIELDS.dust, progress: voter ? 1 : dust ? Math.min(1, blocks / dust) : -1 }); + // 2. the signature in a locked checkpoint + var signedIdx = ld.last_signed_locked || 0, part = inTable ? best.participation : 0; + rungs.push({ id: 'signed', name: 'Your signature is in a checkpoint', state: signedIdx > 0 ? 'done' : voter ? 'now' : 'next', line: signedIdx > 0 ? 'Your signature is in checkpoint ' + withCommas(signedIdx) + '.' : voter ? 'Signing: the next locked checkpoint carries your vote.' : 'Comes with the vote.', sub: signedIdx > 0 ? withCommas(ld.signed_locked_count || 0) + ' signed and locked' + (inTable ? ' · presence ' + Math.round(part * 100) + '%' : '') + (c.locked ? ' · latest lock ' + withCommas(c.locked) : '') : (c.locked ? 'latest lock ' + withCommas(c.locked) : ''), field: FIELDS.signed + ' · ' + FIELDS.participation + ' · ' + FIELDS.locked, progress: -1 }); + // 3. the full window + var d = daysIn(ld, c.windowS, now), full = first && d.have >= d.of; + rungs.push({ id: 'window', name: 'Full window', state: full ? 'done' : first ? 'now' : 'next', line: full ? d.of + ' of ' + d.of + ' ' + d.unit + '. Full weight.' : d.have + ' of ' + d.of + ' ' + d.unit + '.', sub: 'weight is your blocks over the last ' + windowWords(c.windowS).text + '; a key that mined the whole window weighs what it mined', field: FIELDS.days + ' · ' + FIELDS.window, progress: d.of ? Math.min(1, d.have / d.of) : -1 }); + // 4. rank by weight + var rankLine = inTable ? 'Rank ' + withCommas(best.rank) + ' of ' + withCommas(c.listed) + ' keys.' : c.ok ? (c.cap ? 'Below the top ' + c.cap + ' keys.' : 'Not in the table yet.') : 'Rank: not read yet.'; + rungs.push({ id: 'rank', name: 'Rank by weight', state: inTable ? 'done' : first ? 'now' : 'next', line: rankLine, sub: c.ok ? withCommas(c.voters) + ' keys vote' + (c.cap && c.listed >= c.cap ? ' · the top ' + c.cap + ' are listed' : '') + ' · weight, never hashrate: a card that arrived today sits at the bottom' : '', field: FIELDS.rank + ' · ' + FIELDS.voters, progress: -1 }); + // 5. the signing streak + var streak = ld.streak_s || 0; + rungs.push({ id: 'streak', name: 'Signing streak', state: streak > 0 ? 'done' : voter ? 'now' : 'next', line: streak > 0 ? 'Signing 8 of 80 points, ' + spanWords(streak) + ' unbroken.' : 'The 8 signing points start with the vote.', sub: streak > 0 ? 'miss two hours of signing and the next blocks pay 72 until you sign again' + (inTable ? ' · presence ' + Math.round(part * 100) + '%' : '') : '', field: FIELDS.streak + ' · ' + FIELDS.participation, progress: -1 }); + // P. a paid shard + var sh = (ld.shards || [])[0], prove = !!(s.settings && s.settings.prove); + rungs.push({ id: 'shard', name: 'Your card proved a shard', state: sh ? 'done' : prove ? 'now' : 'off', line: sh ? 'Your card proved shard ' + sh.shard + ' of block ' + withCommas(sh.block) + '.' : prove ? 'Proving: the first paid shard lands here.' : 'Proving is off.', sub: sh ? fmtIgn(ign(sh.wei)) + ' · ' + withCommas(pv.paid || 0) + ' paid' : prove ? (pv.message || '') : 'The Prove page switches it on; 20 points of every block pay the provers', field: FIELDS.shards, progress: -1 }); + return rungs; + } + function cardName(s, key) { var l = ((s.mining || {}).cards || []).filter(function (c) { return c.key === key; }); return l.length ? l[0].name : ''; } + // the rung to put on the Overview strip: the first that is "now", else the last done + function currentRung(rungs) { for (var i = 0; i < rungs.length; i++) if (rungs[i].state === 'now') return rungs[i]; for (var j = rungs.length - 1; j >= 0; j--) if (rungs[j].state === 'done') return rungs[j]; return rungs[0]; } + // the block card: m = state.ladder.milestone, s = state, chain, now. {kind, eyebrow, title, when, cardLine, ignLine, + // rankLine, hash, url, fields} + function blockCard(m, s, chain, now) { + if (!m || !(m.count > 0)) return null; + var c = chainNums(chain), cards = (s.mining || {}).cards || [], cd = cards.filter(function (x) { return x.key === m.card; })[0] || {}; + var name = m.card_name || cd.name || 'your card', mhs = cd.hash_avg > 0 ? cd.hash_avg : cd.hash_now || 0; + var eyebrow = m.kind === 'first' ? 'your first block' : m.kind === 'hundred' ? 'block 100 of this machine' : m.kind === 'thousand' ? 'block 1,000 of this machine' : m.kind === 'ten_thousand' ? 'block ' + withCommas(m.count) + ' of this machine' : 'first block on ' + name; + var title = m.daa > 0 ? 'Block ' + withCommas(m.daa) + ' is yours.' : 'A block is yours.'; + var share = mhs > 0 && c.hps ? ' · ' + pct(mhs * 1e6 / c.hps) + ' of the network' : ''; + var cardLine = name + (mhs > 0 ? ' · ' + (mhs >= 100 ? mhs.toFixed(0) : mhs.toFixed(1)) + ' MH/s' : '') + share; + var addr = (s.address || {}).display || ''; + var ignLine = c.minerIgn ? fmtIgn(c.minerIgn) + (addr ? ' to ' + shortHex(addr, 6, 4) : '') + ' (72 producer points, 8 for signing' + (c.ramp && c.ramp < 1 ? ', ramp ' + Math.round(c.ramp * 100) + '%' : '') + ')' : (addr ? 'Paid to ' + shortHex(addr, 6, 4) : ''); + var d = daysIn(s.ladder, c.windowS, now), rankLine = (c.best ? 'rank ' + withCommas(c.best.rank) + ' of ' + withCommas(c.listed) + ' keys' : 'rank: not in the table yet') + ' · ' + (d.unit === 'days' ? 'day ' : 'hour ') + Math.max(1, d.have) + ' of ' + d.of; + return { kind: m.kind, count: m.count, eyebrow: eyebrow, title: title, when: m.at, cardLine: cardLine, ignLine: ignLine, rankLine: rankLine, hash: m.hash || '', hashShort: m.hash ? shortHex(m.hash, 8, 6) : 'hash: waiting for the node’s line', url: m.hash ? 'https://igneum.network/block/' + m.hash : '', key: 'card:' + m.count + ':' + Math.floor(m.at || 0), fields: { card: FIELDS.found, ign: FIELDS.reward, rank: FIELDS.rank + ' · ' + FIELDS.days } }; + } + // Earnings (reinvent 3.4): IGN first, the window, a typed price, the share and rank, IGN per kWh. price = £ per IGN + // typed by the user (null = none), pence = electricity, now = unix s + function earningsLines(s, chain, price, pence, now) { + s = s || {}; var m = s.mining || {}, ld = s.ladder || {}, pv = s.proving || {}, c = chainNums(chain); + var hour = (m.found || []).length, mhs = m.hash_total || 0; + var measured = c.minerIgn ? hour * 24 * c.minerIgn : null, expected = ignPerDay(mhs, c.hps, c.minerIgn, c.bps); + var out = {}; + if (measured !== null && hour > 0) out.day = { text: fmtIgn(measured) + ' a day', sub: 'at your last hour’s rate (' + hour + ' block' + (hour === 1 ? '' : 's') + ', ' + fmtIgn(c.minerIgn) + ' each)' + (expected !== null ? ' · about ' + fmtIgn(expected) + ' expected at ' + (mhs >= 100 ? mhs.toFixed(0) : mhs.toFixed(1)) + ' MH/s' : ''), field: FIELDS.found_hour + ' × 24 × ' + FIELDS.reward, value: measured }; + else if (expected !== null) out.day = { text: 'about ' + fmtIgn(expected) + ' a day', sub: 'expected at ' + (mhs >= 100 ? mhs.toFixed(0) : mhs.toFixed(1)) + ' MH/s, ' + pct(mhs * 1e6 / c.hps) + ' of today’s network · no block in the last hour yet', field: FIELDS.share + ' × 86,400 × ' + FIELDS.reward, value: expected }; + else out.day = { text: mhs > 0 ? 'IGN a day: reading the network' : '0 IGN a day', sub: mhs > 0 ? (c.error || 'the network rate and the reward come from igneum.network/api/stats') : 'no card is mining', field: FIELDS.reward, value: null }; + var wblocks = c.best ? c.best.blocks : (ld.blocks_30d || 0), wign = c.minerIgn ? wblocks * c.minerIgn : null, ww = windowWords(c.windowS); + var shards = pv.paid || 0, shardIgn = pv.paid_wei && Number(pv.paid_wei) ? Number(pv.paid_wei) / 1e18 : 0; + out.window = { text: withCommas(wblocks) + ' block' + (wblocks === 1 ? '' : 's') + ' in ' + ww.text + (wign !== null ? ', ' + fmtIgn(wign) : ''), sub: (c.best ? 'with your key in the weight window' : 'found by this machine (the chain’s count once your key is in the table)') + (wign !== null ? ' · at today’s reward' : '') + ' · ' + shards + ' shard' + (shards === 1 ? '' : 's') + (shardIgn ? ', ' + fmtIgn(shardIgn) : ''), field: (c.best ? FIELDS.blocks : FIELDS.found) + ' · ' + FIELDS.shards, blocks: wblocks }; + var dayIgn = out.day.value; + out.price = price > 0 && dayIgn !== null ? { text: 'about ' + money(dayIgn * price) + ' a day', sub: 'at £' + price + ' per IGN, your figure · no exchange lists IGN', field: FIELDS.price, set: true } : { text: price > 0 ? 'at £' + price + ' per IGN' : 'Type a price', sub: price > 0 ? 'your figure; the IGN a day line fills in first' : 'a £ per IGN you choose; remembered on this machine, never fetched. Nothing is bought or sold on devnet', field: FIELDS.price, set: price > 0 }; + var share = mhs > 0 && c.hps ? pct(mhs * 1e6 / c.hps) : '', d = daysIn(ld, c.windowS, now); + var parts = []; + if (share) parts.push('your cards are ' + share + ' of the network'); + if (c.best) parts.push('weight rank ' + withCommas(c.best.rank) + ' of ' + withCommas(c.listed) + ' keys'); else if (c.ok) parts.push(c.cap ? 'below the top ' + c.cap + ' keys by weight' : 'not in the weight table yet'); + parts.push(d.have + ' of ' + d.of + ' ' + d.unit); + out.share = { text: parts.join(' · '), sub: c.source === 'observer' ? 'the weights through igneum.network’s observer until this node answers igneum_getFinalityWeights' : c.source === 'node' ? 'the weights from your node' : (c.error || 'reading'), field: FIELDS.share + ' · ' + FIELDS.rank + ' · ' + FIELDS.days }; + var watts = fleetWatts(m, m.cards), kwhDay = watts > 0 ? watts * 24 / 1000 : 0; + out.perKwh = dayIgn !== null && kwhDay > 0 ? { text: fmtIgn(dayIgn / kwhDay) + ' per kWh', sub: 'IGN a day over ' + kwhDay.toFixed(1) + ' kWh a day at ' + Math.round(watts) + ' W', field: FIELDS.watts } : null; + return out; + } + // the per-card IGN a day cell on the Cards page + function cardIgnDay(cd, chain) { var c = chainNums(chain); var v = ignPerDay(cd.hash_now, c.hps, c.minerIgn, c.bps); return v === null ? '' : fmtIgn(v).replace(' IGN', ''); } + // the first hour as this machine lived it: steps with unix times (0 = not yet), from the settings' install stamp, + // the ladder's marks and the engine's events + function timeline(s, now) { + s = s || {}; var ld = s.ladder || {}, st = s.settings || {}, n = s.node || {}, m = s.mining || {}; + var installed = s.installed_at > 0 ? s.installed_at : 0, started = s.uptime_s >= 0 && now ? now - s.uptime_s : 0; + var synced = ld.first_synced_at > 0 ? ld.first_synced_at : (n.synced && started ? started : 0); + var mining = ld.first_mining_at > 0 ? ld.first_mining_at : (m.state === 'mining' && started ? started : 0); + var steps = [ + { id: 'install', name: 'Installed', at: installed, note: installed ? '' : 'not recorded on this install' }, + { id: 'started', name: 'App started', at: started, note: '' }, + { id: 'synced', name: 'Node synced', at: synced, note: synced ? (ld.first_synced_at > 0 ? '' : 'the moment is not recorded; synced now') : 'syncing' }, + { id: 'mining', name: 'Card mining', at: mining, note: mining ? (ld.first_mining_at > 0 ? '' : 'the moment is not recorded; mining now') : 'waiting for the node' }, + { id: 'block', name: 'First block', at: ld.first_block_at || 0, note: ld.first_block_at ? '' : 'waiting' }, + { id: 'payout', name: 'First payout', at: ld.first_block_at || 0, note: ld.first_block_at ? 'the coinbase of the first block, yours when the block is blue' : 'the first block pays, seconds later' } + ]; + var base = installed || started; + steps.forEach(function (x) { x.rel = x.at > 0 && base > 0 && x.at >= base ? '+' + spanWords(x.at - base) : ''; }); + return { base: base, steps: steps, field: 'settings.installed_at, uptime_s, ladder.first_synced_at, ladder.first_mining_at, ladder.first_block_at' }; + } + function profileWords(on) { return on ? 'Your address page on igneum.network is public: your key ids, blocks in the window, weight rank, signing presence and card model are shown. The site reads it from the chain and from this switch.' : 'Off: igneum.network/address keeps your page unlisted (noindex). Nothing about this machine is published.'; } // the confirmations' words (in place, never a dialog) function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? Mining restarts.'; } function trustAsk(on) { return on ? 'Include proofs the node never checked? Devnet only. The node restarts.' : 'Check proofs again? The node restarts.'; } - return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk }; + return { PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords }; })(); /* ---------- Ember Tune: the card row's tuning line (pure; tune-line.test.mjs loads this block) ---------- One line per card from the card state (src/state.rs, src/ember.rs): running (the phase and the step), tuned @@ -908,6 +1076,7 @@ if (typeof document !== 'undefined') (function () { setText('page-title-text', meta.title); setText('page-sub', meta.sub); $('main').scrollTop = 0; if (state) { renderPage(state); if (page === 'overview') size(); } + if (typeof pollChain === 'function') pollChain(true); } $('rail-nav').addEventListener('click', function (e) { var b = e.target.closest('.nav[data-page]'); if (b) showPage(b.dataset.page); }); // the rail answers the arrow keys and Home/End, so the sections are reachable without a mouse @@ -1373,26 +1542,216 @@ if (typeof document !== 'undefined') (function () { } function requestDraw() { if (hiddenNow() || drawRaf) return; clearTimeout(drawTimer); drawTimer = null; drawRaf = requestAnimationFrame(frame); } document.addEventListener('visibilitychange', function () { - if (hiddenNow()) { clearTimeout(drawTimer); drawTimer = null; if (drawRaf) { cancelAnimationFrame(drawRaf); drawRaf = 0; } clearTimeout(logTimer); logTimer = null; } - else { poll(); requestDraw(); if (drawerOpen()) { pollLog(); scheduleLogPoll(); } } + if (hiddenNow()) { clearTimeout(drawTimer); drawTimer = null; if (drawRaf) { cancelAnimationFrame(drawRaf); drawRaf = 0; } clearTimeout(logTimer); logTimer = null; if (typeof liveTimer !== 'undefined') { clearTimeout(liveTimer); liveTimer = null; } } + else { poll(); requestDraw(); if (drawerOpen()) { pollLog(); scheduleLogPoll(); } if (typeof pollLive === 'function') pollLive(); } }); window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function () { size(); }); requestDraw(); - // ---------- the chain scene (live-dag.js, the site's module, copied in): the engine's own api/live, this machine's - // lane as "you"; until the engine serves it (owed) the module reports failed and the blocks strip draws instead ---------- - var dag = null, dagMode = ''; + // ---------- the chain scene (EMBER 02: live-dag.js and proof-core.js, the site lane's byte-identical copies) ---------- + // One owner of the feed: this page reads the engine's api/live every 2 s while the Overview shows and pushes the reply + // into the compact scene on the card and, once opened, the full scene with the block inspector (poll: false on both). + // "mine" is this machine's vote key ids (the 8 hex the feed names as b.miner), never the payout address. The fallback + // strip (this machine's own blocks) draws when the feed fails three times in a row. + var dag = null, dagFull = null, proof = null, dagMode = '', liveTimer = null, liveBusy = false, lastLive = null, liveFails = 0, fullOpen = false; + function mineFn(b) { if (!b) return false; if (b.miner === 'you') return true; if (!state) return false; return View.isMine(b.miner, state.mining.cards); } + function dagFallback(on, why) { + var st = on ? 'failed' : (lastLive ? 'live' : 'connecting'); + if (dagMode !== st) { dagMode = st; setText('dag-state', st === 'live' ? 'live' : st === 'failed' ? 'your blocks only' : 'connecting'); setText('dag-note', st === 'failed' ? (why || 'The chain feed is not answering. This is the last 10 minutes of your own blocks.') : (lastLive ? View.liveNote(lastLive) : '')); } + $('dag-live').hidden = on; $('dag-tip').hidden = true; $('dag-pause').hidden = on; $('dag-inspect').hidden = on; $('dag').hidden = !on; + if (on) { size(); requestDraw(); } + } function mountDag() { if (dag || typeof IgneumDag === 'undefined' || !$('dag-live')) return; try { - var mine = function (b) { if (!b) return false; if (b.miner === 'you') return true; if (!state) return false; return View.isMine(b.miner, state.mining.cards); }; - dag = IgneumDag.mount($('dag-live'), { compact: window.innerWidth < 720, window: 120, url: 'api/live', mine: mine, tooltip: $('dag-tip'), chip: $('dag-chip'), pauseButton: $('dag-pause'), onData: function (d) { var n = View.liveNote(d); if (n && dagMode !== 'failed') setText('dag-note', n); }, onState: function (st, why) { - var live = st === 'live' || st === 'stale'; - if (dagMode !== st) { dagMode = st; setText('dag-state', st === 'live' ? 'live' : st === 'stale' ? 'behind' : st === 'failed' ? 'your blocks only' : 'connecting'); setText('dag-note', st === 'failed' ? 'The full chain view needs the next update. This is the last 10 minutes of your own blocks.' : st === 'stale' ? (why || '') : ''); } - $('dag-live').hidden = !live; $('dag-tip').hidden = true; $('dag-pause').hidden = !live; $('dag').hidden = live; - if (!live) { size(); requestDraw(); } - } }); - } catch (e) { dag = null; $('dag-live').hidden = true; $('dag').hidden = false; size(); } + dag = IgneumDag.mount($('dag-live'), { compact: true, window: 60, poll: false, mine: mineFn, tooltip: $('dag-tip'), chip: $('dag-chip'), pauseButton: $('dag-pause'), + onSelect: function (b, why) { if (b && why !== 'update') openFull(b.hash); }, + onState: function (st, why) { if (dagMode === 'failed') return; if (st === 'live' || st === 'stale') { setText('dag-state', st === 'live' ? 'live' : 'behind'); setText('dag-note', st === 'stale' ? (why || '') : (lastLive ? View.liveNote(lastLive) : '')); } } }); + pollLive(); + } catch (e) { dag = null; dagFallback(true, 'The chain view could not start.'); } + } + function pollLive() { + clearTimeout(liveTimer); liveTimer = null; + if (phase !== 'dashboard' || hiddenNow() || !dag) return; + if (!liveBusy) { + liveBusy = true; + fetch('api/live?window=120', { cache: 'no-store' }).then(function (r) { return r.json(); }).then(function (d) { + liveBusy = false; + if (!d || d.ok === false) { liveFails++; if (liveFails >= 3) dagFallback(true, d && d.error ? 'The chain feed says: ' + d.error + '. This is the last 10 minutes of your own blocks.' : ''); return; } + liveFails = 0; lastLive = d; + if (dagMode === 'failed') dagFallback(false); + if (dag) dag.push(d); if (dagFull) dagFull.push(d); + if (dagMode !== 'failed') setText('dag-note', View.liveNote(d)); + }).catch(function () { liveBusy = false; liveFails++; if (liveFails >= 3) dagFallback(true); }); + } + liveTimer = setTimeout(pollLive, 2000); + } + function openFull(hash) { + if (!fullOpen) { fullOpen = true; $('chain-full').hidden = false; $('dag-inspect').setAttribute('aria-expanded', 'true'); setText('dag-inspect', 'Close'); mountFull(); } + if (hash && dagFull) dagFull.select(hash, 'card'); + } + function closeFull() { fullOpen = false; $('chain-full').hidden = true; $('dag-inspect').setAttribute('aria-expanded', 'false'); setText('dag-inspect', 'Inspect'); if (dagFull) { dagFull.destroy(); dagFull = null; } if (proof) { proof.destroy(); proof = null; } uninspect(); } + function mountFull() { + if (dagFull || typeof IgneumDag === 'undefined') return; + if (typeof IgneumProof !== 'undefined' && !proof) proof = IgneumProof.mount($('proof')); + dagFull = IgneumDag.mount($('dag-full'), { poll: false, window: 90, mine: mineFn, tooltip: $('dag-tip-full'), chip: $('dag-chip-full'), pauseButton: $('dag-pause-full'), + onWindow: function (w) { setText('z-pct', w + ' s'); }, + onSelect: function (b) { if (b) inspect(b); else uninspect(); }, + onPause: function (p) { $('dag-follow').classList.toggle('on', !p); if (proof) proof.setPaused(p || dagFull.getState().state !== 'live'); }, + onFollow: function (f) { $('dag-follow').classList.toggle('on', f); }, + onState: function (st) { if (proof && dagFull) proof.setPaused(st !== 'live' || dagFull.getStats().paused); } }); + if (lastLive) dagFull.push(lastLive); + } + $('dag-inspect').addEventListener('click', function () { if (fullOpen) closeFull(); else openFull(); }); + $('dag-filter').addEventListener('click', function (e) { var b = e.target.closest('.seg-b'); if (!b) return; document.querySelectorAll('#dag-filter .seg-b').forEach(function (x) { var on = x === b; x.classList.toggle('on', on); x.setAttribute('aria-checked', on ? 'true' : 'false'); }); if (dagFull) dagFull.setFilter(b.dataset.filter); }); + $('dag-follow').addEventListener('click', function () { if (dagFull) { dagFull.setPaused(false); dagFull.follow(); } }); + $('z-in').addEventListener('click', function () { if (dagFull) dagFull.setWindow(dagFull.getWindow() - 15); }); + $('z-out').addEventListener('click', function () { if (dagFull) dagFull.setWindow(dagFull.getWindow() + 15); }); + $('insp-close').addEventListener('click', function () { if (dagFull) dagFull.select(null, 'clear'); uninspect(); }); + // the inspector: the site's /live words, from the module's selected record (kept fresh through onSelect(b, 'update')) + function fmtN(v) { return v === null || v === undefined ? 'pending' : withCommas(v); } + function hmsUtc(ms) { var d = new Date(ms); return String(d.getUTCHours()).padStart(2, '0') + ':' + String(d.getUTCMinutes()).padStart(2, '0') + ':' + String(d.getUTCSeconds()).padStart(2, '0'); } + function inspect(b) { + $('insp-close').hidden = false; + var mineB = mineFn(b); + setText('i-kind', mineB ? 'your block' : b.locked ? 'checkpoint' : b.chain ? 'chain block' : b.color === 'red' ? 'excluded' : 'side block'); + setText('i-title', b.locked ? 'Checkpoint locked' : b.proven ? 'Proof complete' : (b.shards || []).some(function (x) { return x.state === 'proving'; }) ? 'Proof in progress' : 'Block observed'); + setText('i-hash', b.hash + (b.number ? ' · #' + fmtN(b.number) : '')); + var sh = b.shards || [], done = sh.filter(function (x) { return x.state === 'verified' || x.state === 'paid'; }).length; + setText('i-shard-count', sh.length ? done + ' / ' + sh.length + ' complete' : 'no shard plan yet'); + $('i-track').innerHTML = sh.map(function (x) { return ''; }).join(''); + $('i-shards').innerHTML = sh.length ? sh.map(function (x, i) { return '
  • Shard ' + String(i + 1).padStart(2, '0') + '' + esc(x.state.charAt(0).toUpperCase() + x.state.slice(1)) + '
  • '; }).join('') : '
  • ' + (b.chain ? 'No shard plan yet.' : 'Not a chain block: no shards.') + '
  • '; + setText('i-incl', (b.color === 'blue' ? 'Included' : b.color === 'red' ? 'Excluded' : 'Pending') + (b.chain ? ' / selected chain' : '')); + setText('i-miner', (b.miner === 'you' ? 'you' : b.miner) + (mineB && b.miner !== 'you' ? ' (you)' : '')); + setText('i-blue', fmtN(b.blue_score)); setText('i-daa', fmtN(b.daa)); + setText('i-parents', (b.parents || []).length + ' observed' + ((b.parents || []).length ? ': ' + b.parents.map(function (h) { return View.shortHex(h, 8, 6); }).join(', ') : '')); + setText('i-time', b.ts ? hmsUtc(b.ts) + ' UTC' : 'not set'); + var fn = lastLive && lastLive.finality, cp = null; if (fn && fn.checkpoints) fn.checkpoints.forEach(function (c) { if (b.locked && c.hash === b.hash) cp = c; }); + setText('i-cp', cp ? 'this block, #' + fmtN(cp.index) + (cp.fraction_total ? ', ' + Math.round(cp.fraction_total * 100) + '% of weight' : '') : 'not a checkpoint'); + setText('i-lock', b.final ? 'Final (reported)' : 'Not marked final'); + if (proof) proof.setBlock(b); + } + function uninspect() { + $('insp-close').hidden = true; setText('i-kind', 'select a block'); setText('i-title', 'Select a block'); setText('i-hash', 'Click the graph.'); setText('i-shard-count', 'not set'); $('i-track').innerHTML = ''; $('i-shards').innerHTML = '
  • No block selected.
  • '; + ['i-incl', 'i-miner', 'i-blue', 'i-daa', 'i-parents', 'i-time', 'i-cp', 'i-lock'].forEach(function (id) { setText(id, 'not set'); }); + if (proof) proof.setBlock(null); + } + // ---------- miner-ui-5: the miner's first month ---------- + // the chain facts (GET api/ladder, src/chainfacts.rs) every 15 s while the dashboard shows; `chain` feeds the + // Poisson line, the block card, Earnings and the ladder; every number names its field on hover (View.FIELDS) + var chain = null, chainAt = 0, chainBusy = false, miningSince = 0, cardDismissed = stored('igneum.card.dismissed') || '', cardSaving = false, chainSig = ''; + function pollChain(force) { + if (phase !== 'dashboard' || hiddenNow() || chainBusy) return; + if (!force && Date.now() - chainAt < 15000) return; + chainBusy = true; + api('api/ladder').then(function (r) { chain = r; chainAt = Date.now(); chainBusy = false; var sig = JSON.stringify(r); if (sig !== chainSig) { chainSig = sig; if (state) renderPage(state); } }).catch(function () { chainBusy = false; chainAt = Date.now(); }); + } + // £ per IGN, typed on this machine (Earnings), never fetched + function ignPrice() { var p = parseFloat(stored('igneum.ign_price') || ''); return p > 0 ? p : null; } + function saveIgnPrice() { + var p = parseFloat($('e-price-input').value); + if (!(p >= 0)) { toast('Type a price in pounds per IGN, for example 0.05'); return; } + store('igneum.ign_price', p > 0 ? p : null); toast(p > 0 ? 'Your price: £' + p + ' per IGN. It stays on this machine' : 'Price cleared'); + if (state) renderPage(state); + } + $('e-price-save').addEventListener('click', saveIgnPrice); + $('e-price-input').addEventListener('keydown', function (e) { if (e.key === 'Enter') saveIgnPrice(); }); + $('ls-more').addEventListener('click', function () { showPage('earnings'); }); + disclosure('tl-toggle', 'tl-details'); + $('s-profile').addEventListener('change', function () { var on = this.checked; api('api/settings', { profile_public: on }).then(function (r) { if (r.ok) toast(on ? 'Your page is public from the next site read' : 'Your page is unlisted again'); else { toast(r.error || 'could not change'); $('s-profile').checked = !on; } }).catch(function () { toast('This engine has no profile switch yet'); $('s-profile').checked = !on; }); }); + // the block card's buttons + $('bc-close').addEventListener('click', function () { cardDismissed = $('block-card').dataset.key || ''; store('igneum.card.dismissed', cardDismissed); $('block-card').hidden = true; }); + $('bc-open').addEventListener('click', function () { var u = $('block-card').dataset.url; if (u) api('api/open', { url: u }); else toast('The hash is not known yet'); }); + $('bc-copy').addEventListener('click', function () { var u = $('block-card').dataset.url; if (u) copyText(u, 'Link copied'); else toast('The hash is not known yet'); }); + $('bc-save').addEventListener('click', function () { + if (!state || cardSaving) return; + var m = View.blockCard(state.ladder.milestone, state, chain, state.now); if (!m) return; + cardSaving = true; setText('bc-saved', 'drawing'); + drawCard($('bc-canvas'), m, state).then(function (png) { + return api('api/card', { name: 'block-' + (m.count || 0), png: png }).then(function (r) { cardSaving = false; if (r.ok) { setText('bc-saved', 'saved: ' + r.path); toast('Card saved. The folder is open'); } else { setText('bc-saved', ''); toast(r.error || 'could not save'); } }); + }).catch(function (e) { cardSaving = false; setText('bc-saved', ''); toast('Could not draw the card: ' + (e && e.message || e)); }); + }); + // the PNG, 1200x630, drawn from the card's own words with the local fonts: no network call. The miner's object, + // never the project's number: the card, the rate, the IGN, the rank and the day, the hash and the link. + function drawCard(cv, m, s) { + var loads = ['900 72px Unbounded', '700 28px Unbounded', '500 30px "IBM Plex Sans"', '400 22px "IBM Plex Mono"', '500 18px "IBM Plex Mono"'].map(function (f) { return document.fonts && document.fonts.load ? document.fonts.load(f).catch(function () { }) : Promise.resolve(); }); + return Promise.all(loads).then(function () { + var W = 1200, H = 630, x = cv.getContext('2d'); + cv.width = W; cv.height = H; + x.fillStyle = '#0C0C0E'; x.fillRect(0, 0, W, H); + var g = x.createRadialGradient(W - 140, H + 60, 20, W - 140, H + 60, 620); g.addColorStop(0, 'rgba(255,179,92,.22)'); g.addColorStop(1, 'rgba(255,179,92,0)'); x.fillStyle = g; x.fillRect(0, 0, W, H); + x.fillStyle = '#F2541B'; x.fillRect(0, 0, 14, H); + // the mark (ui/mark.svg's two polygons) at 64 px + var pts = [[50, 4], [74, 34], [67, 58], [80, 54], [61, 96], [39, 96], [20, 54], [33, 58], [26, 34]], inner = [[50, 42], [59, 58], [50, 82], [41, 58]], ox = 72, oy = 56, k = 0.64; + x.fillStyle = '#F2541B'; x.beginPath(); pts.forEach(function (p, i) { x[i ? 'lineTo' : 'moveTo'](ox + p[0] * k, oy + p[1] * k); }); x.closePath(); x.fill(); + x.fillStyle = '#0C0C0E'; x.beginPath(); inner.forEach(function (p, i) { x[i ? 'lineTo' : 'moveTo'](ox + p[0] * k, oy + p[1] * k); }); x.closePath(); x.fill(); + x.fillStyle = '#F4F1EC'; x.font = '900 28px Unbounded'; x.textBaseline = 'alphabetic'; x.fillText('IGNEUM', 152, 100); + x.fillStyle = '#9A9A9E'; x.font = '500 18px "IBM Plex Mono"'; x.textAlign = 'right'; x.fillText(whenWords(m.when) + ' · Igneum Miner ' + (s.version || ''), W - 56, 100); x.textAlign = 'left'; + x.fillStyle = '#F2541B'; x.font = '500 20px "IBM Plex Mono"'; x.fillText(m.eyebrow.toUpperCase().split('').join(' '), 72, 196); + x.fillStyle = '#F4F1EC'; x.font = '900 72px Unbounded'; + var t = m.title.replace(/\.$/, ''), parts = t.split(' is yours'); + if (x.measureText(t).width <= W - 144) x.fillText(t, 72, 286); + else { x.fillText(parts[0], 72, 266); x.fillStyle = '#FFB35C'; x.fillText('is yours', 72, 346); } + var y = parts.length > 1 && x.measureText(t).width > W - 144 ? 420 : 370; + x.fillStyle = '#C9C7C2'; x.font = '500 30px "IBM Plex Sans"'; x.fillText(m.cardLine.split(' · ').slice(0, 2).join(' · '), 72, y); + x.fillStyle = '#FFB35C'; x.fillText(m.ignLine, 72, y + 50); + x.fillStyle = '#9A9A9E'; x.font = '500 26px "IBM Plex Sans"'; x.fillText(m.rankLine, 72, y + 98); + x.fillStyle = '#9A9A9E'; x.font = '400 22px "IBM Plex Mono"'; + var ids = ((s.mining || {}).cards || []).filter(function (c) { return c.key === (s.ladder.milestone || {}).card; }).map(function (c) { return (c.ids || [])[0]; }).filter(Boolean); + x.fillText((ids.length ? 'Miner key ' + ids[0] + ' · ' : '') + 'hash ' + (m.hash ? m.hashShort : 'pending'), 72, 560); + x.fillStyle = '#F4F1EC'; x.font = '400 20px "IBM Plex Mono"'; x.fillText(m.url ? m.url.replace(/^https:\/\//, '') : 'igneum.network/explorer', 72, 598); + return cv.toDataURL('image/png'); + }); + } + function whenWords(t) { if (!(t > 0)) return ''; var d = new Date(t * 1000); return d.getDate() + ' ' + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][d.getMonth()] + ' ' + d.getFullYear() + ' ' + clock(t); } + var rungsSig = '', stripSig = ''; + function rungHtml(r) { + return '
  • ' + esc(r.name) + '' + esc(r.line) + '' + (r.sub ? '' + esc(r.sub) + '' : '') + (r.progress >= 0 && r.state !== 'done' ? '' : '') + '
  • '; + } + // the Overview's first-month surfaces: the count-up or the block card, then the ladder strip + function renderFirstMonth(s) { + var m = s.mining, now = s.now, ld = s.ladder || {}; + if (m.state === 'mining' && m.hash_total > 0) { if (!miningSince) miningSince = now; } else miningSince = 0; + var c = View.chainNums(chain), p = View.poisson(m.hash_total, c.hps, miningSince ? now - miningSince : 0, c.bps, (m.cards || []).filter(function (x) { return x.state === 'mining'; }).length > 1); + var waiting = !(m.accepted_total > 0) && m.state === 'mining'; + setHidden('first-wait', !waiting); + if (waiting) { + if (p) { setText('first-wait-line', p.line); setText('first-wait-sub', p.pool || 'The chance counts up from the first job. The line gives way to the block card.'); setText('first-wait-pct', p.chanceText); $('first-wait-arc').style.strokeDashoffset = (97.4 * (1 - p.chance)).toFixed(1); } + else { setText('first-wait-line', 'Waiting for your first block.'); setText('first-wait-sub', m.hash_total > 0 ? (c.error || 'The expected gap needs the network rate from igneum.network/api/stats.') : 'The cards are starting.'); setText('first-wait-pct', ''); $('first-wait-arc').style.strokeDashoffset = '97.4'; } + $('first-wait').title = View.FIELDS.share + ' · ' + View.FIELDS.hashrate + ' · gap = 1 / (share × blocks per second); chance = 1 − e^(−elapsed / gap)'; + } + var bc = View.blockCard(ld.milestone, s, chain, now), card = $('block-card'); + var show = !!bc && bc.key !== cardDismissed; + setHidden('block-card', !show); + if (show) { + card.dataset.key = bc.key; card.dataset.url = bc.url; + setText('bc-eyebrow', bc.eyebrow); setText('bc-when', bc.when > 0 ? clock(bc.when) : ''); setText('bc-title', bc.title); + setText('bc-card', bc.cardLine); $('bc-card').title = bc.fields.card; + setText('bc-ign', bc.ignLine); $('bc-ign').title = bc.fields.ign; + setText('bc-rank', bc.rankLine); $('bc-rank').title = bc.fields.rank; + setText('bc-hash', 'hash ' + bc.hashShort); $('bc-hash').title = bc.hash; + $('bc-open').disabled = !bc.url; $('bc-copy').disabled = !bc.url; + } + var rungs = View.ladderRungs(s, chain, now), cur = View.currentRung(rungs), hasCards = (m.cards || []).length > 0; + setHidden('ladder-strip', !hasCards); + var sig = JSON.stringify([rungs.map(function (r) { return r.state; }), cur.id, cur.line, cur.sub]); + if (sig !== stripSig) { + stripSig = sig; + $('ls-rungs').innerHTML = rungs.map(function (r) { return '' + esc(r.name) + ''; }).join(''); + setText('ls-line', cur.line); setText('ls-sub', cur.sub); $('ls-line').title = cur.field; + } + } + function renderLadderCard(s) { + var rungs = View.ladderRungs(s, chain, s.now), c = View.chainNums(chain); + setText('ladder-source', c.source === 'node' ? 'from your node' : c.source === 'observer' ? 'through the observer' : (c.error ? 'not read' : 'reading')); + $('ladder-source').title = c.source === 'observer' ? 'getFinalityWeights relayed by igneum.network/api/live until this node answers igneum_getFinalityWeights; keys cut to 64' : c.source === 'node' ? 'igneum_getFinalityWeights on this machine’s node' : (c.error || ''); + var sig = JSON.stringify(rungs); + if (sig !== rungsSig) { rungsSig = sig; $('rungs').innerHTML = rungs.map(rungHtml).join(''); } + var tl = View.timeline(s, s.now), done = tl.steps.filter(function (x) { return x.at > 0; }).length; + setText('tl-sum', done + ' of ' + tl.steps.length + (tl.steps[4].rel ? ' · first block ' + tl.steps[4].rel : '')); + $('tl-steps').innerHTML = tl.steps.map(function (x) { return '
  • ' + esc(x.name) + '' + esc(x.at > 0 ? (x.rel || clock(x.at)) : '') + '' + (x.note ? '' + esc(x.note) + '' : '') + '
  • '; }).join(''); + $('tl-steps').title = tl.field; + setText('tl-note', 'Times from install (' + (tl.base > 0 ? whenWords(tl.base) : 'not recorded') + '). The site’s /miner page shows the same steps measured on fresh installs.'); } // ---------- render ---------- function badgeHtml(cd) { @@ -1471,11 +1830,13 @@ if (typeof document !== 'undefined') (function () { // ---------- Mine ---------- var mineRowsSig = '', lastEventsKey = ''; - function numCell(cls, v, unit, k, tone) { return '
    ' + esc(v) + (unit ? '' + unit + '' : '') + '' + (k ? '' + esc(k) + '' : '') + '
    '; } + function numCell(cls, v, unit, k, tone, title) { return '
    ' + esc(v) + (unit ? '' + unit + '' : '') + '' + (k ? '' + esc(k) + '' : '') + '
    '; } function gpuLineHtml(cd, s, r) { var cells = View.cells(cd, priceNow()), tw = View.tuneWords(cd, s, s.now), word = View.rowWord(r, s.mining); var nums = ''; if (r.hash) nums += numCell('hash', r.hash, 'MH/s', '', ''); + var ignd = r.hash ? View.cardIgnDay(cd, chain) : ''; + if (ignd) nums += numCell('ign', ignd, 'IGN', 'a day', '', View.FIELDS.share + ' \u00d7 86,400 \u00d7 ' + View.FIELDS.reward); if (cells.power) nums += numCell('power', cells.power.replace(' W', ''), 'W', cells.eff, ''); if (cells.money) nums += numCell('money', cells.money, '', 'a day', ''); else if (cells.needsPrice) nums += '
    £ a day
    '; @@ -1580,7 +1941,7 @@ if (typeof document !== 'undefined') (function () { var tunables = presentCards.filter(function (c) { return View.tunable(c) && c.enabled; }); setHidden('btn-tune-all', !(tunables.length > 0) || !!(s.settings && s.settings.tuning_off) || !!View.tuningCard(presentCards)); var box = $('d-cards'), busy = box.querySelector('input[type=range]:active') || (box.contains(document.activeElement) && document.activeElement.tagName === 'INPUT'); - var sig = JSON.stringify([rows, shown.map(function (c) { return [View.tuneWords(c, s, s.now), View.cells(c, price), c.identities, c.power_pct, c.pinned, c.power_applied, Math.round(c.temp_mem || 0), Math.round(c.gclk_mhz || 0), Math.round(c.mclk_mhz || 0), (c.tune_curve || []).length, c.tune_goal || '', c.sweep_watts, c.sweep_mhs]; }), goalNow()]); + var sig = JSON.stringify([rows, shown.map(function (c) { return [View.tuneWords(c, s, s.now), View.cells(c, price), c.identities, c.power_pct, c.pinned, c.power_applied, Math.round(c.temp_mem || 0), Math.round(c.gclk_mhz || 0), Math.round(c.mclk_mhz || 0), (c.tune_curve || []).length, c.tune_goal || '', c.sweep_watts, c.sweep_mhs, View.cardIgnDay(c, chain)]; }), goalNow()]); if (sig !== mineRowsSig && !busy) { mineRowsSig = sig; box.innerHTML = rows.length ? shown.map(function (cd, i) { return gpuLineHtml(cd, s, rows[i]); }).join('') : '
    ' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can use was found. The node runs on its own.') + '
    '; @@ -1590,6 +1951,7 @@ if (typeof document !== 'undefined') (function () { $('d-cards-note').hidden = !note; setText('d-cards-note', note); renderTuneCard(s); renderNode(s); + if (page === 'overview') renderFirstMonth(s); // activity: the counts, the strip, the newest 8 events in a user's words var nowU = s.now, f10 = m.found.filter(function (x) { return nowU - x <= 600; }).length; setText('d-stats', f10 + ' block' + (f10 === 1 ? '' : 's') + ' in 10 min · ' + m.found.length + ' in the last hour' + (m.fee_total ? ' · ' + withCommas(m.fee_total) + ' to the dev fee' : '')); @@ -1648,11 +2010,21 @@ if (typeof document !== 'undefined') (function () { function renderEarnings(s) { var a = s.address || {}, m = s.mining, pv = s.proving || {}, price = priceNow(); var ign = pv.paid_wei && Number(pv.paid_wei) ? (Number(pv.paid_wei) / 1e18).toFixed(4) : '0.0000'; - setText('e-ign', ign + ' IGN'); setText('e-ign-sub', 'from ' + (pv.paid || 0) + ' proof' + (pv.paid === 1 ? '' : 's') + (pv.segments_paid ? ' and ' + pv.segments_paid + ' segment' + (pv.segments_paid === 1 ? '' : 's') : '')); - setText('e-blocks', withCommas(m.accepted_total) + ' block' + (m.accepted_total === 1 ? '' : 's')); setText('e-blocks-sub', 'lifetime · ' + withCommas(m.accepted_session) + ' this run, ' + uptime(s.uptime_s)); + setText('e-ign-sub', ign + ' IGN from ' + (pv.paid || 0) + ' proof' + (pv.paid === 1 ? '' : 's') + (pv.segments_paid ? ' and ' + pv.segments_paid + ' segment' + (pv.segments_paid === 1 ? '' : 's') : '')); $('e-ign-sub').title = View.FIELDS.shards; + // miner-ui-5: IGN first (reinvent 3.4); every line names its field on hover + var el = View.earningsLines(s, chain, ignPrice(), price, s.now); + setText('e-day', el.day.text); setText('e-day-sub', el.day.sub); $('e-day').title = el.day.field; + setText('e-window', el.window.text); setText('e-window-sub', el.window.sub); $('e-window').title = el.window.field; + setText('e-price', el.price.text); setText('e-price-sub', el.price.sub); $('e-price').title = el.price.field; + if (document.activeElement !== $('e-price-input')) { var ip = ignPrice(); $('e-price-input').value = ip ? String(ip) : ''; } + setText('e-share', el.share.text); setText('e-share-sub', el.share.sub); $('e-share').title = el.share.field; + setText('e-kwh', el.perKwh ? el.perKwh.text : ''); setHidden('e-kwh', !el.perKwh); $('e-kwh').title = el.perKwh ? el.perKwh.sub + ' \u00b7 ' + el.perKwh.field : ''; + renderLadderCard(s); + setText('e-blocks', withCommas(m.accepted_total) + ' block' + (m.accepted_total === 1 ? '' : 's')); setText('e-blocks-sub', 'lifetime · ' + withCommas(m.accepted_session) + ' this run, ' + uptime(s.uptime_s)); $('e-blocks').title = View.FIELDS.found; var watts = View.fleetWatts(m, m.cards), ppd = View.fleetPounds(m, m.cards, price); var bw = View.balanceWords(a, s.now); - if (bw.text) { setText('e-earned', bw.pounds !== null ? money(bw.pounds) : bw.text); setText('e-earned-sub', bw.pounds !== null ? 'at the signed price · ' + bw.text + ' ' + bw.sub : (bw.text === 'not read yet' ? 'balance ' + bw.sub : bw.sub + '. Nothing is bought or sold on devnet')); } + if (bw.text) { setText('e-earned', bw.pounds !== null ? money(bw.pounds) : bw.text); setText('e-earned-sub', bw.pounds !== null ? 'at the signed price · ' + bw.text + ' ' + bw.sub : (bw.text === 'not read yet' ? 'balance ' + bw.sub : bw.sub + '. Nothing is bought or sold on devnet')); $('e-earned').title = 'address.balance_wei: the node\u2019s eth_getBalance of the rewards address'; } + else { setText('e-earned', 'balance: not on this engine'); setText('e-earned-sub', 'the wallet shows it'); } setText('e-cost', ppd === null ? (watts > 0 ? Math.round(watts) + ' W' : 'no draw') : money(ppd) + ' a day'); setText('e-cost-sub', ppd === null ? (watts > 0 ? 'electricity: set a price to see the cost' : 'electricity: no card reports its draw') : 'electricity at ' + price + 'p/kWh for ' + Math.round(watts) + ' W'); setHidden('e-price-btn', !(watts > 0 && ppd === null)); @@ -1672,6 +2044,10 @@ if (typeof document !== 'undefined') (function () { var tiersSig = ''; function renderProve(s) { var pv = s.proving || {}, enabled = !!(s.settings && s.settings.prove); + // miner-ui-5: "your card proved shard N of block M" from the newest paid record (ladder.shards) + var sh = ((s.ladder || {}).shards || [])[0]; + setHidden('shard-card', !sh); + if (sh) { setText('sc-when', clock(sh.at)); setText('sc-title', 'Your card proved shard ' + sh.shard + ' of block ' + withCommas(sh.block) + '.'); setText('sc-line', View.fmtIgn(View.ign(sh.wei)) + ' paid for it \u00b7 ' + withCommas(pv.paid || 0) + ' shard' + (pv.paid === 1 ? '' : 's') + ' paid on this machine'); setText('sc-sub', '20 points of every block pay the provers, split by shard. A paid record is a chain fact: anyone can read it.'); $('sc-title').title = View.FIELDS.shards; $('sc-line').title = 'proving.paid_wei, proving.paid'; } if (document.activeElement !== $('s-prove')) $('s-prove').checked = enabled; var w = View.proveWords(pv, enabled, s.node.synced); var cards = shownCards(s.mining.cards).filter(View.present), tiers = cards.map(View.proveTier); @@ -1694,7 +2070,8 @@ if (typeof document !== 'undefined') (function () { var jobsSig = ''; function renderSettings(s) { var st = s.settings || {}, sw = function (id, on) { if (document.activeElement !== $(id)) $(id).checked = !!on; }; - sw('s-vote', st.vote); sw('s-login', st.start_at_login); sw('s-jobs-allow', s.jobs && s.jobs.allowed); sw('s-sweep', st.sweep); sw('s-trust', st.proof_verify_trust); sw('s-power-control', st.power_control); sw('s-climb', st.tune_climb); + sw('s-vote', st.vote); sw('s-login', st.start_at_login); sw('s-jobs-allow', s.jobs && s.jobs.allowed); sw('s-sweep', st.sweep); sw('s-trust', st.proof_verify_trust); sw('s-power-control', st.power_control); sw('s-climb', st.tune_climb); sw('s-profile', st.profile_public); + setText('s-profile-text', View.profileWords(!!st.profile_public)); setHidden('s-climb-row', st.tune_climb === undefined); setText('s-power-note', st.tuning_off ? ' ' + (st.tuning_note || '') : (st.power_note ? ' ' + st.power_note : '')); if (document.activeElement !== $('s-price')) { var p = priceNow(); $('s-price').value = p > 0 ? String(p) : ''; } @@ -1939,6 +2316,7 @@ if (typeof document !== 'undefined') (function () { if (params.get('logs') === '1' && start === 'dashboard') setDrawer(true); // screenshots: the log drawer open } render(s); + pollChain(false); perf.render += performance.now() - a; perf.polls++; if (failures === 0 && $('pill').classList.contains('away')) { $('pill').classList.remove('away', 'warn'); } }).catch(function () { diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index 5bd14791..d44a3b59 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -185,15 +185,73 @@ + + + + +
    -

    The chain, live

    connecting
    +

    The chain, live

    connecting
    +
    - +
    -
    yourspendingincludedexcludedprovenlocked checkpoint
    +
    Your blockspendingIncludedExcludedProvenLocked checkpoint
    + +
    @@ -265,16 +323,35 @@