diff --git a/app/igneum-app/src/live.rs b/app/igneum-app/src/live.rs new file mode 100644 index 000000000..c1eab9367 --- /dev/null +++ b/app/igneum-app/src/live.rs @@ -0,0 +1,140 @@ +//! GET /api/live for the dashboard's chain scene (ui/live-dag.js, the site's module). The scene wants the network's +//! blocks over the last minutes with lanes, parents, colours, the selected chain, checkpoints and proof state. The +//! local node speaks gRPC and wRPC only and the engine carries no client for either (no TLS, no websocket stack), so +//! this reads the observer's /api/live (the same URL ota.rs already polls for the identity count) through curl, caches +//! it for 2 s, and marks this machine's blocks: the observer names a block's miner by the first 8 hex of its vote key +//! hash, and every card's `ids` carry the same 8 characters for this machine's keys, so a match becomes the lane "you". +//! Read-only; one upstream call per 2 s whatever the window asks. A local-node source is owed (a JSON RPC on igneumd or +//! a wRPC client here); the shape stays the same when it comes. +use serde_json::{json, Value}; +use std::collections::HashSet; +use std::process::Command; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +static CACHE: Mutex> = Mutex::new(None); +const TTL: Duration = Duration::from_secs(2); + +/// The observer's reply with this machine's lane marked. `ids` are the 8-character vote key ids of this machine's +/// cards. Blocks whose `miner` is one of them read `miner: "you"`; `state.you_blocks` counts them; `state.source` names +/// where the data came from. Anything else passes through untouched, so the module's contract holds. +pub fn shape(mut reply: Value, ids: &HashSet) -> Value { + let mut yours = 0u64; + if let Some(blocks) = reply.get_mut("blocks").and_then(|b| b.as_array_mut()) { + for b in blocks.iter_mut() { + let mine = b.get("miner").and_then(|m| m.as_str()).map(|m| ids.contains(m)).unwrap_or(false); + if mine { + yours += 1; + if let Some(o) = b.as_object_mut() { o.insert("miner".into(), json!("you")); } + } + } + } + if let Some(st) = reply.get_mut("state").and_then(|s| s.as_object_mut()) { + st.insert("you_blocks".into(), json!(yours)); + st.insert("source".into(), json!("observer")); + } + reply +} + +/// The observer URL for a window: `?window=`, the window clamped to the module's 30 to 300 s. +pub fn url_for(live_api: &str, window_s: u32) -> String { + format!("{live_api}?window={}", window_s.clamp(30, 300)) +} + +/// The reply for the dashboard: cached 2 s per window; `{ok:false, error}` when the observer is unreachable or the +/// build has no observer address (the UI then draws its own blocks strip). +pub fn fetch(live_api: &str, window_s: u32, ids: &HashSet) -> Value { + if live_api.is_empty() { + return json!({ "ok": false, "error": "no observer address in this build" }); + } + let window_s = window_s.clamp(30, 300); + if let Some((at, w, v)) = CACHE.lock().unwrap().as_ref() { + if *w == window_s && at.elapsed() < TTL { return v.clone(); } + } + let url = url_for(live_api, window_s); + let out = crate::detect::run_timeout( + Command::new(crate::platform::tool("curl")).args(["-s", "--max-time", "4", &url]), + None, + Duration::from_secs(6), + ); + let reply = match out.and_then(|t| serde_json::from_str::(&t).ok()) { + Some(v) if v.get("ok").and_then(|o| o.as_bool()).unwrap_or(false) && v.get("blocks").map(|b| b.is_array()).unwrap_or(false) => shape(v, ids), + _ => json!({ "ok": false, "error": "the observer did not answer" }), + }; + if reply.get("ok").and_then(|o| o.as_bool()).unwrap_or(false) { + *CACHE.lock().unwrap() = Some((Instant::now(), window_s, reply.clone())); + } + reply +} + +#[cfg(test)] +mod tests { + use super::*; + + // a fixture in the observer's shape (site/api/live.mjs): three miners, this machine is 8fafda27, one of its blocks on + // the selected chain and proven, one excluded; a locked and a pending checkpoint + fn fixture() -> Value { + json!({ + "ok": true, "now": 1791301670, + "state": { "stale": false, "age_s": 1, "height": 198490 }, + "blocks": [ + { "hash": "a1", "ts": 1791301600000i64, "blue_score": 194690, "daa": 198486, "parents": [], "chain": true, "color": "blue", "miner": "8fafda27", "locked": true, "final": true, "shards": [{ "state": "paid" }], "proven": true }, + { "hash": "a2", "ts": 1791301601000i64, "blue_score": 194691, "daa": 198487, "parents": ["a1"], "chain": true, "color": "blue", "miner": "1b2c3d4e", "locked": false, "final": false, "shards": [{ "state": "proving" }], "proven": false }, + { "hash": "a3", "ts": 1791301601500i64, "blue_score": null, "daa": 198487, "parents": ["a1"], "chain": false, "color": "red", "miner": "8fafda27", "locked": false, "final": false, "shards": [], "proven": false }, + { "hash": "a4", "ts": 1791301602000i64, "blue_score": 194692, "daa": 198488, "parents": ["a2", "a3"], "chain": true, "color": "pending", "miner": "deadbeef", "locked": false, "final": false, "shards": [], "proven": false } + ], + "finality": { "checkpoints": [ + { "index": 6490, "blue_score": 194690, "daa": 198486, "state": "locked", "fraction_total": 0.71 }, + { "index": 6491, "blue_score": 194720, "daa": 198516, "state": "pending", "fraction_total": 0.44 } + ] } + }) + } + + #[test] + fn this_machines_blocks_become_the_lane_you_and_nothing_else_moves() { + let ids: HashSet = ["8fafda27".to_string(), "9a8b7c6d".to_string()].into_iter().collect(); + let out = shape(fixture(), &ids); + let blocks = out["blocks"].as_array().unwrap(); + assert_eq!(blocks.len(), 4); + assert_eq!(blocks[0]["miner"], "you"); + assert_eq!(blocks[2]["miner"], "you"); + assert_eq!(blocks[1]["miner"], "1b2c3d4e"); + assert_eq!(blocks[3]["miner"], "deadbeef"); + // the rest of the contract passes through: parents, chain, colour, proof state, checkpoints + assert_eq!(blocks[3]["parents"], json!(["a2", "a3"])); + assert_eq!(blocks[0]["chain"], true); + assert_eq!(blocks[2]["color"], "red"); + assert_eq!(blocks[0]["proven"], true); + assert_eq!(blocks[1]["shards"][0]["state"], "proving"); + assert_eq!(out["finality"]["checkpoints"].as_array().unwrap().len(), 2); + assert_eq!(out["finality"]["checkpoints"][0]["state"], "locked"); + assert_eq!(out["state"]["you_blocks"], 2); + assert_eq!(out["state"]["source"], "observer"); + assert_eq!(out["state"]["height"], 198490); + assert_eq!(out["ok"], true); + } + + #[test] + fn no_ids_means_no_lane_is_yours_and_a_bare_reply_survives() { + let out = shape(fixture(), &HashSet::new()); + assert!(out["blocks"].as_array().unwrap().iter().all(|b| b["miner"] != "you")); + assert_eq!(out["state"]["you_blocks"], 0); + let bare = shape(json!({ "ok": true, "blocks": [] }), &HashSet::new()); + assert_eq!(bare["blocks"].as_array().unwrap().len(), 0); + assert!(bare.get("state").is_none(), "no state object is invented"); + } + + #[test] + fn the_window_is_clamped_to_the_modules_range() { + assert_eq!(url_for("https://igneum.network/api/live", 120), "https://igneum.network/api/live?window=120"); + assert_eq!(url_for("https://igneum.network/api/live", 5), "https://igneum.network/api/live?window=30"); + assert_eq!(url_for("https://igneum.network/api/live", 9000), "https://igneum.network/api/live?window=300"); + } + + #[test] + fn a_build_without_an_observer_address_says_so() { + let out = fetch("", 120, &HashSet::new()); + assert_eq!(out["ok"], false); + assert_eq!(out["error"], "no observer address in this build"); + } +} diff --git a/app/igneum-app/src/main.rs b/app/igneum-app/src/main.rs index 809453382..d4929563b 100644 --- a/app/igneum-app/src/main.rs +++ b/app/igneum-app/src/main.rs @@ -37,6 +37,7 @@ mod sweep; mod ember; mod powertask; mod watchdog; +mod live; use std::io::{BufRead, Write}; use std::sync::mpsc::channel; diff --git a/app/igneum-app/src/ota.rs b/app/igneum-app/src/ota.rs index f10563d27..981e630d8 100644 --- a/app/igneum-app/src/ota.rs +++ b/app/igneum-app/src/ota.rs @@ -954,7 +954,7 @@ impl Updater { // ---- the threads --------------------------------------------------------------------------------------------------- /// https://igneum.network/live -> https://igneum.network/api/live; "" when the build carries no live page. -fn live_api_from(live_page: &str) -> String { +pub fn live_api_from(live_page: &str) -> String { let Some(rest) = live_page.strip_prefix("https://") else { return String::new() }; let host = rest.split('/').next().unwrap_or(""); if host.is_empty() { String::new() } else { format!("https://{host}/api/live") } diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index bc888ee1b..0b8b6240a 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -180,6 +180,13 @@ fn handle(mut stream: TcpStream, shared: Arc) { let v = shared.state_json(); json_resp(&mut stream, 200, v); } + // the chain scene's feed (src/live.rs): the observer's /api/live with this machine's lane as "you", cached 2 s + ("GET", "/api/live") => { + let window = query_param(&req.query, "window").and_then(|s| s.parse().ok()).unwrap_or(120u32); + let live_api = std::env::var("IGNEUM_APP_LIVE_API").unwrap_or_else(|_| crate::ota::live_api_from(&shared.packaged.live_page)); + let ids: std::collections::HashSet = shared.state.lock().unwrap().mining.cards.iter().flat_map(|c| c.ids.iter().cloned()).collect(); + json_resp(&mut stream, 200, crate::live::fetch(&live_api, window, &ids)); + } ("GET", "/api/log") => { let after = query_param(&req.query, "after").and_then(|s| s.parse().ok()).unwrap_or(0u64); let limit = query_param(&req.query, "limit").and_then(|s| s.parse().ok()).unwrap_or(400usize).min(2000); diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index c22242ec4..61fe15973 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -266,10 +266,10 @@ var UpdateCard = (function () { var View = (function () { 'use strict'; var PAGES = [ - { id: 'overview', title: 'Overview', sub: 'the rate, what it costs, the chain as it grows' }, + { id: 'overview', title: 'Overview', sub: 'your rate, what it costs, the chain as it grows' }, { id: 'cards', title: 'Cards', sub: 'every card, tuned by Ember' }, - { id: 'earnings', title: 'Earnings', sub: 'what it made, where it goes' }, - { id: 'prove', title: 'Prove', sub: 'turn blocks into proofs, paid in IGN' }, + { id: 'earnings', title: 'Earnings', sub: 'what you made, where it goes' }, + { id: 'prove', title: 'Prove', sub: 'prove blocks, earn IGN' }, { id: 'settings', title: 'Settings', sub: 'tuning, this machine, updates, logs' } ]; var DAYS = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; @@ -311,14 +311,14 @@ var View = (function () { var on = !!cd.enabled && !removed && !unusable, st = removed ? 'removed' : unusable ? 'unusable' : (cd.state || 'off'); // the rule (docs/plans/ember-tune.md, 6 October 2026): a card never shows 0 MH/s without a reason word var tuning = st === 'tuning', held = st === 'held'; - var word = removed ? 'removed' : unusable ? ('not usable (' + cd.problem + ')') : !on ? 'off' : st === 'mining' ? 'mining' : tuning ? tuneWord(cd) : held ? (cd.message || 'held for a remote job') : st === 'restarting' ? 'restart in ' + (cd.restart_in_s || 0) + ' s' : st === 'waiting' ? 'waiting for the node' : st === 'faulted' ? 'stopped after repeated faults' : st === 'failed' ? 'failed' : st === 'starting' ? 'starting' : st === 'ready' ? 'ready' : st; + var word = removed ? 'removed' : unusable ? ('not usable (' + cd.problem + ')') : !on ? 'off' : st === 'mining' ? 'mining' : tuning ? tuneWord(cd) : held ? (cd.message || 'paused for a job from the team') : st === 'restarting' ? 'restart in ' + (cd.restart_in_s || 0) + ' s' : st === 'waiting' ? 'waiting for the node' : st === 'faulted' ? 'stopped after repeated errors' : st === 'failed' ? 'failed' : st === 'starting' ? 'starting' : st === 'ready' ? 'ready' : st; var tone = removed ? 'off' : unusable ? 'bad' : !on ? 'off' : (st === 'mining' || tuning) ? 'on' : (st === 'failed' || st === 'faulted' || st === 'restarting') ? 'bad' : ''; var hash = on && (st === 'mining' || tuning) ? (cd.hash_now >= 100 ? cd.hash_now.toFixed(0) : (cd.hash_now || 0).toFixed(1)) : ''; var temp = cd.temp_gpu > 0 ? Math.round(cd.temp_gpu) + ' °C' : ''; var tempTone = cd.temp_mem > 95 || cd.temp_gpu > 92 ? 'hot' : cd.temp_mem > 90 || cd.temp_gpu > 86 ? 'warm' : ''; var power = cd.power_w > 0 ? Math.round(cd.power_w) + ' W' : ''; var cap = cd.power_default_w > 0 ? Math.round(cd.power_default_w * (cd.power_pct || 80) / 100) : 0; - var sub = unusable ? (cd.reason || '') : removed ? 'unplugged; its worker stopped. The row goes in five minutes.' : cd.message || (!on && cd.reason ? cd.reason : '') || ''; + var sub = unusable ? (cd.reason || '') : removed ? 'unplugged. Its row goes in five minutes.' : cd.message || (!on && cd.reason ? cd.reason : '') || ''; var meta = []; if (cd.vram_mb) { var gb = cd.vram_mb / 1024; meta.push((gb >= 10 ? Math.round(gb) : gb.toFixed(gb < 4 ? 1 : 0)) + ' GB' + (cd.kind === 'apple' ? ' unified' : '')); } if (on && cd.accepted) meta.push(cd.accepted + ' block' + (cd.accepted === 1 ? '' : 's')); @@ -399,7 +399,7 @@ var View = (function () { if (cd.tune_line) { var nums = String(cd.tune_line).replace(/^(Tuned|Measured):\s*/, ''); if (cd.tune_source === 'baseline' || !cd.tune_control) return { kind: 'measured', text: 'Measured ' + nums + ' as it runs' + (when ? ', ' + when : ''), note: reasonOf(cd.sweep_note), button: 'tune', progress: -1 }; - var src = cd.tune_source === 'confirm' ? ' from the fleet prior, confirmed' : ''; + var src = cd.tune_source === 'confirm' ? ' from what other miners found, confirmed' : ''; var pin = cd.pinned ? 'Your cap stays pinned at ' + (cd.power_pct || 0) + '% · ' : ''; var pt = tunePoint(cd); var stopped = cd.sweep_note && /^tuning stopped/i.test(cd.sweep_note) ? ' · ' + cap(cd.sweep_note) : ''; @@ -420,10 +420,11 @@ var View = (function () { list.forEach(function (c) { if (c.sweep_at > 0 && (!last || c.sweep_at > last.sweep_at)) last = c; }); if (last) return 'Last tune ' + ago(now - last.sweep_at) + ' (' + last.name + ') · ' + nextCheck(last.sweep_at, st.tune_period_s, now) + '.'; if (!list.length) return 'No card here can be tuned.'; - return st.sweep === false ? 'Ember Tune is off. Tune and Tune all still work by hand.' : 'Not tuned yet: the first tune starts 2 min into steady mining.'; + return st.sweep === false ? 'Ember Tune is off. Tune and Tune all still work when you press them.' : 'Not tuned yet: the first tune starts 2 min into steady mining.'; } // the goal control's consequence: goal = efficiency|balanced|rate, cards, pence var GOALS = { efficiency: 'the most hashes per watt', balanced: 'hashes per watt within 1% of the top rate', rate: 'the top rate, whatever the watts' }; + var GOAL_NAMES = { efficiency: 'Efficiency', balanced: 'Balanced', rate: 'Maximum' }; function goalWords(goal, cards, pence) { goal = GOALS[goal] ? goal : 'balanced'; var list = (cards || []).filter(present), w = 0; @@ -445,11 +446,11 @@ var View = (function () { var tc = tuningCard(cards); if (tc) return { label: 'Tuning', sub: 'mining again in ' + tuneEta(tc.tune_eta_s) + ' · ' + tc.name, cls: 'stop', disabled: true, act: '' }; var heldCards = cards.filter(function (c) { return c.state === 'held'; }).length; - if (heldCards && !mining) return { label: 'Held', sub: 'a remote job holds the cards; mining restarts when it ends', cls: 'stop', disabled: true, act: '' }; - if (!on) return { label: 'Start mining', sub: cards.length ? 'switch a GPU on below first' : 'no GPU this app can drive', cls: '', disabled: true, act: '' }; + if (heldCards && !mining) return { label: 'Held', sub: 'a job from the team is using the cards. Mining restarts when it ends', cls: 'stop', disabled: true, act: '' }; + if (!on) return { label: 'Start mining', sub: cards.length ? 'switch a card on in Cards first' : 'no card this app can use', cls: '', disabled: true, act: '' }; if (m.paused) return { label: 'Start mining', sub: 'paused · the node keeps running', cls: '', disabled: false, act: 'resume' }; if (m.state === 'mining') return { label: 'Stop mining', sub: 'mining on ' + mining + ' of ' + on + ' card' + (on === 1 ? '' : 's'), cls: 'stop', disabled: false, act: 'pause' }; - if (m.state === 'waiting') return { label: 'Stop mining', sub: n && n.synced ? 'worker starting' : 'waiting for the node to sync', cls: 'stop', disabled: false, act: 'pause' }; + if (m.state === 'waiting') return { label: 'Stop mining', sub: n && n.synced ? 'card starting' : 'waiting for the node to sync', cls: 'stop', disabled: false, act: 'pause' }; return { label: 'Stop mining', sub: m.state || 'starting', cls: 'stop', disabled: false, act: 'pause' }; } // the row's state word while the miner is paused: the card is on, the miner is not @@ -490,25 +491,25 @@ var View = (function () { return null; } function switchLine(sw, daa) { - if (!sw) return (daa > 0 ? 'Every planned switch is behind this node. ' : '') + 'A switch is a planned rule change. The node applies it by itself when the chain reaches that height.'; + if (!sw) return (daa > 0 ? 'Every planned rule change is behind this node. ' : '') + 'A rule change is planned ahead. The node applies it by itself when the chain reaches that height.'; var blocks = daa > 0 ? withCommas(sw.away) + ' blocks away, about ' + (sw.away < 3600 ? Math.max(1, Math.round(sw.away / 60)) + ' min' : sw.away < 86400 ? Math.floor(sw.away / 3600) + ' h ' + Math.round(sw.away % 3600 / 60) + ' min' : Math.floor(sw.away / 86400) + ' d') + ' at one block a second.' : 'the node applies it by itself at that height.'; - return sw.name + ' at DAA ' + withCommas(sw.daa) + ': ' + blocks; + return sw.name + ' at block ' + withCommas(sw.daa) + ': ' + blocks; } // the Prove page: pv = state.proving, enabled = state.settings.prove, synced = state.node.synced function proveWords(pv, enabled, synced) { pv = pv || {}; - if (!enabled) return { word: 'off', sub: 'not proving', tone: 'off', note: 'Off. Switch it on and this machine proves the pieces the chain assigns to its keys.' }; + if (!enabled) return { word: 'off', sub: 'not proving', tone: 'off', note: 'Off. Switch it on and this machine proves the pieces it is given.' }; if (pv.status === 'setup' || (pv.enabled && !pv.available && pv.setup_hint)) return { word: 'needs setup', sub: 'one-time install', tone: 'bad', note: pv.setup_hint || pv.message || 'The prover is not installed yet.' }; if (!synced || pv.status === 'waiting') return { word: 'waiting', sub: 'for the node to sync', tone: '', note: pv.message || 'Proving starts once the node has every block.' }; - if (pv.status === 'proving') return { word: 'proving', sub: pv.current || 'a shard', tone: 'on', note: pv.message || 'A proof takes minutes on a CPU and seconds on a CUDA card.' }; + if (pv.status === 'proving') return { word: 'proving', sub: pv.current || 'a piece', tone: 'on', note: pv.message || 'A proof takes minutes on a CPU and seconds on an NVIDIA card.' }; if (pv.status === 'submitted') return { word: 'submitted', sub: 'waiting to be paid', tone: 'on', note: pv.message || 'The node checks the proof, then the chain pays it.' }; - if (pv.status === 'idle') return { word: 'idle', sub: 'nothing assigned', tone: 'ok', note: pv.message || 'On. Nothing is assigned to your keys right now; the next shard arrives within a minute.' }; + if (pv.status === 'idle') return { word: 'idle', sub: 'nothing assigned', tone: 'ok', note: pv.message || 'On. Nothing to prove right now. The next piece arrives within a minute.' }; return { word: pv.status || 'on', sub: pv.backend ? pv.backend.toUpperCase() : '', tone: '', note: pv.message || '' }; } function verifierWords(pv) { var vm = (pv && pv.verifier_mode) || 'unknown'; - var word = { command: 'verifying', trust: 'trusting (devnet only)', off: 'off: relay only', unknown: 'not read yet' }[vm] || vm; + var word = { command: 'checking proofs', trust: 'trusting without checking (devnet only)', off: 'off, passing proofs along', unknown: 'not read yet' }[vm] || vm; if (vm === 'command' && pv.pool_entries) word += ' · pool ' + pv.pool_verified + '/' + pv.pool_entries + (pv.pool_failed ? ', ' + pv.pool_failed + ' rejected' : ''); return { word: word, tone: vm === 'command' ? 'ok' : (vm === 'off' || vm === 'trust') ? 'warn' : 'dim', needsSetup: vm === 'off' && /WSL2 prover is not installed/.test((pv && pv.verifier_reason) || '') }; } @@ -517,8 +518,8 @@ var View = (function () { function proveTier(cd) { var gb = cd.vram_mb ? Math.round(cd.vram_mb / 1024) : 0; if (cd.vendor === 'apple') return cd.name + ': proves on the CPU, slowly.'; - if (cd.vendor === 'nvidia') return gb >= PROVE_GB ? cd.name + ': proves while it mines (' + gb + ' GB; a full shard needs ' + SHARD_GB + ' GB).' : cd.name + ': cannot prove a full shard (' + gb + ' GB; a full shard needs ' + SHARD_GB + ' GB).'; - return cd.name + ': cannot prove yet (the prover is CUDA only).'; + if (cd.vendor === 'nvidia') return gb >= PROVE_GB ? cd.name + ': proves while it mines (' + gb + ' GB; a full piece needs ' + SHARD_GB + ' GB).' : cd.name + ': cannot prove a full piece (' + gb + ' GB; a full piece needs ' + SHARD_GB + ' GB).'; + return cd.name + ': cannot prove yet (proving needs an NVIDIA card).'; } // the counts in one sentence: "idle · nothing assigned · 0 assigned · 0 proven · 0 paid · 0.0000 IGN" function proveLine(pv, enabled, synced) { @@ -543,10 +544,10 @@ var View = (function () { function jobsNote(j, now, jobTitle) { var parts = []; if (j.account) parts.push(j.account); - if (!j.allowed) parts.push('Off: nothing runs here until the switch above allows remote jobs.'); - else if (!j.url_set) parts.push('No jobs address in this build.'); + if (!j.allowed) parts.push('Off. Nothing runs here until the switch above allows jobs from the team.'); + else if (!j.url_set) parts.push('This build has no jobs address.'); else if (j.error) parts.push(j.error + '.'); - else if (j.active) parts.push('Running ' + jobTitle(j) + ' (' + j.id + ').'); + else if (j.active) parts.push('Running: ' + jobTitle(j) + '.'); else parts.push('Nothing running' + (j.checked_at ? '; checked ' + rel((now || 0) - j.checked_at) : '') + (j.queued ? '; ' + j.queued + ' queued' : '') + ((j.history || []).length ? '; ' + j.history.length + ' job' + (j.history.length === 1 ? '' : 's') + ' ran here' : '') + '.'); return parts.join(' '); } @@ -574,64 +575,64 @@ var View = (function () { [/^(\d+) remote jobs? received from Igneum/, function (m) { return m[1] + ' job' + (m[1] === '1' ? '' : 's') + ' from the team received'; }], [/^remote job: update check now/, function () { return 'The team asked for an update check'; }], [/^remote job: the app restarts/, function () { return 'The team asked the app to restart'; }], - [/^remote job asked for a node restart, but the node is external/, function () { return 'The team asked for a node restart; this node is not the app’s, so nothing was done'; }], + [/^remote job asked for a node restart, but the node is external/, function () { return 'The team asked for a node restart. This node is not the app’s, so nothing was done'; }], [/^remote jobs allowed/, function () { return 'Jobs from the team can run here'; }], [/^remote jobs switched off/, function () { return 'Jobs from the team are off'; }], [/^job finished; the miners restart/, function () { return 'The job finished; mining restarts'; }], [/^node synced: (\d+) blocks, (\d+) peer/, function (m) { return 'Node synced: ' + withCommas(+m[1]) + ' blocks, ' + m[2] + ' peer' + (m[2] === '1' ? '' : 's'); }], [/^a node already answers on/, function () { return 'Using the node already running on this machine'; }], [/^consensus parameters from the signed manifest/, function () { return 'Rule settings read from the team'; }], - [/^this node build does not understand the consensus parameters/, function () { return 'This node is too old for the team’s rule settings; an app update carries the newer node'; }], + [/^this node build does not understand the consensus parameters/, function () { return 'This node is too old for the team’s rule settings. An app update carries the newer node'; }], [/^restarting the node with the new consensus parameters/, function () { return 'Restarting the node with the new rule settings'; }], [/^kernel tuning from the signed manifest/, function () { return 'Tuning starting points read from the team'; }], [/^block accepted by the node \((.+)\)/, function (m) { return 'Block found by ' + m[1]; }], - [/^igneumd exited with code (\d+) after (\d+) s; restarting/, function (m) { return 'The node stopped (code ' + m[1] + '); restarting'; }], + [/^igneumd exited with code (\d+) after (\d+) s; restarting/, function (m) { return 'The node stopped (code ' + m[1] + '). Restarting'; }], [/^igneumd exited at once \(code (\d+)\)/, function (m) { return 'The node stopped at once (code ' + m[1] + ')'; }], [/^igneumd could not start/, function () { return 'The node could not start'; }], - [/^miner (\S+) exited with code (\d+); restarting in (\d+) s/, function (m) { return 'A worker stopped (code ' + m[2] + '); restarting in ' + m[3] + ' s'; }], - [/^no reading from the node for (\d+) s/, function (m) { return 'No reading from the node for ' + m[1] + ' s; restarting it'; }], - [/^no peer after 90 s/, function () { return 'No other node found after 90 s; still looking'; }], + [/^miner (\S+) exited with code (\d+); restarting in (\d+) s/, function (m) { return 'A card stopped (code ' + m[2] + '). Restarting in ' + m[3] + ' s'; }], + [/^no reading from the node for (\d+) s/, function (m) { return 'No reading from the node for ' + m[1] + ' s. Restarting it'; }], + [/^no peer after 90 s/, function () { return 'No other node found after 90 s. Still looking'; }], [/^no payout address/, function () { return 'No rewards address yet: finish the setup'; }], - [/^mining paused; the node keeps running/, function () { return 'Mining paused; the node keeps running'; }], + [/^mining paused; the node keeps running/, function () { return 'Mining paused. The node keeps running'; }], [/^mining resumed/, function () { return 'Mining resumed'; }], [/^GPU power cap in force: (.+)/, function (m) { return 'Power cap in force: ' + m[1]; }], - [/^GPU power cap NOT applied/, function () { return 'Power cap not applied: the administrator prompt was refused (Retry on the card)'; }], + [/^GPU power cap NOT applied/, function () { return 'Power cap not set. The administrator prompt was refused (Retry on the card)'; }], [/^power control on:/, function () { return 'Power control on'; }], [/^rewards go to (\S+)/, function (m) { return 'Rewards go to ' + shortHex(m[1], 6, 4); }], - [/^settings changed \((.+)\); the miners restart/, function (m) { return 'Settings changed (' + m[1] + '); mining restarts'; }], + [/^settings changed \((.+)\); the miners restart/, function (m) { return 'Settings changed (' + m[1] + '). Mining restarts'; }], [/^(.+): tune queued; it starts once/, function (m) { return m[1] + ': tune queued'; }], [/^(.+): tuning started: /, function (m) { return m[1] + ': tuning started'; }], - [/^(.+): best point (.+?) \((.+?)\); your setting stays pinned/, function (m) { return m[1] + ': best point ' + m[2] + ' (' + m[3] + '); your cap stays pinned'; }], + [/^(.+): best point (.+?) \((.+?)\); your setting stays pinned/, function (m) { return m[1] + ': best point ' + m[2] + ' (' + m[3] + '). Your cap stays pinned'; }], [/^(.+): measured as it runs: (.+)/, function (m) { return m[1] + ': measured ' + m[2]; }], [/^(.+): no tune: (.+)/, function (m) { return m[1] + ': no tune (' + m[2] + ')'; }], - [/^(.+): worker fault: (.+); the miner restarts the worker/, function (m) { return m[1] + ': worker fault; restarting it'; }], - [/^(.+): the worker could not compile the next program/, function (m) { return m[1] + ': the next program compiles at the hour boundary instead'; }], - [/^(.+) worker ready/, function (m) { return m[1] + ': worker ready'; }], - [/^(.+) kernel race: (.+?) at ([\d.]+) MH\/s/, function (m) { return m[1] + ': program variant chosen, ' + m[3] + ' MH/s'; }], + [/^(.+): worker fault: (.+); the miner restarts the worker/, function (m) { return m[1] + ': an error. Restarting it'; }], + [/^(.+): the worker could not compile the next program/, function (m) { return m[1] + ': the next program is built at the top of the hour instead'; }], + [/^(.+) worker ready/, function (m) { return m[1] + ': ready'; }], + [/^(.+) kernel race: (.+?) at ([\d.]+) MH\/s/, function (m) { return m[1] + ': fastest program chosen, ' + m[3] + ' MH/s'; }], [/^(.+): switched on, (\d+) identit/, function (m) { return m[1] + ': on'; }], [/^(.+): switched off/, function (m) { return m[1] + ': off'; }], - [/^(.+): (\d+) identit(y|ies); its worker restarts/, function (m) { return m[1] + ': ' + m[2] + ' identit' + m[3] + '; its worker restarts'; }], + [/^(.+): (\d+) identit(y|ies); its worker restarts/, function (m) { return m[1] + ': ' + m[2] + ' miner' + (m[2] === '1' ? '' : 's') + ' on it. The card restarts'; }], [/^next hourly program compiled and ready/, function () { return 'Next hourly program ready'; }], - [/^the hourly program changed; the worker restarts/, function () { return 'New hourly program; the worker restarts on it'; }], + [/^the hourly program changed; the worker restarts/, function () { return 'New hourly program. The card restarts on it'; }], [/^hourly program changed: /, function () { return 'New hourly program'; }], - [/^program pack out of date, rebuilding/, function () { return 'Rebuilding the program pack'; }], - [/^GPU worker built from source/, function () { return 'Worker built'; }], + [/^program pack out of date, rebuilding/, function () { return 'Rebuilding the mining programs'; }], + [/^GPU worker built from source/, function () { return 'Mining program built'; }], [/^block (\d+) shard (\d+) paid (.+) IGN/, function (m) { return 'Proof paid: ' + m[3] + ' IGN'; }], [/^block (\d+) shard (\d+) proven and submitted in (\d+) s/, function (m) { return 'Proof sent in ' + m[3] + ' s'; }], - [/^segment .+ paid (.+) IGN to the aggregator/, function (m) { return 'Segment paid: ' + m[1] + ' IGN'; }], - [/^segment (\S+)\.\.(\S+) aggregated and submitted in (\d+) s/, function (m) { return 'Segment proven in ' + m[3] + ' s'; }], - [/^segment (\S+)\.\.(\S+): (\d+) shards proven, aggregated and submitted in (\d+) s/, function (m) { return 'Segment proven: ' + m[3] + ' pieces in ' + m[4] + ' s'; }], - [/^segment (\S+)\.\.(\S+): (\d+) shards proven and submitted in (\d+) s; the segment record is held/, function (m) { return 'Segment proven: ' + m[3] + ' pieces in ' + m[4] + ' s; its record waits for the chain rule'; }], - [/^segment (\S+)\.\.(\S+) record accepted on retry/, function () { return 'Segment record accepted'; }], + [/^segment .+ paid (.+) IGN to the aggregator/, function (m) { return 'Run of blocks paid: ' + m[1] + ' IGN'; }], + [/^segment (\S+)\.\.(\S+) aggregated and submitted in (\d+) s/, function (m) { return 'Run of blocks proven in ' + m[3] + ' s'; }], + [/^segment (\S+)\.\.(\S+): (\d+) shards proven, aggregated and submitted in (\d+) s/, function (m) { return 'Run of blocks proven: ' + m[3] + ' pieces in ' + m[4] + ' s'; }], + [/^segment (\S+)\.\.(\S+): (\d+) shards proven and submitted in (\d+) s; the segment record is held/, function (m) { return 'Run of blocks proven: ' + m[3] + ' pieces in ' + m[4] + ' s. Its record waits for the chain'; }], + [/^segment (\S+)\.\.(\S+) record accepted on retry/, function () { return 'Run of blocks accepted'; }], [/^WSL2 prover setup started/, function () { return 'Prover setup started in its own window'; }], [/^Card removed: (.+); its worker stopped/, function (m) { return 'Card removed: ' + m[1]; }], [/^(.+): not usable \((.+?)\); its worker stopped/, function (m) { return m[1] + ': not usable (' + m[2] + ')'; }], [/^clock agrees with the network again/, function () { return 'Clock agrees with the network again'; }], [/^asking the system to set its clock/, function () { return 'Setting the clock from a time server'; }], [/^clock: (.+)/, function (m) { return 'Clock: ' + m[1]; }], - [/^devnet only: the node trusts proof records/, function () { return 'Devnet trust mode: proof records are not verified (Settings)'; }], - [/^a sweep is setting the caps right now/, function () { return 'A tune is setting the caps now; retry when it is done'; }], - [/^updates are off for this engine/, function () { return 'Updates are off for this engine (a measurement run)'; }], + [/^devnet only: the node trusts proof records/, function () { return 'Devnet trust mode: proofs are not checked (Settings)'; }], + [/^a sweep is setting the caps right now/, function () { return 'A tune is setting the caps now. Retry when it is done'; }], + [/^updates are off for this engine/, function () { return 'Updates are off for this copy (a measurement run)'; }], [/^(.+); the node restarts$/, function (m) { return cap(m[1]) + '; the node restarts'; }], [/^(.+); the node is external, so the app cannot restart it/, function (m) { return cap(m[1]) + '; the node is not the app’s, so it is not restarted'; }] ]; @@ -681,11 +682,11 @@ var View = (function () { if (tw.kind === 'running') { out.fill = tw.progress >= 0 ? tw.progress : 0.1; out.label = cd.tune_steps > 0 ? 'Tuning: step ' + (cd.tune_step || 1) + ' of ' + cd.tune_steps : 'Tuning'; out.sub = cd.tune_eta_s > 0 ? tuneEta(cd.tune_eta_s) + ' left' : ''; return out; } if (tw.kind === 'paused') { out.label = 'Tuning paused by Igneum'; out.sub = ''; return out; } if (tw.kind === 'stopped') { out.label = tw.text; return out; } - if (tw.kind === 'measured') { out.fill = 0.5; out.label = 'Measured, not tuned'; out.sub = /Power control/i.test(cd.sweep_note || '') ? 'Power control is off' : cd.vendor === 'apple' ? 'Apple silicon sets its own clocks' : (tw.note || '').replace(/^Measured only /, ''); return out; } + if (tw.kind === 'measured') { out.fill = 0.5; out.label = 'Measured, not tuned'; out.sub = /Power control/i.test(cd.sweep_note || '') ? 'Power control is off' : cd.vendor === 'apple' ? 'Apple silicon sets its own power' : (tw.note || '').replace(/^Measured only /, ''); return out; } if (tw.kind === 'tuned') { out.fill = 1; var when = cd.sweep_at > 0 && now ? ago(now - cd.sweep_at) : ''; - out.label = 'Tuned' + (when ? ' ' + when : '') + (cd.tune_source === 'confirm' ? ' from the fleet prior' : ''); - out.sub = tunePoint(cd) + (cd.tune_floor ? ' (the ladder’s floor)' : '') + (out.next ? ' · next check ' + out.next : ''); + out.label = 'Tuned' + (when ? ' ' + when : '') + (cd.tune_source === 'confirm' ? ' from what other miners found' : ''); + out.sub = tunePoint(cd) + (cd.tune_floor ? ' (the lowest clock Ember will set)' : '') + (out.next ? ' · next check ' + out.next : ''); var b = beforeOf(cd); out.after = cd.sweep_watts > 0 ? cd.sweep_watts : 0; out.before = b ? b.watts : 0; if (out.before > 0 && out.after > 0 && out.before > out.after) { out.savingW = Math.round(out.before - out.after); @@ -702,12 +703,12 @@ var View = (function () { (cards || []).forEach(function (c) { if (!present(c)) return; var e = ember(c, s, now); if (e.kind === 'tuned' && e.savingW > 0) { w += e.savingW; n++; } }); if (!n) return ''; var ppd = poundsPerDay(w, pence); - return 'Ember saves ' + Math.round(w) + ' W' + (ppd !== null ? ', ' + money(ppd) + ' a day' : '') + ' over ' + n + ' tuned card' + (n === 1 ? '' : 's'); + return 'Ember saves ' + Math.round(w) + ' W' + (ppd !== null ? ', ' + money(ppd) + ' a day' : '') + ' across ' + n + ' tuned card' + (n === 1 ? '' : 's'); } // the confirmations' words (in place, never a dialog) - function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? The miner restarts.'; } - function trustAsk(on) { return on ? 'Include proof records the node never checked? Devnet only; the node restarts.' : 'Verify proof records again? The node restarts.'; } - return { 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, 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, goalWords: goalWords, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk }; + 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 { 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, 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, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk }; })(); /* ---------- 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 @@ -878,7 +879,7 @@ if (typeof document !== 'undefined') (function () { if (!r.ok) { $('addr-err').textContent = r.error || 'could not save'; $('addr-err').hidden = false; return; } if (mode === 'generate' && r.private_key) { showKeySheet(r); } else { api('api/start', {}).then(function () { show('dashboard'); }); } - }).catch(function () { $('btn-start').disabled = false; toast('The engine did not answer'); }); + }).catch(function () { $('btn-start').disabled = false; toast('The app did not answer'); }); }); function showKeySheet(r) { $('key-address').textContent = r.display || r.address; @@ -908,7 +909,7 @@ if (typeof document !== 'undefined') (function () { if (!state) return; var t = View.toggle(state.mining, state.node, state); if (!t.act) return; - api('api/' + t.act, {}).then(function () { toast(t.act === 'pause' ? 'Mining stops; the node keeps running' : 'Mining starts'); }); + api('api/' + t.act, {}).then(function () { toast(t.act === 'pause' ? 'Mining stops. The node keeps running' : 'Mining starts'); }); }); // a row's switch, slider or stepper sends the whole list to the engine (pending[key] holds a flipped switch until // the engine agrees, so the row does not flicker back) @@ -917,7 +918,7 @@ if (typeof document !== 'undefined') (function () { clearTimeout(cardsT); cardsT = setTimeout(function () { var choices = readCardRows($('d-cards')); - if (choices.length) api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(msg || 'Applied; only the changed worker restarts'); else toast(r.error || 'could not change'); }); + if (choices.length) api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(msg || 'Applied. Only that card restarts'); else toast(r.error || 'could not change'); }); }, 400); } $('d-cards').addEventListener('change', function (e) { @@ -926,14 +927,14 @@ if (typeof document !== 'undefined') (function () { var key = e.target.closest('.gpu-line').dataset.key, on = e.target.checked; pending[key] = { on: on, at: Date.now() }; var choices = shownCards(state.mining.cards).filter(View.present).map(function (c) { var it = { key: c.key, enabled: c.key === key ? on : c.enabled, identities: c.identities || 1 }; if (c.vendor === 'nvidia' && c.power_default_w > 0) it.power_pct = c.power_pct || 80; return it; }); - api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(on ? 'Worker starting on that card' : 'Worker stopped on that card'); else { delete pending[key]; toast(r.error || 'could not change'); } }); - } else if (e.target.matches('.ids input')) sendCards('Identities set; that card’s worker restarts'); + api('api/cards', { cards: choices }).then(function (r) { if (r.ok) toast(on ? 'Starting that card' : 'That card is off'); else { delete pending[key]; toast(r.error || 'could not change'); } }); + } else if (e.target.matches('.ids input')) sendCards('Set. The card restarts'); }); $('d-cards').addEventListener('input', function (e) { if (e.target.matches('input[type=range]')) { var row = e.target.closest('.gpu-line'), cd = cardOf(row.dataset.key), pct = parseInt(e.target.value, 10); row.querySelector('.pv').textContent = pct + '% · ' + (cd && cd.power_default_w ? Math.round(cd.power_default_w * pct / 100) + ' W' : ''); - sendCards('Power cap set; pinned until you unpin it'); + sendCards('Power cap set. It stays until you choose Default again'); } }); $('d-cards').addEventListener('click', function (e) { @@ -941,13 +942,13 @@ if (typeof document !== 'undefined') (function () { var row = b.closest('.gpu-line'), key = row ? row.dataset.key : ''; if (b.dataset.open !== undefined) { openCards[key] = !openCards[key]; applyOpen(row); } else if (b.dataset.tune !== undefined) api('api/sweep/start', { key: key }).then(function (r) { toast(r && r.ok === false ? (r.error || 'could not start the tune') : 'Tune queued for ' + (cardOf(key) || {}).name); }); - else if (b.dataset.stop !== undefined) api('api/sweep/stop', {}).then(function () { toast('Tune stopped; the card is back where it was'); }); + else if (b.dataset.stop !== undefined) api('api/sweep/stop', {}).then(function () { toast('Tune stopped. The card is back where it was'); }); else if (b.dataset.d) { var inp = b.parentNode.querySelector('input'), v = parseInt(inp.value, 10) || 1; inp.value = Math.max(1, Math.min(64, v + parseInt(b.dataset.d, 10))); sendCards('Identities set; that card’s worker restarts'); } - else if (b.dataset.pin !== undefined) api('api/sweep/pin', { key: key, pinned: b.dataset.pin === '1' }).then(function () { toast(b.dataset.pin === '1' ? 'Cap pinned' : 'The tune chooses the cap again'); }); - else if (b.dataset.powerRetry !== undefined) api('api/power/apply', {}).then(function () { toast('Administrator prompt: allow it to set the cap'); }); + else if (b.dataset.pin !== undefined) api('api/sweep/pin', { key: key, pinned: b.dataset.pin === '1' }).then(function () { toast(b.dataset.pin === '1' ? 'Cap pinned' : 'Default: Ember chooses the cap again'); }); + else if (b.dataset.powerRetry !== undefined) api('api/power/apply', {}).then(function () { toast('Allow the administrator prompt to set the cap'); }); else if (b.dataset.price !== undefined) { showPage('settings'); $('s-price').focus(); } else if (b.closest('[data-card-goal]') && b.dataset.goal !== undefined) { - api('api/tune/goal', { key: key, goal: b.dataset.goal || '' }).then(function (r) { if (r && r.ok) toast(b.dataset.goal ? 'Goal for this card: ' + View.GOALS[b.dataset.goal] : 'This card follows the fleet goal'); else toast('A per-card goal needs the Ember Tune update'); }).catch(function () { toast('A per-card goal needs the Ember Tune update'); }); + api('api/tune/goal', { key: key, goal: b.dataset.goal || '' }).then(function (r) { if (r && r.ok) toast(b.dataset.goal ? 'Goal for this card: ' + View.GOALS[b.dataset.goal] : 'Default: this card follows the goal set for all cards'); else toast('A goal for one card needs the next Ember Tune update'); }).catch(function () { toast('A per-card goal needs the Ember Tune update'); }); } }); function applyOpen(row) { var open = !!openCards[row.dataset.key], det = row.querySelector('.gl-details'), chev = row.querySelector('[data-open]'); if (det) det.hidden = !open; if (chev) { chev.setAttribute('aria-expanded', open ? 'true' : 'false'); chev.classList.toggle('open', open); } row.classList.toggle('open', open); } @@ -957,21 +958,21 @@ if (typeof document !== 'undefined') (function () { var list = shownCards(state.mining.cards).filter(function (c) { return View.tunable(c) && c.enabled; }); if (!list.length) { toast('No card to tune'); return; } var n = 0; - list.reduce(function (p, c) { return p.then(function () { return api('api/sweep/start', { key: c.key }).then(function (r) { if (!r || r.ok !== false) n++; }); }); }, Promise.resolve()).then(function () { toast(n === 1 ? 'Tune queued for 1 card' : 'Tune queued for ' + n + ' cards, one at a time'); }); + list.reduce(function (p, c) { return p.then(function () { return api('api/sweep/start', { key: c.key }).then(function (r) { if (!r || r.ok !== false) n++; }); }); }, Promise.resolve()).then(function () { toast(n === 1 ? 'Tune queued for 1 card' : 'Tune queued for ' + n + ' cards, one after another'); }); }); // the goal (ember-tune: POST api/tune/goal); a build without the route says so and keeps the old value function setGoal(goal) { - api('api/tune/goal', { goal: goal }).then(function (r) { if (r && r.ok) { store('igneum.goal', goal); toast('Goal: ' + View.GOALS[goal]); } else toast('The goal needs the Ember Tune update'); if (state) renderTuneCard(state); }).catch(function () { toast('The goal needs the Ember Tune update'); }); + api('api/tune/goal', { goal: goal }).then(function (r) { if (r && r.ok) { store('igneum.goal', goal); toast('Goal: ' + View.GOALS[goal]); } else toast('The goal needs the next Ember Tune update'); if (state) renderTuneCard(state); }).catch(function () { toast('The goal needs the Ember Tune update'); }); } ['goal-seg', 'goal-seg-2'].forEach(function (id) { $(id).addEventListener('click', function (e) { var b = e.target.closest('.seg-b'); if (b) setGoal(b.dataset.goal); }); }); - function powerControl(on) { api('api/power/control', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Power control on: Windows asks for administrator rights once' : 'Power control off; nothing asks'); }); } + function powerControl(on) { api('api/power/control', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Power control on. Windows asks for administrator rights once' : 'Power control off. Nothing asks'); }); } $('m-power-control').addEventListener('change', function () { powerControl(this.checked); }); $('s-power-control').addEventListener('change', function () { powerControl(this.checked); }); $('m-clock-sync').addEventListener('click', function () { api('api/clock/sync', {}); }); $('btn-open-log').addEventListener('click', function () { setDrawer(true); }); // ---------- Earnings ---------- - $('s-devfee').addEventListener('change', function () { api('api/settings', { dev_fee: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); }); + $('s-devfee').addEventListener('change', function () { api('api/settings', { dev_fee: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied. Mining restarts' : 'Applied'); }); }); $('e-price-btn').addEventListener('click', function () { showPage('settings'); $('s-price').focus(); }); $('t-money-cell').addEventListener('click', function () { if ($('t-money').classList.contains('ask-price')) { showPage('settings'); $('s-price').focus(); } }); $('s-address-save').addEventListener('click', function () { @@ -994,7 +995,7 @@ if (typeof document !== 'undefined') (function () { $('r-wallet').addEventListener('click', function () { api('api/open', { url: 'https://igneum.network/wallet' }); }); // ---------- Prove ---------- - $('s-prove').addEventListener('change', function () { var on = this.checked; api('api/prove', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Proving on; the first piece arrives within a minute' : 'Proving off'); else { toast(r.error || 'could not change'); $('s-prove').checked = !on; } }); }); + $('s-prove').addEventListener('change', function () { var on = this.checked; api('api/prove', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Proving on. The first piece arrives within a minute' : 'Proving off'); else { toast(r.error || 'could not change'); $('s-prove').checked = !on; } }); }); $('pv-setup').addEventListener('click', function () { api('api/prove/setup', {}).then(function (r) { toast(r.ok ? 'Setup started in its own window' : (r.error || 'could not start')); }); }); // ---------- Settings ---------- @@ -1006,11 +1007,11 @@ if (typeof document !== 'undefined') (function () { $('s-vote').addEventListener('change', function () { api('api/settings', { vote: this.checked }).then(function (r) { if (r.ok) toast(r.restart ? 'Applied; the miner restarts' : 'Applied'); }); }); $('s-login').addEventListener('change', function () { var on = this.checked; api('api/settings', { start_at_login: on }).then(function (r) { if (!r.ok) { toast(r.error || 'could not change'); $('s-login').checked = !on; } }); }); $('s-jobs-allow').addEventListener('change', function () { api('api/jobs/allow', { on: this.checked }); }); - $('s-sweep').addEventListener('change', function () { api('api/sweep/enable', { on: this.checked }).then(function (r) { if (r.ok) toast($('s-sweep').checked ? 'Ember Tune on: once after install, then every 7 days' : 'Ember Tune off'); }); }); + $('s-sweep').addEventListener('change', function () { api('api/sweep/enable', { on: this.checked }).then(function (r) { if (r.ok) toast($('s-sweep').checked ? 'Ember Tune on. Once after install, then every 7 days' : 'Ember Tune off'); }); }); $('s-climb').addEventListener('change', function () { var on = this.checked; api('api/tune/goal', { climb: on }).then(function (r) { if (!r || !r.ok) { toast('Needs the Ember Tune update'); $('s-climb').checked = !on; } }); }); function savePrice() { var p = parseFloat($('s-price').value); - if (!(p >= 0)) { toast('A price in pence per kWh, for example 28'); return; } + if (!(p >= 0)) { toast('Type a price in pence per kWh, for example 28'); return; } store('igneum.price', p > 0 ? p : null); api('api/tune/goal', { price_pence: p }).then(function () { toast(p > 0 ? 'Price saved: ' + p + 'p per kWh' : 'Price cleared'); if (state) renderPage(state); }).catch(function () { toast(p > 0 ? 'Price saved on this window: ' + p + 'p per kWh' : 'Price cleared'); if (state) renderPage(state); }); } @@ -1020,7 +1021,7 @@ if (typeof document !== 'undefined') (function () { $('ask-trust-no').addEventListener('click', function () { $('ask-trust').hidden = true; }); $('ask-trust-yes').addEventListener('click', function () { var on = $('ask-trust').dataset.on === '1'; $('ask-trust').hidden = true; - api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) { $('s-trust').checked = on; toast(on ? 'Trust mode on (devnet only); the node restarts' : 'Trust mode off; the node restarts'); } else toast(r.error || 'could not change'); }); + api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) { $('s-trust').checked = on; toast(on ? 'Trust mode on (devnet only). The node restarts' : 'Trust mode off. The node restarts'); } else toast(r.error || 'could not change'); }); }); $('s-live').addEventListener('click', function () { if (state && state.live_page) api('api/open', { url: state.live_page }); }); $('s-log-open').addEventListener('click', function () { setDrawer(true); }); @@ -1289,7 +1290,7 @@ if (typeof document !== 'undefined') (function () { if (found.length && Object.keys(born).length > found.length + 50) { var keep = {}; for (var k = 0; k < found.length; k++) if (born[String(found[k])]) keep[String(found[k])] = born[String(found[k])]; born = keep; } if (!shown) { x.fillStyle = css('--ash') || '#9A9A9E'; x.font = '500 12px IBM Plex Mono, monospace'; x.textAlign = 'center'; - var msg = !state ? 'waiting for the engine' : state.mining.state === 'mining' ? 'mining: a block this machine finds appears here' : state.mining.state === 'paused' ? 'paused: no blocks while mining is stopped' : state.node.synced ? 'waiting for the worker' : 'waiting for the node to sync'; + var msg = !state ? 'waiting for the engine' : state.mining.state === 'mining' ? 'Mining. A block this machine finds appears here' : state.mining.state === 'paused' ? 'Paused. No blocks while mining is stopped' : state.node.synced ? 'waiting for the card' : 'waiting for the node to sync'; x.fillText(msg, W / 2, rail - 10); } return animating && !reduce; @@ -1319,7 +1320,7 @@ if (typeof document !== 'undefined') (function () { try { dag = IgneumDag.mount($('dag-live'), { compact: window.innerWidth < 720, window: 120, url: 'api/live', mine: 'you', tooltip: $('dag-tip'), pauseButton: $('dag-pause'), onState: function (st, why) { var live = st === 'live' || st === 'stale'; - if (dagMode !== st) { dagMode = st; setText('dag-state', st === 'live' ? 'live' : st === 'stale' ? 'behind' : st === 'failed' ? 'your blocks' : 'connecting'); setText('dag-note', st === 'failed' ? 'The full chain view needs the next engine update; this is the last 10 minutes of your own blocks.' : st === 'stale' ? (why || '') : ''); } + if (dagMode !== st) { dagMode = st; setText('dag-state', st === 'live' ? 'live' : st === 'stale' ? 'behind' : st === 'failed' ? 'your blocks only' : 'connecting'); setText('dag-note', st === 'failed' ? 'The full chain view needs the next update. This is the last 10 minutes of your own blocks.' : st === 'stale' ? (why || '') : ''); } $('dag-live').hidden = !live; $('dag-tip').hidden = true; $('dag-pause').hidden = !live; $('dag').hidden = live; if (!live) { size(); requestDraw(); } } }); @@ -1374,7 +1375,7 @@ if (typeof document !== 'undefined') (function () { var sig = cards.map(function (c) { return c.key + ':' + (c.problem || '') + ':' + (c.removed_at > 0 ? 'r' : '') + ':' + (c.enabled ? 'on' : 'off'); }).join('|'); if (sig !== cardsRendered) { cardsRendered = sig; renderCardRows(list, cards); } if (!cards.length) { - $('cards-sub').textContent = 'No graphics card this app can drive was found.'; + $('cards-sub').textContent = 'No graphics card this app can use was found.'; $('cards-note').textContent = (s.detect_message || '') + ' The node can still run on its own; mining needs a supported card.'; $('cards-note').hidden = false; $('cards-help').hidden = true; $('btn-cards-retry').hidden = false; $('btn-cards-next').disabled = false; $('btn-cards-next').textContent = 'Continue without mining'; return; @@ -1456,19 +1457,19 @@ if (typeof document !== 'undefined') (function () { var dots = sorted.map(function (p) { return ''; }).join(''); return '
' + dots + '' + Math.round(w0) + ' W' + Math.round(w1) + ' W' + - '' + m0.toFixed(0) + '' + m1.toFixed(0) + 'Hash rate against draw for every step of the last tune. The ringed point is the one Ember kept; a hollow point was rejected (a fault, a hot card or a dragged memory clock).
'; + '' + m0.toFixed(0) + '' + m1.toFixed(0) + 'Hash rate against power for every step of the last tune. The ringed point is the one Ember kept. A hollow point was rejected: a fault, a hot card or a slowed memory clock.'; } // the card's details (one chevron): the cap slider, identities, pin, the telemetry, the facts, the last tune's table function detailsHtml(cd, s) { var nv = cd.vendor === 'nvidia' && cd.power_default_w > 0, pct = cd.power_pct || 80, h = ''; - if (cd.removed_at > 0) return '

