diff --git a/app/igneum-app/src/chainfacts.rs b/app/igneum-app/src/chainfacts.rs index 3cbc0e1d8..4c65454eb 100644 --- a/app/igneum-app/src/chainfacts.rs +++ b/app/igneum-app/src/chainfacts.rs @@ -136,6 +136,16 @@ pub fn stats_api_from(live_api: &str) -> String { } } +/// earned-windows-26: the miner's reward per block in wei from the cached reply, without a fetch (the dashboard's poll +/// keeps the cache warm while a window is open; `None` when nothing is cached or the figure is missing). The ladder +/// prices a block at this figure when the block lands, so a window sums what the chain paid at the time. +pub fn cached_reward_wei() -> Option { + let c = CACHE.lock().unwrap(); + let (_, v) = c.as_ref()?; + let ign = num(get(v, &["network", "miner_ign_per_block"])); + if ign > 0.0 { Some((ign * 1e18).round() as u128) } else { None } +} + /// The reply for the dashboard, cached 10 s: the node first, the observer second, `{ok:false}` with the reason when /// neither answers. pub fn fetch(evm_port: u16, live_api: &str, ids: &[String]) -> Value { diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index edf67eb1d..fb5fe9711 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -295,7 +295,17 @@ impl Shared { st.uptime_s = self.started.elapsed().as_secs(); st.started_at = self.started_unix; st.events = self.rings.lock().unwrap().events.iter().cloned().collect(); - st.ladder = self.ladder.lock().unwrap().state(st.now, LADDER_GAP_S); + st.ladder = { + let mut ld = self.ladder.lock().unwrap(); + // earned-windows-26: a block that landed while the reward was unread is priced on the first read that has it + if let Some(wei) = crate::chainfacts::cached_reward_wei() { + let lifetime = self.settings.lock().unwrap().accepted_total; + let filled = ld.backfill_earned(lifetime, wei); + let priced = ld.price_blocks(wei); + if filled || priced > 0 { ld.save(&self.ladder_path); } + } + ld.state(st.now, LADDER_GAP_S) + }; match serde_json::to_value(st) { Ok(v) => v, Err(e) => { @@ -5338,7 +5348,17 @@ impl Engine { self.shared.event("block", &format!("block accepted by the node ({card_name})")); // miner-ui-5: the machine's own ladder record (the block card, the days ring); the hash joins by nonce let nonce = crate::ladder::parse_miner_accepted(text).unwrap_or_default(); - let milestone = self.shared.ladder.lock().unwrap().on_block(unix, &card_key, &card_name, &nonce, total); + let milestone = { + let mut ld = self.shared.ladder.lock().unwrap(); + let m = ld.on_block(unix, &card_key, &card_name, &nonce, total); + // earned-windows-26: the block is priced at the reward the chain facts carry now (the cache), or on the + // next state read that has the figure; the back-fill of an older record runs once, here or there + if let Some(wei) = crate::chainfacts::cached_reward_wei() { + ld.backfill_earned(total, wei); + ld.price_blocks(wei); + } + m + }; self.shared.save_ladder(); if let Some(m) = milestone { self.shared.event("block", &crate::ladder::milestone_event(&m)); diff --git a/app/igneum-app/src/ladder.rs b/app/igneum-app/src/ladder.rs index cb39602c1..3514ee00f 100644 --- a/app/igneum-app/src/ladder.rs +++ b/app/igneum-app/src/ladder.rs @@ -49,6 +49,43 @@ pub struct ShardPaid { pub at: f64, } +/// earned-windows-26 (the founder's audit, 8 October 2026, item 2): one row per coinbase the node credited and per shard +/// paid, with the wei it paid at the time. `wei` is empty until the block is priced (the chain facts' reward at the +/// block's time, read from the engine's cache when the block lands or on the next read that has it); a priced row +/// never changes, so a window is the sum of what the chain paid, never today's reward applied to old blocks. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct Earned { + pub at: f64, + #[serde(default)] + pub shard: bool, + #[serde(default)] + pub wei: String, +} + +/// One earned window for the dashboard: the wei as a decimal string, the IGN as a number for the view, the counts, +/// and whether every row in it is priced (`priced` false while a block waits for its reward figure). +#[derive(Clone, Debug, Default, Serialize, PartialEq)] +pub struct Window { + pub wei: String, + pub ign: f64, + pub blocks: u64, + pub shards: u64, + pub priced: bool, +} + +#[derive(Clone, Debug, Default, Serialize, PartialEq)] +pub struct EarnedWindows { + pub h24: Window, + pub d7: Window, + pub all: Window, + /// the all-time figure was back-filled once from the lifetime count at one reward (a record from before the rows) + pub estimated: bool, +} + +/// the earned log keeps 7 days plus an hour of rows; the all-time window is a running total +const KEEP_EARNED_S: f64 = 7.0 * DAY_S + 3600.0; +const WEI_PER_IGN: f64 = 1e18; + #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] pub struct Milestone { /// first | hundred | thousand | ten_thousand | card @@ -83,6 +120,20 @@ pub struct Ladder { /// 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, + /// earned-windows-26: the rows of the last 7 days and the all-time running totals (wei as a decimal string) + #[serde(default)] + pub earned_log: Vec, + #[serde(default)] + pub earned_all_wei: String, + #[serde(default)] + pub earned_all_blocks: u64, + #[serde(default)] + pub earned_all_shards: u64, + /// the one-time back-fill of a record from before the rows (the lifetime count at the reward then read) + #[serde(default)] + pub earned_backfilled: bool, + #[serde(default)] + pub earned_estimated: bool, #[serde(default)] pub votes_signed: u64, #[serde(default)] @@ -145,6 +196,8 @@ pub struct LadderState { 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, + /// earned-windows-26: what the chain paid this machine in the last 24 hours, 7 days and all time + pub earned: EarnedWindows, pub days: Vec, pub cards_first: BTreeMap, pub blocks: Vec, @@ -163,6 +216,10 @@ pub struct LadderState { pub first_block_shown: bool, } +fn parse_wei(s: &str) -> u128 { + s.trim().parse::().unwrap_or(0) +} + pub fn day_of(unix: f64) -> u64 { (unix.max(0.0) / DAY_S).floor() as u64 } @@ -293,6 +350,9 @@ impl Ladder { self.blocks.truncate(KEEP_BLOCKS); self.block_times.push(now); self.block_times.retain(|t| *t > now - DAY_S - 3600.0); + self.earned_log.push(Earned { at: now, shard: false, wei: String::new() }); + self.earned_all_blocks += 1; + self.trim_earned(now); 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; @@ -349,6 +409,81 @@ impl Ladder { } self.shards.insert(0, ShardPaid { block, shard, wei: wei.to_string(), at: now }); self.shards.truncate(KEEP_SHARDS); + self.earned_log.push(Earned { at: now, shard: true, wei: wei.to_string() }); + self.earned_all_shards += 1; + self.earned_all_wei = (parse_wei(&self.earned_all_wei) + wei).to_string(); + self.trim_earned(now); + } + + fn trim_earned(&mut self, now: f64) { + self.earned_log.retain(|e| e.at > now - KEEP_EARNED_S); + } + + /// Price every block row that has no wei yet at `reward_wei` (the chain facts' miner reward when the engine reads it; + /// a block lands and is priced within the same minute when the cache is warm). Returns how many rows it priced. + pub fn price_blocks(&mut self, reward_wei: u128) -> usize { + if reward_wei == 0 { + return 0; + } + let mut n = 0usize; + let mut add: u128 = 0; + for e in self.earned_log.iter_mut() { + if !e.shard && e.wei.is_empty() { + e.wei = reward_wei.to_string(); + add += reward_wei; + n += 1; + } + } + if n > 0 { + self.earned_all_wei = (parse_wei(&self.earned_all_wei) + add).to_string(); + } + n + } + + /// A record from before the rows (0.3.25 and earlier): the all-time window starts from the lifetime count the engine + /// persists (settings.json accepted_total) at the reward read now, once; the shards kept in the list join it. The + /// windows then say `estimated` for all time until the counts are all from rows. + pub fn backfill_earned(&mut self, lifetime_blocks: u64, reward_wei: u128) -> bool { + if self.earned_backfilled || reward_wei == 0 { + return false; + } + self.earned_backfilled = true; + if lifetime_blocks > self.earned_all_blocks { + let missing = lifetime_blocks - self.earned_all_blocks; + self.earned_all_blocks = lifetime_blocks; + self.earned_all_wei = (parse_wei(&self.earned_all_wei) + reward_wei * missing as u128).to_string(); + self.earned_estimated = true; + } + let logged_shards = self.earned_log.iter().filter(|e| e.shard).count() as u64; + if self.shards.len() as u64 > logged_shards { + let extra: Vec<&ShardPaid> = self.shards.iter().skip(logged_shards as usize).collect(); + let wei: u128 = extra.iter().map(|s| parse_wei(&s.wei)).sum(); + self.earned_all_shards += extra.len() as u64; + self.earned_all_wei = (parse_wei(&self.earned_all_wei) + wei).to_string(); + } + true + } + + /// The three windows at `now`: 24 hours and 7 days from the rows, all time from the running totals. + pub fn earned_windows(&self, now: f64) -> EarnedWindows { + let window = |from: f64| -> Window { + let mut w = Window { priced: true, ..Default::default() }; + let mut wei: u128 = 0; + for e in self.earned_log.iter().filter(|e| e.at > from) { + if e.shard { w.shards += 1; } else { w.blocks += 1; } + if e.wei.is_empty() { w.priced = false; } else { wei += parse_wei(&e.wei); } + } + w.wei = wei.to_string(); + w.ign = wei as f64 / WEI_PER_IGN; + w + }; + let all_wei = parse_wei(&self.earned_all_wei); + EarnedWindows { + h24: window(now - DAY_S), + d7: window(now - 7.0 * DAY_S), + all: Window { wei: all_wei.to_string(), ign: all_wei as f64 / WEI_PER_IGN, blocks: self.earned_all_blocks, shards: self.earned_all_shards, priced: !self.earned_log.iter().any(|e| !e.shard && e.wei.is_empty()) }, + estimated: self.earned_estimated, + } } /// Seconds of unbroken signing at `now` (0 when the last vote is older than `gap_s`). @@ -386,6 +521,7 @@ impl Ladder { 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), + earned: self.earned_windows(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(), @@ -455,6 +591,83 @@ pub fn parse_vote_index(text: &str) -> Option { #[cfg(test)] mod tests { + // earned-windows-26: three windows over a fixed record; a block at the 24 h edge counts once; the wei is what the + // chain paid at the time, never today's reward applied to old blocks; the back-fill runs once + #[test] + fn the_earned_windows_sum_what_the_chain_paid_at_the_time_and_the_edge_block_counts_once() { + let mut l = Ladder::default(); + let t0 = 1_791_400_000.0; + let reward_a: u128 = 4_880_000_000_000_000_000; // 4.88 IGN + let reward_b: u128 = 4_000_000_000_000_000_000; // 4.00 IGN, the ramp moved + // eight days ago: a block at reward_a (outside every window but all time) + l.on_block(t0 - 8.0 * DAY_S, "c", "c", "", 1); + assert_eq!(l.price_blocks(reward_a), 1); + // three days ago: two blocks at reward_a and a shard + l.on_block(t0 - 3.0 * DAY_S, "c", "c", "", 2); + l.on_block(t0 - 3.0 * DAY_S + 1.0, "c", "c", "", 3); + assert_eq!(l.price_blocks(reward_a), 2); + l.on_shard_paid(t0 - 3.0 * DAY_S + 2.0, 100, 3, 120_000_000_000_000_000); // 0.12 IGN + // exactly 24 h ago plus a second: inside the day; exactly at the edge: outside (strict) + l.on_block(t0 - DAY_S + 1.0, "c", "c", "", 4); + l.on_block(t0 - DAY_S, "c", "c", "", 5); + assert_eq!(l.price_blocks(reward_b), 2); + // an hour ago: a block not yet priced + l.on_block(t0 - 3600.0, "c", "c", "", 6); + let w = l.earned_windows(t0); + assert_eq!(w.h24.blocks, 2, "the edge block counts once, on the far side"); + assert_eq!(w.h24.shards, 0); + assert_eq!(w.h24.wei, reward_b.to_string()); + assert!(!w.h24.priced, "the last block waits for its reward"); + assert_eq!(w.d7.blocks, 5); + assert_eq!(w.d7.shards, 1); + assert_eq!(w.d7.wei, (2 * reward_a + 2 * reward_b + 120_000_000_000_000_000).to_string()); + assert!((w.d7.ign - 17.88).abs() < 1e-9); + assert_eq!(w.all.blocks, 6); + assert_eq!(w.all.shards, 1); + assert_eq!(w.all.wei, (3 * reward_a + 2 * reward_b + 120_000_000_000_000_000).to_string(), "all time holds the eight-day-old block at its own reward"); + assert!(!w.estimated); + // the late reward prices the waiting block only; priced rows never move + assert_eq!(l.price_blocks(reward_a), 1); + let w2 = l.earned_windows(t0); + assert!(w2.h24.priced); + assert_eq!(w2.h24.wei, (reward_a + reward_b).to_string()); + assert_eq!(l.price_blocks(reward_b), 0); + assert_eq!(l.earned_windows(t0).h24.wei, (reward_a + reward_b).to_string()); + // the log keeps seven days plus an hour: the eight-day block is gone from the rows, kept in all time + assert_eq!(l.earned_log.len(), 6); + assert_eq!(l.state(t0, 600.0).earned.all.blocks, 6); + } + + #[test] + fn a_record_from_before_the_rows_is_back_filled_once_from_the_lifetime_count_and_says_estimated() { + let mut l = Ladder::default(); + let t0 = 1_791_400_000.0; + l.shards.push(ShardPaid { block: 1, shard: 1, wei: "250000000000000000".into(), at: t0 - 100.0 }); + let reward: u128 = 4_880_000_000_000_000_000; + assert!(l.backfill_earned(40, reward)); + let w = l.earned_windows(t0); + assert_eq!(w.all.blocks, 40); + assert_eq!(w.all.shards, 1); + assert_eq!(w.all.wei, (40 * reward + 250_000_000_000_000_000).to_string()); + assert!(w.estimated); + assert_eq!(w.h24.blocks, 0, "the back-fill never invents rows in a window"); + assert!(!l.backfill_earned(41, reward), "once"); + assert_eq!(l.earned_windows(t0).all.blocks, 40); + // a fresh record with rows for every block is not estimated + let mut f = Ladder::default(); + f.on_block(t0, "c", "c", "", 1); + f.price_blocks(reward); + assert!(f.backfill_earned(1, reward)); + let fw = f.earned_windows(t0 + 1.0); + assert!(!fw.estimated); + assert_eq!(fw.all.blocks, 1); + assert_eq!(fw.all.wei, reward.to_string()); + // a reward of zero back-fills nothing and leaves the flag for the next read + let mut z = Ladder::default(); + assert!(!z.backfill_earned(5, 0)); + assert!(!z.earned_backfilled); + } + #[test] fn the_24_hour_count_reads_every_coinbase_of_the_day_past_the_list_cap_and_rolls_off() { let mut l = Ladder::default(); diff --git a/app/igneum-app/ui/app.css b/app/igneum-app/ui/app.css index 6c1e937d9..616c85bce 100644 --- a/app/igneum-app/ui/app.css +++ b/app/igneum-app/ui/app.css @@ -231,7 +231,7 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card .tot .v{font-family:var(--head);font-weight:700;font-size:var(--t-hero);line-height:1.1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:-.01em} .tot .v.ember{color:var(--ember)} .tot .v.ask-price{font-family:var(--sans);font-size:var(--t-md);font-weight:600;color:var(--molten);text-decoration:underline;text-underline-offset:3px;cursor:pointer;padding:9px 0 8px} -.tot .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.14em;text-transform:uppercase;color:var(--ash);margin-top:4px} +.tot .k{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.06em;color:var(--ash);margin-top:4px} /* a unit label, never uppercased (A8) */ .tot .s{font-size:var(--t-sm);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:2px} .card-head{display:flex;justify-content:space-between;align-items:center;gap:var(--s-3);margin-bottom:var(--s-3);flex-wrap:wrap} .head-right{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;min-width:0} @@ -383,12 +383,14 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)} 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 .eyebrow{margin-bottom:2px} .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-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-three.two{grid-template-columns:repeat(2,minmax(0,1fr))} .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)} @@ -934,3 +936,30 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} .tc-details .gl-details .ctl .pv{min-width:48px} .tune-foot{margin:var(--s-3) 0 0;padding:0 4px} #power-note .btn{margin-left:6px} + +/* app-ia-26: the Earnings rows, the Standing details and the projection foot */ +.earn-rows{display:flex;flex-direction:column;border-top:1px solid var(--line)} +.earn-row{display:grid;grid-template-columns:110px minmax(0,1fr) auto;grid-template-areas:"k v a" "k s a";align-items:baseline;column-gap:var(--s-4);row-gap:2px;padding:10px 0;border-bottom:1px solid var(--line)} +.earn-row .k{grid-area:k;font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.14em;text-transform:uppercase;color:var(--ash)} +.earn-row .v{grid-area:v;font-family:var(--head);font-weight:700;font-size:var(--t-lg);line-height:1.2;min-width:0} +.earn-row .v.mono{font-family:var(--mono);font-weight:500;font-size:var(--t-md);color:var(--molten)} +.earn-row .s{grid-area:s;font-size:var(--t-sm);color:var(--ash);line-height:1.4} +.earn-row .acts{grid-area:a;display:flex;gap:6px;align-self:center} +.earn-details{border-bottom:1px solid var(--line)} +.earn-details>summary{cursor:pointer;list-style:none;padding:10px 0;font-size:var(--t-sm);color:var(--ash);display:flex;align-items:center;gap:8px} +.earn-details>summary::-webkit-details-marker{display:none} +.earn-details>summary::after{content:"";width:7px;height:7px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(45deg) translateY(-2px);transition:transform .15s ease} +.earn-details[open]>summary::after{transform:rotate(225deg) translateY(-2px)} +.earn-foot{margin:var(--s-3) 0 0} +.help{text-wrap:pretty} + +/* app-ia-26: the node line on Mine, the node facts behind Details on Settings */ +.node-go{display:flex;align-items:center;gap:10px;width:100%;border:1px solid var(--line);background:var(--graphite);border-radius:var(--card-r);padding:14px 18px;cursor:pointer;text-align:left;color:var(--bone);font-size:var(--t-md);font-weight:600} +.node-go:hover{border-color:var(--line-2)} +.node-go.bad{color:var(--ember)} +.node-go .chev{margin-left:auto;color:var(--ash);transform:rotate(-45deg) translateY(2px)} +.node-details{margin-top:var(--s-3);border-top:1px solid var(--line)} +.node-details>summary{cursor:pointer;list-style:none;padding:10px 0;font-size:var(--t-sm);color:var(--ash);display:flex;align-items:center;gap:8px} +.node-details>summary::-webkit-details-marker{display:none} +.node-details>summary::after{content:"";width:7px;height:7px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(45deg) translateY(-2px);transition:transform .15s ease} +.node-details[open]>summary::after{transform:rotate(225deg) translateY(-2px)} diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 2c28edd51..3c33d0f14 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -586,7 +586,7 @@ var View = (function () { var heldCards = cards.filter(function (c) { return c.state === 'held'; }).length; if (heldCards && !mining) return { label: 'Held', sub: 'a job from the team is using the cards. Mining restarts when it ends', cls: 'stop', disabled: true, act: '' }; if (s && s.heat && s.heat.on && s.heat.phase === 'resting' && !mining && !m.paused) return { label: 'Stop mining', sub: 'resting · heat mode holds ' + s.heat.set_c.toFixed(1) + ' °C, heats again in ' + (s.heat.until_s < 45 ? 'under a minute' : Math.round(s.heat.until_s / 60) + ' min'), cls: 'stop', disabled: false, act: 'pause' }; - if (!on) return { label: 'Start mining', sub: cards.length ? 'switch a card on in Cards first' : 'no card this app can use', cls: '', disabled: true, act: '' }; + if (!on) return { label: 'Start mining', sub: cards.length ? 'switch a card on first' : 'no card this app can use', cls: '', disabled: true, act: '' }; if (m.paused) return { label: 'Start mining', sub: 'paused · the node keeps running', cls: '', disabled: false, act: 'resume' }; if (m.state === 'mining') return { label: 'Stop mining', sub: 'mining on ' + mining + ' of ' + on + ' card' + (on === 1 ? '' : 's'), cls: 'stop', disabled: false, act: 'pause' }; if (m.state === 'waiting') return { label: 'Stop mining', sub: n && n.synced ? 'card starting' : n && n.state === 'behind' ? 'waiting: the node is behind the chain' : n && n.state === 'no peers' ? 'waiting: no peers on our chain' : 'waiting for the node to sync', cls: 'stop', disabled: false, act: 'pause' }; @@ -622,11 +622,16 @@ var View = (function () { // "12 s" / "4 min" / "53 min" / "2 h" for a tip age function tipAge(s) { s = Math.max(0, Math.round(s)); return s < 90 ? s + ' s' : s < 5400 ? Math.round(s / 60) + ' min' : Math.round(s / 3600) + ' h'; } // the one node line on Mine: "Node synced · 6 peers · 109,859 blocks · last block 4 s ago" + // app-ia-26 (A14): one line, the state word and what matters for it; the facts live in Settings > Node function nodeLine(n, clock, eta) { - var w = nodeWords(n, clock, eta), parts = ['Node ' + w.word]; - if (n.peers > 0) parts.push(n.peers + ' peer' + (n.peers === 1 ? '' : 's')); - if (n.blocks > 0) parts.push(withCommas(n.blocks) + ' blocks'); - if (n.tip_age_s >= 0 && (n.state === 'synced' || n.state === 'behind')) parts.push('last block ' + tipAge(n.tip_age_s) + ' ago'); + var w = nodeWords(n, clock, eta), parts = ['Node ' + w.word], bad = clock && clock.severity === 'block'; + if (bad) parts.push('clock off by ' + skewWord(clock.skew_s)); + else if (n.state === 'synced') { if (n.peers > 0) parts.push(n.peers + ' peer' + (n.peers === 1 ? '' : 's')); } + else if (n.state === 'syncing') { if (n.headers > n.blocks) parts.push(withCommas(n.blocks) + ' of ' + withCommas(n.headers) + ' blocks'); var left = /^(about [^·]+? left)/.exec(eta || n.message || ''); if (left) parts.push(left[1]); } + else if (n.state === 'behind') { if (n.sync_cause === 'not merging') parts.push('your blocks are not merging'); else if (n.tip_age_s >= 0) parts.push('last block ' + tipAge(n.tip_age_s) + ' ago'); } + else if (n.state === 'no peers') parts.push('looking for the seed node'); + else if (n.state === 'restarting') parts.push(n.message || ('restart in ' + (n.restart_in_s || 0) + ' s')); + else if (n.state === 'failed' || n.state === 'stopped' || n.state === 'starting') { if (n.message) parts.push(n.message); } return { text: parts.join(' · '), sub: w.line, tone: w.tone }; } function skewWord(sk) { sk = Math.round(Math.abs(sk || 0)); return sk < 120 ? sk + ' s' : sk < 7200 ? Math.floor(sk / 60) + ' min ' + (sk % 60) + ' s' : Math.floor(sk / 3600) + ' h ' + Math.floor(sk % 3600 / 60) + ' min'; } @@ -818,7 +823,7 @@ var View = (function () { if ((s.jobs && s.jobs.active) || m.state === 'held') return { text: 'Job running', tone: '' }; if (tuningCard(m.cards)) return { text: 'Tuning', tone: 'on' }; if (m.state === 'mining') return { text: 'Mining', tone: 'on' }; - if (s.heat && s.heat.on && s.heat.phase === 'resting') return { text: 'Resting', tone: '' }; + if (s.heat && s.heat.on && s.heat.phase === 'resting') return { text: 'Resting · heat mode', tone: '' }; // never a bare rest word (the heat lane's rule) if (m.state === 'paused') return { text: 'Paused', tone: '' }; if (n.state === 'syncing' || n.state === 'starting') return { text: 'Syncing', tone: '' }; if (n.state === 'restarting' || n.state === 'behind' || n.state === 'no peers' || n.state === 'failed' || n.state === 'stopped' || n.state === 'stub') return { text: 'Node down', tone: 'bad' }; @@ -958,7 +963,8 @@ var View = (function () { if (cd.tier_note || cd.lock_note === 'no lever') return 'No lever on ' + (cd.vendor === 'apple' ? 'Apple silicon' : 'this card') + ': the system sets the clocks. Ember measures and reports.'; var ts = tierSet(cd, s, 0); if (ts.kind === 'table') return 'From the team’s table, ' + (ts.tableLabel === 'measured' ? 'measured on this card class' : 'estimated for this card class') + '.'; - var lk = lockWords(cd), capW = cd.lock_w > 0 ? cd.lock_w : (cd.sweep_watts > 0 ? cd.sweep_watts : 0), knee = lk.kneeMhz > 0 ? ' Knee at ' + withCommas(lk.kneeMhz) + ' MHz.' : ''; + // the cap is the slider's want (power_default_w × pct), never the draw (cd.lock_w is what the card draws at the lock) + var lk = lockWords(cd), capW = cd.power_default_w > 0 ? Math.round(cd.power_default_w * (cd.power_pct || 80) / 100) : 0, knee = lk.kneeMhz > 0 ? ' Knee at ' + withCommas(lk.kneeMhz) + ' MHz.' : ''; if (lk.kind === 'searching') return 'Locking clocks: step ' + (cd.lock_step || 1) + ' of ' + cd.lock_steps + '.'; if (lk.kind === 'locked') return 'Locked at ' + withCommas(cd.lock_mhz) + ' MHz' + (capW > 0 ? ', cap ' + Math.round(capW) + ' W' : '') + '.' + knee; if (lk.kind === 'unlocked') return 'Clock unlocked' + (capW > 0 ? ', cap ' + Math.round(capW) + ' W' : '') + '.' + knee; @@ -981,7 +987,7 @@ var View = (function () { var st = (s && s.settings) || {}, cards = shownCards(((s && s.mining) || {}).cards); return !st.power_control && cards.some(function (c) { return c.vendor === 'nvidia' && present(c); }) ? 'Power control is off, so NVIDIA cards are measured and never set. Turn it on in Settings.' : ''; } - var MINERS_SENTENCE = 'Each one votes and is paid on its own. 8 for a big card, 2 for a small one, 1 for a built-in GPU.'; + var MINERS_SENTENCE = 'Each one votes and is paid on its own. 8 for a big card, 2 for a small one, 1 for a built-in\u00a0GPU.'; // the last two words glued (copy law 18) var CURVE_CAPTION = 'Hash rate against power for every step of the last measurement. The ringed point is the one Ember kept; a hollow point was rejected; the dashed point is the knee.'; function tierButtonsHtml(tiers, label) { return (tiers || []).map(function (t) { return ''; }).join(''); @@ -1447,42 +1453,56 @@ var View = (function () { 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. + // app-ia-26 (the founder's audit, 4.3): what this machine earned, measured. The windows come from the engine's earned + // sums (state.ladder.earned, earned-windows-26: what the chain paid at the time, never today's reward applied to old + // blocks); an engine without them reads "reading" with the block counts. Electricity is one row, Payouts one line, + // Standing one row (the rungs sit behind Details on the page), and the projection is one dim foot line that hides, + // with its reason, under a remote job, a pause, a still chain, no card mining or an unread network rate. + function windowOf(w, blocksOnly) { + w = w || null; + var blocks = w ? (w.blocks || 0) : (blocksOnly || 0), shards = w ? (w.shards || 0) : 0, ign = w && w.ign >= 0 ? w.ign : null; + var parts = []; + if (!blocks && !shards) parts.push('no block yet'); + else { parts.push(withCommas(blocks) + ' block' + (blocks === 1 ? '' : 's')); if (shards) parts.push(withCommas(shards) + ' shard' + (shards === 1 ? '' : 's')); } + if (w && w.priced === false) parts.push('one block not priced yet'); + return { ign: ign, blocks: blocks, shards: shards, text: ign === null ? 'reading' : (ign > 0 ? fmtIgn(ign) : '0 IGN'), sub: parts.join(' · '), field: 'ladder.earned: the coinbase and shard records the node credited, at the reward each paid' }; + } 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, 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); + s = s || {}; var m = s.mining || {}, ld = s.ladder || {}, n = s.node || {}, j = s.jobs || {}, a = s.address || {}, c = chainNums(chain), ea = ld.earned || null; var out = {}; - // 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; - // 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 + // an engine before the windows: the counts come from the ladder's own record, never zero (blocks_24h, else the block + // list filtered to the day; the day ring for 7 days; the lifetime count for all time) 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'; - 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 / 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 }; + var day7 = (ld.days || []).filter(function (d) { return (d.day + 1) * 86400 > now - 7 * 86400; }).reduce(function (a, d) { return a + (d.blocks || 0); }, 0); + out.h24 = windowOf(ea && ea.h24, day24); out.d7 = windowOf(ea && ea.d7, day7); out.all = windowOf(ea && ea.all, m.accepted_total || 0); + if (ea && ea.estimated && out.all.blocks) out.all.sub += ' · estimated'; + out.headline = { text: out.h24.text, eyebrow: 'last 24 hours', sub: out.h24.sub, field: out.h24.field }; + // electricity: one row (A4: one sentence for the absence) + var watts = fleetWatts(m, m.cards), ppd = fleetPounds(m, m.cards, pence), cards = (m.cards || []).filter(present); + if (!(watts > 0)) out.cost = { text: cards.some(function (x) { return x.vendor === 'apple'; }) ? 'No power reading on Apple silicon.' : 'No power reading.', sub: '', field: FIELDS.watts }; + else if (ppd === null) out.cost = { text: Math.round(watts) + ' W', sub: 'Set a price in Settings for the cost.', field: FIELDS.watts }; + else out.cost = { text: money(ppd) + ' a day', sub: 'at ' + (pence / priceFactor(currency.code)) + (minor || minorOf(currency.code).abbr) + '/kWh for ' + Math.round(watts) + ' W', field: FIELDS.watts + ' × 24 × the price in Settings' }; + // payouts + out.payouts = { text: a.display ? 'Pays ' + a.display.slice(0, 6) + '…' + a.display.slice(-4) : 'No address yet', full: a.display || '', sub: a.display ? 'Your balance is in the wallet. Change the address in Settings.' : 'Finish the setup to choose one.' }; + // standing: the rank and the signing points, the window in the sub + var d = daysIn(ld, c.windowS, now), ww = windowWords(c.windowS), streak = ld.streak_s || 0; + var rank = 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') : 'Standing: not read'; + out.standing = { text: c.best ? rank + ' · ' + (streak > 0 ? 'signing 8 of 80 points' : 'no signing points yet') : rank, sub: c.ok ? 'weight is your blocks over the last ' + ww.text + ', ' + d.have + ' of ' + d.of + ' ' + d.unit + ' mined' : (c.error || ''), field: FIELDS.rank + ' · ' + FIELDS.days + (c.source === 'observer' ? ' · through igneum.network’s observer until this node answers igneum_getFinalityWeights' : '') }; + // the projection: one line, or its reason + var mhs = m.hash_total || 0, expected = ignPerDay(mhs, c.hps, c.minerIgn, c.bps), held = j.active || m.state === 'held' || (m.cards || []).some(function (cd) { return cd.state === 'held'; }); + out.projection = null; out.projectionNote = ''; + if (held) out.projectionNote = 'Projection paused: ' + (j.active && j.title ? j.title + ' running since ' + hhmm(j.started_at) : 'a job from the team running') + '.'; + else if (n.tip_age_s >= NETWORK_PAUSED_S) out.projectionNote = 'Network paused: no block on the chain for ' + spanWords(n.tip_age_s) + '.'; + else if (m.paused || m.state === 'paused') out.projectionNote = ''; + else if (m.state !== 'mining' || !(mhs > 0)) out.projectionNote = ''; + else if (expected === null || !c.hps) out.projectionNote = ''; + else out.projection = 'About ' + fmtIgn(expected) + ' a day at ' + (mhs >= 100 ? mhs.toFixed(0) : mhs.toFixed(1)) + ' MH/s, ' + pct(mhs * 1e6 / c.hps) + ' of the network.'; + out.projectionField = FIELDS.share + ' × 86,400 × ' + FIELDS.reward; return out; } + // the rail foot (T-R11): the machine's name and its uptime; the address lives in Settings and on the Payouts line + function railFoot(s) { s = s || {}; return { name: s.display_name || '', up: 'up ' + uptimeFoot(s.uptime_s) }; } + function uptimeFoot(u) { u = Math.max(0, Math.round(u || 0)); var h = Math.floor(u / 3600), mi = Math.floor(u % 3600 / 60); return h ? h + ' h ' + mi + ' min' : mi + ' min'; } // the first hour as this machine lived it: steps with unix times (0 = not yet), from the settings' install stamp, // the ladder's marks and the engine's events function timeline(s, now) { @@ -1533,7 +1553,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 { hhmm: hhmm, NETWORK_PAUSED_S: NETWORK_PAUSED_S, tuneLine: tuneLine, capSentence: capSentence, powerNote: powerNote, tuneCardHtml: tuneCardHtml, tierButtonsHtml: tierButtonsHtml, curveHtml: curveHtml, MINERS_SENTENCE: MINERS_SENTENCE, CURVE_CAPTION: CURVE_CAPTION, 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, lockWords: lockWords, kneeWords: kneeWords, TIERS: TIERS, DEFAULT_PRICE_MINOR: DEFAULT_PRICE_MINOR, tierOf: tierOf, tierName: tierName, priceOrDefault: priceOrDefault, 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, 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, railFoot: railFoot, NETWORK_PAUSED_S: NETWORK_PAUSED_S, tuneLine: tuneLine, capSentence: capSentence, powerNote: powerNote, tuneCardHtml: tuneCardHtml, tierButtonsHtml: tierButtonsHtml, curveHtml: curveHtml, MINERS_SENTENCE: MINERS_SENTENCE, CURVE_CAPTION: CURVE_CAPTION, 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, lockWords: lockWords, kneeWords: kneeWords, TIERS: TIERS, DEFAULT_PRICE_MINOR: DEFAULT_PRICE_MINOR, tierOf: tierOf, tierName: tierName, priceOrDefault: priceOrDefault, 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, 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 @@ -1800,7 +1820,8 @@ if (typeof document !== 'undefined') (function () { function disclosure(btnId, detId, onOpen) { $(btnId).addEventListener('click', function () { var open = $(detId).hidden; setOpen($(btnId), $(detId), open); if (open && onOpen) onOpen(); }); } - disclosure('node-toggle', 'node-details'); + // the node line on Mine opens Settings > Node with its facts open + $('node-go').addEventListener('click', function () { showPage('settings'); var d = $('node-details'); if (d) { d.open = true; d.scrollIntoView({ block: 'start' }); } }); disclosure('pv-toggle', 'pv-details'); disclosure('addr-toggle', 'addr-details', function () { $('s-address-input').focus(); }); disclosure('key-toggle', 'key-details'); @@ -1888,10 +1909,8 @@ if (typeof document !== 'undefined') (function () { function powerControl(on) { api('api/power/control', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Power control on. Windows asks for administrator rights once' : 'Power control off. Nothing asks'); }); } $('s-power-control').addEventListener('change', function () { powerControl(this.checked); }); $('m-clock-sync').addEventListener('click', function () { api('api/clock/sync', {}); }); - $('btn-open-log').addEventListener('click', function () { setDrawer(true); }); // ---------- Earnings ---------- - $('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(); if (!validAddr(a)) { $('s-address-err').textContent = 'An address is 0x followed by 40 hex characters.'; $('s-address-err').hidden = false; return; } @@ -1909,6 +1928,7 @@ if (typeof document !== 'undefined') (function () { function hideKey() { $('s-key-box').hidden = true; $('s-key').textContent = ''; $('ask-key').hidden = false; } $('s-hide').addEventListener('click', hideKey); $('ask-key-no').addEventListener('click', function () { setOpen($('key-toggle'), $('key-details'), false); }); + $('s-wallet').addEventListener('click', function () { api('api/open', { url: 'https://igneum.network/wallet' }); }); $('r-wallet').addEventListener('click', function () { api('api/open', { url: 'https://igneum.network/wallet' }); }); // ---------- Prove ---------- @@ -2352,8 +2372,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(); }); } - $('ls-more').addEventListener('click', function () { showPage('earnings'); }); - disclosure('tl-toggle', 'tl-details'); $('s-profile').addEventListener('change', function () { var on = this.checked; api('api/settings', { profile_public: on }).then(function (r) { if (r.ok) toast(on ? 'Your page is public from the next site read' : 'Your page is unlisted again'); else { toast(r.error || 'could not change'); $('s-profile').checked = !on; } }).catch(function () { toast('This engine has no profile switch yet'); $('s-profile').checked = !on; }); }); // the block card's buttons // first-block-21: the card is spent once a window has shown it. The first render in a visible window reports it to @@ -2437,26 +2455,13 @@ if (typeof document !== 'undefined') (function () { setText('bc-hash', 'hash ' + bc.hashShort); $('bc-hash').title = bc.hash; $('bc-open').disabled = !bc.url; $('bc-copy').disabled = !bc.url; } - var rungs = View.ladderRungs(s, chain, now), cur = View.currentRung(rungs), hasCards = (m.cards || []).length > 0; - setHidden('ladder-strip', !hasCards); - var sig = JSON.stringify([rungs.map(function (r) { return r.state; }), cur.id, cur.line, cur.sub]); - if (sig !== stripSig) { - stripSig = sig; - $('ls-rungs').innerHTML = rungs.map(function (r) { return '' + esc(r.name) + ''; }).join(''); - setText('ls-line', cur.line); setText('ls-sub', cur.sub); $('ls-line').title = cur.field; - } } - function renderLadderCard(s) { + // the rungs behind the Standing row's Details (A17: no intro; the source on hover) + function renderStanding(s) { var rungs = View.ladderRungs(s, chain, s.now), c = View.chainNums(chain); - setText('ladder-source', c.source === 'node' ? 'from your node' : c.source === 'observer' ? 'through the observer' : (c.error ? 'not read' : 'reading')); - $('ladder-source').title = c.source === 'observer' ? 'getFinalityWeights relayed by igneum.network/api/live until this node answers igneum_getFinalityWeights; keys cut to 64' : c.source === 'node' ? 'igneum_getFinalityWeights on this machine’s node' : (c.error || ''); + $('standing-details').title = c.source === 'observer' ? 'getFinalityWeights relayed by igneum.network/api/live until this node answers igneum_getFinalityWeights; keys cut to 64' : c.source === 'node' ? 'igneum_getFinalityWeights on this machine’s node' : (c.error || ''); var sig = JSON.stringify(rungs); if (sig !== rungsSig) { rungsSig = sig; $('rungs').innerHTML = rungs.map(rungHtml).join(''); } - var tl = View.timeline(s, s.now), done = tl.steps.filter(function (x) { return x.at > 0; }).length; - setText('tl-sum', done + ' of ' + tl.steps.length + (tl.steps[4].rel ? ' · first block ' + tl.steps[4].rel : '')); - $('tl-steps').innerHTML = tl.steps.map(function (x) { return '
  • ' + esc(x.name) + '' + esc(x.at > 0 ? (x.rel || clock(x.at)) : '') + '' + (x.note ? '' + esc(x.note) + '' : '') + '
  • '; }).join(''); - $('tl-steps').title = tl.field; - setText('tl-note', 'Times from install (' + (tl.base > 0 ? whenWords(tl.base) : 'not recorded') + '). The site’s /miner page shows the same steps measured on fresh installs.'); } // ---------- render ---------- // the vendor mark and the name block come from View.rowHead (one mark per row, never two) @@ -2565,17 +2570,18 @@ if (typeof document !== 'undefined') (function () { var m = s.mining, n = s.node, p = s.program; var t = View.toggle(m, n, s), tb = $('btn-toggle'); tb.className = 'toggle-big' + (t.cls ? ' ' + t.cls : ''); tb.disabled = t.disabled; - setText('btn-toggle-text', t.label); setText('btn-toggle-sub', t.sub); + setText('btn-toggle-text', t.label); tb.title = t.sub; var shown = shownCards(m.cards), presentCards = shown.filter(View.present); var mining = presentCards.filter(function (c) { return c.state === 'mining'; }).length; // the live total: rate, watts, £ a day, blocks setText('t-hash', m.hash_total >= 100 ? m.hash_total.toFixed(0) : m.hash_total.toFixed(1)); setText('t-hash-sub', m.state === 'paused' ? 'paused' : (s.heat && s.heat.on && s.heat.phase === 'resting' && !mining) ? 'resting · heat mode' : mining ? (mining + ' of ' + presentCards.length + ' card' + (presentCards.length === 1 ? '' : 's') + ' mining') : m.state === 'waiting' ? (n.synced ? 'card starting' : 'waiting for the node') : m.state === 'idle' ? 'no card mining' : m.state); - var watts = View.fleetWatts(m, m.cards), price = priceNow(), eff = watts > 0 && m.hash_total > 0 ? View.effText(m.hash_total / watts) : ''; - setHidden('t-power-cell', !(watts > 0)); setText('t-power', String(Math.round(watts))); setText('t-power-sub', eff || 'drawn now'); - var ppd = View.fleetPounds(m, m.cards, price); - setHidden('t-money-cell', !(watts > 0)); setText('t-money', ppd === null ? 'Set a price' : money(ppd)); $('t-money').classList.toggle('ask-price', ppd === null); setText('t-money-sub', ppd === null ? 'in Settings, per kWh' : 'electricity at ' + (price / View.priceFactor(View.currencyNow().code)) + View.minorOf(View.currencyNow().code).abbr + '/kWh'); - setText('t-blocks', withCommas(m.accepted_session)); setText('t-blocks-sub', 'this run · ' + withCommas(m.accepted_total) + ' lifetime' + (m.rejected_session ? ' · ' + m.rejected_session + ' rejected' : '')); + // the four tiles (4.1 item 2): MH/s, W, MH/W, IGN in the last 24 hours (the engine's earned window) + var watts = View.fleetWatts(m, m.cards), eff = watts > 0 && m.hash_total > 0 ? View.effText(m.hash_total / watts) : ''; + setHidden('t-power-cell', !(watts > 0)); setText('t-power', String(Math.round(watts))); + setHidden('t-eff-cell', !eff); setText('t-eff', eff.replace(' MH/W', '')); + var ew = View.earningsLines(s, chain, 0, s.now).h24; + setText('t-ign', ew.ign === null ? 'reading' : ew.ign > 0 ? View.fmtIgn(ew.ign).replace(' IGN', '') : '0'); setText('t-ign-sub', ew.ign === null ? 'last 24 hours' : ew.blocks || ew.shards ? 'last 24 hours' : 'no block yet'); $('t-ign').title = ew.field; // the rows: every card, on or off; rebuilt only when their words change and never under a hand on a slider var rows = shown.map(View.cardRow), now = Date.now(); rows.forEach(function (r) { var pd = pending[r.key]; if (pd) { if (pd.on === r.on || now - pd.at > 8000) delete pending[r.key]; else { r.on = pd.on; r.word = pd.on ? 'starting' : 'stopping'; r.tone = ''; } } }); @@ -2593,13 +2599,11 @@ if (typeof document !== 'undefined') (function () { $('d-cards-note').hidden = !note; setText('d-cards-note', note); renderNode(s); if (page === 'mine') renderFirstMonth(s); - // activity: the counts, the strip, the newest 8 events in a user's words - var nowU = s.now, f10 = m.found.filter(function (x) { return nowU - x <= 600; }).length; - setText('d-stats', f10 + ' block' + (f10 === 1 ? '' : 's') + ' in 10 min · ' + m.found.length + ' in the last hour' + (m.fee_total ? ' · ' + withCommas(m.fee_total) + ' to the dev fee' : '')); + // activity: the newest five events in a person's words (A15: no counts line) var key = s.events.length ? s.events[0].t + ':' + s.events.length : ''; if (key !== lastEventsKey) { lastEventsKey = key; - $('d-events').innerHTML = s.events.length ? s.events.slice(0, 8).map(function (e) { var pe = View.plainEvent(e.text); return '' + esc(pe.text) + '' + clock(e.t) + ''; }).join('') : '
    No events yet. The node and the miner report here as they start.
    '; + $('d-events').innerHTML = s.events.length ? s.events.slice(0, 5).map(function (e) { var pe = View.plainEvent(e.text); return '' + esc(pe.text) + '' + clock(e.t) + ''; }).join('') : '
    No events yet. The node and the miner report here as they start.
    '; } } // the Tuning card on Mine and the Tuning rows on Settings share the goal, the schedule and the Power control switch @@ -2631,9 +2635,10 @@ if (typeof document !== 'undefined') (function () { function renderNode(s) { var n = s.node, f = s.finality, p = s.program, eta = syncEta(n, s.now); var nl = View.nodeLine(n, s.clock, eta), fw = View.finalityWords(f); - setText('node-line-text', nl.text); setText('node-sub', fw.paused && nl.tone !== 'bad' ? fw.note : nl.sub); + setText('node-line-text', nl.text); $('node-go').title = fw.paused && nl.tone !== 'bad' ? fw.note : nl.sub; $('node-dot').className = 'dot' + (nl.tone === 'ok' ? ' live' : nl.tone === 'bad' ? ' bad' : ''); - $('node-card').classList.toggle('bad', nl.tone === 'bad'); + $('node-go').classList.toggle('bad', nl.tone === 'bad'); + setText('s-node-line', nl.text + (n.blocks > 0 ? ' · ' + withCommas(n.blocks) + ' blocks' : '')); setText('s-node-sub', fw.paused && nl.tone !== 'bad' ? fw.note : nl.sub); setText('n-state-v', nl.text.replace(/^Node /, '')); setText('n-state-m', nl.sub + (n.override_restart_wait ? ' ' + n.override_restart_wait + '.' : '')); var ns = View.nodeSource(n); setText('n-source', ns.text); setText('n-source-m', ns.sub); setHidden('n-source', !ns.text); setText('n-blocks', withCommas(n.blocks)); setText('n-blocks-sub', View.heightLine(n)); @@ -2660,15 +2665,18 @@ if (typeof document !== 'undefined') (function () { // ---------- Earnings ---------- function renderEarnings(s) { - 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; - $('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-head', el.headline.text); setText('e-head-sub', el.headline.sub); $('e-head').title = el.headline.field; + ['d7', 'all'].forEach(function (k) { setText('e-' + k, el[k].text); setText('e-' + k + '-sub', el[k].sub); $('e-' + k).title = el[k].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-pays', el.payouts.text); setText('e-pays-sub', el.payouts.sub); setText('e-pays-full', el.payouts.full || 'not set'); + setText('e-standing', el.standing.text); setText('e-standing-sub', el.standing.sub); $('e-standing').title = el.standing.field; + var pj = el.projection || el.projectionNote; setHidden('e-projection', !pj); setText('e-projection', pj || ''); $('e-projection').title = el.projection ? el.projectionField : ''; + renderStanding(s); + } + // Settings > Wallet: the address, its source, the key (app-ia-26, 4.4 item 1; the card moved from Earnings) + function renderWallet(s) { + var a = s.address || {}; 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.'); @@ -2688,7 +2696,7 @@ if (typeof document !== 'undefined') (function () { setHidden('shard-card', !sh); if (sh) { setText('sc-when', clock(sh.at)); setText('sc-title', 'Your card proved shard ' + sh.shard + ' of block ' + withCommas(sh.block) + '.'); setText('sc-line', View.fmtIgn(View.ign(sh.wei)) + ' paid for it \u00b7 ' + withCommas(pv.paid || 0) + ' shard' + (pv.paid === 1 ? '' : 's') + ' paid on this machine'); setText('sc-sub', '20 points of every block pay the provers, split by shard. A paid record is a chain fact: anyone can read it.'); $('sc-title').title = View.FIELDS.shards; $('sc-line').title = 'proving.paid_wei, proving.paid'; } if (document.activeElement !== $('s-prove')) $('s-prove').checked = enabled; - var pi = proveInsteadWords(pv, s.settings); + var pi = View.proveInsteadWords(pv, s.settings); $('pv-instead-row').hidden = !pi.show; if (pi.show) { $('pv-instead-help').textContent = pi.help; if (document.activeElement !== $('s-prove-instead')) $('s-prove-instead').checked = pi.on; } var w = View.proveWords(pv, enabled, s.node.synced); @@ -2712,6 +2720,8 @@ if (typeof document !== 'undefined') (function () { var jobsSig = ''; function renderSettings(s) { renderProvingSection(s); + renderWallet(s); + renderNode(s); // the Node card's line and facts live here now (app-ia-26, 4.4 item 2) var st = s.settings || {}, sw = function (id, on) { if (document.activeElement !== $(id)) $(id).checked = !!on; }; sw('s-vote', st.vote); sw('s-login', st.start_at_login); sw('s-jobs-allow', s.jobs && s.jobs.allowed); sw('s-trust', st.proof_verify_trust); sw('s-power-control', st.power_control); sw('s-profile', st.profile_public); setText('s-profile-text', View.profileWords(!!st.profile_public)); @@ -2738,7 +2748,7 @@ if (typeof document !== 'undefined') (function () { 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.'); + setText('s-ui-line', iw.line || 'Interface'); setText('s-ui-help', iw.help || ''); setHidden('s-ui-help', !iw.help); sw('s-ui-builtin', st.ui_builtin); var u = s.update || {}; setText('s-version', 'Igneum Miner ' + s.version + (s.preview ? ' ' + s.preview : '')); @@ -2771,7 +2781,7 @@ if (typeof document !== 'undefined') (function () { pill.classList.remove('on', 'warn'); if (pw.tone === 'on') pill.classList.add('on'); else if (pw.tone === 'bad') pill.classList.add('warn'); $('pill-text').textContent = pw.text; - var foot = (s.display_name ? '' + esc(s.display_name) + '
    ' : '') + (s.address.display ? 'pays ' + esc(s.address.display.slice(0, 6) + '…' + s.address.display.slice(-4)) : 'no address yet') + '
    up ' + uptime(s.uptime_s); + var rf = View.railFoot(s), foot = (rf.name ? '' + esc(rf.name) + '
    ' : '') + esc(rf.up); if (foot !== lastFoot) { lastFoot = foot; $('rail-status').innerHTML = foot; } var fv = 'v' + esc(s.version) + (s.preview ? ' ' + esc(s.preview) : '') + ' · ' + esc(s.chain || s.network || '') + ''; if (fv !== lastVersion) { lastVersion = fv; $('foot-version').innerHTML = fv; } @@ -2996,13 +3006,15 @@ if (typeof document !== 'undefined') (function () { uiReloadIfChanged(s); perf.render += performance.now() - a; perf.polls++; if (failures === 0 && $('pill').classList.contains('away')) { $('pill').classList.remove('away', 'warn'); } - }).catch(function () { + }).catch(function (e) { + // a render that throws is a bug in this window, never the engine's silence: say so in the console (app-ia-26) + if (e && e.stack && typeof console !== 'undefined') console.error('render failed: ' + e.stack); failures++; if (failures >= 4) { $('pill').classList.add('warn', 'away'); $('pill-text').textContent = state && state.quitting ? 'Stopped' : 'App not answering'; if (phase === 'dashboard') { - setText('node-line-text', 'Node: no answer'); setText('node-sub', 'the app is not answering. The window reconnects by itself'); $('node-card').classList.add('bad'); - setText('btn-toggle-sub', 'the app is not answering'); $('btn-toggle').disabled = true; + setText('node-line-text', 'Node: no answer'); $('node-go').title = 'the app is not answering. The window reconnects by itself'; $('node-go').classList.add('bad'); + $('btn-toggle').title = 'the app is not answering'; $('btn-toggle').disabled = true; setText('t-hash-sub', 'no answer from the app'); } } diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index b7dfab7ef..89831b6f5 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -116,7 +116,7 @@ - +
    @@ -221,13 +221,13 @@ +
    0MH/swaiting
    -
    0Wdrawn now
    -
    £0.00a dayelectricity
    -
    0blocksthis run
    +
    0Wdrawn now
    +
    0MH/Whashes per watt
    +
    readingIGNlast 24 hours
    @@ -247,10 +247,6 @@
    -
    @@ -306,43 +302,11 @@
    -
    - - -
    + +
    -

    Activity

    +

    Activity

    No events yet.
    @@ -369,39 +333,36 @@ -
    -

    Rewards address

    + + + +
    - - +
    + +

    Node

    +

    +

    Node starting

    +

    +
    + + +
    Details +
    +
    state
    +
    whose node
    +
    height0blocks this node holds
    +
    peers0other nodes it talks to
    +
    version--devnet v4
    +
    headers0headers arrive before blocks
    +
    network blocks0blocks the whole network has made (the DAA score)
    +
    difficulty0how hard the next block is to find
    +
    tips0open ends of the chain right now
    +
    blue score0blocks on the agreed main chain (the blue score)
    +
    next program--:--the hourly mining program
    +
    last checkpointnone yeta point the miners agreed can never be undone
    +
    votes sent0this machine signs a checkpoint every 30 s (a point the miners agree can never be undone)
    +
    +
    +
    rules fingerprint
    +
    not printed yet
    +

    Every node on the network shows the same fingerprint of the rules. A peer with another one is refused.

    +
    +
    +
    next rule change
    +
    none planned
    +

    +
    +
    +
    +
    + + +
    +
    +
    +

    Igneum Miner

    +

    Not checked yet.

    +
    +
    + + +
    +
    + + +

    Interface 1.0.0, built in

    +

    Small changes to this window arrive over the air, signed by Igneum.

    +

    + +
    - - ', 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(); @@ -942,53 +942,7 @@ test('no price input exists: no £ per IGN field, no "Use it", no remembered pri 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.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'); - 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-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.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', () => { - 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, undefined, 'the Earnings lines carry no fee words'); - const ec = earningsCard(), three = ec.slice(ec.indexOf('class="earn-three"'), ec.indexOf('\n ', 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'); + 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 row'); }); test('the "Prove instead of mining" row shows only on an under-12 GB machine and names the choice (main, 7 October 2026)', () => { @@ -1302,43 +1256,7 @@ test('the tiers from the team\'s table read as the table\'s until the card is me 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'); -}); +// the Earnings tab's two truths of earnings-26 (the paused projection, the measured 24 hours) live in once.test.mjs (T-R19, T-A1) 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'); diff --git a/docs/design/app-screens/0.3.26/earnings-dark.png b/docs/design/app-screens/0.3.26/earnings-dark.png new file mode 100644 index 000000000..958b6296d Binary files /dev/null and b/docs/design/app-screens/0.3.26/earnings-dark.png differ diff --git a/docs/design/app-screens/0.3.26/earnings-job-dark.png b/docs/design/app-screens/0.3.26/earnings-job-dark.png new file mode 100644 index 000000000..4d6921b87 Binary files /dev/null and b/docs/design/app-screens/0.3.26/earnings-job-dark.png differ diff --git a/docs/design/app-screens/0.3.26/earnings-light.png b/docs/design/app-screens/0.3.26/earnings-light.png new file mode 100644 index 000000000..59708b52a Binary files /dev/null and b/docs/design/app-screens/0.3.26/earnings-light.png differ diff --git a/docs/design/app-screens/0.3.26/earnings-still-dark.png b/docs/design/app-screens/0.3.26/earnings-still-dark.png new file mode 100644 index 000000000..479f72f73 Binary files /dev/null and b/docs/design/app-screens/0.3.26/earnings-still-dark.png differ diff --git a/docs/design/app-screens/0.3.26/mine-dark.png b/docs/design/app-screens/0.3.26/mine-dark.png new file mode 100644 index 000000000..b88453896 Binary files /dev/null and b/docs/design/app-screens/0.3.26/mine-dark.png differ diff --git a/docs/design/app-screens/0.3.26/mine-first-minute-dark.png b/docs/design/app-screens/0.3.26/mine-first-minute-dark.png new file mode 100644 index 000000000..0a1af5445 Binary files /dev/null and b/docs/design/app-screens/0.3.26/mine-first-minute-dark.png differ diff --git a/docs/design/app-screens/0.3.26/mine-light.png b/docs/design/app-screens/0.3.26/mine-light.png new file mode 100644 index 000000000..b84b51466 Binary files /dev/null and b/docs/design/app-screens/0.3.26/mine-light.png differ diff --git a/docs/design/app-screens/0.3.26/mine-syncing-dark.png b/docs/design/app-screens/0.3.26/mine-syncing-dark.png new file mode 100644 index 000000000..6cf6f25d8 Binary files /dev/null and b/docs/design/app-screens/0.3.26/mine-syncing-dark.png differ diff --git a/docs/design/app-screens/0.3.26/settings-dark.png b/docs/design/app-screens/0.3.26/settings-dark.png new file mode 100644 index 000000000..924e4a352 Binary files /dev/null and b/docs/design/app-screens/0.3.26/settings-dark.png differ diff --git a/docs/design/app-screens/0.3.26/settings-light.png b/docs/design/app-screens/0.3.26/settings-light.png new file mode 100644 index 000000000..4c67ee4a6 Binary files /dev/null and b/docs/design/app-screens/0.3.26/settings-light.png differ diff --git a/docs/design/app-screens/0.3.26/tune-dark.png b/docs/design/app-screens/0.3.26/tune-dark.png new file mode 100644 index 000000000..d9a90946f Binary files /dev/null and b/docs/design/app-screens/0.3.26/tune-dark.png differ diff --git a/docs/design/app-screens/0.3.26/tune-first-minute-dark.png b/docs/design/app-screens/0.3.26/tune-first-minute-dark.png new file mode 100644 index 000000000..47d6c3cff Binary files /dev/null and b/docs/design/app-screens/0.3.26/tune-first-minute-dark.png differ diff --git a/docs/design/app-screens/0.3.26/tune-light.png b/docs/design/app-screens/0.3.26/tune-light.png new file mode 100644 index 000000000..c100aa9d5 Binary files /dev/null and b/docs/design/app-screens/0.3.26/tune-light.png differ diff --git a/docs/design/app-screens/0.3.26/tune-table-dark.png b/docs/design/app-screens/0.3.26/tune-table-dark.png new file mode 100644 index 000000000..7af80867b Binary files /dev/null and b/docs/design/app-screens/0.3.26/tune-table-dark.png differ diff --git a/tools/ci/pre-push.sh b/tools/ci/pre-push.sh index 3e42c9701..deb27a994 100755 --- a/tools/ci/pre-push.sh +++ b/tools/ci/pre-push.sh @@ -109,11 +109,11 @@ tree_checks() { run "the interface and the app entries are one UI tree (version pair; self-test, then the staged manifest when one is given)" bash -c 'node tools/ui-ota/pair-check.mjs --self-test && { [ -z "${IGNEUM_MANIFEST:-}" ] || node tools/ui-ota/pair-check.mjs "$IGNEUM_MANIFEST"; }' run "heat gate reader self-test (a known hold passes, a known drift fails)" node tools/heat-gate.mjs --self-test run "relay unit tests" node --test relay/test/parse.test.mjs relay/test/auth.test.mjs relay/test/wake.test.mjs relay/test/ember.test.mjs - run "miner app notice strip and update card tests" node --test app/igneum-app/tiers/class-v5-tiers.test.mjs app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/once.test.mjs app/igneum-app/ui/tune-line.test.mjs app/igneum-app/ui/ui-ota.test.mjs app/igneum-app/ui/fold.test.mjs + run "miner app notice strip and update card tests" node --test app/igneum-app/tiers/class-v5-tiers.test.mjs app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/once.test.mjs app/igneum-app/ui/tune-line.test.mjs app/igneum-app/ui/heat-region.test.mjs app/igneum-app/ui/fold.test.mjs app/igneum-app/ui/ui-ota.test.mjs app/igneum-app/ui/ui-ota.test.mjs app/igneum-app/ui/fold.test.mjs run "the opening window size: the one rule for both hosts (primary work area, 1920 and 16:10 caps, a remembered frame kept only when it fits)" bash -c 'cc -std=c99 -Wall -Wextra -o "${TMPDIR:-/tmp}/opening_size_test.$$" app/windows/opening_size_test.c && "${TMPDIR:-/tmp}/opening_size_test.$$"; rc=$?; rm -f "${TMPDIR:-/tmp}/opening_size_test.$$"; exit $rc' run "the Intel rotate-fold rewrite: Intel gets the shift form, AMD and NVIDIA untouched (proto-opencl/test_intel_rotr.c)" bash -c 'cc -std=c99 -Wall -Wextra -o "${TMPDIR:-/tmp}/test_intel_rotr.$$" proto-opencl/test_intel_rotr.c && "${TMPDIR:-/tmp}/test_intel_rotr.$$"; s=$?; rm -f "${TMPDIR:-/tmp}/test_intel_rotr.$$"; exit $s' run "no secret file names and no 64-hex secrets in the tree" bash -c 'bash tools/ci/no-secrets-check.sh --self-test && bash tools/ci/no-secrets-check.sh' - run "miner app notice strip and update card tests" node --test app/igneum-app/tiers/class-v5-tiers.test.mjs app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/once.test.mjs app/igneum-app/ui/tune-line.test.mjs + run "miner app notice strip and update card tests" node --test app/igneum-app/tiers/class-v5-tiers.test.mjs app/igneum-app/ui/notices.test.mjs app/igneum-app/ui/update-card.test.mjs app/igneum-app/ui/view.test.mjs app/igneum-app/ui/once.test.mjs app/igneum-app/ui/tune-line.test.mjs app/igneum-app/ui/heat-region.test.mjs app/igneum-app/ui/fold.test.mjs app/igneum-app/ui/ui-ota.test.mjs run "launch gates: every row with its check, the handoff text clean (self-test, then the tree)" bash -c 'node tools/ci/launch-gates-check.mjs --self-test && node tools/ci/launch-gates-check.mjs' run "income per tier: the public table equals its inputs, the schedule arithmetic" bash -c 'node tools/launch/income-tiers.mjs --check && node --test tools/launch/income-tiers.test.mjs' run "hash-origin report: a known-finished day and a known-failed day" node --test tools/observer/hash-origin.test.mjs diff --git a/tools/ui-mock/server.mjs b/tools/ui-mock/server.mjs index 6254cb161..eeacc5b75 100644 --- a/tools/ui-mock/server.mjs +++ b/tools/ui-mock/server.mjs @@ -131,12 +131,14 @@ function liveNow(windowS) { } // miner-ui-5: this machine's ladder record (state.ladder, src/ladder.rs) per scenario function ladderNow(t) { - const base = { first_block_at: 0, first_block_card: '', first_block_hash: '', first_block_daa: 0, days_mined_30: 0, blocks_30d: 0, blocks_today: 0, days: [], cards_first: {}, blocks: [], votes_signed: 0, last_vote_index: 0, last_vote_at: 0, last_signed_locked: 0, signed_locked_count: 0, streak_s: 0, shards: [], milestone: null, first_synced_at: t0 - 2800, first_mining_at: t0 - 2700, first_block_shown: false }; + // app-ia-26: the engine's earned windows (earned-windows-26) on every scenario, the recorded machine's day (253 blocks, 3 shards), week and lifetime (8,054 blocks) at 4.88 IGN + const earnedBase = { h24: { wei: '1234640000000000000000', ign: 1234.64, blocks: 253, shards: 3, priced: true }, d7: { wei: '8930400000000000000000', ign: 8930.4, blocks: 1830, shards: 19, priced: true }, all: { wei: '39303520000000000000000', ign: 39303.52, blocks: 8054, shards: 61, priced: true }, estimated: false }; + const base = { earned: earnedBase, first_block_at: 0, first_block_card: '', first_block_hash: '', first_block_daa: 0, days_mined_30: 0, blocks_30d: 0, blocks_today: 0, days: [], cards_first: {}, blocks: [], votes_signed: 0, last_vote_index: 0, last_vote_at: 0, last_signed_locked: 0, signed_locked_count: 0, streak_s: 0, shards: [], milestone: null, first_synced_at: t0 - 2800, first_mining_at: t0 - 2700, first_block_shown: false }; const hash = '9f3a7c1e5d2b4a6f8e0c1d3b5a7f9e2c4d6b8a0f1e3c5d7b9a2f4e6c8d0b1a3c21e'; 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 === '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, earned: { h24: { wei: '14640000000000000000', ign: 14.64, blocks: 3, shards: 0, priced: true }, d7: { wei: '89200000000000000000', ign: 89.2, blocks: 18, shards: 1, priced: true }, all: { wei: '393070000000000000000', ign: 393.07, blocks: 80, shards: 6, priced: true }, estimated: false }, 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; } diff --git a/tools/ui-mock/shots-audit-26.txt b/tools/ui-mock/shots-audit-26.txt new file mode 100644 index 000000000..d70265a54 --- /dev/null +++ b/tools/ui-mock/shots-audit-26.txt @@ -0,0 +1,16 @@ +# audit-26 (app-ia-26, 8 October 2026): the four pages of the founder's audit, dark and light, the first minute and the +# measured state. One line per shot: ; tools/ui-mock/capture.sh < this file (on build-2). +mine-dark.png scenario=tiers&page=mine&theme=dark 1700 +mine-light.png scenario=tiers&page=mine&theme=light 1700 +mine-first-minute-dark.png scenario=tiers-first&page=mine&theme=dark 1700 +mine-syncing-dark.png scenario=syncing&page=mine&theme=dark 1700 +tune-dark.png scenario=tiers&page=tune&theme=dark 1300 +tune-light.png scenario=tiers&page=tune&theme=light 1300 +tune-first-minute-dark.png scenario=tiers-first&page=tune&theme=dark 1100 +tune-table-dark.png scenario=tiers-table&page=tune&theme=dark 1300 +earnings-dark.png scenario=tiers&page=earnings&theme=dark 1100 +earnings-light.png scenario=tiers&page=earnings&theme=light 1100 +earnings-job-dark.png scenario=earn-held&page=earnings&theme=dark 1100 +earnings-still-dark.png scenario=earn-still&page=earnings&theme=dark 1100 +settings-dark.png scenario=tiers&page=settings&theme=dark 2900 +settings-light.png scenario=tiers&page=settings&theme=light 2900