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 '
Weight is your blocks over the window, written into consensus. No points server, no badge: the chain computes every rung and your node reports it. Hover a line for the field it reads.
+