The card was unplugged; its worker stopped. The row goes in five minutes.

'; - if (cd.problem) return '

' + esc(cd.reason || 'No worker can drive this card.') + ' Reboot with the card attached; if it persists, reinstall the driver with the card attached.

'; + if (cd.removed_at > 0) return '

The card was unplugged. Its row goes in five minutes.

'; + if (cd.problem) return '

' + esc(cd.reason || 'This app cannot use this card.') + ' Reboot with the card attached; if it persists, reinstall the driver with the card attached.

'; if (nv) { var want = Math.round(cd.power_default_w * pct / 100); h += '
Power cap' + pct + '% · ' + want + ' W' + - '

Lower is cooler and quieter; most cards hash nearly as fast at 70%. ' + (cd.pinned ? 'Set by hand, so the tune leaves it. ' : 'Ember Tune may move it.') + '

'; - h += cd.power_applied ? '
cap applied: ' + want + ' W' + (cd.sweep_pct === pct && cd.sweep_pct ? ' chosen by the tune' : '') + '
' : '
cap not applied: Windows needs the administrator prompt
'; - } else if (cd.vendor === 'apple') h += '

Apple silicon manages its own power; there is no cap to set.

'; + '

Lower is cooler and quieter. Most cards hash nearly as fast at 70%. ' + (cd.pinned ? 'Set by hand, so Ember leaves it. ' : 'Default: Ember may move it.') + '

