diff --git a/app/igneum-app/ui/app.css b/app/igneum-app/ui/app.css index cd608c44..62b7c4fa 100644 --- a/app/igneum-app/ui/app.css +++ b/app/igneum-app/ui/app.css @@ -374,15 +374,21 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)} .clock-msg{font-size:var(--t-md);color:var(--bone);line-height:1.45} .clock-card .note{margin-top:0} -/* earnings: money first */ -.money{display:flex;flex-direction:column;margin-bottom:var(--s-2)} -.money-row{display:flex;align-items:baseline;gap:var(--s-3);padding:8px 0;border-bottom:1px solid var(--line);flex-wrap:wrap} -.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} +/* earnings (earnings-tidy, 7 October 2026): the day rate first in ember with its reason line, the run line, then a quiet + row of three (weight, electricity, lifetime) in the totals' idiom, the dev-fee switch last */ +.earn{display:flex;flex-direction:column;margin-bottom:var(--s-3)} +.earn-head{display:flex;flex-direction:column;gap:4px;padding-bottom:var(--s-3);border-bottom:1px solid var(--line)} +.earn-head .v{font-family:var(--head);font-weight:700;font-size:var(--t-hero);line-height:1.1;letter-spacing:-.01em;color:var(--ember);text-wrap:balance} +.earn-head .s,.earn-run .s{font-size:var(--t-base);color:var(--ash);line-height:1.45;min-width:0} +.earn-run{display:flex;align-items:baseline;gap:var(--s-3);flex-wrap:wrap;padding:var(--s-3) 0;border-bottom:1px solid var(--line)} +.earn-run .v{font-family:var(--head);font-weight:700;font-size:var(--t-num);line-height:1.15;white-space:nowrap} +.earn-three{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-4);padding-top:var(--s-3)} +.earn-cell{display:flex;flex-direction:column;gap:3px;min-width:0;padding-left:var(--s-4);border-left:1px solid var(--line)} +.earn-cell:first-child{padding-left:0;border-left:0} +.earn-cell .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.14em;text-transform:uppercase;color:var(--ash)} +.earn-cell .v{font-family:var(--head);font-weight:700;font-size:var(--t-lg);line-height:1.2;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.earn-cell .s{font-size:var(--t-sm);color:var(--ash);line-height:1.4} +@media (max-width:720px){.earn-head .v{font-size:var(--t-h2)}.earn-three{grid-template-columns:1fr;gap:var(--s-2)}.earn-cell{padding-left:0;border-left:0;border-top:1px solid var(--line);padding-top:var(--s-2)}.earn-cell:first-child{border-top:0;padding-top:0}} /* prove */ .tier-list{list-style:none;margin:var(--s-3) 0 0;padding:0;display:flex;flex-direction:column;gap:6px;font-size:var(--t-md);color:var(--ink-2)} @@ -415,9 +421,6 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)} .heat-line.rest,.line-text.rest{color:var(--ash)} .field.grow{flex:1 1 260px;min-width:0} .gl-tune .n.heat{color:var(--molten)} -.money-row.rent .v{font-size:var(--t-num)} -.money-row.rent.under .v{color:var(--ember)} -.money-row.rent.ask .v{color:var(--ash)} .option:not(.on) .addr-input{display:none} .err{font-size:var(--t-base);color:var(--ember)} @@ -584,7 +587,6 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} h1{font-size:40px} .totals{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:var(--s-3)} .tot:nth-child(3){padding-left:0;border-left:0} - .money-row .v{min-width:0} } @media (max-width:720px){ :root{--rail:0px;--gutter:var(--s-4);--bottom:64px} @@ -625,7 +627,6 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} .disclose-right .dim{display:none} .lead-row{flex-direction:column} .step{padding:32px 0} - .money-row .v{font-size:var(--t-num)} .drawer-head{padding-left:var(--s-4);padding-right:var(--s-4)} .notice{padding-left:var(--s-4);padding-right:var(--s-2);gap:var(--s-2)} .notice-text{flex-basis:160px} @@ -778,8 +779,6 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} .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}} diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 661954fc..4407ff8a 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -988,7 +988,6 @@ var View = (function () { 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' }; @@ -1082,27 +1081,37 @@ var View = (function () { } // 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) { + function uptimeWords(sec) { sec = sec || 0; var h = Math.floor(sec / 3600), mi = Math.floor(sec % 3600 / 60); return h ? h + ' h ' + mi + ' min' : mi + ' min'; } + // the Earnings card (earnings-tidy, 7 October 2026): the day rate with its reason, the run line, then weight, + // electricity and lifetime, and the dev-fee sentence. IGN only: no typed price, no money but the electricity cell. + function earningsLines(s, chain, pence, now, minor) { 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 hour = (m.found || []).length, mhs = m.hash_total || 0, rate = mhs >= 100 ? mhs.toFixed(0) : mhs.toFixed(1); 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 }; + if (measured !== null && hour > 0) out.day = { text: fmtIgn(measured) + ' a day', sub: 'at your last hour’s rate: ' + hour + ' block' + (hour === 1 ? '' : 's') + ' at ' + fmtIgn(c.minerIgn) + ' each' + (expected !== null ? ' · about ' + fmtIgn(expected) + ' expected at ' + rate + ' 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 ' + rate + ' 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; + // this run: blocks, their IGN at today’s reward, the shards paid, the uptime + var run = m.accepted_session || 0, shards = pv.paid || 0, shardIgn = pv.paid_wei && Number(pv.paid_wei) ? Number(pv.paid_wei) / 1e18 : 0, runParts = []; + if (c.minerIgn) runParts.push(fmtIgn(run * c.minerIgn) + ' at today’s reward'); + if (shards) runParts.push(shards + ' shard' + (shards === 1 ? '' : 's') + (shardIgn ? ', ' + fmtIgn(shardIgn) : '')); + runParts.push(uptimeWords(s.uptime_s)); + out.run = { text: withCommas(run) + ' block' + (run === 1 ? '' : 's') + ' this run', sub: runParts.join(' · '), field: FIELDS.found + ' · ' + FIELDS.shards }; + // weight: the rank in the table, the window, the days, the share + var wblocks = c.best ? c.best.blocks : (ld.blocks_30d || 0), ww = windowWords(c.windowS), d = daysIn(ld, c.windowS, now), share = mhs > 0 && c.hps ? pct(mhs * 1e6 / c.hps) : ''; + var wtext = c.best ? 'rank ' + withCommas(c.best.rank) + ' of ' + withCommas(c.listed) + ' keys' : c.ok ? (c.cap ? 'below the top ' + c.cap + ' keys' : 'not in the weight table yet') : 'reading'; + var wparts = [withCommas(wblocks) + ' block' + (wblocks === 1 ? '' : 's') + ' in ' + ww.text, d.have + ' of ' + d.of + ' ' + d.unit]; if (share) wparts.push(share + ' of the network'); + out.weight = { text: wtext, sub: wparts.join(' · '), field: FIELDS.rank + ' · ' + FIELDS.days + ' · ' + FIELDS.share + (c.source === 'observer' ? ' · through igneum.network’s observer until this node answers igneum_getFinalityWeights' : '') }; + // electricity: £ a day at the Settings price, or the draw; IGN per kWh beside it + var watts = fleetWatts(m, m.cards), kwhDay = watts > 0 ? watts * 24 / 1000 : 0, ppd = fleetPounds(m, m.cards, pence); + var perKwh = dayIgn !== null && kwhDay > 0 ? fmtIgn(dayIgn / kwhDay) + ' per kWh' : ''; + out.cost = ppd === null ? { text: watts > 0 ? Math.round(watts) + ' W' : 'no draw', sub: watts > 0 ? 'set a price in Settings' + (perKwh ? ' · ' + perKwh : '') : 'no card reports its draw', field: FIELDS.watts } + : { text: money(ppd) + ' a day', sub: 'at ' + pence + (minor || 'p') + '/kWh for ' + Math.round(watts) + ' W' + (perKwh ? ' · ' + perKwh : ''), field: FIELDS.watts + ' × 24 × the price in Settings' }; + var life = m.accepted_total || 0; + out.lifetime = { text: withCommas(life) + ' block' + (life === 1 ? '' : 's'), sub: c.minerIgn ? fmtIgn(life * c.minerIgn) + ' at today’s reward' : '', field: FIELDS.found }; + out.devFee = function (on, feeTotal) { return on ? 'Dev fee: 1 block in 100 pays the people who make this app' + (feeTotal ? ' (' + withCommas(feeTotal) + ' block' + (feeTotal === 1 ? '' : 's') + ' so far)' : '') + '.' : 'Dev fee off. Every block pays your address.'; }; return out; } // the per-card IGN a day cell on the Cards page @@ -1464,7 +1473,6 @@ if (typeof document !== 'undefined') (function () { // ---------- Earnings ---------- $('s-devfee').addEventListener('change', function () { api('api/settings', { dev_fee: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied. Mining restarts' : 'Applied'); }); }); - $('e-price-btn').addEventListener('click', function () { showPage('settings'); $('s-price').focus(); }); $('t-money-cell').addEventListener('click', function () { if ($('t-money').classList.contains('ask-price')) { showPage('settings'); $('s-price').focus(); } }); $('s-address-save').addEventListener('click', function () { var a = $('s-address-input').value.trim(); @@ -1918,16 +1926,6 @@ if (typeof document !== 'undefined') (function () { 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; }); }); @@ -2293,32 +2291,16 @@ if (typeof document !== 'undefined') (function () { // ---------- Earnings ---------- 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-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); + var a = s.address || {}, m = s.mining; + var el = View.earningsLines(s, chain, priceNow(), s.now, View.currencyOf(regionNow()).minor); 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 : ''; + setText('e-run', el.run.text); setText('e-run-sub', el.run.sub); $('e-run').title = el.run.field; + setText('e-rung', el.weight.text); setText('e-rung-sub', el.weight.sub); $('e-rung').title = el.weight.field; + setText('e-cost', el.cost.text); setText('e-cost-sub', el.cost.sub); $('e-cost').title = el.cost.field; + setText('e-blocks', el.lifetime.text); setText('e-blocks-sub', el.lifetime.sub); $('e-blocks').title = el.lifetime.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')); $('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 + View.currencyOf(regionNow()).minor + '/kWh for ' + Math.round(watts) + ' W'); - setHidden('e-price-btn', !(watts > 0 && ppd === null)); - var rent = View.rentLine(m.cards, price, regionNow()); - setText('e-rent', rent.text); setText('e-rent-sub', rent.sub); - $('e-rent-row').classList.toggle('under', rent.kind === 'line' && rent.ratio <= 0.95); $('e-rent-row').classList.toggle('ask', rent.kind === 'price'); if (document.activeElement !== $('s-devfee')) $('s-devfee').checked = !!(s.settings && s.settings.dev_fee); - setText('s-devfee-text', s.settings.dev_fee ? 'Dev fee: 1 block in 100 pays the people who make this app' : 'Dev fee off. Every block pays your address'); - setText('r-devfee-line', s.settings.dev_fee ? 'The network itself takes nothing. This is the app’s fee, the same way every mining app takes one' + (m.fee_total ? ' (' + withCommas(m.fee_total) + ' block' + (m.fee_total === 1 ? '' : 's') + ' so far)' : '') + '.' : 'The network itself takes nothing either.'); + setText('s-devfee-text', el.devFee(!!(s.settings && s.settings.dev_fee), m.fee_total)); setText('r-address', a.display || 'not set'); setText('r-source', a.source === 'generated' ? 'made on this machine' : a.source === 'pasted' ? 'your own address' : ''); setText('r-address-help', a.display ? 'Every block this machine finds pays this address.' : 'No address yet. Finish the setup to choose one.'); diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index 3e05e8f9..11753fae 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -350,20 +350,19 @@