Merge cards-201 d6337a86 into release-2.0.1 (the Cards tab back as its own page, the first-block card once per payout address, the four finality words on the window, one positioning line, the watchdog's real cause; the founder's calls of 8 October 2026)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 17:35:21 +00:00
commit 3c13a71cef
12 changed files with 220 additions and 48 deletions

View file

@ -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<String, FirstBlockMark>,
/// 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 }
}
}

View file

@ -170,7 +170,7 @@ pub struct Shared {
}
impl Shared {
pub fn new(token: String, runtime: Runtime, packaged: Packaged, settings: Settings, cmd_tx: Sender<Cmd>, engine_log: Option<std::fs::File>, log_path: PathBuf) -> Shared {
pub fn new(token: String, runtime: Runtime, packaged: Packaged, mut settings: Settings, cmd_tx: Sender<Cmd>, engine_log: Option<std::fs::File>, 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 ") {

View file

@ -264,6 +264,24 @@ impl Ladder {
dirty
}
/// The miner's `dev-fee block <hash>` 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<Milestone> {
// 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<Milestone> {
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<u64> {
#[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]

View file

@ -507,8 +507,19 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
"/api/card/seen" => {
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" => {

View file

@ -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();

View file

@ -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 '<i class="' + esc(x.state) + '"></i>'; }).join('');
$('i-shards').innerHTML = sh.length ? sh.map(function (x, i) { return '<li class="' + esc(x.state) + '"><span>Shard ' + String(i + 1).padStart(2, '0') + '</span><span>' + esc((sw.items[i] ? sw.items[i].word : x.state).replace(/^./, function (c) { return c.toUpperCase(); })) + '</span></li>'; }).join('') : '<li class="muted">' + esc(sw.summary.replace(/^./, function (c) { return c.toUpperCase(); })) + '.</li>';
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 = '<label class="switch" title="' + (r.on ? 'mining on this card' : 'not mining on this card') + '"><input type="checkbox" aria-label="Mine on ' + esc(r.name) + '"' + (r.on ? ' checked' : '') + (r.canToggle ? '' : ' disabled') + '><span class="track"></span></label>';
var subs = [];
var subs = [], rw = View.cardsRowWords(cd, s, s.now);
if (r.sub) subs.push('<div class="msg' + (r.tone === 'bad' ? '' : ' dim') + '">' + esc(r.sub) + '</div>');
if (cells.noReading && r.on) subs.push('<div class="msg dim">' + esc(cells.noReading) + '</div>');
if (rw.tune && r.on) subs.push('<div class="msg dim tune-state">' + esc(rw.tune) + '</div>');
if (rw.proving) subs.push('<div class="msg dim">' + esc(rw.proving) + '</div>');
return '<div class="gpu-line' + (r.on ? ' on' : ' off') + (r.removed ? ' removed' : '') + (r.unusable ? ' unusable' : '') + '" data-key="' + esc(r.key) + '">' +
'<div class="gl-main">' + View.markHtml(cd) +
'<div class="who">' + View.nameHtml(cd) +
@ -2576,6 +2597,25 @@ if (typeof document !== 'undefined') (function () {
'<div class="nums">' + nums + '</div>' +
'<div class="acts">' + sw + '</div></div></div>';
}
// 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('') : '<div class="empty' + (s.detecting ? '' : ' err') + '">' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can use was found. The node runs on its own.') + '</div>';
}
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('') : '<div class="empty' + (s.detecting ? '' : ' err') + '">' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can use was found. The node runs on its own.') + '</div>';
}
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);

View file

@ -19,6 +19,7 @@
</div>
<nav class="rail-nav" id="rail-nav" aria-label="Sections">
<button class="nav on" data-page="mine" aria-current="page"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/></svg><span>Mine</span></button>
<button class="nav" data-page="cards"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="2" y="6" width="20" height="11" rx="2"/><path d="M6 17v3M10 17v3M14 17v3M18 17v3M6 11h4"/></svg><span>Cards</span></button>
<button class="nav" data-page="tune"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="11" rx="2"/><path d="M7 20h10M9 16v4M15 16v4M7 10h3M14 10h3"/></svg><span>Tune</span></button>
<button class="nav" data-page="earnings"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 15h2"/></svg><span>Earnings</span></button>
<button class="nav" data-page="settings"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/></svg><span>Settings</span><i class="nav-dot" id="nav-updates-dot" hidden></i></button>
@ -248,12 +249,6 @@
<canvas id="bc-canvas" width="1200" height="630" hidden aria-hidden="true"></canvas>
</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">
<div class="card-head"><h3>Your cards</h3><div class="head-right"><span class="eyebrow" id="d-cards-eyebrow">detecting</span></div></div>
<div class="gpu-list" id="d-cards"><div class="empty">Waiting for the engine.</div></div>
<p class="note" id="d-cards-note" hidden></p>
</div>
<div class="card dag-card" id="dag-card">
<div class="card-head"><h3>The chain, live</h3><div class="head-right"><span class="eyebrow" id="dag-state">connecting</span><button class="btn tiny ghost" id="dag-inspect" aria-expanded="false" aria-controls="chain-full">Inspect</button><button class="btn tiny ghost" id="dag-pause" aria-pressed="false">Pause</button></div></div>
<!-- the compact scene (EMBER 02 live-dag.js, the site lane's module, byte-identical): the app feeds it its own api/live reply -->
@ -312,6 +307,16 @@
</section>
<!-- Cards: every card with Ember as its second layer -->
<section class="page" id="page-cards" data-page="cards" hidden>
<!-- v2.0.1 (the founder's call, 8 October 2026): every card row on its own page: the state, the rate, the watts, the
degrees, the tune state, the switch, the 16 GB proving note -->
<div class="card" id="cards-card">
<div class="card-head"><h3>Your cards</h3><div class="head-right"><span class="eyebrow" id="d-cards-eyebrow">detecting</span></div></div>
<div class="gpu-list" id="d-cards"><div class="empty">Waiting for the engine.</div></div>
<p class="note" id="d-cards-note" hidden></p>
</div>
</section>
<section class="page" id="page-tune" data-page="tune" hidden>
<!-- app-ia-26 (the founder's audit, section 4.2): the strip (the fleet's three tiers, Tune all, ONE sentence, the measure
switch, the heat and power lines when they matter), then one card per graphics card (rendered by renderTune from
@ -443,7 +448,6 @@
<div class="lead-row">
<div class="lead-text">
<h3 id="s-version">Igneum Miner</h3>
<p class="help" id="s-positioning">A GPU-secured network for Ethereum-compatible applications and verifiable computation</p>
<p class="help" id="s-update-note">Not checked yet.</p>
</div>
<div class="row">

View file

@ -308,7 +308,8 @@
function words(b,nowMs){
if(!b)return {title:'No block selected',lines:[]};
var inclusion=b.color==='blue'?'included':b.color==='red'?'excluded':'pending';
var bits=[inclusion];if(b.chain)bits.push('selected chain');if(b.proven)bits.push('proven');if(b.locked)bits.push('locked checkpoint');if(b.final)bits.push('final');
// the four interface words (docs/spec/finality-guarantees.md section 9): included / executed / proven / finalised
var bits=[inclusion];if(b.chain)bits.push('selected chain');if(b.chain&&finite(b.number))bits.push('executed as chain block '+fmt(b.number));if(b.proven)bits.push('proven');if(b.locked)bits.push('locked checkpoint');if(b.final)bits.push('finalised');
return {title:b.hash+' '+bits.join(' / '),lines:[(own(b)?'Your block / ':'')+'miner key '+b.miner,
'Blue score '+fmt(b.blue_score===undefined?b.blue:b.blue_score)+' / DAA '+fmt(b.daa),b.parents.length+' parent'+(b.parents.length===1?'':'s'),
'Shards: '+shardWords(b,nowMs).summary]};
@ -422,5 +423,5 @@
entries.forEach(function(e){var sp=doc.createElement('span');sp.setAttribute('data-lg',e.id);var i=doc.createElement('i');i.className='lg '+e.shape+(e.faded?' faded':'');i.style.setProperty('--lg','var('+e.token+')');if(e.fill)i.style.setProperty('--lg-fill','var('+e.fill+')');sp.appendChild(i);sp.appendChild(doc.createTextNode(e.label));el.appendChild(sp);});
keep.forEach(function(n){el.appendChild(n);});return entries;
}
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,shardWords:shardWords,version:'2.0.6'};
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,shardWords:shardWords,version:'2.0.8'};
})(window);