'; + h += cd.power_applied ? '
Cap in force: ' + want + ' W' + (cd.sweep_pct === pct && cd.sweep_pct ? ' Ember’s choice' : '') + '
' : '
Cap not set yet. Windows needs the administrator prompt.
'; + } else if (cd.vendor === 'apple') h += '

Apple silicon manages its own power. There is no cap to set.

'; var tl = []; if (cd.temp_mem > 0) tl.push('memory ' + Math.round(cd.temp_mem) + ' °C'); if (cd.gclk_mhz > 0) tl.push('core clock ' + Math.round(cd.gclk_mhz) + ' MHz' + (cd.clock_cap_mhz > 0 ? ' (cap ' + cd.clock_cap_mhz + ')' : '')); @@ -1478,13 +1479,13 @@ if (typeof document !== 'undefined') (function () { if (tl.length) h += '
' + tl.join('') + '
'; var tm = TuneLine.model(cd, s.now), em = View.ember(cd, s, s.now); if (View.tunable(cd)) { - var g = cd.tune_goal || '', goalSeg = '
' + [['', 'As the fleet'], ['efficiency', 'Efficiency'], ['balanced', 'Balanced'], ['rate', 'Maximum']].map(function (o) { return ''; }).join('') + '
'; - h += '
Goal' + goalSeg + (em.kind === 'running' ? '' : '') + '

