Merge app-ia-26 0ab19a4e into preview-26-2 (the four pages whole: Tune, Earnings on the engine's earned windows, Mine, Settings; the founder's preview 2)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
|
@ -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<u128> {
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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<f64>,
|
||||
/// 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<Earned>,
|
||||
#[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<DayBlocks>,
|
||||
pub cards_first: BTreeMap<String, f64>,
|
||||
pub blocks: Vec<BlockFound>,
|
||||
|
|
@ -163,6 +216,10 @@ pub struct LadderState {
|
|||
pub first_block_shown: bool,
|
||||
}
|
||||
|
||||
fn parse_wei(s: &str) -> u128 {
|
||||
s.trim().parse::<u128>().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<u64> {
|
|||
|
||||
#[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();
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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 '<button class="tier-b' + (t.on ? ' on' : '') + '" data-tier="' + escH(t.id) + '" role="radio" aria-checked="' + (t.on ? 'true' : 'false') + '" title="' + escH(t.note ? t.word + '. ' + t.note : t.word) + '"><span class="n">' + escH(t.name) + '</span><span class="l">' + escH(t.line) + '</span>' + (t.sub ? '<span class="s">' + escH(t.sub) + '</span>' : '') + '</button>'; }).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 '<span class="ls-rung ' + esc(r.state) + (r.id === cur.id ? ' cur' : '') + '" role="listitem" title="' + esc(r.line + ' — ' + r.field).replace(/—/, '·') + '"><i></i><span>' + esc(r.name) + '</span></span>'; }).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 '<li class="' + (x.at > 0 ? 'done' : '') + '"><span class="tl-name">' + esc(x.name) + '</span><span class="tl-at mono">' + esc(x.at > 0 ? (x.rel || clock(x.at)) : '') + '</span>' + (x.note ? '<span class="tl-note dim">' + esc(x.note) + '</span>' : '') + '</li>'; }).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 '<div' + (pe.tech ? ' title="' + esc(pe.tech) + '"' : '') + '><span><i class="k ' + esc(e.kind) + '"></i>' + esc(pe.text) + '</span><span class="t">' + clock(e.t) + '</span></div>'; }).join('') : '<div class="empty">No events yet. The node and the miner report here as they start.</div>';
|
||||
$('d-events').innerHTML = s.events.length ? s.events.slice(0, 5).map(function (e) { var pe = View.plainEvent(e.text); return '<div' + (pe.tech ? ' title="' + esc(pe.tech) + '"' : '') + '><span><i class="k ' + esc(e.kind) + '"></i>' + esc(pe.text) + '</span><span class="t">' + clock(e.t) + '</span></div>'; }).join('') : '<div class="empty">No events yet. The node and the miner report here as they start.</div>';
|
||||
}
|
||||
}
|
||||
// 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 ? '<b title="' + esc(s.display_name) + '">' + esc(s.display_name) + '</b><br>' : '') + (s.address.display ? 'pays ' + esc(s.address.display.slice(0, 6) + '…' + s.address.display.slice(-4)) : 'no address yet') + '<br>up ' + uptime(s.uptime_s);
|
||||
var rf = View.railFoot(s), foot = (rf.name ? '<b title="' + esc(rf.name) + '">' + esc(rf.name) + '</b><br>' : '') + esc(rf.up);
|
||||
if (foot !== lastFoot) { lastFoot = foot; $('rail-status').innerHTML = foot; }
|
||||
var fv = 'v' + esc(s.version) + (s.preview ? ' ' + esc(s.preview) : '') + '<span class="chain"> · ' + esc(s.chain || s.network || '') + '</span>';
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<p class="note" id="cards-note" hidden></p>
|
||||
<p class="note" id="cards-power" hidden>NVIDIA cards start at 80% of their power limit, which keeps them stable. Windows asks for administrator rights once for that. The card row has a slider.</p>
|
||||
<p class="note" id="cards-power" hidden>NVIDIA cards start at 80% of their power limit, which keeps them stable. Windows asks for administrator rights once for that.</p>
|
||||
<p class="note" id="cards-help" hidden>A built-in GPU starts off. It is slow and shares the machine's memory.</p>
|
||||
<div class="cta">
|
||||
<button class="btn primary" id="btn-cards-next" disabled>Continue</button>
|
||||
|
|
@ -221,13 +221,13 @@
|
|||
<button class="toggle-big" id="btn-toggle" disabled>
|
||||
<span class="ring"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3v9"/><path d="M6.5 6.5a8 8 0 1 0 11 0"/></svg></span>
|
||||
<span class="tl" id="btn-toggle-text">Start mining</span>
|
||||
<span class="ts" id="btn-toggle-sub">waiting for the engine</span>
|
||||
</button>
|
||||
<!-- app-ia-26 (4.1 item 2): four numbers, each with its unit and one sub-line; W and MH/W hide when no card reports power -->
|
||||
<div class="totals" id="totals">
|
||||
<div class="tot"><span class="v ember" id="t-hash">0</span><span class="k">MH/s</span><span class="s" id="t-hash-sub">waiting</span></div>
|
||||
<div class="tot" id="t-power-cell"><span class="v" id="t-power">0</span><span class="k">W</span><span class="s" id="t-power-sub">drawn now</span></div>
|
||||
<div class="tot" id="t-money-cell"><span class="v" id="t-money">£0.00</span><span class="k">a day</span><span class="s" id="t-money-sub">electricity</span></div>
|
||||
<div class="tot"><span class="v" id="t-blocks">0</span><span class="k">blocks</span><span class="s" id="t-blocks-sub">this run</span></div>
|
||||
<div class="tot" id="t-power-cell"><span class="v" id="t-power">0</span><span class="k">W</span><span class="s">drawn now</span></div>
|
||||
<div class="tot" id="t-eff-cell"><span class="v" id="t-eff">0</span><span class="k">MH/W</span><span class="s">hashes per watt</span></div>
|
||||
<div class="tot"><span class="v" id="t-ign">reading</span><span class="k">IGN</span><span class="s" id="t-ign-sub">last 24 hours</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -247,10 +247,6 @@
|
|||
<div class="row wrap"><button class="btn small primary" id="bc-open">Open in the explorer</button><button class="btn small" id="bc-save">Save the card</button><button class="btn small ghost" id="bc-copy">Copy the link</button><span class="note mono small" id="bc-saved"></span></div>
|
||||
<canvas id="bc-canvas" width="1200" height="630" hidden aria-hidden="true"></canvas>
|
||||
</div>
|
||||
<div class="card ladder-strip" id="ladder-strip" hidden>
|
||||
<div class="ls-rungs" id="ls-rungs" role="list" aria-label="The ladder"></div>
|
||||
<div class="ls-line"><span class="t" id="ls-line"></span><span class="s dim" id="ls-sub"></span><button class="btn tiny ghost" id="ls-more">The ladder</button></div>
|
||||
</div>
|
||||
|
||||
<!-- app-ia-26 (4.1 item 5): one row per card: mark, name, the kind tag, the state word, MH/s, W, °C, the switch -->
|
||||
<div class="card" id="cards-card">
|
||||
|
|
@ -306,43 +302,11 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card node-card" id="node-card">
|
||||
<button class="disclose" id="node-toggle" aria-expanded="false" aria-controls="node-details">
|
||||
<span class="node-line"><i class="dot" id="node-dot"></i><span id="node-line-text">Node starting</span></span>
|
||||
<span class="disclose-right"><span class="dim" id="node-sub"></span><span class="chev" aria-hidden="true"></span></span>
|
||||
</button>
|
||||
<div class="details" id="node-details" hidden>
|
||||
<div class="kv">
|
||||
<div><span class="k">state</span><span class="v mono" id="n-state-v"></span><span class="m" id="n-state-m"></span></div>
|
||||
<div><span class="k">whose node</span><span class="v" id="n-source"></span><span class="m" id="n-source-m"></span></div>
|
||||
<div><span class="k">height</span><span class="v mono" id="n-blocks">0</span><span class="m" id="n-blocks-sub">blocks this node holds</span></div>
|
||||
<div><span class="k">peers</span><span class="v mono" id="n-peers">0</span><span class="m" id="n-peers-sub">other nodes it talks to</span></div>
|
||||
<div><span class="k">version</span><span class="v mono" id="n-version">--</span><span class="m" id="d-node-net">devnet v4</span></div>
|
||||
<div><span class="k">headers</span><span class="v mono" id="n-headers">0</span><span class="m">headers arrive before blocks</span></div>
|
||||
<div><span class="k">network blocks</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made (the DAA score)</span></div>
|
||||
<div><span class="k">difficulty</span><span class="v mono" id="n-diff">0</span><span class="m">how hard the next block is to find</span></div>
|
||||
<div><span class="k">tips</span><span class="v mono" id="n-tips">0</span><span class="m">open ends of the chain right now</span></div>
|
||||
<div><span class="k">blue score</span><span class="v mono" id="n-blue">0</span><span class="m">blocks on the agreed main chain (the blue score)</span></div>
|
||||
<div><span class="k">next program</span><span class="v mono" id="d-eta">--:--</span><span class="m" id="d-eta-sub">the hourly mining program</span></div>
|
||||
<div><span class="k">last checkpoint</span><span class="v mono" id="f-lock">none yet</span><span class="m" id="f-note">a point the miners agreed can never be undone</span></div>
|
||||
<div><span class="k">votes sent</span><span class="v mono" id="f-votes">0</span><span class="m">this machine signs a checkpoint every 30 s (a point the miners agree can never be undone)</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">rules fingerprint</div>
|
||||
<div class="box mono"><span id="n-digest">not printed yet</span><button class="btn tiny" data-copy="n-digest">Copy</button></div>
|
||||
<p class="help">Every node on the network shows the same fingerprint of the rules. A peer with another one is refused.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">next rule change</div>
|
||||
<div class="line-text" id="n-switch">none planned</div>
|
||||
<p class="help" id="n-switch-help"></p>
|
||||
<div class="switch-list mono" id="n-switches"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- app-ia-26 (4.1 item 7): one node line; a click opens Settings > Node where the facts live -->
|
||||
<button class="node-go" id="node-go" title=""><i class="dot" id="node-dot"></i><span id="node-line-text">Node starting</span><span class="chev" aria-hidden="true"></span></button>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Activity</h3><div class="head-right"><span class="stats mono" id="d-stats"></span><button class="btn small ghost" id="btn-open-log">Open the log</button></div></div>
|
||||
<div class="card-head"><h3>Activity</h3></div>
|
||||
<div class="feed" id="d-events"><div class="empty">No events yet.</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -369,39 +333,36 @@
|
|||
|
||||
<!-- Earnings -->
|
||||
<section class="page" id="page-earnings" data-page="earnings" hidden>
|
||||
<!-- app-ia-26 (the founder's audit, 4.3): the headline is the measured last 24 hours from the engine's earned sums;
|
||||
the three windows; Electricity, Payouts and Standing as one row each; the rungs behind Details; the projection
|
||||
as one dim foot line that hides with its reason. No fee, no address card, no run line, no lifetime cell, no
|
||||
first-hour timeline. Every number names its source on hover (title). -->
|
||||
<div class="card">
|
||||
<!-- 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-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 class="earn-head"><span class="eyebrow">last 24 hours</span><span class="v" id="e-head">reading</span><span class="s" id="e-head-sub"></span></div>
|
||||
<!-- the headline is the 24 h figure; the row is the two longer windows (the audit's amendment of 15:2x BST) -->
|
||||
<div class="earn-three two">
|
||||
<div class="earn-cell"><span class="k">7 days</span><span class="v" id="e-d7">reading</span><span class="s" id="e-d7-sub"></span></div>
|
||||
<div class="earn-cell"><span class="k">All time</span><span class="v" id="e-all">reading</span><span class="s" id="e-all-sub"></span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="ladder-card">
|
||||
<div class="card-head"><h3>The ladder</h3><div class="head-right"><span class="eyebrow" id="ladder-source">reading</span></div></div>
|
||||
<p class="help">Weight is your blocks over the window, written into consensus. No points server, no badge: the chain computes every rung and your node reports it. Hover a line for the field it reads.</p>
|
||||
<ol class="rungs" id="rungs"></ol>
|
||||
</div>
|
||||
|
||||
<div class="card" id="timeline-card">
|
||||
<button class="disclose" id="tl-toggle" aria-expanded="false" aria-controls="tl-details"><span>Your first hour</span><span class="disclose-right"><span class="dim" id="tl-sum"></span><span class="chev" aria-hidden="true"></span></span></button>
|
||||
<div class="details" id="tl-details" hidden>
|
||||
<ol class="timeline" id="tl-steps"></ol>
|
||||
<p class="note" id="tl-note"></p>
|
||||
<div class="earn-rows">
|
||||
<div class="earn-row"><span class="k">Electricity</span><span class="v" id="e-cost">reading</span><span class="s" id="e-cost-sub"></span></div>
|
||||
<div class="earn-row"><span class="k">Payouts</span><span class="v mono" id="e-pays">not set</span><span class="s" id="e-pays-sub"></span><span class="acts"><button class="btn tiny" data-copy="e-pays-full">Copy</button><button class="btn tiny ghost" id="r-wallet">Open the wallet</button></span><span id="e-pays-full" hidden>not set</span></div>
|
||||
<div class="earn-row"><span class="k">Standing</span><span class="v" id="e-standing">reading</span><span class="s" id="e-standing-sub"></span></div>
|
||||
</div>
|
||||
<details class="earn-details" id="standing-details"><summary>Details</summary><ol class="rungs" id="rungs"></ol></details>
|
||||
<p class="line-text dim earn-foot" id="e-projection" hidden></p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Rewards address</h3><div class="eyebrow" id="r-source"></div></div>
|
||||
<!-- Prove -->
|
||||
|
||||
<!-- Settings -->
|
||||
<section class="page" id="page-settings" data-page="settings" hidden>
|
||||
<!-- app-ia-26 (4.4 item 1): Wallet, the address card from the old Earnings page -->
|
||||
<div class="card" id="s-wallet-card">
|
||||
<div class="card-head"><h3>Wallet</h3><div class="eyebrow" id="r-source"></div></div>
|
||||
<div class="addr-big mono"><span id="r-address">not set</span><button class="btn small" data-copy="r-address">Copy</button></div>
|
||||
<p class="help" id="r-address-help">Every block this machine finds pays this address.</p>
|
||||
|
||||
|
|
@ -431,14 +392,72 @@
|
|||
<p class="note mono small" id="r-key-file"></p>
|
||||
</div>
|
||||
|
||||
<div class="row wrap top-gap"><button class="btn small" id="r-wallet">Open the wallet</button><span class="note">Your balance is in the wallet.</span></div>
|
||||
<div class="row wrap top-gap"><button class="btn small" id="s-wallet">Open the wallet</button><span class="note">Your balance is in the wallet.</span></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Prove -->
|
||||
<div class="card">
|
||||
<!-- app-ia-26 (4.4 item 2): Node: the network cards, one line, the facts behind Details (moved from Mine) -->
|
||||
<div class="card-head"><h3>Node</h3><span class="eyebrow" id="s-network-eyebrow"></span></div>
|
||||
<p class="help" id="s-network-line"></p>
|
||||
<p class="line-text" id="s-node-line">Node starting</p>
|
||||
<p class="note" id="s-node-sub"></p>
|
||||
<div class="net-cards" id="s-network-cards" role="radiogroup" aria-label="Network"></div>
|
||||
<p class="help ask-cur" id="s-network-note" hidden></p>
|
||||
<div class="ask inline" id="ask-network" hidden>
|
||||
<span class="ask-text" id="ask-network-text"></span>
|
||||
<button class="btn small primary" id="ask-network-yes">Switch</button>
|
||||
<button class="btn small ghost" id="ask-network-no">Keep</button>
|
||||
</div>
|
||||
<details class="node-details" id="node-details"><summary>Details</summary>
|
||||
<div class="kv">
|
||||
<div><span class="k">state</span><span class="v mono" id="n-state-v"></span><span class="m" id="n-state-m"></span></div>
|
||||
<div><span class="k">whose node</span><span class="v" id="n-source"></span><span class="m" id="n-source-m"></span></div>
|
||||
<div><span class="k">height</span><span class="v mono" id="n-blocks">0</span><span class="m" id="n-blocks-sub">blocks this node holds</span></div>
|
||||
<div><span class="k">peers</span><span class="v mono" id="n-peers">0</span><span class="m" id="n-peers-sub">other nodes it talks to</span></div>
|
||||
<div><span class="k">version</span><span class="v mono" id="n-version">--</span><span class="m" id="d-node-net">devnet v4</span></div>
|
||||
<div><span class="k">headers</span><span class="v mono" id="n-headers">0</span><span class="m">headers arrive before blocks</span></div>
|
||||
<div><span class="k">network blocks</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made (the DAA score)</span></div>
|
||||
<div><span class="k">difficulty</span><span class="v mono" id="n-diff">0</span><span class="m">how hard the next block is to find</span></div>
|
||||
<div><span class="k">tips</span><span class="v mono" id="n-tips">0</span><span class="m">open ends of the chain right now</span></div>
|
||||
<div><span class="k">blue score</span><span class="v mono" id="n-blue">0</span><span class="m">blocks on the agreed main chain (the blue score)</span></div>
|
||||
<div><span class="k">next program</span><span class="v mono" id="d-eta">--:--</span><span class="m" id="d-eta-sub">the hourly mining program</span></div>
|
||||
<div><span class="k">last checkpoint</span><span class="v mono" id="f-lock">none yet</span><span class="m" id="f-note">a point the miners agreed can never be undone</span></div>
|
||||
<div><span class="k">votes sent</span><span class="v mono" id="f-votes">0</span><span class="m">this machine signs a checkpoint every 30 s (a point the miners agree can never be undone)</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">rules fingerprint</div>
|
||||
<div class="box mono"><span id="n-digest">not printed yet</span><button class="btn tiny" data-copy="n-digest">Copy</button></div>
|
||||
<p class="help">Every node on the network shows the same fingerprint of the rules. A peer with another one is refused.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">next rule change</div>
|
||||
<div class="line-text" id="n-switch">none planned</div>
|
||||
<p class="help" id="n-switch-help"></p>
|
||||
<div class="switch-list mono" id="n-switches"></div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- app-ia-26 (4.4 item 3): Updates: the app version and its update, the Interface row -->
|
||||
<div class="card" id="s-updates-card">
|
||||
<div class="lead-row">
|
||||
<div class="lead-text">
|
||||
<h3 id="s-version">Igneum Miner</h3>
|
||||
<p class="help" id="s-update-note">Not checked yet.</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="btn small primary" id="s-install" hidden>Install now</button>
|
||||
<button class="btn small" id="s-update">Check for an update</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="s-auto-update"><span class="track"></span><span class="sw-text"><b>Install updates by itself</b> <span class="dim">Downloads in the background and installs at a quiet moment, never mid-program.</span></span></label>
|
||||
<!-- app-ia-26 (4.4 item 3, A19): the Interface row under Updates -->
|
||||
<p class="line-text" id="s-ui-line">Interface 1.0.0, built in</p>
|
||||
<p class="help" id="s-ui-help-static">Small changes to this window arrive over the air, signed by Igneum.</p>
|
||||
<p class="note" id="s-ui-help"></p>
|
||||
<label class="switch"><input type="checkbox" id="s-ui-builtin"><span class="track"></span><span class="sw-text"><b>Use the built-in interface</b> <span class="dim">Serves the interface this version of the app was built with, even when a newer one has arrived over the air.</span></span></label>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<section class="page" id="page-settings" data-page="settings" hidden>
|
||||
<div class="card" id="s-power-card">
|
||||
<div class="card-head"><h3>Power control</h3></div>
|
||||
<label class="switch"><input type="checkbox" id="s-power-control"><span class="track"></span><span class="sw-text"><b>Power control</b> <span class="dim">Lets the app set NVIDIA power and clock limits. Windows asks for administrator rights once at install. Off, NVIDIA cards are measured and never set.</span><span class="dim" id="s-power-note"></span></span></label>
|
||||
|
|
@ -468,7 +487,8 @@
|
|||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Heat mode</h3><span class="eyebrow">Ember Heat</span></div>
|
||||
<label class="switch"><input type="checkbox" id="s-heat"><span class="track"></span><span class="sw-text"><b>Hold a room temperature</b> <span class="dim">The cards heat for a share of every 10 minutes and rest for the rest. The hash follows. In heat mode your card is an electric heater that also earns; whether it beats your boiler depends on the network and the price, shown live.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-heat"><span class="track"></span><span class="sw-text"><b>Hold a room temperature</b></span></label>
|
||||
<p class="help">The cards heat for a share of every 10 minutes and rest for the rest. Your card is an electric heater that also earns.</p>
|
||||
<p class="line-text" id="s-heat-line"></p>
|
||||
<div class="row wrap">
|
||||
<div class="field">
|
||||
|
|
@ -488,18 +508,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Network</h3><span class="eyebrow" id="s-network-eyebrow"></span></div>
|
||||
<p class="help" id="s-network-line"></p>
|
||||
<div class="net-cards" id="s-network-cards" role="radiogroup" aria-label="Network"></div>
|
||||
<p class="help ask-cur" id="s-network-note" hidden></p>
|
||||
<div class="ask inline" id="ask-network" hidden>
|
||||
<span class="ask-text" id="ask-network-text"></span>
|
||||
<button class="btn small primary" id="ask-network-yes">Switch</button>
|
||||
<button class="btn small ghost" id="ask-network-no">Keep</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Proving (app-ia-26): the Prove page's switches, tiers, line and details, folded under Settings -->
|
||||
<div class="card shard-card" id="shard-card" hidden>
|
||||
<div class="bc-head"><span class="eyebrow ember">proof shards</span><span class="bc-when mono" id="sc-when"></span></div>
|
||||
|
|
@ -570,27 +578,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" id="s-interface-card">
|
||||
<div class="card-head"><h3>Interface</h3><span class="eyebrow" id="s-ui-eyebrow">built in</span></div>
|
||||
<p class="line-text" id="s-ui-line">Interface 1.0.0, built in</p>
|
||||
<p class="help" id="s-ui-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.</p>
|
||||
<label class="switch"><input type="checkbox" id="s-ui-builtin"><span class="track"></span><span class="sw-text"><b>Use the built-in interface</b> <span class="dim">Serves the interface this version of the app was built with, even when a newer one has arrived over the air.</span></span></label>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="lead-row">
|
||||
<div class="lead-text">
|
||||
<h3 id="s-version">Igneum Miner</h3>
|
||||
<p class="help" id="s-update-note">Not checked yet.</p>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="btn small primary" id="s-install" hidden>Install now</button>
|
||||
<button class="btn small" id="s-update">Check for an update</button>
|
||||
</div>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="s-auto-update"><span class="track"></span><span class="sw-text"><b>Install updates by itself</b> <span class="dim">Downloads in the background and installs at a quiet moment, never mid-program.</span></span></label>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Logs</h3></div>
|
||||
<div class="row wrap">
|
||||
|
|
@ -658,7 +645,7 @@
|
|||
<div class="k">rewards address</div>
|
||||
<div class="box mono"><span id="key-address"></span><button class="btn tiny" data-copy="key-address">Copy</button></div>
|
||||
</div>
|
||||
<p class="note">Stored at <span class="mono" id="key-file"></span>, readable by your user only. Earnings can show it again.</p>
|
||||
<p class="note">Stored at <span class="mono" id="key-file"></span>, readable by your user only. Settings can show it again.</p>
|
||||
<label class="check"><input type="checkbox" id="key-ack"><span>I have saved my key</span></label>
|
||||
<div class="cta">
|
||||
<button class="btn primary big" id="btn-key-done" disabled>Start mining</button>
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ test('T-R8: the pill is a state word, the rate lives on Mine', () => {
|
|||
assert.deepEqual(V.pill({ ...base, mining: { state: 'mining', hash_total: 100, cards: [] }, jobs: { active: true } }), { text: 'Job running', tone: '' });
|
||||
for (const st of ['restarting', 'behind', 'no peers', 'failed', 'stopped', 'stub']) assert.deepEqual(V.pill({ ...base, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: st } }), { text: 'Node down', tone: 'bad' }, st);
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'mining', hash_total: 1, cards: [{ state: 'tuning' }] } }), { text: 'Tuning', tone: 'on' });
|
||||
assert.deepEqual(V.pill({ ...base, heat: { on: true, phase: 'resting' }, mining: { state: 'paused', hash_total: 0, cards: [] } }), { text: 'Resting', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, heat: { on: true, phase: 'resting' }, mining: { state: 'paused', hash_total: 0, cards: [] } }), { text: 'Resting · heat mode', tone: '' }, 'a rest names heat mode, never a bare word (heat-region.test.mjs)');
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'idle', hash_total: 0, cards: [] } }), { text: 'Not mining', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, setup_done: false, detecting: true }), { text: 'Detecting cards', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, quitting: true }), { text: 'Stopping', tone: '' });
|
||||
|
|
@ -98,9 +98,11 @@ test('T-R5: the knee rule once, at the foot of Tune, in the markup', () => {
|
|||
});
|
||||
test('T-R6 and T-A10: one lock line per card, MHz and watts only, one baseline', () => {
|
||||
const l = V.tuneLine(cardFx(), stateFx(), NOW);
|
||||
assert.equal(l, 'Locked at 1,950 MHz, cap 372 W. Knee at 1,200 MHz.');
|
||||
assert.equal(l, 'Locked at 1,950 MHz, cap 405 W. Knee at 1,200 MHz.');
|
||||
assert.equal(/cap (\d+) W/.exec(l)[1], /^Cap (\d+) W/.exec(V.capSentence(cardFx()))[1], 'the lock line and the cap sentence carry the same cap figure');
|
||||
assert.equal(/Tuned:|MH\/s|of rate|saves/.test(l), false);
|
||||
assert.equal(V.tuneLine(cardFx({ lock_mhz: 0, lock_note: 'rate fell 2.3 percent at 2400 MHz' }), stateFx(), NOW), 'Clock unlocked, cap 372 W. Knee at 2,400 MHz.');
|
||||
assert.equal(V.tuneLine(cardFx({ lock_mhz: 0, lock_note: 'rate fell 2.3 percent at 2400 MHz' }), stateFx(), NOW), 'Clock unlocked, cap 405 W. Knee at 2,400 MHz.');
|
||||
assert.equal(V.tuneLine(cardFx({ vendor: 'amd', power_default_w: 0 }), stateFx(), NOW), 'Locked at 1,950 MHz. Knee at 1,200 MHz.', 'no cap slider, no cap figure');
|
||||
assert.equal(V.tuneLine(cardFx({ state: 'tuning', sweep_state: 'running', tune_step: 3, tune_steps: 9, tune_eta_s: 230 }), stateFx(), NOW), 'Measuring: step 3 of 9, 4 min left.');
|
||||
assert.equal(V.tuneLine(cardFx({ vendor: 'apple', kind: 'apple', tiers: [], tier: '', tier_note: 'no lever: Apple silicon sets its own clocks and power', lock_note: 'no lever', lock_at: 0, lock_mhz: 0 }), stateFx(), NOW), 'No lever on Apple silicon: the system sets the clocks. Ember measures and reports.');
|
||||
assert.equal(V.tuneLine(cardFx({ tiers: cardFx().tiers.map((t) => ({ ...t, table: true, label: 'measured' })), tiers_table: true, lock_at: 0, lock_mhz: 0, tune_line: '', sweep_at: 0 }), stateFx(), NOW), 'From the team’s table, measured on this card class.');
|
||||
|
|
@ -135,10 +137,10 @@ test('T-R16 and T-R17: the Tune card carries one Tune button and the MH/W once i
|
|||
const head = h.slice(0, h.indexOf('class="tc-tiers"'));
|
||||
assert.equal((head.match(/MH\/W/g) || []).length, 1); assert.equal((head.match(/MH\/s/g) || []).length, 1);
|
||||
assert.equal(once(h, 'Retune'), 1);
|
||||
assert.equal(once(h, 'Locked at 1,950 MHz, cap 372 W. Knee at 1,200 MHz.'), 1);
|
||||
assert.equal(once(h, 'Locked at 1,950 MHz, cap 405 W. Knee at 1,200 MHz.'), 1);
|
||||
assert.equal(once(h, '<details'), 1, 'one closed Details disclosure'); assert.equal(once(h, '<details open'), 0);
|
||||
assert.equal(once(h, 'Cap 405 W (90%). The card draws 372 W at the lock. Lower is cooler and quieter.'), 1);
|
||||
assert.equal(once(h, '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.'), 1);
|
||||
assert.equal(once(h, '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.'), 1, 'the last two words glued');
|
||||
assert.equal(once(h, 'memory 68 °C · core 1,950 MHz · memory clock 13,801 MHz · fan 62%'), 1);
|
||||
assert.equal(once(h, 'runs on CUDA · 21,760 cores · driver 617.42'), 1);
|
||||
assert.equal(once(h, 'The ringed point is the one Ember kept; a hollow point was rejected; the dashed point is the knee.'), 1);
|
||||
|
|
@ -150,3 +152,146 @@ test('T-R20: the heat line once, on Tune, only while heat mode is on', () => {
|
|||
assert.equal(once(html, 'id="heat-line"'), 1); assert.equal(inPage(html, 'tune', 'id="heat-line"'), 1);
|
||||
assert.equal(V.heatLine({ on: false }).text, '');
|
||||
});
|
||||
|
||||
// item 5: Earnings. The windows from state.ladder.earned (item 2), the Electricity row, the Payouts line, the Standing
|
||||
// row with the rungs behind Details, the projection as one dim foot line that hides with its reason
|
||||
const earnedFx = { 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 };
|
||||
const chainFx = (over) => ({ ok: true, source: 'node', params: { dust: 5, weight_window: 7200 }, network: { hashrate_hps: 100e9, miner_ign_per_block: 4.88, ign_per_block: 6.1, ramp_factor: 0.8, daa: 1, blocks_per_day_measured: 86400, bps: 1, stale: false }, keys_listed: 4, keys_cap: 0, voters: 14, mine: [], best: { id: '8fafda27', blocks: 251, voter: true, participation: 1, rank: 3 }, ...over });
|
||||
const earnState = (over) => ({ now: NOW, uptime_s: 1800, setup_done: true, address: { display: '0xdd442fCbb964A3aFDc90D49B408e8DD296FA86E8', source: 'generated' }, settings: { prove: false, dev_fee: true, power_price_pence: 28 }, node: { synced: true, state: 'synced', tip_age_s: 2 }, proving: { paid: 0, paid_wei: 0 }, jobs: { active: false }, mining: { state: 'mining', hash_total: 17, watts_total: 622, accepted_total: 80, accepted_session: 12, found: [], cards: [cardFx({ hash_now: 17, power_w: 622 })] }, ladder: { first_block_at: NOW - 86000, blocks_30d: 251, blocks_today: 2, blocks_24h: 3, days: [], blocks: [], streak_s: 3600, last_signed_locked: 8490, signed_locked_count: 40, shards: [], earned: earnedFx, first_synced_at: NOW - 86400, first_mining_at: NOW - 86400 }, ...over });
|
||||
|
||||
test('T-A1 and T-A2: the three windows read the engine\'s earned sums; no "day", no "this run", no reward applied to old blocks', () => {
|
||||
const e = V.earningsLines(earnState(), chainFx(), 28, NOW);
|
||||
assert.equal('day' in e, false); assert.equal('run' in e, false); assert.equal('lifetime' in e, false); assert.equal('weight' in e, false);
|
||||
assert.deepEqual([e.h24.ign, e.h24.blocks, e.h24.shards], [14.64, 3, 0]);
|
||||
assert.deepEqual([e.d7.ign, e.d7.blocks, e.d7.shards], [89.2, 18, 1]);
|
||||
assert.deepEqual([e.all.ign, e.all.blocks, e.all.shards], [393.07, 80, 6]);
|
||||
assert.equal(e.h24.text, '14.64 IGN'); assert.equal(e.h24.sub, '3 blocks');
|
||||
assert.equal(e.d7.text, '89.20 IGN'); assert.equal(e.d7.sub, '18 blocks · 1 shard');
|
||||
assert.equal(e.all.text, '393.1 IGN'); assert.equal(e.all.sub, '80 blocks · 6 shards');
|
||||
assert.equal(e.headline.text, '14.64 IGN'); assert.equal(e.headline.eyebrow, 'last 24 hours'); assert.equal(e.headline.sub, '3 blocks');
|
||||
assert.equal(/this run|at today|a day/.test(JSON.stringify([e.headline, e.h24, e.d7, e.all])), false);
|
||||
// the sums come from the engine, never count × today's reward: a different reward changes nothing
|
||||
assert.equal(V.earningsLines(earnState(), chainFx({ network: { ...chainFx().network, miner_ign_per_block: 99 } }), 28, NOW).h24.text, '14.64 IGN');
|
||||
// before the first block
|
||||
const z = V.earningsLines(earnState({ ladder: { ...earnState().ladder, earned: { ...earnedFx, h24: { wei: '0', ign: 0, blocks: 0, shards: 0, priced: true }, d7: { wei: '0', ign: 0, blocks: 0, shards: 0, priced: true }, all: { wei: '0', ign: 0, blocks: 0, shards: 0, priced: true } } } }), chainFx(), 28, NOW);
|
||||
assert.equal(z.headline.text, '0 IGN'); assert.equal(z.headline.sub, 'no block yet'); assert.equal(z.h24.sub, 'no block yet');
|
||||
// an engine before the windows: the IGN reads "reading", the windows show blocks only, counted from the ladder's own
|
||||
// record and never zero (blocks_24h, else the block list filtered to the day; the day ring for 7 days; the lifetime count)
|
||||
const old = V.earningsLines(earnState({ ladder: { ...earnState().ladder, earned: undefined } }), chainFx(), 28, NOW);
|
||||
assert.equal(old.headline.text, 'reading'); assert.equal(old.h24.text, 'reading'); assert.equal(old.h24.sub, '3 blocks'); assert.equal(old.all.sub, '80 blocks');
|
||||
const dayOf = Math.floor(NOW / 86400);
|
||||
const older = V.earningsLines(earnState({ ladder: { ...earnState().ladder, earned: undefined, blocks_24h: undefined, blocks: [{ at: NOW - 100 }, { at: NOW - 5000 }, { at: NOW - 90000 }], days: [{ day: dayOf, blocks: 2 }, { day: dayOf - 1, blocks: 5 }, { day: dayOf - 6, blocks: 4 }, { day: dayOf - 9, blocks: 7 }] } }), chainFx(), 28, NOW);
|
||||
assert.equal(older.headline.sub, '2 blocks', 'blocks_24h absent: the block list filtered to the day, never zero');
|
||||
assert.equal(older.h24.sub, '2 blocks'); assert.equal(older.d7.sub, '11 blocks'); assert.equal(older.d7.text, 'reading'); assert.equal(older.all.sub, '80 blocks');
|
||||
// the page shows the 24 h figure once, as the headline; the row is 7 days and all time
|
||||
assert.equal(once(html, 'id="e-h24"'), 0); assert.equal(inPage(html, 'earnings', 'id="e-d7"'), 1); assert.equal(inPage(html, 'earnings', 'id="e-all"'), 1); assert.equal(inPage(html, 'earnings', 'id="e-head"'), 1);
|
||||
assert.equal((section(html, 'earnings').match(/class="earn-cell"/g) || []).length, 2, 'two columns under the headline');
|
||||
// an estimated all-time figure says so; an unpriced window says so
|
||||
assert.equal(V.earningsLines(earnState({ ladder: { ...earnState().ladder, earned: { ...earnedFx, estimated: true } } }), chainFx(), 28, NOW).all.sub, '80 blocks · 6 shards · estimated');
|
||||
assert.equal(V.earningsLines(earnState({ ladder: { ...earnState().ladder, earned: { ...earnedFx, h24: { ...earnedFx.h24, priced: false } } } }), chainFx(), 28, NOW).h24.sub, '3 blocks · one block not priced yet');
|
||||
});
|
||||
test('T-A3 and A17: one Standing row; the rungs behind Details with no intro', () => {
|
||||
const e = V.earningsLines(earnState(), chainFx(), 28, NOW);
|
||||
assert.equal(e.standing.text, 'Rank 3 of 4 keys · signing 8 of 80 points');
|
||||
assert.equal(e.standing.sub, 'weight is your blocks over the last 2 hours, 2 of 2 hours mined');
|
||||
assert.equal(V.earningsLines(earnState({ ladder: { ...earnState().ladder, streak_s: 0 } }), chainFx(), 28, NOW).standing.text, 'Rank 3 of 4 keys · no signing points yet');
|
||||
assert.equal(V.earningsLines(earnState(), chainFx({ best: null, mine: [] }), 28, NOW).standing.text, 'Not in the weight table yet');
|
||||
assert.equal(V.earningsLines(earnState(), { ok: false, error: 'neither answered' }, 28, NOW).standing.text, 'Standing: not read');
|
||||
assert.equal(once(html, 'id="rungs"'), 1); assert.equal(inPage(html, 'earnings', 'id="rungs"'), 1);
|
||||
const sec = section(html, 'earnings'), d = sec.indexOf('id="standing-details"');
|
||||
assert.ok(d > 0 && sec.indexOf('id="rungs"') > d, 'the rungs sit behind the Standing details');
|
||||
assert.equal(inPage(html, 'earnings', 'No points server'), 0, 'no ladder intro'); assert.equal(once(html, 'id="ladder-card"'), 0);
|
||||
assert.equal(once(html, 'timeline-card'), 0, 'no first-hour timeline (A18)'); assert.equal(once(html, 'id="tl-toggle"'), 0);
|
||||
});
|
||||
test('T-R9, T-R11, T-R18 and A4: Electricity once, Payouts once, no lifetime cell, no run line, the address in Settings', () => {
|
||||
const e = V.earningsLines(earnState(), chainFx(), 28, NOW);
|
||||
assert.equal(e.cost.text, '£4.18 a day'); assert.equal(e.cost.sub, 'at 28p/kWh for 622 W');
|
||||
const noPrice = V.earningsLines(earnState(), chainFx(), 0, NOW);
|
||||
assert.equal(noPrice.cost.text, '622 W'); assert.equal(noPrice.cost.sub, 'Set a price in Settings for the cost.');
|
||||
const mac = V.earningsLines(earnState({ mining: { ...earnState().mining, watts_total: 0, cards: [cardFx({ vendor: 'apple', kind: 'apple', power_w: 0, temp_gpu: 0 })] } }), chainFx(), 28, NOW);
|
||||
assert.equal(mac.cost.text, 'No power reading on Apple silicon.'); assert.equal(mac.cost.sub, '');
|
||||
assert.equal(e.payouts.text, 'Pays 0xdd44…86E8'); assert.equal(e.payouts.sub, 'Your balance is in the wallet. Change the address in Settings.');
|
||||
assert.equal(V.earningsLines(earnState({ address: {} }), chainFx(), 28, NOW).payouts.text, 'No address yet');
|
||||
assert.equal(once(html, 'e-run'), 0); assert.equal(once(html, 'id="e-blocks"'), 0); assert.equal(once(html, 'id="e-rung"'), 0);
|
||||
assert.equal(once(html, 'id="r-address"'), 1); assert.equal(inPage(html, 'settings', 'id="r-address"'), 1);
|
||||
assert.equal(inPage(html, 'earnings', 's-address-input'), 0); assert.equal(inPage(html, 'earnings', 'key-toggle'), 0);
|
||||
assert.equal(inPage(html, 'earnings', 'id="e-cost"'), 1); assert.equal(inPage(html, 'earnings', 'id="e-pays"'), 1);
|
||||
assert.equal(inPage(html, 'settings', '<h3>Wallet</h3>'), 1);
|
||||
const f = V.railFoot(earnState({ display_name: 'study-pc' }));
|
||||
assert.equal(f.name, 'study-pc'); assert.equal(f.up, 'up 30 min'); assert.equal('pays' in f, false); assert.equal(/pays|0x/.test(JSON.stringify(f)), false);
|
||||
});
|
||||
test('T-R19: one projection, a dim foot line, hidden with its reason under a job, a pause, a still chain, no card mining or an unread rate', () => {
|
||||
const e = V.earningsLines(earnState(), chainFx(), 28, NOW);
|
||||
assert.equal(e.projection, 'About 71.68 IGN a day at 17.0 MH/s, 0.017% of the network.');
|
||||
assert.equal(V.cardIgnDay, undefined);
|
||||
const held = earnState({ jobs: { active: true, title: 'class v6 floor lane 2 (the 5090 grid)', started_at: NOW - 7260 }, mining: { ...earnState().mining, state: 'held' } });
|
||||
const h = V.earningsLines(held, chainFx(), 28, NOW);
|
||||
assert.equal(h.projection, null); assert.equal(h.projectionNote, 'Projection paused: class v6 floor lane 2 (the 5090 grid) running since ' + V.hhmm(NOW - 7260) + '.');
|
||||
assert.equal(/IGN/.test(h.projectionNote), false);
|
||||
const still = V.earningsLines(earnState({ node: { ...earnState().node, tip_age_s: 900 } }), chainFx(), 28, NOW);
|
||||
assert.equal(still.projection, null); assert.equal(still.projectionNote, 'Network paused: no block on the chain for 15 min.');
|
||||
assert.equal(V.earningsLines(earnState({ mining: { ...earnState().mining, state: 'paused', paused: true } }), chainFx(), 28, NOW).projection, null);
|
||||
assert.equal(V.earningsLines(earnState({ mining: { ...earnState().mining, state: 'waiting' } }), chainFx(), 28, NOW).projection, null);
|
||||
assert.equal(V.earningsLines(earnState(), chainFx({ network: { ...chainFx().network, hashrate_hps: 0 } }), 28, NOW).projection, null);
|
||||
assert.equal(V.earningsLines(earnState(), { ok: false, error: 'neither answered' }, 28, NOW).projection, null);
|
||||
// the earned figures hold whatever the projection does
|
||||
assert.equal(h.headline.text, '14.64 IGN'); assert.equal(still.h24.text, '14.64 IGN');
|
||||
assert.equal(once(html, 'id="e-projection"'), 1); assert.equal(inPage(html, 'earnings', 'id="e-projection"'), 1);
|
||||
const sec = section(html, 'earnings');
|
||||
assert.ok(sec.indexOf('id="e-projection"') > sec.indexOf('id="standing-details"'), 'the projection is the last thing on the page');
|
||||
});
|
||||
|
||||
// item 3: Mine. The toggle without its sub, four tiles, the card rows, the chain scene, one node line, five activity
|
||||
// lines; the ladder strip, the money and blocks tiles, the node details, the stats line and the log button leave
|
||||
test('T-R8 (the toggle), T-A5, T-R9, T-R18 and T-R10: the hero is four tiles with their units; no money, no blocks, no ladder strip on Mine', () => {
|
||||
assert.equal(once(html, 'btn-toggle-sub'), 0);
|
||||
const sec = section(html, 'mine'), tiles = sec.match(/<div class="tot"[^>]*>[\s\S]*?<span class="k">([^<]+)<\/span>/g) || [];
|
||||
assert.deepEqual(tiles.map((t) => /<span class="k">([^<]+)<\/span>/.exec(t)[1]), ['MH/s', 'W', 'MH/W', 'IGN']);
|
||||
assert.equal(inPage(html, 'mine', 'id="t-ign"'), 1); assert.equal(inPage(html, 'mine', 'id="t-eff"'), 1);
|
||||
assert.equal(inPage(html, 'mine', 't-money'), 0); assert.equal(inPage(html, 'mine', 't-blocks'), 0);
|
||||
assert.equal(once(html, 'ladder-strip'), 0); assert.equal(once(html, 'ls-more'), 0); assert.equal(once(src, "'ls-"), 0);
|
||||
assert.equal(/£|\$|€/.test(sec.slice(sec.indexOf('class="totals"'), sec.indexOf('id="first-wait"'))), false, 'no currency in the hero');
|
||||
assert.equal(V.toggle({ state: 'waiting', paused: false, cards: [{ key: 'a', enabled: false, state: 'off', vendor: 'nvidia', kind: 'discrete' }], found: [] }, { synced: true }, {}).sub, 'switch a card on first');
|
||||
});
|
||||
test('T-A14 and T-R14: one node line on Mine that opens Settings; the node facts live in Settings', () => {
|
||||
const n = (over) => ({ state: 'synced', synced: true, blocks: 39587, headers: 39587, peers: 4, daa: 40000, last_reading_age_s: 4, tip_age_s: 3, message: '', restart_in_s: 0, ...over });
|
||||
assert.equal(V.nodeLine(n(), { severity: 'none' }, '').text, 'Node synced · 4 peers');
|
||||
assert.equal(V.nodeLine(n({ peers: 1 }), { severity: 'none' }, '').text, 'Node synced · 1 peer');
|
||||
assert.equal(V.nodeLine(n({ state: 'syncing', synced: false, blocks: 41000, headers: 52000 }), { severity: 'none' }, 'about 12 min left at 300 blocks/s').text, 'Node syncing · 41,000 of 52,000 blocks · about 12 min left');
|
||||
assert.equal(V.nodeLine(n({ state: 'restarting', synced: false, message: 'igneumd exited; restart in 20 s' }), { severity: 'none' }, '').text, 'Node restarting · igneumd exited; restart in 20 s');
|
||||
assert.equal(V.nodeLine(n({ state: 'behind', synced: false, tip_age_s: 3180 }), { severity: 'none' }, '').text, 'Node behind · last block 53 min ago');
|
||||
assert.equal(once(html, 'id="node-details"'), 1); assert.equal(inPage(html, 'settings', 'id="node-details"'), 1);
|
||||
assert.equal(inPage(html, 'mine', 'n-digest'), 0); assert.equal(inPage(html, 'mine', 'node-toggle'), 0);
|
||||
assert.equal(inPage(html, 'mine', 'id="node-go"'), 1); assert.equal(inPage(html, 'settings', '<h3>Node</h3>'), 1);
|
||||
});
|
||||
test('T-R13, T-A15 and T-R12 (the stats): Activity is five lines, no stats line, no log button; the log opens from the rail and from Settings', () => {
|
||||
assert.equal(once(html, 'btn-open-log'), 0); assert.equal(once(html, 'id="btn-logs"'), 1); assert.equal(once(html, 'id="s-log-open"'), 1);
|
||||
assert.equal(once(html, 'd-stats'), 0); assert.equal(V.activityStats, undefined); assert.equal(once(src, 'to the dev fee'), 0);
|
||||
assert.equal(once(src, 's.events.slice(0, 5)'), 1, 'five events');
|
||||
});
|
||||
|
||||
// items 6 and 7: Settings in 4.4's order, Updates with the Interface row, the Heat mode label, the unit labels, the copy law
|
||||
test('T-R21, T-A19, T-A20 and the order of 4.4: eleven Settings sections, Updates holds the Interface row, the fee last', () => {
|
||||
const sec = section(html, 'settings'), heads = (sec.match(/<h3(?: [^>]*)?>([^<]+)<\/h3>/g) || []).map((h) => />([^<]+)</.exec(h)[1]).filter((t) => t !== 'Prove instead of mining');
|
||||
assert.deepEqual(heads, ['Wallet', 'Node', 'Igneum Miner', 'Power control', 'Electricity', 'Heat mode', 'Proving', 'This machine', 'Logs', 'Advanced', 'The dev fee']);
|
||||
assert.equal(once(html, 's-interface-card'), 0); assert.equal(once(html, 'id="s-ui-line"'), 1);
|
||||
const u = sec.indexOf('id="s-updates-card"'), uEnd = sec.indexOf('\n </div>', u);
|
||||
assert.ok(u > 0 && sec.indexOf('id="s-ui-line"') > u && sec.indexOf('id="s-ui-line"') < uEnd, 'the Interface row sits inside the Updates card');
|
||||
assert.ok(sec.indexOf('id="s-ui-builtin"') > u && sec.indexOf('id="s-ui-builtin"') < uEnd);
|
||||
const w = V.interfaceWords({ embedded_version: '1.0.0', active_version: '1.0.0', source: 'embedded', confirmed: true });
|
||||
assert.equal(w.line, 'Interface 1.0.0, built in');
|
||||
const helpStatic = /id="s-ui-help-static"[^>]*>([^<]+)</.exec(sec)[1];
|
||||
assert.equal(helpStatic, 'Small changes to this window arrive over the air, signed by Igneum.');
|
||||
const heat = /<label class="switch"><input type="checkbox" id="s-heat">[\s\S]*?<span class="sw-text">([\s\S]*?)<\/span><\/label>/.exec(sec)[1].replace(/<[^>]+>/g, '').trim();
|
||||
assert.ok(heat.length < 30, 'the Heat mode label is five words: ' + heat);
|
||||
assert.equal(inPage(html, 'settings', 'The cards heat for a share of every 10 minutes and rest for the rest. Your card is an electric heater that also earns.'), 1);
|
||||
assert.equal(once(html, 'Settings can show it again.'), 1); assert.equal(once(html, 'Earnings can show it again.'), 0);
|
||||
});
|
||||
test('T-A8 and the copy law: no uppercase on a unit label; no em dash in the window', () => {
|
||||
const css = readFileSync(join(here, 'app.css'), 'utf8');
|
||||
for (const sel of ['.tot .k', '.num .v .unit', '.tier-b .l']) {
|
||||
const rules = css.split('\n').filter((l) => l.startsWith(sel + '{') || l.startsWith(sel + ','));
|
||||
for (const r of rules) assert.equal(/text-transform:\s*uppercase/.test(r), false, sel + ' is a unit label: ' + r);
|
||||
}
|
||||
assert.equal(once(html, '—'), 0, 'no em dash in index.html'); assert.equal(once(src, '—'), 0, 'no em dash in app.js');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -167,7 +167,7 @@ test('the big button: start when paused, stop when mining, disabled with no card
|
|||
const none = V.toggle(m({ cards: [card({ enabled: false })] }), { synced: true }, {});
|
||||
assert.equal(none.disabled, true);
|
||||
assert.equal(none.act, '');
|
||||
assert.equal(none.sub, 'switch a card on in Cards first');
|
||||
assert.equal(none.sub, 'switch a card on first');
|
||||
assert.equal(V.toggle(m({ cards: [] }), { synced: true }, {}).sub, 'no card this app can use');
|
||||
assert.equal(V.toggle(m({ state: 'waiting' }), { synced: false }, {}).sub, 'waiting for the node to sync');
|
||||
assert.equal(V.toggle(m(), { synced: true }, { quitting: true }).disabled, true);
|
||||
|
|
@ -402,8 +402,8 @@ test('the prove tier sentence per card, and the counts in one line', () => {
|
|||
test('the node line, the activity line, the asks', () => {
|
||||
const n = { state: 'synced', blocks: 109859, headers: 109859, peers: 6, daa: 198000, last_reading_age_s: 5, message: '' };
|
||||
const l = V.nodeLine(n, { severity: 'none' }, '');
|
||||
assert.equal(l.text, 'Node synced · 6 peers · 109,859 blocks'); assert.equal(l.tone, 'ok'); assert.match(l.sub, /every block/);
|
||||
assert.equal(V.nodeLine({ ...n, peers: 1, blocks: 0, state: 'starting' }, { severity: 'none' }, '').text, 'Node starting · 1 peer');
|
||||
assert.equal(l.text, 'Node synced · 6 peers'); assert.equal(l.tone, 'ok'); assert.match(l.sub, /every block/);
|
||||
assert.equal(V.nodeLine({ ...n, peers: 1, blocks: 0, state: 'starting' }, { severity: 'none' }, '').text, 'Node starting');
|
||||
assert.equal(V.nodeLine(n, { severity: 'block', skew_s: -412 }, '').tone, 'bad');
|
||||
assert.equal(V.eventLine('consensus parameters from the signed manifest: {"difficulty_v2_activation_daa":33000}'), 'consensus parameters from the signed manifest (details in the log)');
|
||||
assert.equal(V.eventLine('node synced: 151512 blocks, 6 peer(s)'), 'node synced: 151512 blocks, 6 peer(s)');
|
||||
|
|
@ -509,16 +509,16 @@ test('the 0.3.16 engine fields (ember-tune fd03f35): the balance row, the fleet
|
|||
test('N4 (6 October 2026): a frozen tip reads behind, never synced; the tip age is on the node line', () => {
|
||||
const n = (over) => ({ state: 'synced', blocks: 133000, headers: 133000, peers: 1, daa: 140000, last_reading_age_s: 4, tip_age_s: 3, message: '', restart_in_s: 0, ...over });
|
||||
const ok = V.nodeLine(n(), { severity: 'none' }, '');
|
||||
assert.equal(ok.text, 'Node synced · 1 peer · 133,000 blocks · last block 3 s ago'); assert.equal(ok.tone, 'ok');
|
||||
assert.equal(ok.text, 'Node synced · 1 peer'); assert.equal(ok.tone, 'ok');
|
||||
assert.match(ok.sub, /Last block 3 s ago/);
|
||||
const behind = V.nodeLine(n({ state: 'behind', synced: false, tip_age_s: 3180, sync_cause: 'frozen' }), { severity: 'none' }, '');
|
||||
assert.equal(behind.text, 'Node behind · 1 peer · 133,000 blocks · last block 53 min ago'); assert.equal(behind.tone, 'bad');
|
||||
assert.equal(behind.text, 'Node behind · last block 53 min ago'); assert.equal(behind.tone, 'bad');
|
||||
assert.match(behind.sub, /^No new block for 53 min\. The chain may have moved on without this node\./);
|
||||
const none = V.nodeLine(n({ state: 'no peers', synced: false, peers: 0, tip_age_s: 500 }), { severity: 'none' }, '');
|
||||
assert.equal(none.text, 'No peers · 133,000 blocks'.replace('No peers', 'Node no peers')); assert.equal(none.tone, 'bad');
|
||||
assert.equal(none.text, 'Node no peers · looking for the seed node'); assert.equal(none.tone, 'bad');
|
||||
assert.match(none.sub, /^No other node on our chain is connected/);
|
||||
// an older node without the age keeps the old words
|
||||
assert.equal(V.nodeLine(n({ tip_age_s: -1 }), { severity: 'none' }, '').text, 'Node synced · 1 peer · 133,000 blocks');
|
||||
assert.equal(V.nodeLine(n({ tip_age_s: -1 }), { severity: 'none' }, '').text, 'Node synced · 1 peer');
|
||||
assert.equal(V.tipAge(12), '12 s'); assert.equal(V.tipAge(240), '4 min'); assert.equal(V.tipAge(3180), '53 min'); assert.equal(V.tipAge(7300), '2 h');
|
||||
const base = { setup_done: true, quitting: false, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'behind' } };
|
||||
assert.deepEqual(V.pill(base), { text: 'Node down', tone: 'bad' });
|
||||
|
|
@ -725,8 +725,8 @@ test('first-block-21 (the project lead, 7 October 2026): the first-block card is
|
|||
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).earned.text, '80.00 IGN earned');
|
||||
assert.equal(V.earningsLines(s, c, 28, T5).lifetime.text, '2 blocks');
|
||||
assert.equal(V.earningsLines(s, c, 28, T5).all.sub, '2 blocks', 'an engine without the earned windows: the lifetime count alone');
|
||||
assert.equal(V.earningsLines(s, c, 28, T5).all.text, 'reading');
|
||||
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');
|
||||
});
|
||||
|
|
@ -933,7 +933,7 @@ test('brand/marks/vendor-marks.mjs carries the app\'s marks and tokens verbatim,
|
|||
|
||||
// ---------- the Earnings card, tidied (the project lead, 7 October 2026: "remove all the type a price stuff") ----------
|
||||
const indexHtml = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'index.html'), 'utf8');
|
||||
const earningsCard = () => { const a = indexHtml.indexOf('id="page-earnings"'); return indexHtml.slice(a, indexHtml.indexOf('id="ladder-card"', a)); };
|
||||
const earningsCard = () => { const a = indexHtml.indexOf('id="page-earnings"'); return indexHtml.slice(a, indexHtml.indexOf('</section>', 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('</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');
|
||||
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');
|
||||
|
|
|
|||
BIN
docs/design/app-screens/0.3.26/earnings-dark.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
docs/design/app-screens/0.3.26/earnings-job-dark.png
Normal file
|
After Width: | Height: | Size: 147 KiB |
BIN
docs/design/app-screens/0.3.26/earnings-light.png
Normal file
|
After Width: | Height: | Size: 148 KiB |
BIN
docs/design/app-screens/0.3.26/earnings-still-dark.png
Normal file
|
After Width: | Height: | Size: 135 KiB |
BIN
docs/design/app-screens/0.3.26/mine-dark.png
Normal file
|
After Width: | Height: | Size: 590 KiB |
BIN
docs/design/app-screens/0.3.26/mine-first-minute-dark.png
Normal file
|
After Width: | Height: | Size: 581 KiB |
BIN
docs/design/app-screens/0.3.26/mine-light.png
Normal file
|
After Width: | Height: | Size: 612 KiB |
BIN
docs/design/app-screens/0.3.26/mine-syncing-dark.png
Normal file
|
After Width: | Height: | Size: 536 KiB |
BIN
docs/design/app-screens/0.3.26/settings-dark.png
Normal file
|
After Width: | Height: | Size: 544 KiB |
BIN
docs/design/app-screens/0.3.26/settings-light.png
Normal file
|
After Width: | Height: | Size: 539 KiB |
BIN
docs/design/app-screens/0.3.26/tune-dark.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
docs/design/app-screens/0.3.26/tune-first-minute-dark.png
Normal file
|
After Width: | Height: | Size: 206 KiB |
BIN
docs/design/app-screens/0.3.26/tune-light.png
Normal file
|
After Width: | Height: | Size: 252 KiB |
BIN
docs/design/app-screens/0.3.26/tune-table-dark.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
16
tools/ui-mock/shots-audit-26.txt
Normal file
|
|
@ -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: <file> <query> <height>; tools/ui-mock/capture.sh <out dir> < 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
|
||||