View file

@ -33,15 +33,15 @@ test('the counters count', () => {
});
// item 1: the four pages
test('T-R15: four pages, mine, tune, earnings, settings; Prove is a Settings section, not a page', () => {
assert.deepEqual(V.PAGES.map((p) => p.id), ['mine', 'tune', 'earnings', 'settings']);
test('T-R15 (as the founder amended it for v2.0.1): five pages, mine, cards, tune, earnings, settings; Prove is a Settings section, not a page', () => {
assert.deepEqual(V.PAGES.map((p) => p.id), ['mine', 'cards', 'tune', 'earnings', 'settings']);
assert.equal(V.page('nonsense').id, 'mine');
assert.equal(once(html, 'id="page-prove"'), 0);
assert.equal(once(html, 'id="s-prove"'), 1); assert.equal(inPage(html, 'settings', 'id="s-prove"'), 1);
assert.equal(once(html, 'data-page="prove"'), 0);
for (const id of ['mine', 'tune', 'earnings', 'settings']) { assert.equal(once(html, 'id="page-' + id + '"'), 1, id); assert.equal(once(html, '<button class="nav' + (id === 'mine' ? ' on' : '') + '" data-page="' + id + '"'), 1, 'rail ' + id); }
assert.equal(once(html, 'id="page-overview"'), 0); assert.equal(once(html, 'id="page-cards"'), 0);
assert.equal((html.match(/<button class="nav[^"]*" data-page="/g) || []).length, 4, 'four rail entries');
for (const id of ['mine', 'cards', 'tune', 'earnings', 'settings']) { assert.equal(once(html, 'id="page-' + id + '"'), 1, id); assert.equal(once(html, '<button class="nav' + (id === 'mine' ? ' on' : '') + '" data-page="' + id + '"'), 1, 'rail ' + id); }
assert.equal(once(html, 'id="page-overview"'), 0);
assert.equal((html.match(/<button class="nav[^"]*" data-page="/g) || []).length, 5, 'five rail entries');
assert.equal(V.page('mine').sub, 'what this machine is doing now'); assert.equal(V.page('tune').sub, 'how Ember drives each card');
assert.equal(V.page('earnings').sub, 'what this machine earned'); assert.equal(V.page('settings').sub, 'set once, left alone');
assert.equal(/renderProve\b/.test(src), false, 'renderProve is renderProvingSection under Settings');
@ -322,3 +322,37 @@ test('every screen in index.html has its display rule in app.css', () => {
assert.ok(screens.length >= 6, 'the setup screens and the dashboard: ' + screens.join(','));
for (const id of screens) assert.ok(css.includes('body[data-phase="' + id + '"] #screen-' + id), 'app.css shows #screen-' + id);
});
// v2.0.1 (the founder's call at 18:1x BST, 8 October 2026, reversing the audit's four-page layout on this point): the Cards
// tab is back in the rail as its own page with every card row (the state, the hash rate, the power, the temperature, the
// tune state, the enable switch, the 16 GB proving note); the home page keeps the headline rate and the chain status.
// Known-failed first on release-2.0.1 95f3068e: the rows on Mine, no Cards page.
test('v2.0.1: the Cards page holds every card row; the home page keeps the headline rate and the chain status', () => {
assert.equal(V.page('cards').title, 'Cards'); assert.equal(V.page('cards').sub, 'every card, its state and its switch');
assert.equal(inPage(html, 'cards', 'id="d-cards"'), 1); assert.equal(inPage(html, 'mine', 'id="d-cards"'), 0); assert.equal(inPage(html, 'mine', 'cards-card'), 0);
assert.equal(inPage(html, 'mine', 'id="totals"'), 1); assert.equal(inPage(html, 'mine', 'id="dag-card"'), 1); assert.equal(inPage(html, 'mine', 'id="node-go"'), 1);
assert.equal(once(src, "page === 'cards'"), 1, 'the Cards page has its own render');
// the row's words: the tune state and the proving note come from the View (the row builder is page-side)
const r = V.cardsRowWords(cardFx(), stateFx(), NOW);
assert.equal(r.tune, 'Locked at 1,950 MHz, cap 405 W. Knee at 1,200 MHz.');
assert.equal(r.proving, '', 'a 32 GB card carries no proving note');
const small = V.cardsRowWords(cardFx({ vram_mb: 12288 }), stateFx(), NOW);
assert.equal(small.proving, 'NVIDIA GeForce RTX 5090: mining and proving together need a 16 GB card; this card does one at a time; mining continues (12 GB).');
assert.equal(V.cardsRowWords(cardFx({ vendor: 'apple', kind: 'apple', tiers: [], tier_note: 'no lever: Apple silicon sets its own clocks and power', lock_note: 'no lever', lock_at: 0, lock_mhz: 0 }), stateFx(), NOW).proving, '');
});
// the four interface words (docs/spec/finality-guarantees.md section 9): the window's block inspector reads included,
// executed, proven, finalised; a block under the lock is finalised even while the network's finality is paused (the pause
// sits on the status strip), and no block ever reads "not active". Known-failed first: "Final (reported)" / "Not marked final".
test('the block inspector reads the four words; a locked block is finalised even in a pause', () => {
const b = { color: 'blue', chain: true, number: 28175, proven: true, final: true, locked: false };
assert.equal(V.blockState(b, false), 'Included / executed as chain block 28,175 / proven / finalised');
assert.equal(V.blockState(b, true), 'Included / executed as chain block 28,175 / proven / finalised', 'a lock holds in a pause');
assert.equal(V.blockState({ ...b, final: false }, true), 'Included / executed as chain block 28,175 / proven · finality paused on the network above the lock');
assert.equal(V.blockState({ ...b, final: false, proven: false }, false), 'Included / executed as chain block 28,175');
assert.equal(V.blockState({ color: 'red', chain: false, proven: false, final: false }, false), 'Excluded');
assert.equal(V.blockState({ color: '', chain: false }, false), 'Pending');
assert.equal(/not active|Not marked final|\(reported\)/.test(src), false);
assert.equal(once(html, 'id="s-positioning"'), 0, 'the positioning line once, on the hero'); assert.equal(once(html, 'id="positioning"'), 1);
});

View file

@ -14,8 +14,8 @@ const V = mod.exports.View;
const card = (over) => ({ key: 'nvidia:0:RTX 5090', name: 'NVIDIA GeForce RTX 5090', vendor: 'nvidia', kind: 'discrete', worker: 'CUDA', vram_mb: 32768, enabled: true, state: 'mining', hash_now: 124.3, hash_avg: 120, accepted: 3, rejected: 0, identities: 8, ids: [], prepared: true, restart_in_s: 0, message: '', reason: '', power_w: 410.2, power_limit_w: 460, power_default_w: 575, power_pct: 80, power_applied: true, temp_gpu: 61, temp_mem: 72, telemetry_at: 1, ...over });
test('the four sections and their order (miner-ui-4, app-ia-26)', () => {
assert.deepEqual(V.PAGES.map((p) => p.id), ['mine', 'tune', 'earnings', 'settings']);
test('the five sections and their order (miner-ui-4, app-ia-26, the Cards tab back in v2.0.1)', () => {
assert.deepEqual(V.PAGES.map((p) => p.id), ['mine', 'cards', 'tune', 'earnings', 'settings']);
assert.equal(V.page('tune').title, 'Tune');
assert.equal(V.page('earnings').title, 'Earnings');
assert.equal(V.page('nonsense').id, 'mine');

View file

@ -308,7 +308,8 @@
function words(b,nowMs){
if(!b)return {title:'No block selected',lines:[]};
var inclusion=b.color==='blue'?'included':b.color==='red'?'excluded':'pending';
var bits=[inclusion];if(b.chain)bits.push('selected chain');if(b.proven)bits.push('proven');if(b.locked)bits.push('locked checkpoint');if(b.final)bits.push('final');
// the four interface words (docs/spec/finality-guarantees.md section 9): included / executed / proven / finalised
var bits=[inclusion];if(b.chain)bits.push('selected chain');if(b.chain&&finite(b.number))bits.push('executed as chain block '+fmt(b.number));if(b.proven)bits.push('proven');if(b.locked)bits.push('locked checkpoint');if(b.final)bits.push('finalised');
return {title:b.hash+' '+bits.join(' / '),lines:[(own(b)?'Your block / ':'')+'miner key '+b.miner,
'Blue score '+fmt(b.blue_score===undefined?b.blue:b.blue_score)+' / DAA '+fmt(b.daa),b.parents.length+' parent'+(b.parents.length===1?'':'s'),
'Shards: '+shardWords(b,nowMs).summary]};
@ -422,5 +423,5 @@
entries.forEach(function(e){var sp=doc.createElement('span');sp.setAttribute('data-lg',e.id);var i=doc.createElement('i');i.className='lg '+e.shape+(e.faded?' faded':'');i.style.setProperty('--lg','var('+e.token+')');if(e.fill)i.style.setProperty('--lg-fill','var('+e.fill+')');sp.appendChild(i);sp.appendChild(doc.createTextNode(e.label));el.appendChild(sp);});
keep.forEach(function(n){el.appendChild(n);});return entries;
}
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,shardWords:shardWords,version:'2.0.6'};
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,shardWords:shardWords,version:'2.0.8'};
})(window);

View file

@ -308,7 +308,8 @@
function words(b,nowMs){
if(!b)return {title:'No block selected',lines:[]};
var inclusion=b.color==='blue'?'included':b.color==='red'?'excluded':'pending';
var bits=[inclusion];if(b.chain)bits.push('selected chain');if(b.proven)bits.push('proven');if(b.locked)bits.push('locked checkpoint');if(b.final)bits.push('final');
// the four interface words (docs/spec/finality-guarantees.md section 9): included / executed / proven / finalised
var bits=[inclusion];if(b.chain)bits.push('selected chain');if(b.chain&&finite(b.number))bits.push('executed as chain block '+fmt(b.number));if(b.proven)bits.push('proven');if(b.locked)bits.push('locked checkpoint');if(b.final)bits.push('finalised');
return {title:b.hash+' '+bits.join(' / '),lines:[(own(b)?'Your block / ':'')+'miner key '+b.miner,
'Blue score '+fmt(b.blue_score===undefined?b.blue:b.blue_score)+' / DAA '+fmt(b.daa),b.parents.length+' parent'+(b.parents.length===1?'':'s'),
'Shards: '+shardWords(b,nowMs).summary]};
@ -422,5 +423,5 @@
entries.forEach(function(e){var sp=doc.createElement('span');sp.setAttribute('data-lg',e.id);var i=doc.createElement('i');i.className='lg '+e.shape+(e.faded?' faded':'');i.style.setProperty('--lg','var('+e.token+')');if(e.fill)i.style.setProperty('--lg-fill','var('+e.fill+')');sp.appendChild(i);sp.appendChild(doc.createTextNode(e.label));el.appendChild(sp);});
keep.forEach(function(n){el.appendChild(n);});return entries;
}
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,shardWords:shardWords,version:'2.0.6'};
root.IgneumDag={mount:mount,legend:legend,renderLegend:renderLegend,shardWords:shardWords,version:'2.0.8'};
})(window);