diff --git a/app/igneum-app/src/config.rs b/app/igneum-app/src/config.rs index 6f2622c4e..f30ba8763 100644 --- a/app/igneum-app/src/config.rs +++ b/app/igneum-app/src/config.rs @@ -67,6 +67,15 @@ pub struct CardPref { pub sweep_mem_mhz: u32, } +/// One first-block card shown: the block's hash (empty until the node's line names it) and the unix time. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] +pub struct FirstBlockMark { + #[serde(default)] + pub hash: String, + #[serde(default)] + pub at: f64, +} + #[derive(Clone, Serialize, Deserialize)] pub struct Settings { #[serde(default)] @@ -167,6 +176,11 @@ pub struct Settings { /// Lifetime dev-fee blocks this machine found (the miner's `dev-fee block` lines), carried across runs. #[serde(default)] pub fee_total: u64, + /// The first-block celebration, once per payout address, ever (the founder, 8 October 2026, v2.0.1): the lower-case + /// address a window showed the card for, with the block's hash and time. Kept in settings.json so it survives runs, + /// updates and a reinstall that keeps the wallet; a new address shows it once again; a dev-fee block never raises it. + #[serde(default)] + pub first_block_shown: HashMap, /// Devnet only: when no verifier is found next to the engine, start the node with `IGNEUM_PROOF_VERIFY=trust` /// so it includes proof records it never verified (src/verifier.rs). Default off; a found verifier always wins. #[serde(default)] @@ -234,7 +248,7 @@ fn default_tier() -> String { impl Default for Settings { fn default() -> Settings { - Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, power_control: false, tune_goal: "balanced".into(), tune_tier: "balanced".into(), power_price_pence: 0.0, tune_climb: false, region: String::new(), currency: String::new(), network: String::new(), heat_on: false, heat_set_c: 19.0, heat_schedule: Vec::new(), heat_tz_min: 0, heat_room_c: 0.0, heat_room_at: 0.0, heat_offset_c: 0.0, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false, profile_public: false, ui_builtin: false, prove_instead: false } + Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, power_control: false, tune_goal: "balanced".into(), tune_tier: "balanced".into(), power_price_pence: 0.0, tune_climb: false, region: String::new(), currency: String::new(), network: String::new(), heat_on: false, heat_set_c: 19.0, heat_schedule: Vec::new(), heat_tz_min: 0, heat_room_c: 0.0, heat_room_at: 0.0, heat_offset_c: 0.0, installed_at: 0, dev_fee: true, fee_total: 0, first_block_shown: HashMap::new(), proof_verify_trust: false, prove_default_applied: false, profile_public: false, ui_builtin: false, prove_instead: false } } } diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index 711d15f22..bbd9eac5a 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -170,7 +170,7 @@ pub struct Shared { } impl Shared { - pub fn new(token: String, runtime: Runtime, packaged: Packaged, settings: Settings, cmd_tx: Sender, engine_log: Option, log_path: PathBuf) -> Shared { + pub fn new(token: String, runtime: Runtime, packaged: Packaged, mut settings: Settings, cmd_tx: Sender, engine_log: Option, log_path: PathBuf) -> Shared { let settings_path = runtime.app_dir.join("settings.json"); let wallet_path = runtime.app_dir.join("wallet.json"); let ladder_path = runtime.app_dir.join("ladder.json"); @@ -180,6 +180,15 @@ impl Shared { if ladder.start_run(settings.accepted_total) { ladder.save(&ladder_path); } + // v2.0.1: the first-block card is once per payout address, kept in settings.json; an install that showed it under + // the machine rule (the ladder's flag) takes the mark for its current address once, so the card does not come again + { + let addr = settings.address.to_ascii_lowercase(); + if ladder.first_block_shown && !addr.is_empty() && !settings.first_block_shown.contains_key(&addr) { + settings.first_block_shown.insert(addr, crate::config::FirstBlockMark { hash: ladder.first_block_hash.clone(), at: ladder.first_block_at }); + settings.save(&settings_path); + } + } let mut st = State { version: VERSION.into(), preview: PREVIEW.unwrap_or("").into(), ..Default::default() }; st.network = runtime.network.clone(); st.network_name = if runtime.network == "testnet" { "igneum-testnet-1".into() } else if let Some(n) = runtime.devnet_suffix { format!("igneum-devnet-{n}") } else if runtime.network == "devnet" && runtime.devnet_suffix.is_none() { "igneum-devnet-v4".into() } else { format!("igneum-{}-{}", runtime.network, runtime.devnet_suffix.unwrap_or(0)) }; @@ -5407,10 +5416,11 @@ impl Engine { } } let unix = crate::platform::unix_now_f(); - let total = { + let (total, shown_for_address) = { let mut s = self.shared.settings.lock().unwrap(); s.accepted_total += 1; - s.accepted_total + let addr = s.address.to_ascii_lowercase(); + (s.accepted_total, addr.is_empty() || s.first_block_shown.contains_key(&addr)) }; let mut st = self.st(); st.mining.accepted_session += 1; @@ -5426,7 +5436,7 @@ impl Engine { let nonce = crate::ladder::parse_miner_accepted(text).unwrap_or_default(); let milestone = { let mut ld = self.shared.ladder.lock().unwrap(); - let m = ld.on_block(unix, &card_key, &card_name, &nonce, total); + let m = ld.on_block_for(unix, &card_key, &card_name, &nonce, total, shown_for_address); // 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() { @@ -5486,6 +5496,16 @@ impl Engine { } // miner-ui-5: the first-hour timeline's "card mining" mark, once per install, on the first status with a rate if s.hash_now > 0.0 && self.shared.ladder.lock().unwrap().mark("mining", crate::platform::unix_now_f()) { self.shared.save_ladder(); } + } else if text.contains(" dev-fee block ") { + // the software dev fee's audit line, after the fee block's ACCEPTED line: the counters, and a first card the + // block raised is withdrawn (the block paid the dev address, v2.0.1) + let unix = crate::platform::unix_now_f(); + let fee_total = { let mut s = self.shared.settings.lock().unwrap(); s.fee_total += 1; s.fee_total }; + self.shared.save_settings(); + { let mut st = self.st(); st.mining.fee_session += 1; st.mining.fee_total = fee_total; } + if self.shared.ladder.lock().unwrap().on_fee_block(unix) { + self.shared.save_ladder(); + } } else if text.contains(" worker: race ") { self.race_line(card, &card_name, text); } else if text.contains(" worker: ready ") { diff --git a/app/igneum-app/src/ladder.rs b/app/igneum-app/src/ladder.rs index 3514ee00f..ec56650b2 100644 --- a/app/igneum-app/src/ladder.rs +++ b/app/igneum-app/src/ladder.rs @@ -264,6 +264,24 @@ impl Ladder { dirty } + /// The miner's `dev-fee block ` line, which follows a fee template's ACCEPTED line: a first card raised by that + /// block (within two seconds) is withdrawn, since the block paid the dev address, not the user's. Returns true when + /// the record changed. + pub fn on_fee_block(&mut self, now: f64) -> bool { + match self.milestone.as_ref() { + Some(m) if m.kind == "first" && !self.milestone_seen && (now - m.at).abs() < 2.0 => { + self.milestone = None; + true + } + _ => false, + } + } + + /// The first card a window has, as the mark settings.json keeps per address: (the block's hash, its time). + pub fn first_card_mark(&self) -> Option<(String, f64)> { + self.milestone.as_ref().filter(|m| m.kind == "first").map(|m| (m.hash.clone(), m.at)) + } + /// A window showed (or the user dismissed) the card for the milestone at `count` and `at` (first-block-21): the /// card is spent, and a first-block card sets the lifetime flag. Returns true when the record changed. pub fn card_seen(&mut self, count: u64, at: f64) -> bool { @@ -315,6 +333,17 @@ impl Ladder { /// The miner's own ACCEPTED line: `total` is this machine's lifetime count including this block. Returns the /// milestone this block raised, if any. pub fn on_block(&mut self, now: f64, card: &str, card_name: &str, nonce: &str, total: u64) -> Option { + // the machine-level rule as 0.3.21 had it: the first card when the lifetime count crosses to 1 on a record with + // no block and no flag (the engine passes the per-address rule through `on_block_for`, v2.0.1) + let shown = self.first_block_shown || total != 1 || self.first_block_at != 0.0; + self.on_block_for(now, card, card_name, nonce, total, shown) + } + + /// v2.0.1 (the founder, 8 October 2026): the first-block card shows once per payout address, ever; the engine reads + /// `shown_for_address` from settings.json (`first_block_shown[address]`) and the card is raised whenever the address + /// has no mark, whatever the lifetime count says. A dev-fee block never keeps it: the miner's `dev-fee block` line + /// follows its ACCEPTED line, and `on_fee_block` withdraws a first card raised by that block. + pub fn on_block_for(&mut self, now: f64, card: &str, card_name: &str, nonce: &str, total: u64, shown_for_address: bool) -> Option { let nonce = norm_nonce(nonce); let (hash, daa) = match self.pending.iter().position(|p| !nonce.is_empty() && p.0 == nonce) { Some(i) => { @@ -335,7 +364,7 @@ impl Ladder { } // the first-block card: once in the app's life, when the persisted lifetime count crosses from 0 to 1 on a // record with no block and no flag (first-block-21); a count that starts over raises nothing here - let first = total == 1 && !self.first_block_shown && self.first_block_at == 0.0; + let first = !shown_for_address; if self.first_block_at == 0.0 { self.first_block_at = now; self.first_block_card = card.to_string(); @@ -591,6 +620,31 @@ pub fn parse_vote_index(text: &str) -> Option { #[cfg(test)] mod tests { + // v2.0.1: the first card once per address (the engine's mark gates it, not the lifetime count); a dev-fee block's line + // withdraws a first card it raised; a later block of the user's raises it again + #[test] + fn the_first_card_follows_the_address_mark_and_a_dev_fee_block_withdraws_it() { + let mut l = Ladder::default(); + let t0 = 1_791_400_000.0; + assert!(l.on_block_for(t0, "c", "c", "", 1, true).is_none(), "an address with a mark: nothing at count 1"); + assert!(l.first_card_mark().is_none()); + let m = l.on_block_for(t0 + 10.0, "c", "c", "", 7, false).expect("the first card for a new address"); + assert_eq!(m.kind, "first"); + assert_eq!(l.first_card_mark().map(|x| x.1), Some(t0 + 10.0)); + assert!(l.on_fee_block(t0 + 10.2), "the dev-fee line right after withdraws it"); + assert!(l.milestone.is_none()); + assert!(!l.on_fee_block(t0 + 10.3), "nothing to withdraw twice"); + let m2 = l.on_block_for(t0 + 60.0, "c", "c", "", 8, false).expect("raised again by the next block of the user's"); + assert_eq!(m2.kind, "first"); + assert!(!l.on_fee_block(t0 + 70.0), "a fee line long after touches nothing"); + assert!(l.first_card_mark().is_some()); + // the 0.3.21 wrapper keeps the machine rule: count 2 on a fresh record raises no first card (a card milestone at most) + let mut o = Ladder::default(); + assert_ne!(o.on_block(t0, "c", "c", "", 2).map(|m| m.kind), Some("first".to_string())); + let mut f = Ladder::default(); + assert_eq!(f.on_block(t0 + 1.0, "c", "c", "", 1).map(|m| m.kind), Some("first".to_string())); + } + // 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] diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index f52e4b1a6..ae21d9b37 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -507,8 +507,19 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result { let count = body.get("count").and_then(|v| v.as_u64()).unwrap_or(0); let at = body.get("at").and_then(|v| v.as_f64()).unwrap_or(0.0); - let changed = shared.ladder.lock().unwrap().card_seen(count, at); + let (changed, mark) = { let mut ld = shared.ladder.lock().unwrap(); let mark = ld.first_card_mark(); (ld.card_seen(count, at), mark) }; if changed { shared.save_ladder(); } + // v2.0.1: a first card seen writes the per-address mark into settings.json (once per payout address, ever) + if changed { + if let Some((hash, when)) = mark { + let mut s = shared.settings.lock().unwrap(); + let addr = s.address.to_ascii_lowercase(); + if !addr.is_empty() && !s.first_block_shown.contains_key(&addr) { + s.first_block_shown.insert(addr, crate::config::FirstBlockMark { hash, at: when }); + s.save(&shared.settings_path); + } + } + } Ok(json!({ "ok": true, "changed": changed })) } "/api/card" => { diff --git a/app/igneum-app/src/watchdog.rs b/app/igneum-app/src/watchdog.rs index 4df6b33bf..d3654f8a9 100644 --- a/app/igneum-app/src/watchdog.rs +++ b/app/igneum-app/src/watchdog.rs @@ -379,7 +379,10 @@ impl CardWatch { // is bounded by LOAD_S let Some(ready_at) = self.ready_s else { if now_s - started > LOAD_S { - return self.escalate(format!("the worker did not load its program within {} s of starting", LOAD_S as u64)); + // the founder's word (8 October 2026): with no program to load the worker is waiting for the node's execution + // state for this epoch (the node serves no epoch state stream); the restart keeps the worker fresh for when it + // arrives, and the row says what is waited for, never that the worker failed + return self.escalate(format!("waiting for the node's execution state for this epoch: no program to load {} s after starting", LOAD_S as u64)); } return Action::None; }; @@ -725,7 +728,10 @@ mod tests { // loading: the status clock has not started; a worker that never loads is restarted at LOAD_S assert_eq!(w.tick(290.0, true), Action::None); let (reason, delay, _) = restart(&w.tick(301.0, true)); - assert!(reason.contains("did not load its program within 300 s"), "{reason}"); + // the founder's word (8 October 2026): a worker with no program to load is waiting for the node's execution state + // for this epoch (the node serves no epoch state stream); the message says that, never "did not load its program" + assert!(reason.contains("waiting for the node's execution state for this epoch"), "{reason}"); + assert!(!reason.contains("did not load its program"), "{reason}"); assert_eq!(delay, 10); // loaded at 200 s (a slow self-test behind another card's export): the 60 s status clock starts there let mut w = CardWatch::new(); diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index a21aa06e7..f3ce11a42 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -269,6 +269,7 @@ var View = (function () { // card is driven. Earnings: what this machine earned, measured. Settings: everything you set once (proving included). var PAGES = [ { id: 'mine', title: 'Mine', sub: 'what this machine is doing now' }, + { id: 'cards', title: 'Cards', sub: 'every card, its state and its switch' }, // v2.0.1: the founder's call, the Cards tab back as its own page { id: 'tune', title: 'Tune', sub: 'how Ember drives each card' }, { id: 'earnings', title: 'Earnings', sub: 'what this machine earned' }, { id: 'settings', title: 'Settings', sub: 'set once, left alone' } @@ -323,6 +324,23 @@ var View = (function () { } function networkAsk(want, runningName) { var n = NETWORKS.filter(function (x) { return x.id === want; })[0]; return 'Switch this machine to ' + (n ? n.name : want) + '? The node’s data and the mining state on ' + runningName + ' reset at the next start. Your key and your address stay.'; } + // v2.0.1 (the founder, 8 October 2026): the Cards page's row carries the tune state and the 16 GB proving note beside the + // state, the rate, the watts, the degrees and the switch. {tune, proving} + function cardsRowWords(cd, s, now) { + cd = cd || {}; var gb = cd.vram_mb ? Math.round(cd.vram_mb / 1024) : 0; + return { tune: tunable(cd) ? tuneLine(cd, s, now) : '', proving: cd.vendor === 'nvidia' && gb > 0 && gb < PROVE_MIN_GB ? proveTier(cd) : '' }; + } + // the four interface words (docs/spec/finality-guarantees.md section 9) for one block of the scene: included (blue) or + // excluded (red) or pending; executed as chain block N; proven; finalised (under the lock, which holds even while the + // network's finality is paused: the pause is the network's word, said after the block's own) + function blockState(b, finalityPaused) { + b = b || {}; var bits = [b.color === 'blue' ? 'Included' : b.color === 'red' ? 'Excluded' : 'Pending']; + if (b.chain && b.number >= 0 && isFinite(b.number)) bits.push('executed as chain block ' + withCommas(b.number)); + if (b.proven) bits.push('proven'); + if (b.final) bits.push('finalised'); + return bits.join(' / ') + (!b.final && finalityPaused && b.color === 'blue' ? ' · finality paused on the network above the lock' : ''); + } + // ---------- vendor marks (gpu-logos, 7 October 2026) ---------- // One simplified monochrome glyph per GPU vendor, drawn by hand from the public mark's geometry (never a copied logo // file, never a raster): a nominative use that names the hardware. Each glyph is under 2 KB and takes currentColor, @@ -1563,7 +1581,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, 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 }; + return { hhmm: hhmm, railFoot: railFoot, cardsRowWords: cardsRowWords, blockState: blockState, 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 @@ -2357,14 +2375,15 @@ if (typeof document !== 'undefined') (function () { setText('i-shard-count', sh.length ? done + ' / ' + sh.length + ' complete' : sw.summary); $('i-track').innerHTML = sh.map(function (x) { return ''; }).join(''); $('i-shards').innerHTML = sh.length ? sh.map(function (x, i) { return '
  • Shard ' + String(i + 1).padStart(2, '0') + '' + esc((sw.items[i] ? sw.items[i].word : x.state).replace(/^./, function (c) { return c.toUpperCase(); })) + '
  • '; }).join('') : '
  • ' + esc(sw.summary.replace(/^./, function (c) { return c.toUpperCase(); })) + '.
  • '; - setText('i-incl', (b.color === 'blue' ? 'Included' : b.color === 'red' ? 'Excluded' : 'Pending') + (b.chain ? ' / selected chain' : '')); + var fpaused = !!(state && state.finality && state.finality.paused); + setText('i-incl', View.blockState(b, fpaused) + (b.chain ? ' / selected chain' : '')); setText('i-miner', (b.miner === 'you' ? 'you' : b.miner) + (mineB && b.miner !== 'you' ? ' (you)' : '')); setText('i-blue', fmtN(b.blue_score)); setText('i-daa', fmtN(b.daa)); setText('i-parents', (b.parents || []).length + ' observed' + ((b.parents || []).length ? ': ' + b.parents.map(function (h) { return View.shortHex(h, 8, 6); }).join(', ') : '')); setText('i-time', b.ts ? hmsUtc(b.ts) + ' UTC' : 'not set'); var fn = lastLive && lastLive.finality, cp = null; if (fn && fn.checkpoints) fn.checkpoints.forEach(function (c) { if (b.locked && c.hash === b.hash) cp = c; }); setText('i-cp', cp ? 'this block, #' + fmtN(cp.index) + (cp.fraction_total ? ', ' + Math.round(cp.fraction_total * 100) + '% of weight' : '') : 'not a checkpoint'); - setText('i-lock', b.final ? 'Final (reported)' : 'Not marked final'); + setText('i-lock', b.final ? 'Finalised: under the lock' + (fpaused ? '; the network’s finality is paused above it' : '') : fpaused ? 'Not finalised; finality paused on the network' : 'Not finalised'); if (proof) proof.setBlock(b); } function uninspect() { @@ -2566,9 +2585,11 @@ if (typeof document !== 'undefined') (function () { if (cells.power) nums += numCell('power', cells.power.replace(' W', ''), 'W', '', ''); if (r.temp) nums += numCell('temp', r.temp.replace(' °C', ''), '°C', '', r.tempTone); var sw = ''; - var subs = []; + var subs = [], rw = View.cardsRowWords(cd, s, s.now); if (r.sub) subs.push('
    ' + esc(r.sub) + '
    '); if (cells.noReading && r.on) subs.push('
    ' + esc(cells.noReading) + '
    '); + if (rw.tune && r.on) subs.push('
    ' + esc(rw.tune) + '
    '); + if (rw.proving) subs.push('
    ' + esc(rw.proving) + '
    '); return '
    ' + '
    ' + View.markHtml(cd) + '
    ' + View.nameHtml(cd) + @@ -2576,6 +2597,25 @@ if (typeof document !== 'undefined') (function () { '
    ' + nums + '
    ' + '
    ' + sw + '
    '; } + // v2.0.1: the Cards page, every card row (the founder's call of 8 October 2026 reversing the audit's four pages on this point) + function renderCardsPage(s) { + var m = s.mining, shown = shownCards(m.cards), presentCards = shown.filter(View.present); + // 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 = ''; } } }); + var on = rows.filter(function (r) { return r.on; }).length; + setText('d-cards-eyebrow', s.detecting ? 'detecting' : !shown.length ? 'none found' : on + ' of ' + presentCards.length + ' on'); + var tunables = presentCards.filter(function (c) { return View.tunable(c) && c.enabled; }); + setHidden('btn-tune-all', !(tunables.length > 0) || !!(s.settings && s.settings.tuning_off) || !!View.tuningCard(presentCards)); + var box = $('d-cards'), busy = box.querySelector('input[type=range]:active') || (box.contains(document.activeElement) && document.activeElement.tagName === 'INPUT'); + var sig = JSON.stringify([rows, shown.map(function (c) { return [View.cells(c, 0), c.message, c.reason]; })]); + if (sig !== mineRowsSig && !busy) { + mineRowsSig = sig; + box.innerHTML = rows.length ? shown.map(function (cd, i) { return gpuLineHtml(cd, s, rows[i]); }).join('') : '
    ' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can use was found. The node runs on its own.') + '
    '; + } + var note = !shown.length && s.detect_message ? s.detect_message : ''; + $('d-cards-note').hidden = !note; setText('d-cards-note', note); + } function renderMine(s) { var m = s.mining, n = s.node, p = s.program; var t = View.toggle(m, n, s), tb = $('btn-toggle'); @@ -2592,21 +2632,6 @@ if (typeof document !== 'undefined') (function () { 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 = ''; } } }); - var on = rows.filter(function (r) { return r.on; }).length; - setText('d-cards-eyebrow', s.detecting ? 'detecting' : !shown.length ? 'none found' : on + ' of ' + presentCards.length + ' on'); - var tunables = presentCards.filter(function (c) { return View.tunable(c) && c.enabled; }); - setHidden('btn-tune-all', !(tunables.length > 0) || !!(s.settings && s.settings.tuning_off) || !!View.tuningCard(presentCards)); - var box = $('d-cards'), busy = box.querySelector('input[type=range]:active') || (box.contains(document.activeElement) && document.activeElement.tagName === 'INPUT'); - var sig = JSON.stringify([rows, shown.map(function (c) { return [View.cells(c, 0), c.message, c.reason]; })]); - if (sig !== mineRowsSig && !busy) { - mineRowsSig = sig; - box.innerHTML = rows.length ? shown.map(function (cd, i) { return gpuLineHtml(cd, s, rows[i]); }).join('') : '
    ' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can use was found. The node runs on its own.') + '
    '; - } - var note = !shown.length && s.detect_message ? s.detect_message : ''; - $('d-cards-note').hidden = !note; setText('d-cards-note', note); renderNode(s); if (page === 'mine') renderFirstMonth(s); // activity: the newest five events in a person's words (A15: no counts line) @@ -2782,6 +2807,7 @@ if (typeof document !== 'undefined') (function () { function renderPage(s) { if (page === 'mine') renderMine(s); + else if (page === 'cards') renderCardsPage(s); else if (page === 'tune') renderTune(s); else if (page === 'earnings') renderEarnings(s); else if (page === 'settings') renderSettings(s); diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index 5fbb7eaf0..05192ce84 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -19,6 +19,7 @@