Merge branch 'earnings-26' into app-ia-26
# Conflicts: # app/igneum-app/ui/view.test.mjs # tools/ui-mock/server.mjs
This commit is contained in:
commit
670d3a4de2
13 changed files with 176 additions and 45 deletions
2
app/igneum-app/Cargo.lock
generated
2
app/igneum-app/Cargo.lock
generated
|
|
@ -219,7 +219,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "igneum-app"
|
||||
version = "0.3.25"
|
||||
version = "0.3.26"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"getrandom",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "igneum-app"
|
||||
version = "0.3.25"
|
||||
version = "0.3.26"
|
||||
edition = "2021"
|
||||
description = "Igneum Miner engine: supervises the node, the miner and the GPU workers, and serves the dashboard on 127.0.0.1"
|
||||
license = "MIT"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
1 ICON "igneum.ico"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,3,25,0
|
||||
PRODUCTVERSION 0,3,25,0
|
||||
FILEVERSION 0,3,26,0
|
||||
PRODUCTVERSION 0,3,26,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
|
|
@ -20,12 +20,12 @@ BEGIN
|
|||
BEGIN
|
||||
VALUE "CompanyName", "Igneum"
|
||||
VALUE "FileDescription", "Igneum Miner engine"
|
||||
VALUE "FileVersion", "0.3.25"
|
||||
VALUE "FileVersion", "0.3.26"
|
||||
VALUE "InternalName", "igneum-app"
|
||||
VALUE "LegalCopyright", "Igneum contributors"
|
||||
VALUE "OriginalFilename", "igneum-app.exe"
|
||||
VALUE "ProductName", "Igneum Miner"
|
||||
VALUE "ProductVersion", "0.3.25"
|
||||
VALUE "ProductVersion", "0.3.26"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
|||
|
|
@ -78,6 +78,11 @@ pub struct Ladder {
|
|||
pub cards_first: BTreeMap<String, f64>,
|
||||
#[serde(default)]
|
||||
pub blocks: Vec<BlockFound>,
|
||||
/// earnings-26 (8 October 2026): the unix time of every block the node credited in the last day, so the Earnings tab's
|
||||
/// earned line is the measured 24 hours (`blocks` keeps only the last KEEP_BLOCKS; a fast card finds more in a day).
|
||||
/// Trimmed to the day on every read; a record from before the field reads its times from `blocks` once.
|
||||
#[serde(default)]
|
||||
pub block_times: Vec<f64>,
|
||||
#[serde(default)]
|
||||
pub votes_signed: u64,
|
||||
#[serde(default)]
|
||||
|
|
@ -138,6 +143,8 @@ pub struct LadderState {
|
|||
pub days_mined_30: u64,
|
||||
pub blocks_30d: u64,
|
||||
pub blocks_today: u64,
|
||||
/// blocks the node credited to this address in the last 24 hours (the coinbase reads), never a projection
|
||||
pub blocks_24h: u64,
|
||||
pub days: Vec<DayBlocks>,
|
||||
pub cards_first: BTreeMap<String, f64>,
|
||||
pub blocks: Vec<BlockFound>,
|
||||
|
|
@ -284,6 +291,8 @@ impl Ladder {
|
|||
}
|
||||
self.blocks.insert(0, BlockFound { hash: hash.clone(), daa, nonce, at: now, card: card.to_string(), card_name: card_name.to_string(), count: total });
|
||||
self.blocks.truncate(KEEP_BLOCKS);
|
||||
self.block_times.push(now);
|
||||
self.block_times.retain(|t| *t > now - DAY_S - 3600.0);
|
||||
let kind = if first { "first" } else if total == 100 { "hundred" } else if total == 1_000 { "thousand" } else if total >= 10_000 && total % 10_000 == 0 { "ten_thousand" } else if new_card && total > 1 { "card" } else { "" };
|
||||
if kind.is_empty() {
|
||||
return None;
|
||||
|
|
@ -355,6 +364,15 @@ impl Ladder {
|
|||
self.days.iter().filter(|d| (d.day as f64 + 1.0) * DAY_S > from).map(|d| d.blocks).sum()
|
||||
}
|
||||
|
||||
/// the blocks credited in the last 24 hours: `block_times`, or the block list for a record from before the field
|
||||
pub fn blocks_24h(&self, now: f64) -> u64 {
|
||||
let from = now - DAY_S;
|
||||
if self.block_times.is_empty() {
|
||||
return self.blocks.iter().filter(|b| b.at > from).count() as u64;
|
||||
}
|
||||
self.block_times.iter().filter(|t| **t > from).count() as u64
|
||||
}
|
||||
|
||||
pub fn state(&self, now: f64, gap_s: f64) -> LadderState {
|
||||
let today = day_of(now);
|
||||
let window_from = now - 30.0 * DAY_S;
|
||||
|
|
@ -367,6 +385,7 @@ impl Ladder {
|
|||
days_mined_30,
|
||||
blocks_30d: self.blocks_since(window_from),
|
||||
blocks_today: self.days.iter().find(|d| d.day == today).map(|d| d.blocks).unwrap_or(0),
|
||||
blocks_24h: self.blocks_24h(now),
|
||||
days: self.days.iter().filter(|d| (d.day as f64 + 1.0) * DAY_S > window_from).cloned().collect(),
|
||||
cards_first: self.cards_first.clone(),
|
||||
blocks: self.blocks.iter().take(10).cloned().collect(),
|
||||
|
|
@ -436,6 +455,26 @@ pub fn parse_vote_index(text: &str) -> Option<u64> {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn the_24_hour_count_reads_every_coinbase_of_the_day_past_the_list_cap_and_rolls_off() {
|
||||
let mut l = Ladder::default();
|
||||
let t0 = 1_791_400_000.0;
|
||||
for i in 0..40u64 {
|
||||
l.blocks.insert(0, BlockFound { hash: format!("{i}"), daa: i, nonce: String::new(), at: t0 + i as f64 * 600.0, card: "c".into(), card_name: "c".into(), count: i + 1 });
|
||||
l.blocks.truncate(KEEP_BLOCKS);
|
||||
l.block_times.push(t0 + i as f64 * 600.0);
|
||||
}
|
||||
let now = t0 + 39.0 * 600.0 + 1.0;
|
||||
assert_eq!(l.blocks.len(), KEEP_BLOCKS);
|
||||
assert_eq!(l.state(now, 600.0).blocks_24h, 40, "every block of the day counts, past the list's cap");
|
||||
assert_eq!(l.blocks_24h(now + DAY_S - 20.0 * 600.0), 20, "the first 20 rolled off the day");
|
||||
assert_eq!(l.blocks_24h(now + 2.0 * DAY_S), 0);
|
||||
// a record from before the field reads its block list
|
||||
let mut old = Ladder::default();
|
||||
old.blocks.push(BlockFound { hash: "a".into(), daa: 1, nonce: String::new(), at: now - 100.0, card: "c".into(), card_name: "c".into(), count: 1 });
|
||||
old.blocks.push(BlockFound { hash: "b".into(), daa: 1, nonce: String::new(), at: now - DAY_S - 100.0, card: "c".into(), card_name: "c".into(), count: 1 });
|
||||
assert_eq!(old.blocks_24h(now), 1);
|
||||
}
|
||||
use super::*;
|
||||
const NODE: &str = "2026-10-04 20:08:38.486+00:00 [INFO ] PoW accepted 28abaa8c28ffb4c3eab96b5dc202f8328df7038dc7b2653c9e4b7dd6369ae5ce by igneum-lottery-v2-bound (daa 38637, epoch seed c8b574fbbbd6ba93b34f8c9a81042b6970b140ffb24219e381d82b653673ee81, day 20730, nonce 0x267dd27200a91c80)";
|
||||
const MINER: &str = "1791140918.500 ACCEPTED block nonce=0x267dd27200a91c80 (gpu worker, cpu re-check ok, identity mac-01234567-1)";
|
||||
|
|
|
|||
|
|
@ -384,9 +384,10 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
.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-head .s,.earn-earned .s{font-size:var(--t-base);color:var(--ash);line-height:1.45;min-width:0}
|
||||
.earn-earned{display:flex;align-items:baseline;gap:var(--s-3);flex-wrap:wrap;padding:var(--s-3) 0;border-bottom:1px solid var(--line)}
|
||||
.earn-head .v.paused{color:var(--ash)}
|
||||
.earn-earned .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}
|
||||
|
|
@ -916,3 +917,6 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)}
|
|||
1920 by 1080 and 1920 by 1200 on build-2 (known-failed first at 1080); view.test.mjs checks these rules. */
|
||||
@media (min-width:1700px){.screen{max-width:1280px}#page-settings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gap);align-items:start}#page-settings>.card{max-width:640px;width:100%}#page-settings>.card.wide,#page-settings>#s-interface-card{grid-column:1/-1;max-width:none}}
|
||||
@media (min-width:2500px){.screen{max-width:1440px}}
|
||||
|
||||
.card.fee{padding:var(--s-4)}
|
||||
.card.fee .help{margin:0}
|
||||
|
|
|
|||
|
|
@ -693,8 +693,12 @@ var View = (function () {
|
|||
if (d.line && !d.on && !d.address) return 'Dev fee: ' + d.line;
|
||||
return 'Dev fee ' + (d.percent || 1) + '% (1 block in 100)' + addr + (d.address ? '' : '; the miner names the address when it starts');
|
||||
}
|
||||
// earnings-26 (the founder, 8 October 2026): the fee's one place in the app is the last section of Settings: the rate, what it
|
||||
// funds, the site's wording (site/miner.html, the dev fee block), no control. Nothing about the fee on Earnings or anywhere else.
|
||||
var FEE_RATE = '1%';
|
||||
function devFeeLine(s) {
|
||||
return s.settings.dev_fee ? 'One block in 100 pays the miner software’s dev fee' + (s.mining.fee_total ? ' (' + withCommas(s.mining.fee_total) + ' so far)' : '') + '. The switch above turns it off.' : 'The dev fee is off. Every block pays this address.';
|
||||
var fee = (s.mining || {}).fee_total || 0;
|
||||
return s.settings.dev_fee ? 'The Ember software takes a ' + FEE_RATE + ' dev fee. One block template in 100 is requested with the dev payout address instead of yours' + (fee ? ' (' + withCommas(fee) + ' so far)' : '') + '. The fee goes to Igneum Labs LTD, the company that ships the software.' : 'The dev fee is off on this install (--dev-fee 0). Every block pays your address.';
|
||||
}
|
||||
|
||||
// the remote-jobs line on Settings: j = state.jobs, now = state.now
|
||||
|
|
@ -1256,6 +1260,8 @@ var View = (function () {
|
|||
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
|
||||
var hhmm = Notices.hhmm;
|
||||
var NETWORK_PAUSED_S = 600; // the projection reads "Network paused" when the chain has no block for ten minutes
|
||||
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) {
|
||||
|
|
@ -1366,16 +1372,23 @@ var View = (function () {
|
|||
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') + ' at ' + fmtIgn(c.minerIgn) + ' each' + (expected !== null ? ' · about ' + fmtIgn(expected) + ' expected at ' + rate + ' MH/s' : ''), field: FIELDS.found_hour + ' × 24 × ' + FIELDS.reward, value: measured };
|
||||
// earnings-26 (the founder read PC 1's tab, 8 October 2026: "100k+ IGN/day" above "0 blocks in 2 hours"): the projection
|
||||
// pauses, with its reason, while a job holds the card or the chain stands still (no block for ten minutes); no IGN a day then
|
||||
var j = s.jobs || {}, n = s.node || {}, heldCard = m.state === 'held' || (m.cards || []).some(function (cd) { return cd.state === 'held'; });
|
||||
if (j.active || heldCard) out.day = { text: 'Projection paused', sub: (j.active && j.title ? j.title + ' running since ' + hhmm(j.started_at) : 'a job from the team running') + '; the card is not mining', field: 'jobs.active, jobs.title, jobs.started_at · the projection resumes when the card mines again', value: null, paused: 'job' };
|
||||
else if (n.tip_age_s >= NETWORK_PAUSED_S) out.day = { text: 'Network paused', sub: 'no block on the chain for ' + spanWords(n.tip_age_s) + '; the projection resumes with the next block', field: 'node.tip_age_s · no block in ' + (NETWORK_PAUSED_S / 60) + ' minutes', value: null, paused: 'network' };
|
||||
else 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 dayIgn = out.day.value;
|
||||
// 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 };
|
||||
// earned: always the measured last 24 hours from the node's coinbase reads (ladder.blocks_24h, or the block list on an
|
||||
// older engine), at today's reward, labelled earned; never the projection. The run count and the shards paid sit under it
|
||||
var day24 = ld.blocks_24h >= 0 ? ld.blocks_24h : (ld.blocks || []).filter(function (b) { return b.at > now - 86400; }).length;
|
||||
var run = m.accepted_session || 0, shards = pv.paid || 0, shardIgn = pv.paid_wei && Number(pv.paid_wei) ? Number(pv.paid_wei) / 1e18 : 0, earnedParts = [];
|
||||
earnedParts.push(withCommas(day24) + ' block' + (day24 === 1 ? '' : 's') + ' in the last 24 hours' + (day24 && c.minerIgn ? ' at ' + fmtIgn(c.minerIgn) + ' each' : ''));
|
||||
earnedParts.push(withCommas(run) + ' this run');
|
||||
if (shards) earnedParts.push(shards + ' shard' + (shards === 1 ? '' : 's') + (shardIgn ? ', ' + fmtIgn(shardIgn) : ''));
|
||||
out.earned = { text: (c.minerIgn ? (day24 ? fmtIgn(day24 * c.minerIgn) : '0 IGN') : withCommas(day24) + ' block' + (day24 === 1 ? '' : 's')) + ' earned', sub: earnedParts.join(' · '), field: FIELDS.found + ' · the blocks the node credited to this address in the last 24 hours', value: c.minerIgn ? day24 * c.minerIgn : null };
|
||||
// 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';
|
||||
|
|
@ -1388,7 +1401,6 @@ var View = (function () {
|
|||
: { text: money(ppd) + ' a day', sub: 'at ' + (pence / priceFactor(currency.code)) + (minor || minorOf(currency.code).abbr) + '/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
|
||||
|
|
@ -1443,7 +1455,7 @@ var View = (function () {
|
|||
}
|
||||
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 { NETWORKS: NETWORKS, networkWords: networkWords, networkAsk: networkAsk, networkIdOf: networkIdOf, ISO_CURRENCY: ISO_CURRENCY, EU_MEMBERS: EU_MEMBERS, CURRENCY_CODES: CURRENCY_CODES, minorOf: minorOf, priceFactor: priceFactor, currencyInfo: currencyInfo, regionFromLocale: regionFromLocale, currencyChoice: currencyChoice, currencyAsk: currencyAsk, applyMoney: applyMoney, fmtMoney: fmtMoney, setLocale: setLocale, setCurrency: setCurrency, currencyNow: currencyNow, unitWords: unitWords, VENDORS: VENDORS, MARKS: MARKS, WELL_ALPHA_LIGHT: WELL_ALPHA_LIGHT, WELL_ALPHA_DARK: WELL_ALPHA_DARK, vendorOf: vendorOf, seriesLine: seriesLine, vendorMark: vendorMark, markHtml: markHtml, nameHtml: nameHtml, rowHead: rowHead, vendorContrast: vendorContrast, contrast: contrast, hexRgb: hexRgb, 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, driverWords: driverWords, vendorWord: vendorWord, 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, proveInsteadWords: proveInsteadWords, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, LOCK_RULE: LOCK_RULE, lockWords: lockWords, kneeWords: kneeWords, TIERS: TIERS, TIER_RULE: TIER_RULE, DEFAULT_PRICE_MINOR: DEFAULT_PRICE_MINOR, tierOf: tierOf, tierName: tierName, priceOrDefault: priceOrDefault, priceFigure: priceFigure, tierSet: tierSet, fleetTiers: fleetTiers, tierSentence: tierSentence, classFlip: classFlip, minuteOf: minuteOf, kneeNote: kneeNote, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, firstWait: firstWait, staleCard: staleCard, cardSeenBody: cardSeenBody, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords };
|
||||
return { hhmm: hhmm, NETWORK_PAUSED_S: NETWORK_PAUSED_S, NETWORKS: NETWORKS, networkWords: networkWords, networkAsk: networkAsk, networkIdOf: networkIdOf, ISO_CURRENCY: ISO_CURRENCY, EU_MEMBERS: EU_MEMBERS, CURRENCY_CODES: CURRENCY_CODES, minorOf: minorOf, priceFactor: priceFactor, currencyInfo: currencyInfo, regionFromLocale: regionFromLocale, currencyChoice: currencyChoice, currencyAsk: currencyAsk, applyMoney: applyMoney, fmtMoney: fmtMoney, setLocale: setLocale, setCurrency: setCurrency, currencyNow: currencyNow, unitWords: unitWords, VENDORS: VENDORS, MARKS: MARKS, WELL_ALPHA_LIGHT: WELL_ALPHA_LIGHT, WELL_ALPHA_DARK: WELL_ALPHA_DARK, vendorOf: vendorOf, seriesLine: seriesLine, vendorMark: vendorMark, markHtml: markHtml, nameHtml: nameHtml, rowHead: rowHead, vendorContrast: vendorContrast, contrast: contrast, hexRgb: hexRgb, 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, driverWords: driverWords, vendorWord: vendorWord, 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, proveInsteadWords: proveInsteadWords, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, LOCK_RULE: LOCK_RULE, lockWords: lockWords, kneeWords: kneeWords, TIERS: TIERS, TIER_RULE: TIER_RULE, DEFAULT_PRICE_MINOR: DEFAULT_PRICE_MINOR, tierOf: tierOf, tierName: tierName, priceOrDefault: priceOrDefault, priceFigure: priceFigure, tierSet: tierSet, fleetTiers: fleetTiers, tierSentence: tierSentence, classFlip: classFlip, minuteOf: minuteOf, kneeNote: kneeNote, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, firstWait: firstWait, staleCard: staleCard, cardSeenBody: cardSeenBody, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords };
|
||||
})();
|
||||
/* ---------- 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
|
||||
|
|
@ -1807,7 +1819,6 @@ if (typeof document !== 'undefined') (function () {
|
|||
$('btn-open-log').addEventListener('click', function () { setDrawer(true); });
|
||||
|
||||
// ---------- 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'); }); });
|
||||
$('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();
|
||||
|
|
@ -2670,13 +2681,12 @@ if (typeof document !== 'undefined') (function () {
|
|||
var a = s.address || {}, m = s.mining;
|
||||
var el = View.earningsLines(s, chain, priceNow(), s.now);
|
||||
setText('e-day', el.day.text); setText('e-day-sub', el.day.sub); $('e-day').title = el.day.field;
|
||||
setText('e-run', el.run.text); setText('e-run-sub', el.run.sub); $('e-run').title = el.run.field;
|
||||
$('e-day').classList.toggle('paused', !!el.day.paused);
|
||||
setText('e-earned', el.earned.text); setText('e-earned-sub', el.earned.sub); $('e-earned').title = el.earned.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);
|
||||
if (document.activeElement !== $('s-devfee')) $('s-devfee').checked = !!(s.settings && s.settings.dev_fee);
|
||||
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.');
|
||||
|
|
@ -2743,6 +2753,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
setText('s-mid', 'machine id ' + (s.machine_id || '').slice(0, 8));
|
||||
setText('s-node-dir', 'chain data: ' + s.node_dir);
|
||||
setText('s-log-dir', 'log files: ' + s.log_dir);
|
||||
setText('s-fee-line', View.devFeeLine(s));
|
||||
$('s-live').hidden = !s.live_page;
|
||||
var iw = View.interfaceWords(s.ui);
|
||||
setText('s-ui-line', iw.line || 'Interface'); setText('s-ui-eyebrow', iw.eyebrow || 'built in'); setText('s-ui-help', iw.help || 'Small changes to this window arrive over the air, signed by Igneum, without a new version of the app. A bundle that fails to load is rolled back to the built-in interface by itself.');
|
||||
|
|
|
|||
|
|
@ -370,19 +370,20 @@
|
|||
<!-- Earnings -->
|
||||
<section class="page" id="page-earnings" data-page="earnings" hidden>
|
||||
<div class="card">
|
||||
<!-- 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). -->
|
||||
<!-- earnings-tidy (7 October 2026): the day rate first with its reason, then a quiet row of three (weight, electricity,
|
||||
lifetime). earnings-26 (8 October 2026): the projection pauses with its reason under a job or a still chain; the
|
||||
earned line is the measured last 24 hours from the node's coinbase reads, labelled earned. The 1% section lives at
|
||||
the bottom of Settings, nowhere on this page. 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-earned"><span class="v" id="e-earned">0 IGN earned</span><span class="s" id="e-earned-sub">0 blocks in the last 24 hours</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>
|
||||
</div>
|
||||
|
||||
<div class="card" id="ladder-card">
|
||||
|
|
@ -619,6 +620,13 @@
|
|||
</div>
|
||||
<div class="row wrap"><button class="btn small ghost" id="s-live" hidden>Open the live devnet page</button><span class="note mono small" id="s-mid"></span></div>
|
||||
</details>
|
||||
|
||||
<!-- earnings-26 (the founder, 8 October 2026): the dev fee's one place in the app, last on Settings: the rate, what it
|
||||
funds, the site's wording (site/miner.html), no control -->
|
||||
<div class="card fee" id="s-fee-card">
|
||||
<div class="card-head"><h3>The dev fee</h3><div class="head-right"><span class="eyebrow">1%</span></div></div>
|
||||
<p class="help" id="s-fee-line">The Ember software takes a 1% dev fee. One block template in 100 is requested with the dev payout address instead of yours. The fee goes to Igneum Labs LTD, the company that ships the software.</p>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -226,8 +226,8 @@ test('the prove words follow the switch, the setup, the node and the status', ()
|
|||
|
||||
test('the dev-fee lines name the share and the switch that turns it off', () => {
|
||||
const s = (on, fee) => ({ settings: { dev_fee: on }, mining: { fee_total: fee }, dev_fee: { on, percent: on ? 1 : 0, address: '0x1234567890abcdef1234567890abcdef12345678', line: '' } });
|
||||
assert.equal(V.devFeeLine(s(true, 12)), 'One block in 100 pays the miner software’s dev fee (12 so far). The switch above turns it off.');
|
||||
assert.equal(V.devFeeLine(s(false, 0)), 'The dev fee is off. Every block pays this address.');
|
||||
assert.equal(V.devFeeLine(s(true, 12)), 'The Ember software takes a 1% dev fee. One block template in 100 is requested with the dev payout address instead of yours (12 so far). The fee goes to Igneum Labs LTD, the company that ships the software.');
|
||||
assert.equal(V.devFeeLine(s(false, 0)), 'The dev fee is off on this install (--dev-fee 0). Every block pays your address.');
|
||||
assert.equal(V.devFeeText(s(true, 0)), 'Dev fee 1% (1 block in 100) to 0x123456…5678');
|
||||
assert.match(V.devFeeText(s(false, 0)), /^Dev fee off/);
|
||||
});
|
||||
|
|
@ -724,10 +724,10 @@ test('first-block-21 (the project lead, 7 October 2026): the first-block card is
|
|||
assert.equal(V.ladderRungs(s, chainOf(), T5)[0].state, 'done');
|
||||
assert.equal(V.blockCard(null, s, c, T5), null);
|
||||
// block 2: the ordinary accepted pulse only (the block on the strip's list, the activity line, the run count), no card
|
||||
s = afterFirst({ uptime_s: 1800, started_at: T5 - 1800, mining: { ...stateOf().mining, accepted_total: 2, accepted_session: 1, found: [T5 - 2] } }, { milestone: null, blocks_30d: 2, blocks_today: 2 });
|
||||
s = afterFirst({ uptime_s: 1800, started_at: T5 - 1800, mining: { ...stateOf().mining, accepted_total: 2, accepted_session: 1, found: [T5 - 2] } }, { milestone: null, blocks_30d: 2, blocks_today: 2, blocks_24h: 1 });
|
||||
assert.equal(V.blockCard(s.ladder.milestone, s, c, T5), null);
|
||||
assert.equal(V.plainEvent('block accepted by the node (NVIDIA GeForce RTX 4070)').text, 'Block found by NVIDIA GeForce RTX 4070');
|
||||
assert.equal(V.earningsLines(s, c, 28, T5).run.text, '1 block this run');
|
||||
assert.equal(V.earningsLines(s, c, 28, T5).earned.text, '80.00 IGN earned');
|
||||
assert.equal(V.earningsLines(s, c, 28, T5).lifetime.text, '2 blocks');
|
||||
assert.deepEqual(s.mining.found, [T5 - 2], 'the strip flashes the block that just arrived');
|
||||
assert.equal(V.ladderRungs(s, c, T5)[0].state, 'done');
|
||||
|
|
@ -956,8 +956,8 @@ test('the headline reads the day rate with its reason line, then blocks and IGN
|
|||
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');
|
||||
assert.equal(e.earned.text, '0 IGN earned');
|
||||
assert.equal(e.earned.sub, '0 blocks in the last 24 hours · 140 this run · 14 shards, 16.10 IGN');
|
||||
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');
|
||||
|
|
@ -965,10 +965,10 @@ test('the headline reads the day rate with its reason line, then blocks and IGN
|
|||
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.ok(ec.indexOf('id="e-day"') < ec.indexOf('id="e-earned"') && ec.indexOf('id="e-earned"') < ec.indexOf('class="earn-three"'), 'headline, earned line, row of three, in that order');
|
||||
assert.equal(ec.indexOf('id="s-devfee"'), -1, 'no dev-fee switch on Earnings');
|
||||
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);
|
||||
for (const line of [e.day.sub, e.earned.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', () => {
|
||||
|
|
@ -988,9 +988,8 @@ test('the row of three renders: weight rung, electricity and lifetime, each with
|
|||
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(e.devFee, undefined, 'the Earnings lines carry no fee words');
|
||||
const ec = earningsCard(), three = ec.slice(ec.indexOf('class="earn-three"'), ec.indexOf('</div>\n </div>', ec.indexOf('class="earn-three"')));
|
||||
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');
|
||||
|
|
@ -1313,3 +1312,59 @@ test('the tiers from the team\'s table read as the table\'s until the card is me
|
|||
const mac = V.tierSet(card({ vendor: 'apple', tiers: [{ id: 'max', clock_mhz: 0, power_pct: 100, w: 38, mhs: 27.1, mhw: 0.713, source: 'stock', label: 'measured', table: true, note: 'the meter' }], tier: 'max', tier_note: 'no lever: Apple silicon sets its own clocks and power', tiers_table: true }), settings(), 28);
|
||||
assert.equal(mac.kind, 'nolever'); assert.equal(mac.tiers[0].line, '27.1 MH/s · 38 W'); assert.equal(mac.tiers[0].sub, '0.713 MH/W · table');
|
||||
});
|
||||
|
||||
// the Earnings tab's two truths (8 October 2026, the founder read PC 1's tab: "100k+ IGN/day" above "0 blocks in 2 hours", both
|
||||
// shown as earnings while a job held the card and the chain stood still): the projection line pauses with its reason, and the
|
||||
// earned line is always the measured last 24 hours from the node's coinbase reads, labelled earned. Known-failed first on
|
||||
// release-0.3.25: the projection read as earnings under a job, and "blocks this run" stood where earned belongs.
|
||||
test('the projection pauses under a job and when the network stands still; the earned line is the last 24 hours, labelled earned', () => {
|
||||
const held = stateOf({ jobs: { active: true, id: 'job-20261008-1240', kind: 'run', title: 'class v6 floor lane 2 (the 5090 grid)', started_at: T5 - 2 * 3600 - 60 }, mining: { ...stateOf().mining, state: 'held', cards: stateOf().mining.cards.map((c) => ({ ...c, state: 'held', message: 'paused for a job from the team' })) } });
|
||||
const h = V.earningsLines(held, chainOf(), 28, T5);
|
||||
assert.equal(h.day.text, 'Projection paused');
|
||||
assert.equal(h.day.sub, 'class v6 floor lane 2 (the 5090 grid) running since ' + V.hhmm(T5 - 2 * 3600 - 60) + '; the card is not mining');
|
||||
assert.equal(h.day.paused, 'job'); assert.equal(h.day.value, null);
|
||||
assert.equal(/IGN/.test(h.day.text + h.day.sub), false, 'no IGN a day figure under a job');
|
||||
// a held card without a job object (an older runner): the card's own words
|
||||
const held2 = stateOf({ mining: { ...stateOf().mining, state: 'held', cards: stateOf().mining.cards.map((c) => ({ ...c, state: 'held', message: 'paused for a job from the team' })) } });
|
||||
assert.equal(V.earningsLines(held2, chainOf(), 28, T5).day.sub, 'a job from the team running; the card is not mining');
|
||||
// the network paused: no block in ten minutes
|
||||
const still = stateOf({ node: { ...stateOf().node, tip_age_s: 900 } });
|
||||
const n = V.earningsLines(still, chainOf(), 28, T5);
|
||||
assert.equal(n.day.text, 'Network paused'); assert.equal(n.day.sub, 'no block on the chain for 15 min; the projection resumes with the next block'); assert.equal(n.day.paused, 'network'); assert.equal(n.day.value, null);
|
||||
assert.equal(V.earningsLines(stateOf({ node: { ...stateOf().node, tip_age_s: 599 } }), chainOf(), 28, T5).day.paused, undefined, 'ten minutes is the line');
|
||||
// a job wins over the network word when both hold
|
||||
assert.equal(V.earningsLines(stateOf({ ...held, node: { ...stateOf().node, tip_age_s: 900 } }), chainOf(), 28, T5).day.paused, 'job');
|
||||
// earned: the last 24 hours from the ladder's coinbase reads, never the projection, labelled earned
|
||||
const blocks = [{ at: T5 - 600 }, { at: T5 - 7200 }, { at: T5 - 86000 }, { at: T5 - 90000 }];
|
||||
const e = V.earningsLines(stateOf({ ladder: { ...stateOf().ladder, blocks } }), chainOf(), 28, T5);
|
||||
assert.equal(e.earned.text, '240.0 IGN earned');
|
||||
assert.match(e.earned.sub, /^3 blocks in the last 24 hours at 80\.00 IGN each/);
|
||||
assert.equal(e.earned.value, 240);
|
||||
assert.equal(e.earned.field, V.FIELDS.found + ' · the blocks the node credited to this address in the last 24 hours');
|
||||
assert.equal(e.run, undefined, 'the run line is gone');
|
||||
// the earned line holds under a job and a paused network
|
||||
assert.equal(V.earningsLines({ ...held, ladder: { ...held.ladder, blocks } }, chainOf(), 28, T5).earned.text, '240.0 IGN earned');
|
||||
// the engine's own 24-hour count wins over the capped block list (ladder.blocks keeps 30; a fast card finds more in a day)
|
||||
assert.equal(V.earningsLines(stateOf({ ladder: { ...stateOf().ladder, blocks, blocks_24h: 41 } }), chainOf(), 28, T5).earned.text, '3,280 IGN earned');
|
||||
assert.equal(V.earningsLines(stateOf({ ladder: { ...stateOf().ladder, blocks: [], blocks_24h: 0 } }), chainOf(), 28, T5).earned.text, '0 IGN earned');
|
||||
// without a reward figure the count stands alone
|
||||
assert.equal(V.earningsLines(stateOf({ ladder: { ...stateOf().ladder, blocks } }), chainOf({ network: { ...chainOf().network, miner_ign_per_block: 0 } }), 28, T5).earned.text, '3 blocks earned');
|
||||
});
|
||||
test('the dev fee lives at the bottom of Settings alone: the rate, what it funds, the site\'s wording, no control; Earnings says nothing of it', () => {
|
||||
const ec = earningsCard();
|
||||
assert.equal(/dev.?fee|Dev fee|s-devfee/i.test(ec), false, 'no fee words on Earnings');
|
||||
assert.equal(/s-devfee/.test(src), false, 'no fee switch handler or render in the page code');
|
||||
const a = indexHtml.indexOf('id="page-settings"'), settings = indexHtml.slice(a, indexHtml.indexOf('</section>', a));
|
||||
const fee = settings.indexOf('id="s-fee-card"');
|
||||
assert.ok(fee > 0, 'the fee section exists on Settings');
|
||||
assert.equal((settings.slice(fee + 10).match(/<(div|details) class="card[ "]/g) || []).length, 0, 'the fee section is the last card on Settings');
|
||||
const sec = settings.slice(fee);
|
||||
assert.equal(/<input|<button|class="switch"/.test(sec), false, 'no control in the fee section');
|
||||
assert.match(sec, /1% dev fee/); assert.match(sec, /Igneum Labs LTD/);
|
||||
const site = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'site', 'miner.html'), 'utf8');
|
||||
assert.ok(site.includes('One block template in 100 is requested with the dev payout address instead of yours'), 'the site carries the sentence');
|
||||
assert.match(V.devFeeLine({ settings: { dev_fee: true }, mining: { fee_total: 0 } }), /One block template in 100 is requested with the dev payout address instead of yours/);
|
||||
assert.match(V.devFeeLine({ settings: { dev_fee: true }, mining: { fee_total: 0 } }), /Igneum Labs LTD, the company that ships the software/);
|
||||
assert.equal(/switch|turn it off|turns it off/i.test(V.devFeeLine({ settings: { dev_fee: true }, mining: { fee_total: 0 } })), false, 'no control named');
|
||||
assert.equal(/dev.?fee/i.test(JSON.stringify(V.earningsLines(stateOf(), chainOf(), 28, T5))), false, 'the Earnings lines carry no fee');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
// packaging/windows/Igneum-Miner.iss when the app version moves. Include guards, not #pragma once: rc.exe reads it too.
|
||||
#ifndef IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.25"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,25,0
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.26"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,26,0
|
||||
// The WebView2 runtime the host's loader needs at least (the SDK 1.0.2903.40 loader's minimum, from its release note at the
|
||||
// cut); the installer's rights step (src/rights.rs) installs the evergreen runtime when the PC's is absent or below it, and a
|
||||
// raised minimum is a new right the next update asks once for.
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<key>CFBundleVersion</key>
|
||||
<string>VERSION_STAMP</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.25</string>
|
||||
<string>0.3.26</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
#define ArtDir "..\..\brand\icons"
|
||||
#endif
|
||||
#ifndef AppVersion
|
||||
#define AppVersion "0.3.25"
|
||||
#define AppVersion "0.3.26"
|
||||
#endif
|
||||
#define AppName "Igneum Miner"
|
||||
#define Publisher "Igneum"
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
d5b68fae7bafdbe4fb389adf0f26991121f8c443
|
||||
c9ad753a65b04544a2d0653030fce0529811d416
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@
|
|||
// crossing, the 5090 re-measuring with its v4 tiers out of date and the knee note, the 5080 already on v5)
|
||||
// tiers-table tiers-table-26: the install's first minute with the three tiers from the team's table (the 5090 measured, the
|
||||
// 5070 Ti estimated), nothing measured on these cards yet
|
||||
// earn-held the Earnings tab under a job holding the card (the projection paused with the job's name); earn-still: the chain
|
||||
// standing still for 15 min (Network paused); both with the earned 24 hours from the ladder
|
||||
// knob knob-24: the 5090 locked at 1,300 MHz with the knee at 1,200 on its curve, the 4070 in the clock search
|
||||
// (knob-search: both cards searching)
|
||||
// heat Ember Heat (mission item 7): a 5090 and a 4070 in heat mode holding 20 °C (heating, 60% duty), the
|
||||
|
|
@ -134,6 +136,7 @@ function ladderNow(t) {
|
|||
const hash2 = '4c1d9e7a2b5f8c0e3d6a9b1c4e7f0a2d5b8c1e4f7a0b3d6c9e2f5a8b1d4c7e0f3a2';
|
||||
if (scenario === 'secondblock') return { ...base, first_block_at: t - 602, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 2, blocks_today: 2, cards_first: { [recState.mining.cards[0].key]: t - 602 }, blocks: [{ hash: hash2, daa: 267054, nonce: '3a9c0d11e2f4b5a6', at: t - 2, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 2 }, { hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 602, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 20, last_vote_index: 8516, last_vote_at: t - 8, milestone: null, first_block_shown: true };
|
||||
if (scenario === 'firstblock') return { ...base, first_block_at: t - 70, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 266454, days_mined_30: 1, blocks_30d: 1, blocks_today: 1, blocks: [{ hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 70, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, count: 1 }], votes_signed: 2, last_vote_index: 8496, last_vote_at: t - 20, milestone: { kind: 'first', count: 1, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266454, at: t - 70 }, first_block_shown: false };
|
||||
if (scenario === 'earn-held' || scenario === 'earn-still') return { ...base, first_block_at: t - 86000, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 259200, days_mined_30: 2, blocks_30d: 3, blocks_today: 2, blocks_24h: 3, blocks: [{ hash: hash2, daa: 267054, nonce: '3a9c0d11e2f4b5a6', at: t - 2 * 3600 - 900, card: 'nvidia:0:NVIDIA GeForce RTX 5090', card_name: 'NVIDIA GeForce RTX 5090', count: 3 }, { hash, daa: 266454, nonce: '267dd27200a91c80', at: t - 5 * 3600, card: 'nvidia:0:NVIDIA GeForce RTX 5090', card_name: 'NVIDIA GeForce RTX 5090', count: 2 }, { hash: 'b' + hash.slice(1), daa: 259200, nonce: '1', at: t - 86000, card: 'nvidia:0:NVIDIA GeForce RTX 5090', card_name: 'NVIDIA GeForce RTX 5090', count: 1 }], votes_signed: 40, last_vote_index: 8495, last_vote_at: t - 12, first_block_shown: true };
|
||||
if (scenario === 'ladder') return { ...base, first_block_at: t - 7300, first_block_card: recState.mining.cards[0].key, first_block_hash: hash, first_block_daa: 259200, days_mined_30: 1, blocks_30d: 61, blocks_today: 61, votes_signed: 240, last_vote_index: 8495, last_vote_at: t - 12, last_signed_locked: 8495, signed_locked_count: 238, streak_s: 7100, shards: [{ block: 160390, shard: 3, wei: '1150000000000000000', at: t - 400 }], milestone: { kind: 'hundred', count: 100, card: recState.mining.cards[0].key, card_name: recState.mining.cards[0].name, hash, daa: 266400, at: t - 50 }, first_block_shown: true };
|
||||
return base;
|
||||
}
|
||||
|
|
@ -307,6 +310,17 @@ function stateNow() {
|
|||
if (!first) s.events = [{ kind: 'info', t: t - 6900, text: 'Ember Tune set NVIDIA GeForce RTX 5080 to Balanced: 76.1 MH/s at 250 W, 110 W under stock' }, { kind: 'info', t: t - 7200, text: 'Ember Tune set NVIDIA GeForce RTX 5090 to Balanced: 126.8 MH/s at 372 W, 78 W under stock' }].concat(s.events).slice(0, 40);
|
||||
}
|
||||
}
|
||||
if (scenario === 'earn-held' || scenario === 'earn-still') {
|
||||
// the Earnings tab's two truths (8 October 2026): earn-held = PC 1's shape, a floor-lane job holding the card since 12:40 while the
|
||||
// projection would read 100k+ IGN a day; earn-still = the chain standing still (no block for 15 min). Both with 3 blocks earned in
|
||||
// the last 24 hours from the ladder's coinbase reads
|
||||
const heldJob = scenario === 'earn-held';
|
||||
const c0 = { ...recState.mining.cards[0], key: 'nvidia:0:NVIDIA GeForce RTX 5090', name: 'NVIDIA GeForce RTX 5090', kind: 'discrete', vendor: 'nvidia', worker: 'CUDA', detail: '21760 cores', vram_mb: 32768, device: '0', identities: 8, ids: ['8fafda27', '1b2c3d4e', '5f6a7b8c'], power_default_w: 575, power_pct: 80, power_applied: true, power_limit_w: 460, power_w: heldJob ? 410 : 291, temp_gpu: 63, temp_mem: 76, telemetry_at: t, hash_now: heldJob ? 0 : 122.3, hash_avg: 121.8, accepted: 3, state: heldJob ? 'held' : 'mining', message: heldJob ? 'paused for a job from the team' : '', sweep_supported: true, sweep_state: 'idle', sweep_note: '', tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'full', tune_control: true, tune_clock_mhz: 2470, sweep_pct: 80, sweep_at: t - 7200 };
|
||||
s.mining.cards = [c0]; s.mining.state = heldJob ? 'held' : 'mining'; s.mining.hash_total = heldJob ? 0 : 122.3; s.mining.accepted_session = 0; s.display_name = 'pc-1'; s.uptime_s = 2 * 3600 + 300;
|
||||
if (heldJob) s.jobs = { ...s.jobs, active: true, id: 'job-20261008-1240', kind: 'run', title: 'class v6 floor lane 2 (the 5090 grid)', stage: 'row 7 of 24', started_at: t - 2 * 3600 - 60, message: '' };
|
||||
else s.node = { ...s.node, tip_age_s: 900 };
|
||||
s.mining.found = [];
|
||||
}
|
||||
if (scenario === 'knob' || scenario === 'knob-search') {
|
||||
// the core-clock knob (knob-24, 7 October 2026): the 5090 locked at 1,300 MHz after the ladder stopped at the knee
|
||||
// (1,200 MHz, the rate 5.2% under the cap row), read against the unlocked row at the same 80% cap; the 4070 in the
|
||||
|
|
|
|||
Loading…
Reference in a new issue