From d6337a867ba28a5d074e8d0d17d9e292303f6ec2 Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Thu, 8 Oct 2026 17:27:36 +0000 Subject: [PATCH] v2.0.1 window: the Cards tab back as its own page, the first-block card once per payout address, the four interface words, one positioning line, the watchdog's waiting words The founder's calls of 18:1x BST, 8 October 2026 (through the shipper), recorded as F1 to F5 in the window audit doc. F1. The Cards tab is back in the rail as its own page (View.PAGES: mine, cards, tune, earnings, settings); every card row moved out of Mine into it: the state, the hash rate, the power, the temperature, the tune state (one line, the Tune page's words, View.cardsRowWords), the enable switch, the 16 GB proving note on an NVIDIA card under 16 GB. Mine keeps the headline rate and the chain status (the hero tiles, the chain scene, the node line, Activity). F2. The first-block celebration shows once per payout address, ever: settings.json keeps first_block_shown[address] = {hash, at} (config.rs FirstBlockMark), written when a window reports the card seen (POST /api/card/seen), so it survives runs, updates and a reinstall that keeps the wallet; a new address shows it once again (the engine passes the address's mark to Ladder::on_block_for, which raises the card whenever the address has none); an install that showed it under the machine rule takes the mark for its current address once at start. Never on a dev-fee block: the miner's "dev-fee block " line follows the fee block's ACCEPTED line, and Ladder::on_fee_block withdraws a first card that block raised; the engine now counts fee blocks from that line (fee_session, fee_total) as well. F3. The four interface words (docs/spec/finality-guarantees.md section 9) on the window: the block inspector reads included or excluded or pending, executed as chain block N, proven, finalised (View.blockState); a block under the lock is finalised even while the network's finality is paused (the pause is said after the block's own words and on the status strip), and no block reads "not active" or "(reported)"; the scene's hover words say finalised and executed (scene/live-dag.js 2.0.8, the copies synced). F4. One positioning line: the hero keeps it; #s-positioning under Updates is gone. F5. The watchdog's words when the node serves no epoch state stream: "waiting for the node's execution state for this epoch: no program to load 300 s after starting", never "the worker did not load its program". Tests known-failed first: once.test.mjs (the Cards page with the home page's contents, the inspector's words), the view tests' page list, ladder.rs (the first card follows the address mark; a dev-fee line withdraws it; the 0.3.21 wrapper keeps the machine rule), watchdog.rs (the waiting words). The crate: 319 tests green on build-1; 129 UI tests green on build-2. Captures: ~/Desktop/igneum-previews-2026-10-08/cards-201. Co-Authored-By: Claude Fable 5.1 --- app/igneum-app/src/config.rs | 16 ++++++++- app/igneum-app/src/engine.rs | 28 ++++++++++++--- app/igneum-app/src/ladder.rs | 56 ++++++++++++++++++++++++++++- app/igneum-app/src/server.rs | 13 ++++++- app/igneum-app/src/watchdog.rs | 10 ++++-- app/igneum-app/ui/app.js | 64 +++++++++++++++++++++++---------- app/igneum-app/ui/index.html | 18 ++++++---- app/igneum-app/ui/live-dag.js | 5 +-- app/igneum-app/ui/once.test.mjs | 44 ++++++++++++++++++++--- app/igneum-app/ui/view.test.mjs | 4 +-- scene/live-dag.js | 5 +-- site/live-dag.js | 5 +-- 12 files changed, 220 insertions(+), 48 deletions(-) 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 @@