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:
commit
3c13a71cef
12 changed files with 220 additions and 48 deletions
|
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ") {
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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" => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue