Merge remote-tracking branch 'origin/earnings-tidy-21' into release-0.3.21
This commit is contained in:
commit
b75f2e2503
8 changed files with 125 additions and 114 deletions
|
|
@ -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}}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.');
|
||||
|
|
|
|||
|
|
@ -350,20 +350,19 @@
|
|||
<!-- Earnings -->
|
||||
<section class="page" id="page-earnings" data-page="earnings" hidden>
|
||||
<div class="card">
|
||||
<!-- miner-ui-5 (reinvent 3.4): IGN first, the window, a typed price, the share and the rank, electricity with IGN per kWh.
|
||||
Every number names its source on hover (title). -->
|
||||
<div class="money">
|
||||
<div class="money-row"><span class="v" id="e-day">0 IGN a day</span><span class="s" id="e-day-sub">reading the network</span></div>
|
||||
<div class="money-row"><span class="v small" id="e-window">0 blocks in 30 days</span><span class="s" id="e-window-sub"></span></div>
|
||||
<div class="money-row"><span class="v tiny" id="e-earned">not read yet</span><span class="s" id="e-earned-sub">balance: the node answers once it is synced</span></div>
|
||||
<div class="money-row price-row"><span class="v small" id="e-price">Type a price</span><span class="s" id="e-price-sub"></span><span class="row"><span class="mono dim">£</span><input type="number" class="addr-input mono short" id="e-price-input" min="0" step="0.0001" placeholder="0.00" aria-label="Your price for one IGN in pounds"><button class="btn tiny" id="e-price-save">Use it</button></span></div>
|
||||
<div class="money-row"><span class="v tiny" id="e-share">reading</span><span class="s" id="e-share-sub"></span></div>
|
||||
<div class="money-row"><span class="v small" id="e-cost">£0.00 a day</span><span class="s" id="e-cost-sub">electricity</span><span class="s" id="e-kwh"></span><button class="btn tiny ghost" id="e-price-btn">Set the price</button></div>
|
||||
<div class="money-row rent" id="e-rent-row"><span class="v small" id="e-rent">Your cost of hash</span><span class="s" id="e-rent-sub">against the measured rental rate</span></div>
|
||||
<div class="money-row"><span class="v small" id="e-blocks">0 blocks</span><span class="s" id="e-blocks-sub">lifetime</span><span class="s" id="e-ign-sub"></span></div>
|
||||
<!-- earnings-tidy (7 October 2026): the day rate first with its reason, the run line, then a quiet row of three
|
||||
(weight, electricity, lifetime), the dev-fee switch last. IGN only: no typed price anywhere. Every number names
|
||||
its source on hover (title). -->
|
||||
<div class="earn">
|
||||
<div class="earn-head"><span class="v" id="e-day">0 IGN a day</span><span class="s" id="e-day-sub">reading the network</span></div>
|
||||
<div class="earn-run"><span class="v" id="e-run">0 blocks this run</span><span class="s" id="e-run-sub"></span></div>
|
||||
<div class="earn-three">
|
||||
<div class="earn-cell"><span class="k">Weight</span><span class="v" id="e-rung">reading</span><span class="s" id="e-rung-sub"></span></div>
|
||||
<div class="earn-cell"><span class="k">Electricity</span><span class="v" id="e-cost">no draw</span><span class="s" id="e-cost-sub"></span></div>
|
||||
<div class="earn-cell"><span class="k">Lifetime</span><span class="v" id="e-blocks">0 blocks</span><span class="s" id="e-blocks-sub"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="s-devfee"><span class="track"></span><span class="sw-text" id="s-devfee-text">Dev fee: 1 block in 100 pays the people who make this app</span></label>
|
||||
<p class="help" id="r-devfee-line"></p>
|
||||
<label class="switch"><input type="checkbox" id="s-devfee"><span class="track"></span><span class="sw-text" id="s-devfee-text">Dev fee: 1 block in 100 pays the people who make this app.</span></label>
|
||||
</div>
|
||||
|
||||
<div class="card" id="ladder-card">
|
||||
|
|
|
|||
|
|
@ -661,44 +661,11 @@ test('the rungs under mainnet-sized params: 100 blocks to a vote, 30 days to ful
|
|||
assert.deepEqual(V.windowWords(0), { n: 30, unit: 'days', text: '30 days' });
|
||||
});
|
||||
|
||||
test('Earnings in IGN first: measured from the last hour, expected from the share, the typed price, the share and rank, IGN per kWh', () => {
|
||||
// 100 MH/s on 100 GH/s is 86.4 blocks a day, 3.6 an hour: four in the last hour here
|
||||
const found = [T5 - 3400, T5 - 2500, T5 - 1300, T5 - 200];
|
||||
const s = stateOf({ mining: { ...stateOf().mining, accepted_total: 2592, found: found, watts_total: 150 }, proving: { paid: 14, paid_wei: '16100000000000000000' }, ladder: { ...stateOf().ladder, first_block_at: T5 - 23 * 86400, days_mined_30: 23, blocks_30d: 2592 } });
|
||||
const c = chainOf({ source: 'observer', keys_listed: 1204, keys_cap: 64, voters: 1204, mine: [key({ blocks: 2592, voter: true, rank: 41 })], best: key({ blocks: 2592, voter: true, rank: 41 }) }, MAINNET);
|
||||
const e = V.earningsLines(s, c, null, 28, T5);
|
||||
assert.equal(e.day.text, '7,680 IGN a day');
|
||||
assert.equal(e.day.sub, 'at your last hour’s rate (4 blocks, 80.00 IGN each) · about 6,912 IGN expected at 100 MH/s');
|
||||
assert.match(e.day.field, /mining\.found.*block_reward\.miner_ign/);
|
||||
assert.equal(e.window.text, '2,592 blocks in 30 days, 207,360 IGN');
|
||||
assert.equal(e.window.sub, 'with your key in the weight window · at today’s reward · 14 shards, 16.10 IGN');
|
||||
assert.equal(e.price.text, 'Type a price');
|
||||
assert.equal(e.price.set, false);
|
||||
assert.equal(e.share.text, 'your cards are 0.100% of the network · weight rank 41 of 1,204 keys · 23 of 30 days');
|
||||
assert.match(e.share.sub, /through igneum\.network’s observer/);
|
||||
assert.equal(e.perKwh.text, '2,133 IGN per kWh');
|
||||
assert.equal(e.perKwh.sub, 'IGN a day over 3.6 kWh a day at 150 W');
|
||||
// a typed price: £ a day from the IGN line, named as the user's figure
|
||||
const p = V.earningsLines(s, c, 0.05, 28, T5);
|
||||
assert.equal(p.price.text, 'about £384 a day');
|
||||
assert.equal(p.price.sub, 'at £0.05 per IGN, your figure · no exchange lists IGN');
|
||||
assert.match(p.price.field, /never fetched/);
|
||||
// a fresh miner with no block in the hour: the expectation, said as one
|
||||
const f = V.earningsLines(stateOf(), chainOf(), null, 28, T5);
|
||||
assert.equal(f.day.text, 'about 6,912 IGN a day');
|
||||
assert.equal(f.day.sub, 'expected at 100 MH/s, 0.100% of today’s network · no block in the last hour yet');
|
||||
assert.equal(f.window.text, '0 blocks in 2 hours, 0.0000 IGN');
|
||||
// no chain facts: nothing invented
|
||||
const n = V.earningsLines(stateOf(), { ok: false, error: 'neither answered' }, 0.05, 28, T5);
|
||||
assert.equal(n.day.text, 'IGN a day: reading the network');
|
||||
assert.equal(n.day.sub, 'neither answered');
|
||||
assert.equal(n.price.text, 'at £0.05 per IGN');
|
||||
assert.equal(n.perKwh, null);
|
||||
test('the per-card IGN a day cell and the IGN formatter', () => {
|
||||
assert.equal(V.cardIgnDay(card({ hash_now: 25 }), chainOf()), '1,728');
|
||||
assert.equal(V.cardIgnDay(card({ hash_now: 25 }), null), '');
|
||||
assert.equal(V.fmtIgn(0.5), '0.5000 IGN'); assert.equal(V.fmtIgn(4.88044084), '4.88 IGN'); assert.equal(V.fmtIgn(207360), '207,360 IGN');
|
||||
});
|
||||
|
||||
test('the block card on the four thresholds and on a new card; the first-hour timeline; the profile words', () => {
|
||||
const s = stateOf(), c = chainOf({ mine: [key({ blocks: 100, voter: true, rank: 2 })], best: key({ blocks: 100, voter: true, rank: 2 }) });
|
||||
const m = (kind, count, over) => ({ kind, count, card: 'nvidia:0:RTX 4070', card_name: 'NVIDIA GeForce RTX 4070', hash: 'ab'.repeat(32), daa: 1284117, at: T5 - 10, ...over });
|
||||
|
|
@ -872,3 +839,64 @@ test('brand/marks/vendor-marks.mjs carries the app\'s marks and tokens verbatim,
|
|||
// the tokens the module prints are the ones app.css carries
|
||||
for (const line of mod.tokensCss().split('\n')) { const inner = /\{([^{}]*)\}\}?$/.exec(line)[1]; assert.ok(css.includes(inner), 'app.css carries: ' + inner.slice(0, 60)); }
|
||||
});
|
||||
|
||||
// ---------- the Earnings card, tidied (the project lead, 7 October 2026: "remove all the type a price stuff") ----------
|
||||
const indexHtml = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'index.html'), 'utf8');
|
||||
const earningsCard = () => { const a = indexHtml.indexOf('id="page-earnings"'); return indexHtml.slice(a, indexHtml.indexOf('id="ladder-card"', a)); };
|
||||
|
||||
test('no price input exists: no £ per IGN field, no "Use it", no remembered price, no money line derived from a typed price (known-failed on 0.3.20)', () => {
|
||||
const ec = earningsCard();
|
||||
assert.equal(/e-price|Type a price|Use it|price-row|per IGN/.test(ec), false, 'the markup');
|
||||
assert.equal(/<input[^>]*type="number"/.test(ec), false, 'no number input on the card');
|
||||
assert.equal(/ign_price|ignPrice|saveIgnPrice|e-price/.test(src), false, 'the setting and its handlers');
|
||||
const e = V.earningsLines(stateOf(), chainOf(), 28, T5);
|
||||
assert.equal('price' in e, false); assert.equal(V.FIELDS.price, undefined);
|
||||
for (const k of Object.keys(e)) assert.equal(/£|GBP|pounds?\b/.test((e[k] && e[k].text) || ''), k === 'cost', k + ' carries money only on the electricity cell');
|
||||
});
|
||||
|
||||
test('the headline reads the day rate with its reason line, then blocks and IGN this run', () => {
|
||||
const found = [T5 - 3400, T5 - 2500, T5 - 1300, T5 - 200];
|
||||
const s = stateOf({ uptime_s: 5400, mining: { ...stateOf().mining, accepted_total: 2592, accepted_session: 140, found: found, watts_total: 150 }, proving: { paid: 14, paid_wei: '16100000000000000000' }, ladder: { ...stateOf().ladder, first_block_at: T5 - 23 * 86400, days_mined_30: 23, blocks_30d: 2592 } });
|
||||
const c = chainOf({ source: 'observer', keys_listed: 1204, keys_cap: 64, voters: 1204, mine: [key({ blocks: 2592, voter: true, rank: 41 })], best: key({ blocks: 2592, voter: true, rank: 41 }) }, MAINNET);
|
||||
const e = V.earningsLines(s, c, 28, T5);
|
||||
assert.equal(e.day.text, '7,680 IGN a day');
|
||||
assert.equal(e.day.sub, 'at your last hour’s rate: 4 blocks at 80.00 IGN each · about 6,912 IGN expected at 100 MH/s');
|
||||
assert.equal(e.run.text, '140 blocks this run');
|
||||
assert.equal(e.run.sub, '11,200 IGN at today’s reward · 14 shards, 16.10 IGN · 1 h 30 min');
|
||||
const f = V.earningsLines(stateOf(), chainOf(), 28, T5);
|
||||
assert.equal(f.day.text, 'about 6,912 IGN a day');
|
||||
assert.equal(f.day.sub, 'expected at 100 MH/s, 0.100% of today’s network · no block in the last hour yet');
|
||||
const n = V.earningsLines(stateOf(), { ok: false, error: 'neither answered' }, 28, T5);
|
||||
assert.equal(n.day.text, 'IGN a day: reading the network'); assert.equal(n.day.sub, 'neither answered');
|
||||
// the markup: the headline is the first thing on the card, the run line second
|
||||
const ec = earningsCard();
|
||||
assert.ok(ec.indexOf('id="e-day"') < ec.indexOf('id="e-run"') && ec.indexOf('id="e-run"') < ec.indexOf('class="earn-three"'), 'headline, run line, row of three, in that order');
|
||||
assert.ok(ec.indexOf('class="earn-three"') < ec.indexOf('id="s-devfee"'), 'the dev-fee switch is last');
|
||||
assert.equal(/id="r-devfee-line"/.test(ec), false, 'one sentence on the dev fee, no second help line');
|
||||
for (const line of [e.day.sub, e.run.sub, f.day.sub]) assert.equal(/—|–/.test(line), false, 'no dashes: ' + line);
|
||||
});
|
||||
|
||||
test('the row of three renders: weight rung, electricity and lifetime, each with a quiet line', () => {
|
||||
const found = [T5 - 3400, T5 - 2500, T5 - 1300, T5 - 200];
|
||||
const s = stateOf({ mining: { ...stateOf().mining, accepted_total: 2592, accepted_session: 140, found: found, watts_total: 150 }, proving: { paid: 14, paid_wei: '16100000000000000000' }, ladder: { ...stateOf().ladder, first_block_at: T5 - 23 * 86400, days_mined_30: 23, blocks_30d: 2592 } });
|
||||
const c = chainOf({ source: 'observer', keys_listed: 1204, keys_cap: 64, voters: 1204, mine: [key({ blocks: 2592, voter: true, rank: 41 })], best: key({ blocks: 2592, voter: true, rank: 41 }) }, MAINNET);
|
||||
const e = V.earningsLines(s, c, 28, T5);
|
||||
assert.equal(e.weight.text, 'rank 41 of 1,204 keys');
|
||||
assert.equal(e.weight.sub, '2,592 blocks in 30 days · 23 of 30 days · 0.100% of the network');
|
||||
assert.equal(e.cost.text, '£1.01 a day');
|
||||
assert.equal(e.cost.sub, 'at 28p/kWh for 150 W · 2,133 IGN per kWh');
|
||||
assert.equal(e.lifetime.text, '2,592 blocks');
|
||||
assert.equal(e.lifetime.sub, '207,360 IGN at today’s reward');
|
||||
const noPrice = V.earningsLines(s, c, 0, T5);
|
||||
assert.equal(noPrice.cost.text, '150 W'); assert.equal(noPrice.cost.sub, 'set a price in Settings · 2,133 IGN per kWh');
|
||||
const fresh = V.earningsLines(stateOf(), chainOf(), 28, T5);
|
||||
assert.equal(fresh.weight.text, 'not in the weight table yet');
|
||||
assert.equal(fresh.lifetime.text, '0 blocks');
|
||||
assert.equal(V.earningsLines(stateOf({ mining: { ...stateOf().mining, watts_total: 0, cards: [] } }), chainOf(), 28, T5).cost.sub, 'no card reports its draw');
|
||||
assert.equal(e.devFee(true, 12), 'Dev fee: 1 block in 100 pays the people who make this app (12 blocks so far).');
|
||||
assert.equal(e.devFee(false, 0), 'Dev fee off. Every block pays your address.');
|
||||
const ec = earningsCard(), three = ec.slice(ec.indexOf('class="earn-three"'), ec.indexOf('id="s-devfee"'));
|
||||
assert.equal((three.match(/class="earn-cell"/g) || []).length, 3);
|
||||
for (const id of ['e-rung', 'e-cost', 'e-blocks']) assert.ok(three.includes('id="' + id + '"'), id);
|
||||
assert.ok(/\.earn-three\{[^}]*grid-template-columns:\s*repeat\(3/.test(css), 'three columns in app.css');
|
||||
});
|
||||
|
|
|
|||
3
docs/plans/earnings-tidy-shots.txt
Normal file
3
docs/plans/earnings-tidy-shots.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
01-earnings-dark.png scenario=ladder&page=earnings&theme=dark 1100
|
||||
02-earnings-light.png scenario=ladder&page=earnings&theme=light 1100
|
||||
03-earnings-fresh-dark.png scenario=firstwait&page=earnings&theme=dark 1100
|
||||
BIN
docs/plans/earnings-tidy-shots/01-earnings-dark.png
Normal file
BIN
docs/plans/earnings-tidy-shots/01-earnings-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
BIN
docs/plans/earnings-tidy-shots/02-earnings-light.png
Normal file
BIN
docs/plans/earnings-tidy-shots/02-earnings-light.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 261 KiB |
BIN
docs/plans/earnings-tidy-shots/03-earnings-fresh-dark.png
Normal file
BIN
docs/plans/earnings-tidy-shots/03-earnings-fresh-dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 239 KiB |
Loading…
Reference in a new issue