This card’s own goal. "As the fleet" follows the Tuning strip above' + (cd.tune_goal === undefined ? ' (a per-card goal needs the Ember Tune update; the fleet goal applies until then)' : '; a new goal applies from the next tune') + '.

'; + var g = cd.tune_goal || '', goalSeg = '
' + [['', 'Default', 'the goal set for all cards'], ['efficiency', 'Efficiency', 'most hashes per watt, a little rate traded'], ['balanced', 'Balanced', 'a little rate for most of the saving'], ['rate', 'Maximum', 'most hashes, watts second']].map(function (o) { return ''; }).join('') + '
'; + h += '
Goal' + goalSeg + (em.kind === 'running' ? '' : '') + '

' + (cd.tune_goal === undefined ? 'A goal for one card needs the next Ember Tune update. Until then every card follows the goal above.' : 'This card’s own goal. Default follows the goal set for all cards. A new goal applies from the next tune.') + '

'; } if (tm.kind === 'tuned' || tm.kind === 'measured') h += '
' + esc(tm.text) + '' + (tm.note ? ' ' + esc(tm.note) + '' : '') + (em.savingText ? ' ' + esc(em.savingText) + '' : '') + '
'; h += curveHtml(em.points); - h += '
Identities

Each identity votes and is paid on its own. 8 suits a big card, 2 a small one, 1 an integrated GPU. Changing it restarts that card’s worker.

'; - var facts = [cd.worker ? 'worker ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean); + h += '
Miners on this card

How many miners this card runs as. Each one votes and is paid on its own. 8 suits a big card, 2 a small one, 1 a built-in GPU. Changing it restarts the card.

'; + var facts = [cd.worker ? 'runs on ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean); if (facts.length) h += '

' + esc(facts.join(' · ')) + '

'; return h; } @@ -1497,7 +1498,7 @@ if (typeof document !== 'undefined') (function () { var mining = presentCards.filter(function (c) { return c.state === 'mining'; }).length; // the live total: rate, watts, £ a day, blocks setText('t-hash', m.hash_total >= 100 ? m.hash_total.toFixed(0) : m.hash_total.toFixed(1)); - setText('t-hash-sub', m.state === 'paused' ? 'paused' : mining ? (mining + ' of ' + presentCards.length + ' card' + (presentCards.length === 1 ? '' : 's') + ' mining') : m.state === 'waiting' ? (n.synced ? 'worker starting' : 'waiting for the node') : m.state === 'idle' ? 'no GPU worker' : m.state); + setText('t-hash-sub', m.state === 'paused' ? 'paused' : mining ? (mining + ' of ' + presentCards.length + ' card' + (presentCards.length === 1 ? '' : 's') + ' mining') : m.state === 'waiting' ? (n.synced ? 'card starting' : 'waiting for the node') : m.state === 'idle' ? 'no card mining' : m.state); var watts = View.wattsTotal(m.cards), price = priceNow(), eff = watts > 0 && m.hash_total > 0 ? View.effText(m.hash_total / watts) : ''; setHidden('t-power-cell', !(watts > 0)); setText('t-power', String(Math.round(watts))); setText('t-power-sub', eff || 'drawn now'); var ppd = View.poundsPerDay(watts, price); @@ -1511,10 +1512,10 @@ if (typeof document !== 'undefined') (function () { 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.tuneWords(c, s, s.now), View.cells(c, price), c.identities, c.power_pct, c.pinned, c.power_applied, Math.round(c.temp_mem || 0), Math.round(c.gclk_mhz || 0), Math.round(c.mclk_mhz || 0), (c.tune_curve || []).length, c.tune_goal || '', c.sweep_watts, c.sweep_mhs]; })]); + var sig = JSON.stringify([rows, shown.map(function (c) { return [View.tuneWords(c, s, s.now), View.cells(c, price), c.identities, c.power_pct, c.pinned, c.power_applied, Math.round(c.temp_mem || 0), Math.round(c.gclk_mhz || 0), Math.round(c.mclk_mhz || 0), (c.tune_curve || []).length, c.tune_goal || '', c.sweep_watts, c.sweep_mhs]; }), goalNow()]); if (sig !== mineRowsSig && !busy) { mineRowsSig = sig; - box.innerHTML = rows.length ? shown.map(function (cd, i) { return gpuLineHtml(cd, s, rows[i]); }).join('') : '
' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can drive was found. The node runs on its own.') + '
'; + box.innerHTML = rows.length ? shown.map(function (cd, i) { return gpuLineHtml(cd, s, rows[i]); }).join('') : '
' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can use was found. The node runs on its own.') + '
'; box.querySelectorAll('.gpu-line').forEach(applyOpen); } var note = !shown.length && s.detect_message ? s.detect_message : ''; @@ -1557,19 +1558,19 @@ if (typeof document !== 'undefined') (function () { setText('n-version', n.version ? n.version.replace(/^igneumd\s*/, '') : '--'); setText('d-node-net', s.chain || s.network || ''); setText('n-headers', withCommas(n.headers)); setText('n-daa', withCommas(n.daa)); setText('n-diff', compact(n.difficulty)); setText('n-tips', String(n.tips)); setText('n-blue', withCommas(n.blue)); - if (n.daa > 0 && p && p.eta_s >= 0) { setText('d-eta', hms(p.eta_s)); setText('d-eta-sub', 'until the next hourly mining program' + (p.epoch_index ? ' (epoch ' + p.epoch_index + ')' : '') + (p.message ? ' · ' + p.message : '')); } - else { setText('d-eta', '--:--'); setText('d-eta-sub', n.state === 'syncing' ? 'known after the node is synced' : 'known once the node runs'); } - setText('n-digest', n.consensus_digest || (n.state === 'stopped' || n.state === 'failed' ? 'the node is not running' : 'not printed yet')); + if (n.daa > 0 && p && p.eta_s >= 0) { setText('d-eta', hms(p.eta_s)); setText('d-eta-sub', 'until the next hourly program' + (p.epoch_index ? ' (epoch ' + p.epoch_index + ')' : '') + (p.message ? ' · ' + p.message : '')); } + else { setText('d-eta', '--:--'); setText('d-eta-sub', n.state === 'syncing' ? 'known after the node is synced' : 'known once the node is running'); } + setText('n-digest', n.consensus_digest || (n.state === 'stopped' || n.state === 'failed' ? 'the node is not running' : 'not read yet')); var sw = View.nextSwitch(n.consensus_switches, n.daa); setText('n-switch', sw ? sw.name + ' at ' + withCommas(sw.daa) : (n.consensus_switch_daa > 0 && n.consensus_switch_daa > n.daa ? 'DAA ' + withCommas(n.consensus_switch_daa) : (n.consensus_switches && n.consensus_switches.length ? 'none ahead' : 'none planned'))); - setText('n-switch-help', sw ? View.switchLine(sw, n.daa) : (n.consensus_switch_daa > 0 && n.consensus_switch_daa > n.daa ? 'The update manifest announces a switch at DAA ' + withCommas(n.consensus_switch_daa) + ' (' + withCommas(n.consensus_switch_daa - n.daa) + ' blocks away).' : View.switchLine(null, n.daa))); + setText('n-switch-help', sw ? View.switchLine(sw, n.daa) : (n.consensus_switch_daa > 0 && n.consensus_switch_daa > n.daa ? 'A rule change is announced at block ' + withCommas(n.consensus_switch_daa) + ' (' + withCommas(n.consensus_switch_daa - n.daa) + ' blocks away).' : View.switchLine(null, n.daa))); var sig = JSON.stringify([n.consensus_switches, sw && sw.key]); if (sig !== switchesSig) { switchesSig = sig; $('n-switches').innerHTML = (n.consensus_switches || []).map(function (x) { return '' + esc(x.name) + (x.daa <= n.daa ? ' (applied)' : '') + '' + withCommas(x.daa) + ''; }).join(''); } - if (f.last_lock > 0) { setText('f-lock', '#' + withCommas(f.last_lock) + ' · ' + rel(f.age_s)); setText('f-note', 'a point the miners agreed can never be undone; locks at 2/3 of the 30-day weight'); } - else { setText('f-lock', 'none yet'); setText('f-note', f.message || 'locks appear once the miner votes on checkpoints'); } + if (f.last_lock > 0) { setText('f-lock', '#' + withCommas(f.last_lock) + ' · ' + rel(f.age_s)); setText('f-note', 'a point the miners agreed can never be undone. It locks at two thirds of the last 30 days of mining'); } + else { setText('f-lock', 'none yet'); setText('f-note', f.message || 'locks appear once this machine votes'); } setText('f-votes', withCommas(f.votes)); } @@ -1584,14 +1585,14 @@ if (typeof document !== 'undefined') (function () { setText('e-cost-sub', ppd === null ? (watts > 0 ? 'electricity: set a price to see the cost' : 'electricity: no card reports its draw') : 'electricity at ' + price + 'p/kWh for ' + Math.round(watts) + ' W'); setHidden('e-price-btn', !(watts > 0 && ppd === null)); if (document.activeElement !== $('s-devfee')) $('s-devfee').checked = !!(s.settings && s.settings.dev_fee); - setText('s-devfee-text', s.settings.dev_fee ? 'Dev fee: 1 block in 100 pays the miner software’s author' : 'Dev fee off: every block pays your address'); - setText('r-devfee-line', s.settings.dev_fee ? 'The protocol takes nothing; this is the miner software’s fee, the same way every GPU miner takes one' + (m.fee_total ? ' (' + withCommas(m.fee_total) + ' block' + (m.fee_total === 1 ? '' : 's') + ' so far)' : '') + '.' : 'The protocol takes nothing either.'); + setText('s-devfee-text', s.settings.dev_fee ? 'Dev fee: 1 block in 100 pays the people who make this app' : 'Dev fee off. Every block pays your address'); + setText('r-devfee-line', s.settings.dev_fee ? 'The network itself takes nothing. This is the app’s fee, the same way every mining app takes one' + (m.fee_total ? ' (' + withCommas(m.fee_total) + ' block' + (m.fee_total === 1 ? '' : 's') + ' so far)' : '') + '.' : 'The network itself takes nothing either.'); setText('r-address', a.display || 'not set'); setText('r-source', a.source === 'generated' ? 'made on this machine' : a.source === 'pasted' ? 'your own address' : ''); setText('r-address-help', a.display ? 'Every block this machine finds pays this address.' : 'No address yet. Finish the setup to choose one.'); var gen = a.source === 'generated'; setHidden('key-toggle', !gen); if (!gen) { $('key-details').hidden = true; hideKey(); } - setText('r-key-help', 'The key for this address was made on this machine and is stored in the app folder, readable by your user only. Keep a copy somewhere safe.'); + setText('r-key-help', 'The key for this address was made on this machine and is stored in the app folder. Only your user can read it. Keep a copy somewhere safe.'); setText('r-key-file', gen && a.wallet_file ? a.wallet_file : ''); } @@ -1611,8 +1612,8 @@ if (typeof document !== 'undefined') (function () { setText('pv-verifier', v.word); $('pv-verifier').className = 'v ' + v.tone; setText('pv-verifier-help', pv.verifier_note || 'the node checks a proof before it counts'); $('pv-setup-row').hidden = !((pv.enabled && !pv.available && pv.setup_hint) || v.needsSetup); - var seg = pv.segments_submitted ? pv.segments_submitted + ' proven whole, ' + (pv.segments_paid || 0) + ' paid' + (pv.segment_paid_wei && Number(pv.segment_paid_wei) ? ' (' + (Number(pv.segment_paid_wei) / 1e18).toFixed(4) + ' IGN)' : '') + (pv.segment_last_s ? ', the last in ' + Math.round(pv.segment_last_s) + ' s' : '') + (pv.segments_held ? ', ' + pv.segments_held + ' held for the chain rule' : '') : 'none yet'; - setText('pv-seg', seg); setText('pv-seg-note', pv.segment_note && enabled ? pv.segment_note : 'whole segments this machine proved'); + var seg = pv.segments_submitted ? pv.segments_submitted + ' proven whole, ' + (pv.segments_paid || 0) + ' paid' + (pv.segment_paid_wei && Number(pv.segment_paid_wei) ? ' (' + (Number(pv.segment_paid_wei) / 1e18).toFixed(4) + ' IGN)' : '') + (pv.segment_last_s ? ', the last took ' + Math.round(pv.segment_last_s) + ' s' : '') + (pv.segments_held ? ', ' + pv.segments_held + ' waiting for the chain' : '') : 'none yet'; + setText('pv-seg', seg); setText('pv-seg-note', pv.segment_note && enabled ? pv.segment_note : 'whole runs of blocks this machine proved'); setText('pv-program', pv.program_id || 'not read yet'); setText('pv-aggregator', pv.aggregator_id || 'not read yet'); } @@ -1794,11 +1795,11 @@ if (typeof document !== 'undefined') (function () { if (u.rolled_back) parts.push('Rolled back: ' + u.rolled_back + '.'); if (n && n.kind !== 'update-installed' && !(u.rolled_back && u.status === 'error')) parts.push(n.text + (n.detail ? ' ' + Notices.endDot(n.detail) : '')); else if (u.status === 'checking') parts.push('Checking.'); - else if (u.status === 'current') parts.push('Up to date' + (u.checked_at ? ', checked ' + rel(state.now - u.checked_at) : '') + '.'); + else if (u.status === 'current') parts.push('Up to date' + (u.checked_at ? '. Checked ' + rel(state.now - u.checked_at) : '') + '.'); else if (/no update manifest configured/.test(u.error || '')) parts.push('This build has no update address; updates come as a new download.'); else if (u.error) parts.push(u.error); - else if (!u.status || u.status === 'unknown') parts.push('Not checked yet. The engine checks by itself every hour.'); - if (u.activation_height && !u.urgent && state && state.node && u.activation_height > state.node.daa) parts.push('A rule switch comes at height ' + withCommas(u.activation_height) + '.'); + else if (!u.status || u.status === 'unknown') parts.push('Not checked yet. The app checks by itself every hour.'); + if (u.activation_height && !u.urgent && state && state.node && u.activation_height > state.node.daa) parts.push('A rule change comes at block ' + withCommas(u.activation_height) + '.'); return parts.join(' '); } function sampleUpdate(kind) { @@ -1871,11 +1872,11 @@ if (typeof document !== 'undefined') (function () { }).catch(function () { failures++; if (failures >= 4) { - $('pill').classList.add('warn', 'away'); $('pill-text').textContent = state && state.quitting ? 'Stopped' : 'Engine not answering'; + $('pill').classList.add('warn', 'away'); $('pill-text').textContent = state && state.quitting ? 'Stopped' : 'App not answering'; if (phase === 'dashboard') { - setText('node-line-text', 'Node: no answer'); setText('node-sub', 'the engine is not answering; the window reconnects by itself'); $('node-card').classList.add('bad'); - setText('btn-toggle-sub', 'the engine is not answering'); $('btn-toggle').disabled = true; - setText('t-hash-sub', 'no answer from the engine'); + setText('node-line-text', 'Node: no answer'); setText('node-sub', 'the app is not answering. The window reconnects by itself'); $('node-card').classList.add('bad'); + setText('btn-toggle-sub', 'the app is not answering'); $('btn-toggle').disabled = true; + setText('t-hash-sub', 'no answer from the app'); } } }); diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index 5848fee39..971b79b0b 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -118,7 +118,7 @@ - +
@@ -146,7 +146,7 @@ Use my own address - Paste an Ethereum-style address you control: 0x and 40 characters. + Paste an address you control. It starts with 0x and has 40 characters after it. @@ -188,7 +188,7 @@

The chain, live

connecting
- +
@@ -207,21 +207,21 @@
peers0other nodes it talks to
version--devnet v4
headers0headers arrive before blocks
-
DAA score0blocks the whole network has made
+
network blocks0blocks the whole network has made (the DAA score)
difficulty0how hard the next block is to find
-
tips0open ends of the block DAG right now
-
blue score0blocks on the agreed main chain
+
tips0open ends of the chain right now
+
blue score0blocks on the agreed main chain (the blue score)
next program--:--the hourly mining program
last locknone yeta point the miners agreed can never be undone
-
votes sent0this machine signs a checkpoint every 30 s
+
votes sent0this machine signs a lock point every 30 s
-
rules digest
+
rules fingerprint
not printed yet

Every node on the network shows the same fingerprint of the rules. A peer with another one is refused.

-
next rule switch
+
next rule change
none planned

@@ -238,11 +238,12 @@