Merge miner-ui-4 7ce5965 for 0.3.15: GET /api/live (the observer's chain feed with this machine's lane marked), the copy sweep, the goal rulings
Every file under app/igneum-app/ui/ is that branch's side; outside ui/: src/live.rs (new), the /api/live route, mod live, ota.rs's live_api_from made pub, the copy table and the reshot screenshots. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
140
app/igneum-app/src/live.rs
Normal file
|
|
@ -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<Option<(Instant, u32, Value)>> = 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<String>) -> 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: `<live_api>?window=<s>`, 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<String>) -> 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::<Value>(&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<String> = ["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");
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ mod sweep;
|
|||
mod ember;
|
||||
mod powertask;
|
||||
mod watchdog;
|
||||
mod live;
|
||||
|
||||
use std::io::{BufRead, Write};
|
||||
use std::sync::mpsc::channel;
|
||||
|
|
|
|||
|
|
@ -962,7 +962,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") }
|
||||
|
|
|
|||
|
|
@ -180,6 +180,13 @@ fn handle(mut stream: TcpStream, shared: Arc<Shared>) {
|
|||
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<String> = 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);
|
||||
|
|
|
|||
|
|
@ -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 '<circle class="' + (p.chosen ? 'chosen' : p.marked ? 'marked' : '') + '" cx="' + X(p.w).toFixed(1) + '" cy="' + Y(p.mhs).toFixed(1) + '" r="' + (p.chosen ? 5 : 3) + '"/>'; }).join('');
|
||||
return '<div class="curve-wrap"><svg class="curve-svg" viewBox="0 0 ' + W + ' ' + H + '" role="img" aria-label="The tune\'s curve: watts across, hash rate up"><path class="ln" d="' + path + '"/>' + dots +
|
||||
'<text x="' + L + '" y="' + (H - 4) + '">' + Math.round(w0) + ' W</text><text x="' + (W - 8) + '" y="' + (H - 4) + '" text-anchor="end">' + Math.round(w1) + ' W</text>' +
|
||||
'<text x="2" y="' + (H - B - 2) + '">' + m0.toFixed(0) + '</text><text x="2" y="12">' + m1.toFixed(0) + '</text></svg><span class="note">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).</span></div>';
|
||||
'<text x="2" y="' + (H - B - 2) + '">' + m0.toFixed(0) + '</text><text x="2" y="12">' + m1.toFixed(0) + '</text></svg><span class="note">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.</span></div>';
|
||||
}
|
||||
// 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 '<p class="help">The card was unplugged; its worker stopped. The row goes in five minutes.</p>';
|
||||
if (cd.problem) return '<p class="help">' + esc(cd.reason || 'No worker can drive this card.') + ' Reboot with the card attached; if it persists, reinstall the driver with the card attached.</p>';
|
||||
if (cd.removed_at > 0) return '<p class="help">The card was unplugged. Its row goes in five minutes.</p>';
|
||||
if (cd.problem) return '<p class="help">' + esc(cd.reason || 'This app cannot use this card.') + ' Reboot with the card attached; if it persists, reinstall the driver with the card attached.</p>';
|
||||
if (nv) {
|
||||
var want = Math.round(cd.power_default_w * pct / 100);
|
||||
h += '<div class="ctl"><span class="k">Power cap</span><input type="range" min="50" max="100" step="5" value="' + pct + '" aria-label="Power cap for ' + esc(cd.name) + '"><span class="pv mono">' + pct + '% · ' + want + ' W</span>' +
|
||||
'<p class="help">Lower is cooler and quieter; most cards hash nearly as fast at 70%. ' + (cd.pinned ? 'Set by hand, so the tune leaves it. <button class="btn tiny ghost" data-pin="0">Unpin</button>' : 'Ember Tune may move it.') + '</p></div>';
|
||||
h += cd.power_applied ? '<div class="line ok">cap applied: <b>' + want + ' W</b>' + (cd.sweep_pct === pct && cd.sweep_pct ? ' <span class="pin">chosen by the tune</span>' : '') + '</div>' : '<div class="line hot">cap not applied: Windows needs the administrator prompt <button class="btn tiny" data-power-retry="1">Retry</button></div>';
|
||||
} else if (cd.vendor === 'apple') h += '<p class="help">Apple silicon manages its own power; there is no cap to set.</p>';
|
||||
'<p class="help">Lower is cooler and quieter. Most cards hash nearly as fast at 70%. ' + (cd.pinned ? 'Set by hand, so Ember leaves it. <button class="btn tiny ghost" data-pin="0">Back to Default</button>' : 'Default: Ember may move it.') + '</p></div>';
|
||||
h += cd.power_applied ? '<div class="line ok">Cap in force: <b>' + want + ' W</b>' + (cd.sweep_pct === pct && cd.sweep_pct ? ' <span class="pin">Ember’s choice</span>' : '') + '</div>' : '<div class="line hot">Cap not set yet. Windows needs the administrator prompt. <button class="btn tiny" data-power-retry="1">Retry</button></div>';
|
||||
} else if (cd.vendor === 'apple') h += '<p class="help">Apple silicon manages its own power. There is no cap to set.</p>';
|
||||
var tl = [];
|
||||
if (cd.temp_mem > 0) tl.push('memory <b>' + Math.round(cd.temp_mem) + ' °C</b>');
|
||||
if (cd.gclk_mhz > 0) tl.push('core clock <b>' + Math.round(cd.gclk_mhz) + ' MHz</b>' + (cd.clock_cap_mhz > 0 ? ' (cap ' + cd.clock_cap_mhz + ')' : ''));
|
||||
|
|
@ -1478,13 +1479,13 @@ if (typeof document !== 'undefined') (function () {
|
|||
if (tl.length) h += '<div class="line">' + tl.join('<span class="sep"></span>') + '</div>';
|
||||
var tm = TuneLine.model(cd, s.now), em = View.ember(cd, s, s.now);
|
||||
if (View.tunable(cd)) {
|
||||
var g = cd.tune_goal || '', goalSeg = '<div class="seg small" data-card-goal="1" role="radiogroup" aria-label="Goal for ' + esc(cd.name) + '">' + [['', 'As the fleet'], ['efficiency', 'Efficiency'], ['balanced', 'Balanced'], ['rate', 'Maximum']].map(function (o) { return '<button class="seg-b' + (g === o[0] ? ' on' : '') + '" data-goal="' + o[0] + '" role="radio" aria-checked="' + (g === o[0] ? 'true' : 'false') + '">' + o[1] + '</button>'; }).join('') + '</div>';
|
||||
h += '<div class="ctl"><span class="k">Goal</span>' + goalSeg + (em.kind === 'running' ? '<button class="btn small" data-stop="1">Stop</button>' : '<button class="btn small" data-tune="1">' + (em.kind === 'tuned' ? 'Retune' : 'Tune now') + '</button>') + '<p class="help">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') + '.</p></div>';
|
||||
var g = cd.tune_goal || '', goalSeg = '<div class="seg small" data-card-goal="1" role="radiogroup" aria-label="Goal for ' + esc(cd.name) + '">' + [['', '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 '<button class="seg-b' + (g === o[0] ? ' on' : '') + '" data-goal="' + o[0] + '" role="radio" aria-checked="' + (g === o[0] ? 'true' : 'false') + '" title="' + o[2] + '">' + o[1] + (o[0] === '' ? ' <span class="dim">· ' + esc(View.GOAL_NAMES[goalNow()]) + '</span>' : '') + '</button>'; }).join('') + '</div>';
|
||||
h += '<div class="ctl"><span class="k">Goal</span>' + goalSeg + (em.kind === 'running' ? '<button class="btn small" data-stop="1">Stop</button>' : '<button class="btn small" data-tune="1">' + (em.kind === 'tuned' ? 'Retune' : 'Tune now') + '</button>') + '<p class="help">' + (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.') + '</p></div>';
|
||||
}
|
||||
if (tm.kind === 'tuned' || tm.kind === 'measured') h += '<div class="line"><b>' + esc(tm.text) + '</b>' + (tm.note ? ' <span class="dim">' + esc(tm.note) + '</span>' : '') + (em.savingText ? ' <span class="save">' + esc(em.savingText) + '</span>' : '') + '</div>';
|
||||
h += curveHtml(em.points);
|
||||
h += '<div class="ctl"><span class="k">Identities</span><p class="help">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.</p><div class="ids"><button type="button" data-d="-1" aria-label="fewer identities">−</button><input type="number" min="1" max="64" value="' + (cd.identities || 1) + '" aria-label="Identities on ' + esc(cd.name) + '"><button type="button" data-d="1" aria-label="more identities">+</button></div></div>';
|
||||
var facts = [cd.worker ? 'worker ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean);
|
||||
h += '<div class="ctl"><span class="k">Miners on this card</span><p class="help">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.</p><div class="ids"><button type="button" data-d="-1" aria-label="fewer identities">−</button><input type="number" min="1" max="64" value="' + (cd.identities || 1) + '" aria-label="Identities on ' + esc(cd.name) + '"><button type="button" data-d="1" aria-label="more identities">+</button></div></div>';
|
||||
var facts = [cd.worker ? 'runs on ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean);
|
||||
if (facts.length) h += '<p class="note mono small">' + esc(facts.join(' · ')) + '</p>';
|
||||
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('') : '<div class="empty' + (s.detecting ? '' : ' err') + '">' + (s.detecting ? 'Asking the graphics cards to report in.' : 'No graphics card this app can drive was found. The node runs on its own.') + '</div>';
|
||||
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>';
|
||||
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 '<span class="' + (sw && x.key === sw.key ? 'next' : x.daa <= n.daa ? 'past' : '') + '"><span>' + esc(x.name) + (x.daa <= n.daa ? ' (applied)' : '') + '</span><span>' + withCommas(x.daa) + '</span></span>'; }).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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -118,7 +118,7 @@
|
|||
</div>
|
||||
<p class="note" id="cards-note" hidden></p>
|
||||
<p class="note" id="cards-power" hidden>NVIDIA cards start at 80% of their power limit, which keeps them stable. Windows asks for administrator rights once for that. The card row has a slider.</p>
|
||||
<p class="note" id="cards-help" hidden>An integrated GPU is off by default: it is slow and shares the machine's memory.</p>
|
||||
<p class="note" id="cards-help" hidden>A built-in GPU starts off. It is slow and shares the machine's memory.</p>
|
||||
<div class="cta">
|
||||
<button class="btn primary" id="btn-cards-next" disabled>Continue</button>
|
||||
<button class="btn ghost" id="btn-cards-retry" hidden>Detect again</button>
|
||||
|
|
@ -146,7 +146,7 @@
|
|||
<span class="radio"></span>
|
||||
<span class="body">
|
||||
<span class="t">Use my own address</span>
|
||||
<span class="s">Paste an Ethereum-style address you control: 0x and 40 characters.</span>
|
||||
<span class="s">Paste an address you control. It starts with 0x and has 40 characters after it.</span>
|
||||
<input type="text" class="addr-input mono" id="addr-input" placeholder="0x" spellcheck="false" autocomplete="off">
|
||||
<span class="err" id="addr-err" hidden>That is not an address. 0x followed by 40 hex characters.</span>
|
||||
</span>
|
||||
|
|
@ -188,7 +188,7 @@
|
|||
<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-pause" aria-pressed="false">Pause</button></div></div>
|
||||
<div class="dag-wrap">
|
||||
<canvas id="dag-live" aria-label="The last two minutes of blocks: this machine's lane and the others, the selected chain as a lit path, checkpoints as bands"></canvas>
|
||||
<canvas id="dag-live" aria-label="The last two minutes of blocks: your row and the other miners, the main chain as a lit path, locks as bands"></canvas>
|
||||
<div class="dag-tip mono" id="dag-tip" hidden></div>
|
||||
<canvas id="dag" aria-hidden="true" hidden></canvas>
|
||||
</div>
|
||||
|
|
@ -207,21 +207,21 @@
|
|||
<div><span class="k">peers</span><span class="v mono" id="n-peers">0</span><span class="m" id="n-peers-sub">other nodes it talks to</span></div>
|
||||
<div><span class="k">version</span><span class="v mono" id="n-version">--</span><span class="m" id="d-node-net">devnet v4</span></div>
|
||||
<div><span class="k">headers</span><span class="v mono" id="n-headers">0</span><span class="m">headers arrive before blocks</span></div>
|
||||
<div><span class="k">DAA score</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made</span></div>
|
||||
<div><span class="k">network blocks</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made (the DAA score)</span></div>
|
||||
<div><span class="k">difficulty</span><span class="v mono" id="n-diff">0</span><span class="m">how hard the next block is to find</span></div>
|
||||
<div><span class="k">tips</span><span class="v mono" id="n-tips">0</span><span class="m">open ends of the block DAG right now</span></div>
|
||||
<div><span class="k">blue score</span><span class="v mono" id="n-blue">0</span><span class="m">blocks on the agreed main chain</span></div>
|
||||
<div><span class="k">tips</span><span class="v mono" id="n-tips">0</span><span class="m">open ends of the chain right now</span></div>
|
||||
<div><span class="k">blue score</span><span class="v mono" id="n-blue">0</span><span class="m">blocks on the agreed main chain (the blue score)</span></div>
|
||||
<div><span class="k">next program</span><span class="v mono" id="d-eta">--:--</span><span class="m" id="d-eta-sub">the hourly mining program</span></div>
|
||||
<div><span class="k">last lock</span><span class="v mono" id="f-lock">none yet</span><span class="m" id="f-note">a point the miners agreed can never be undone</span></div>
|
||||
<div><span class="k">votes sent</span><span class="v mono" id="f-votes">0</span><span class="m">this machine signs a checkpoint every 30 s</span></div>
|
||||
<div><span class="k">votes sent</span><span class="v mono" id="f-votes">0</span><span class="m">this machine signs a lock point every 30 s</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">rules digest</div>
|
||||
<div class="k">rules fingerprint</div>
|
||||
<div class="box mono"><span id="n-digest">not printed yet</span><button class="btn tiny" data-copy="n-digest">Copy</button></div>
|
||||
<p class="help">Every node on the network shows the same fingerprint of the rules. A peer with another one is refused.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">next rule switch</div>
|
||||
<div class="k">next rule change</div>
|
||||
<div class="line-text" id="n-switch">none planned</div>
|
||||
<p class="help" id="n-switch-help"></p>
|
||||
<div class="switch-list mono" id="n-switches"></div>
|
||||
|
|
@ -238,11 +238,12 @@
|
|||
<!-- Cards: every card with Ember as its second layer -->
|
||||
<section class="page" id="page-cards" data-page="cards" hidden>
|
||||
<div class="card tune-strip" id="tune-card">
|
||||
<div class="card-head"><h3>Ember Tune</h3><span class="eyebrow">tunes every card for hashes per watt</span></div>
|
||||
<div class="ts-row">
|
||||
<div class="seg" id="goal-seg" role="radiogroup" aria-label="Tuning goal">
|
||||
<button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false">Efficiency</button>
|
||||
<button class="seg-b" data-goal="balanced" role="radio" aria-checked="false">Balanced</button>
|
||||
<button class="seg-b" data-goal="rate" role="radio" aria-checked="false">Maximum</button>
|
||||
<button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false" title="most hashes per watt, a little rate traded">Efficiency</button>
|
||||
<button class="seg-b" data-goal="balanced" role="radio" aria-checked="false" title="a little rate for most of the saving">Balanced</button>
|
||||
<button class="seg-b" data-goal="rate" role="radio" aria-checked="false" title="most hashes, watts second">Maximum</button>
|
||||
</div>
|
||||
<span class="goal-line" id="goal-line"></span>
|
||||
<span class="ts-saving" id="fleet-saving"></span>
|
||||
|
|
@ -268,7 +269,7 @@
|
|||
<div class="money-row"><span class="v small" id="e-blocks">0 blocks</span><span class="s" id="e-blocks-sub">lifetime</span></div>
|
||||
<div class="money-row"><span class="v small" id="e-cost">£0.00 a day</span><span class="s" id="e-cost-sub">electricity</span><button class="btn tiny ghost" id="e-price-btn">Set the price</button></div>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="s-devfee"><span class="track"></span><span class="sw-text" id="s-devfee-text">Dev fee: 1 block in 100 pays the miner software's author</span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-devfee"><span class="track"></span><span class="sw-text" id="s-devfee-text">Dev fee: 1 block in 100 pays the people who make this app</span></label>
|
||||
<p class="help" id="r-devfee-line"></p>
|
||||
</div>
|
||||
|
||||
|
|
@ -313,26 +314,26 @@
|
|||
<div class="lead-row">
|
||||
<div class="lead-text">
|
||||
<h3>Prove on this machine</h3>
|
||||
<p class="help">Every block is turned into a short proof. The chain hands pieces to your cards; each piece proven pays IGN.</p>
|
||||
<p class="help">Every block is turned into a short proof. Your cards prove pieces of it and earn IGN for each one.</p>
|
||||
</div>
|
||||
<label class="switch lg" title="Prove on this machine"><input type="checkbox" id="s-prove" aria-label="Prove on this machine"><span class="track"></span></label>
|
||||
</div>
|
||||
<ul class="tier-list" id="pv-tiers"></ul>
|
||||
<p class="line-text" id="pv-line"></p>
|
||||
<p class="note" id="pv-note"></p>
|
||||
<div class="row" id="pv-setup-row" hidden><button class="btn small primary" id="pv-setup">Set up</button><span class="note">About 20 minutes, once.</span></div>
|
||||
<div class="row" id="pv-setup-row" hidden><button class="btn small primary" id="pv-setup">Set up</button><span class="note">Takes about 20 minutes, once.</span></div>
|
||||
<button class="disclose" id="pv-toggle" aria-expanded="false" aria-controls="pv-details"><span>Details</span><span class="chev" aria-hidden="true"></span></button>
|
||||
<div class="details" id="pv-details" hidden>
|
||||
<div class="kv">
|
||||
<div><span class="k">verifier</span><span class="v" id="pv-verifier">not read yet</span><span class="m" id="pv-verifier-help">the node checks a proof before it counts</span></div>
|
||||
<div><span class="k">segments</span><span class="v" id="pv-seg">none yet</span><span class="m" id="pv-seg-note">whole segments this machine proved</span></div>
|
||||
<div><span class="k">segments</span><span class="v" id="pv-seg">none yet</span><span class="m" id="pv-seg-note">whole runs of blocks this machine proved</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">shard program id</div>
|
||||
<div class="k">proof program id</div>
|
||||
<div class="box mono"><span id="pv-program">not read yet</span><button class="btn tiny" data-copy="pv-program">Copy</button></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">aggregator id</div>
|
||||
<div class="k">combiner program id</div>
|
||||
<div class="box mono"><span id="pv-aggregator">not read yet</span><button class="btn tiny" data-copy="pv-aggregator">Copy</button></div>
|
||||
<p class="help">Every proof names the program that made it. Other nodes accept a proof only from these two ids.</p>
|
||||
</div>
|
||||
|
|
@ -346,19 +347,19 @@
|
|||
<div class="card-head"><h3>Tuning</h3><span class="eyebrow" id="s-tune-eyebrow">Ember Tune</span></div>
|
||||
<div class="goal-row">
|
||||
<div class="seg" id="goal-seg-2" role="radiogroup" aria-label="Tuning goal">
|
||||
<button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false">Efficiency</button>
|
||||
<button class="seg-b" data-goal="balanced" role="radio" aria-checked="false">Balanced</button>
|
||||
<button class="seg-b" data-goal="rate" role="radio" aria-checked="false">Maximum</button>
|
||||
<button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false" title="most hashes per watt, a little rate traded">Efficiency</button>
|
||||
<button class="seg-b" data-goal="balanced" role="radio" aria-checked="false" title="a little rate for most of the saving">Balanced</button>
|
||||
<button class="seg-b" data-goal="rate" role="radio" aria-checked="false" title="most hashes, watts second">Maximum</button>
|
||||
</div>
|
||||
<span class="goal-line" id="goal-line-2"></span>
|
||||
</div>
|
||||
<label class="switch"><input type="checkbox" id="s-sweep"><span class="track"></span><span class="sw-text"><b>Ember Tune</b> <span class="dim">Tunes every card for hashes per watt: once after install, then every 7 days, and after a driver or program change.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-power-control"><span class="track"></span><span class="sw-text"><b>Power control</b> <span class="dim">Lets the app set NVIDIA power and clock limits. Windows asks for administrator rights once. Off, NVIDIA cards are measured only.</span><span class="dim" id="s-power-note"></span></span></label>
|
||||
<label class="switch" id="s-climb-row" hidden><input type="checkbox" id="s-climb"><span class="track"></span><span class="sw-text"><b>Hill climb</b> <span class="dim">Searches around the best point instead of walking the ladders.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-sweep"><span class="track"></span><span class="sw-text"><b>Ember Tune</b> <span class="dim">Tunes every card for the most hashes per watt. Once after install, then every 7 days, and after a driver or program change.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-power-control"><span class="track"></span><span class="sw-text"><b>Power control</b> <span class="dim">Lets the app set NVIDIA power and clock limits. Windows asks for administrator rights once. Off, NVIDIA cards are only measured.</span><span class="dim" id="s-power-note"></span></span></label>
|
||||
<label class="switch" id="s-climb-row" hidden><input type="checkbox" id="s-climb"><span class="track"></span><span class="sw-text"><b>Fine tuning</b> <span class="dim">Searches around the best point instead of stepping down a ladder.</span></span></label>
|
||||
<div class="field">
|
||||
<div class="k">electricity price</div>
|
||||
<div class="row">
|
||||
<input type="number" class="addr-input mono short" id="s-price" min="0" max="200" step="0.1" placeholder="28" aria-label="Electricity price in pence per kWh"><span class="note">pence per kWh. Every £ figure uses it.</span>
|
||||
<input type="number" class="addr-input mono short" id="s-price" min="0" max="200" step="0.1" placeholder="28" aria-label="Electricity price in pence per kWh"><span class="note">pence per kWh. Every £ figure uses this.</span>
|
||||
<button class="btn small" id="s-price-save">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -368,7 +369,7 @@
|
|||
<div class="card">
|
||||
<div class="card-head"><h3>This machine</h3></div>
|
||||
<label class="switch"><input type="checkbox" id="s-login"><span class="track"></span><span class="sw-text"><b>Start at login</b> <span class="dim">Opens when you sign in and keeps mining in the background.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-jobs-allow"><span class="track"></span><span class="sw-text"><b>Allow remote jobs from Igneum</b> <span class="dim">Signed jobs (a benchmark, a script, logs to collect) run here once and report back.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-jobs-allow"><span class="track"></span><span class="sw-text"><b>Allow remote jobs from Igneum</b> <span class="dim">Signed jobs from the team (a benchmark, a script, logs to collect) run here once and report back.</span></span></label>
|
||||
<div class="row wrap indent"><span class="note" id="s-jobs-note"></span><button class="btn tiny ghost" id="s-jobs-check">Check now</button><button class="btn tiny ghost" id="s-jobs-history-btn" aria-expanded="false">History</button></div>
|
||||
<div class="details indent" id="s-jobs-history" hidden></div>
|
||||
<p class="note mono small indent" id="s-jobs-key" hidden></p>
|
||||
|
|
@ -378,7 +379,7 @@
|
|||
<input type="text" class="addr-input" id="s-name" placeholder="a name for this machine" maxlength="40" spellcheck="false" aria-label="Machine name">
|
||||
<button class="btn small" id="s-name-save">Rename</button>
|
||||
</div>
|
||||
<p class="help">A label for you only.</p>
|
||||
<p class="help">A name for you only.</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">appearance</div>
|
||||
|
|
@ -417,8 +418,8 @@
|
|||
|
||||
<details class="card adv" id="s-advanced">
|
||||
<summary><h3>Advanced</h3><span class="eyebrow">devnet</span></summary>
|
||||
<label class="switch"><input type="checkbox" id="s-vote"><span class="track"></span><span class="sw-text"><b>Vote on finality checkpoints</b> <span class="dim">Signs a checkpoint every 30 s. Votes lock the chain; leave it on.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-trust"><span class="track"></span><span class="sw-text"><b>Trust proof records without verifying them</b> <span class="dim">Devnet only. Without a verifier the node includes records it never checked. Changing this restarts the node.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-vote"><span class="track"></span><span class="sw-text"><b>Vote on lock points</b> <span class="dim">Signs a lock point every 30 s. Votes are what lock the chain. Leave it on.</span></span></label>
|
||||
<label class="switch"><input type="checkbox" id="s-trust"><span class="track"></span><span class="sw-text"><b>Trust proofs without checking them</b> <span class="dim">Devnet only. The node includes proofs it never checked. Changing this restarts the node.</span></span></label>
|
||||
<div class="ask inline" id="ask-trust" hidden>
|
||||
<span class="ask-text" id="ask-trust-text"></span>
|
||||
<button class="btn small primary" id="ask-trust-yes">Confirm</button>
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@ test('the big button: start when paused, stop when mining, disabled with no card
|
|||
const none = V.toggle(m({ cards: [card({ enabled: false })] }), { synced: true }, {});
|
||||
assert.equal(none.disabled, true);
|
||||
assert.equal(none.act, '');
|
||||
assert.equal(none.sub, 'switch a GPU on below first');
|
||||
assert.equal(V.toggle(m({ cards: [] }), { synced: true }, {}).sub, 'no GPU this app can drive');
|
||||
assert.equal(none.sub, 'switch a card on in Cards first');
|
||||
assert.equal(V.toggle(m({ cards: [] }), { synced: true }, {}).sub, 'no card this app can use');
|
||||
assert.equal(V.toggle(m({ state: 'waiting' }), { synced: false }, {}).sub, 'waiting for the node to sync');
|
||||
assert.equal(V.toggle(m(), { synced: true }, { quitting: true }).disabled, true);
|
||||
});
|
||||
|
|
@ -107,11 +107,11 @@ test('the next consensus switch is the first height above the DAA score, in plai
|
|||
const next = V.nextSwitch(sw, 140000);
|
||||
assert.equal(next.name, 'Fees v1');
|
||||
assert.equal(next.away, 70000);
|
||||
assert.equal(V.switchLine(next, 140000), 'Fees v1 at DAA 210,000: 70,000 blocks away, about 19 h 27 min at one block a second.');
|
||||
assert.equal(V.switchLine(next, 140000), 'Fees v1 at block 210,000: 70,000 blocks away, about 19 h 27 min at one block a second.');
|
||||
assert.equal(V.nextSwitch(sw, 20000).name, 'Difficulty v2');
|
||||
assert.equal(V.nextSwitch(sw, 300000), null);
|
||||
assert.match(V.switchLine(null, 300000), /Every planned switch is behind this node/);
|
||||
assert.match(V.switchLine(null, 0), /^A switch is a planned rule change/);
|
||||
assert.match(V.switchLine(null, 300000), /Every planned rule change is behind this node/);
|
||||
assert.match(V.switchLine(null, 0), /^A rule change is planned ahead/);
|
||||
assert.equal(V.nextSwitch([], 5), null);
|
||||
});
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ test('the prove words follow the switch, the setup, the node and the status', ()
|
|||
assert.equal(V.proveWords(pv({ status: 'proving', current: 'block 59199 shard 0' }), true, true).sub, 'block 59199 shard 0');
|
||||
assert.equal(V.proveWords(pv({ status: 'submitted' }), true, true).tone, 'on');
|
||||
assert.equal(V.proveWords(pv(), true, true).word, 'idle');
|
||||
assert.equal(V.verifierWords(pv()).word, 'verifying · pool 4/4');
|
||||
assert.equal(V.verifierWords(pv()).word, 'checking proofs · pool 4/4');
|
||||
assert.equal(V.verifierWords(pv()).tone, 'ok');
|
||||
assert.equal(V.verifierWords(pv({ verifier_mode: 'off', pool_entries: 0 })).tone, 'warn');
|
||||
assert.equal(V.verifierWords(pv({ verifier_mode: 'off', verifier_reason: 'the WSL2 prover is not installed' })).needsSetup, true);
|
||||
|
|
@ -140,9 +140,9 @@ test('the dev-fee lines name the share and the switch that turns it off', () =>
|
|||
|
||||
test('the remote-jobs line and the helpers', () => {
|
||||
const title = (j) => j.title || j.kind;
|
||||
assert.equal(V.jobsNote({ allowed: false }, 1000, title), 'Off: nothing runs here until the switch above allows remote jobs.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: false }, 1000, title), 'No jobs address in this build.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: true, active: true, id: 'job-3', kind: 'build', title: 'build' }, 1000, title), 'Running build (job-3).');
|
||||
assert.equal(V.jobsNote({ allowed: false }, 1000, title), 'Off. Nothing runs here until the switch above allows jobs from the team.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: false }, 1000, title), 'This build has no jobs address.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: true, active: true, id: 'job-3', kind: 'build', title: 'build' }, 1000, title), 'Running: build.');
|
||||
assert.equal(V.jobsNote({ allowed: true, url_set: true, checked_at: 940, queued: 2 }, 1000, title), 'Nothing running; checked 1 min ago; 2 queued.');
|
||||
assert.equal(V.shortHex('0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a'), '0x2b1a81…79ef7a');
|
||||
assert.equal(V.shortHex('0xabc'), '0xabc');
|
||||
|
|
@ -160,7 +160,7 @@ test('hot-plug (src/hotplug.rs): a removed card and a faulty card are shown as s
|
|||
assert.equal(r.word, 'removed');
|
||||
assert.equal(r.tone, 'off');
|
||||
assert.equal(r.hash, '');
|
||||
assert.match(r.sub, /^unplugged; its worker stopped/);
|
||||
assert.match(r.sub, /^unplugged\. Its row goes/);
|
||||
const bad = card({ key: 'amd:gfx1201#2', name: 'AMD Radeon RX 9070 XT', code: 'gfx1201', vendor: 'amd', enabled: false, state: 'unusable', problem: 'Code 43', message: 'not usable (Code 43)', reason: 'reboot with the card attached; if it persists, reinstall the driver with the card attached', added_at: 900, removed_at: 0, device: '1', platform: 'AMD Accelerated Parallel Processing', bus: '0000:03:00.0' });
|
||||
const b = V.cardRow(bad);
|
||||
assert.equal(b.unusable, true);
|
||||
|
|
@ -195,7 +195,7 @@ test('hot-plug (src/hotplug.rs): a removed card and a faulty card are shown as s
|
|||
assert.equal(V.present(bad), false);
|
||||
const t = V.toggle({ state: 'mining', paused: false, cards: [gone, bad] }, { synced: true }, {});
|
||||
assert.equal(t.disabled, true);
|
||||
assert.equal(t.sub, 'no GPU this app can drive');
|
||||
assert.equal(t.sub, 'no card this app can use');
|
||||
const t2 = V.toggle({ state: 'mining', paused: false, cards: [card(), gone] }, { synced: true }, {});
|
||||
assert.equal(t2.sub, 'mining on 1 of 1 card');
|
||||
});
|
||||
|
|
@ -250,7 +250,7 @@ test('the tune line: never run, running with a step and a bar, tuned with the po
|
|||
assert.equal(tuned.kind, 'tuned'); assert.equal(tuned.note, '122.3 MH/s at 290 W (0.422 MH/W)'); assert.equal(tuned.button, 'tune');
|
||||
assert.match(tuned.text, /^Tuned 2 h ago · 2470 MHz at 100% · next check /);
|
||||
const prior = V.tuneWords(nv({ tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'confirm', tune_clock_mhz: 2470, sweep_pct: 100, sweep_at: NOW - 120 }), settings(), NOW);
|
||||
assert.match(prior.text, /^Tuned 2 min ago from the fleet prior, confirmed · 2470 MHz at 100%/);
|
||||
assert.match(prior.text, /^Tuned 2 min ago from what other miners found, confirmed · 2470 MHz at 100%/);
|
||||
const measured = V.tuneWords(nv({ tune_line: 'Tuned: 26.7 MH/s at 38 W (0.703 MH/W)', tune_source: 'baseline', tune_control: false, sweep_at: NOW - 600, sweep_note: 'measure only on Apple silicon: the system sets the clocks and the power; no control exposed' }), settings(), NOW);
|
||||
assert.equal(measured.kind, 'measured'); assert.equal(measured.text, 'Measured 26.7 MH/s at 38 W (0.703 MH/W) as it runs, 10 min ago');
|
||||
assert.equal(measured.note, 'Measured only on Apple silicon: the system sets the clocks and the power; no control exposed');
|
||||
|
|
@ -280,7 +280,7 @@ test('the next check is a weekday inside a week, a date beyond it, due now when
|
|||
test('the Tuning card schedule and the goal consequence', () => {
|
||||
const nv = (over) => card({ sweep_at: 0, tune_steps: 0, ...over });
|
||||
assert.equal(V.schedule([nv()], settings(), NOW), 'Not tuned yet: the first tune starts 2 min into steady mining.');
|
||||
assert.equal(V.schedule([nv()], settings({ sweep: false }), NOW), 'Ember Tune is off. Tune and Tune all still work by hand.');
|
||||
assert.equal(V.schedule([nv()], settings({ sweep: false }), NOW), 'Ember Tune is off. Tune and Tune all still work when you press them.');
|
||||
assert.equal(V.schedule([nv({ state: 'tuning', tune_step: 2, tune_steps: 9, tune_eta_s: 500 })], settings(), NOW), 'Tuning now: NVIDIA GeForce RTX 5090, step 2 of 9, mining again in about 8 min.');
|
||||
assert.match(V.schedule([nv({ sweep_at: NOW - 7200 }), nv({ key: 'b', name: 'B', sweep_at: NOW - 60 })], settings(), NOW), /^Last tune 1 min ago \(B\) · next check /);
|
||||
assert.equal(V.schedule([], settings(), NOW), 'No card here can be tuned.');
|
||||
|
|
@ -294,10 +294,10 @@ test('the Tuning card schedule and the goal consequence', () => {
|
|||
});
|
||||
|
||||
test('the prove tier sentence per card, and the counts in one line', () => {
|
||||
assert.equal(V.proveTier(card()), 'NVIDIA GeForce RTX 5090: proves while it mines (32 GB; a full shard needs 20.4 GB).');
|
||||
assert.equal(V.proveTier(card({ name: 'NVIDIA GeForce RTX 4070', vram_mb: 12288 })), 'NVIDIA GeForce RTX 4070: cannot prove a full shard (12 GB; a full shard needs 20.4 GB).');
|
||||
assert.equal(V.proveTier(card()), 'NVIDIA GeForce RTX 5090: proves while it mines (32 GB; a full piece needs 20.4 GB).');
|
||||
assert.equal(V.proveTier(card({ name: 'NVIDIA GeForce RTX 4070', vram_mb: 12288 })), 'NVIDIA GeForce RTX 4070: cannot prove a full piece (12 GB; a full piece needs 20.4 GB).');
|
||||
assert.equal(V.proveTier(card({ name: 'Apple M5 Max', vendor: 'apple' })), 'Apple M5 Max: proves on the CPU, slowly.');
|
||||
assert.equal(V.proveTier(card({ name: 'AMD Radeon RX 9070 XT', vendor: 'amd', vram_mb: 16384 })), 'AMD Radeon RX 9070 XT: cannot prove yet (the prover is CUDA only).');
|
||||
assert.equal(V.proveTier(card({ name: 'AMD Radeon RX 9070 XT', vendor: 'amd', vram_mb: 16384 })), 'AMD Radeon RX 9070 XT: cannot prove yet (proving needs an NVIDIA card).');
|
||||
assert.equal(V.proveLine({ assigned: 0, submitted: 0, paid: 0, paid_wei: '0' }, false, true), 'Off · not proving · 0 assigned · 0 proven · 0 paid · 0.0000 IGN');
|
||||
assert.equal(V.proveLine({ status: 'idle', assigned: 3, submitted: 2, paid: 1, paid_wei: '1500000000000000000', available: true, enabled: true }, true, true), 'Idle · nothing assigned · 3 assigned · 2 proven · 1 paid · 1.5000 IGN');
|
||||
});
|
||||
|
|
@ -311,9 +311,9 @@ test('the node line, the activity line, the asks', () => {
|
|||
assert.equal(V.eventLine('consensus parameters from the signed manifest: {"difficulty_v2_activation_daa":33000}'), 'consensus parameters from the signed manifest (details in the log)');
|
||||
assert.equal(V.eventLine('node synced: 151512 blocks, 6 peer(s)'), 'node synced: 151512 blocks, 6 peer(s)');
|
||||
assert.equal(V.eventLine('a '.repeat(100), 40).length <= 41, true);
|
||||
assert.equal(V.addressAsk('0x1234567890abcdef1234567890abcdef12345678'), 'Pay 0x1234…5678 from the next block? The miner restarts.');
|
||||
assert.match(V.trustAsk(true), /^Include proof records the node never checked\? Devnet only; the node restarts\.$/);
|
||||
assert.match(V.trustAsk(false), /^Verify proof records again\?/);
|
||||
assert.equal(V.addressAsk('0x1234567890abcdef1234567890abcdef12345678'), 'Pay 0x1234…5678 from the next block? Mining restarts.');
|
||||
assert.match(V.trustAsk(true), /^Include proofs the node never checked\? Devnet only\. The node restarts\.$/);
|
||||
assert.match(V.trustAsk(false), /^Check proofs again\?/);
|
||||
assert.equal(V.ago(30), 'just now'); assert.equal(V.ago(7200), '2 h ago'); assert.equal(V.ago(200000), '2 d ago');
|
||||
});
|
||||
|
||||
|
|
@ -328,9 +328,9 @@ test('the activity feed and the pill speak in the user\'s voice; the engine line
|
|||
assert.equal(V.plainEvent('1 remote job received from Igneum').text, '1 job from the team received');
|
||||
assert.equal(V.plainEvent('remote job: update check now; a newer version installs at once').text, 'The team asked for an update check');
|
||||
assert.equal(V.plainEvent('a node already answers on 127.0.0.1:26610; using it (it is not stopped by this app)').text, 'Using the node already running on this machine');
|
||||
assert.equal(V.plainEvent('igneumd exited with code 101 after 300 s; restarting').text, 'The node stopped (code 101); restarting');
|
||||
assert.equal(V.plainEvent('miner apple:0 exited with code 1; restarting in 8 s').text, 'A worker stopped (code 1); restarting in 8 s');
|
||||
assert.equal(V.plainEvent('NVIDIA GeForce RTX 5090 kernel race: l128w16 at 122.3 MH/s (+4.1% over base, 6 variants, 40 s)').text, 'NVIDIA GeForce RTX 5090: program variant chosen, 122.3 MH/s');
|
||||
assert.equal(V.plainEvent('igneumd exited with code 101 after 300 s; restarting').text, 'The node stopped (code 101). Restarting');
|
||||
assert.equal(V.plainEvent('miner apple:0 exited with code 1; restarting in 8 s').text, 'A card stopped (code 1). Restarting in 8 s');
|
||||
assert.equal(V.plainEvent('NVIDIA GeForce RTX 5090 kernel race: l128w16 at 122.3 MH/s (+4.1% over base, 6 variants, 40 s)').text, 'NVIDIA GeForce RTX 5090: fastest program chosen, 122.3 MH/s');
|
||||
assert.equal(V.plainEvent('block 59199 shard 0 paid 0.0125 IGN').text, 'Proof paid: 0.0125 IGN');
|
||||
assert.equal(V.plainEvent('rewards go to 0xdd442fcbb964a3afdc90d49b408e8dd296fa86e8').text, 'Rewards go to 0xdd44…86e8');
|
||||
assert.equal(V.plainEvent('mining resumed').text, 'Mining resumed');
|
||||
|
|
@ -360,7 +360,7 @@ test('Ember on the row (miner-ui-4): the flame fill, before -> after, the saving
|
|||
assert.equal(e.points.length, 4); assert.equal(e.points.filter((p) => p.chosen).length, 1); assert.equal(e.points.filter((p) => p.marked).length, 1);
|
||||
// Ember's own fields win over the curve (tune_before_watts / tune_before_mhs, step 0 of the last full plan)
|
||||
const withFields = V.ember(card({ ...tuned, tune_before_watts: 380, tune_before_mhs: 122.4, tune_source: 'confirm' }), settings(), NOW);
|
||||
assert.equal(withFields.before, 380); assert.equal(withFields.savingW, 90); assert.match(withFields.label, /from the fleet prior$/);
|
||||
assert.equal(withFields.before, 380); assert.equal(withFields.savingW, 90); assert.match(withFields.label, /from what other miners found$/);
|
||||
// a confirm plan without the fields claims no saving (its curve's first row is the prior, not an untuned point)
|
||||
assert.equal(V.ember(card({ ...tuned, tune_source: 'confirm' }), settings(), NOW).savingText, '');
|
||||
assert.equal(V.ember(card({ ...tuned, tune_before_watts: 0 }), settings(), NOW).savingText, '', 'zero means never measured');
|
||||
|
|
@ -371,7 +371,7 @@ test('Ember on the row (miner-ui-4): the flame fill, before -> after, the saving
|
|||
const idle = V.ember(card({ sweep_state: 'idle', sweep_note: '', tune_line: '', sweep_at: 0 }), settings(), NOW);
|
||||
assert.equal(idle.kind, 'idle'); assert.equal(idle.fill, 0); assert.equal(idle.label, 'Not tuned yet');
|
||||
assert.equal(V.ember(card({ vendor: 'other', kind: 'integrated', sweep_supported: false }), settings(), NOW).kind, 'none');
|
||||
assert.equal(V.fleetSaving([tuned, card({ key: 'b', sweep_state: 'idle', tune_line: '', sweep_at: 0 })], settings(), NOW, 28), 'Ember saves 84 W, £0.56 a day over 1 tuned card');
|
||||
assert.equal(V.fleetSaving([tuned, card({ key: 'b', sweep_state: 'idle', tune_line: '', sweep_at: 0 })], settings(), NOW, 28), 'Ember saves 84 W, £0.56 a day across 1 tuned card');
|
||||
assert.equal(V.fleetSaving([card({ sweep_state: 'idle', tune_line: '', sweep_at: 0 })], settings(), NOW, 28), '');
|
||||
assert.deepEqual(V.beforeOf({ tune_before_watts: 0, tune_curve: curve }), null);
|
||||
});
|
||||
|
|
|
|||
187
docs/plans/miner-ui-4-copy.md
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
# Igneum Miner UI 4: the copy sweep (6 October 2026, 20:0x UK)
|
||||
|
||||
the project lead, on the Cards screenshot: "'As the fleet' might need some work, check all UI copy for simplicity." Every string in
|
||||
`index.html` and `app.js` was read against the rules: no word a home miner would not use (fleet, lane, identities,
|
||||
prior, hill climb, sweep, DAA, epoch, vote key, digest, manifest, shard, segment, aggregator, verifier, worker, engine
|
||||
become plain words or carry one grey explanation), every state line one short sentence with the number first, labels
|
||||
are nouns and buttons are verbs, no colon where a sentence will do, no two-beat lines, one word for one thing (tune,
|
||||
card). the project lead's rulings folded in: the per-card inherit option is "Default · <shared goal>" in grey, updating when the
|
||||
shared goal changes; "Default" wherever a per-card setting follows the shared one (the cap's "Back to Default"); the
|
||||
Cards strip is titled "Ember Tune"; every goal option carries its meaning as a tooltip: Efficiency "most hashes per
|
||||
watt, a little rate traded", Balanced "a little rate for most of the saving", Maximum "most hashes, watts second",
|
||||
Default "the goal set for all cards". 150 strings changed.
|
||||
|
||||
## The ten most visible
|
||||
|
||||
| Where | Old | New |
|
||||
|---|---|---|
|
||||
| Cards, the per-card goal | As the fleet | Default · Balanced (grey, follows the shared goal) |
|
||||
| Cards, the strip | (no title) | Ember Tune, with each goal's meaning on hover |
|
||||
| Cards, the Ember line from a prior | Tuned 2 min ago from the fleet prior | Tuned 2 min ago from what other miners found |
|
||||
| Cards, the details label | Identities | Miners on this card |
|
||||
| Cards, the cap lines | cap applied: 460 W / cap not applied: Windows needs the administrator prompt / Unpin | Cap in force: 460 W / Cap not set yet. Windows needs the administrator prompt. / Back to Default |
|
||||
| Cards, the row's state word | held for a remote job / stopped after repeated faults | paused for a job from the team / stopped after repeated errors |
|
||||
| Overview, the big button | switch a GPU on below first / no GPU this app can drive / worker starting | switch a card on in Cards first / no card this app can use / card starting |
|
||||
| Prove | verifying / off: relay only / shard program id / aggregator id / a full shard needs 20.4 GB | checking proofs / off, passing proofs along / proof program id / combiner program id / a full piece needs 20.4 GB |
|
||||
| Node details | DAA score / rules digest / next rule switch / signs a checkpoint | network blocks (the DAA score) / rules fingerprint / next rule change / signs a lock point |
|
||||
| Settings | Vote on finality checkpoints / Trust proof records without verifying them / Hill climb | Vote on lock points / Trust proofs without checking them / Fine tuning |
|
||||
|
||||
Every toast and event sentence lost its semicolon or colon joint for a full stop ("Mining stops. The node keeps
|
||||
running"). "Worker" (the engine's word for a card's mining process) became "card" wherever a user reads it; "engine"
|
||||
became "app"; "shard" became "piece"; "segment" became "run of blocks"; "checkpoint" became "lock point"; "fleet
|
||||
prior" became "what other miners found". The engine's own log strings are untouched: `View.plainEvent` maps them.
|
||||
|
||||
## Every string changed
|
||||
|
||||
| File | Where | Old | New |
|
||||
|---|---|---|---|
|
||||
| index.html | The chain scene canvas label | The last two minutes of blocks: this machine's lane and the others, the selected chain as a lit path, checkpoints as bands | The last two minutes of blocks: your row and the other miners, the main chain as a lit path, locks as bands |
|
||||
| index.html | Node details: DAA score row | <span class="k">DAA score</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made</span> | <span class="k">network blocks</span><span class="v mono" id="n-daa">0</span><span class="m">blocks the whole network has made (the DAA score)</span> |
|
||||
| index.html | Node details: votes row | this machine signs a checkpoint every 30 s | this machine signs a lock point every 30 s |
|
||||
| index.html | Node details: digest label | <div class="k">rules digest</div> | <div class="k">rules fingerprint</div> |
|
||||
| index.html | Node details: tips row | open ends of the block DAG right now | open ends of the chain right now |
|
||||
| index.html | Node details: blue score row | blocks on the agreed main chain</span> | blocks on the agreed main chain (the blue score)</span> |
|
||||
| index.html | Node details: next switch label | <div class="k">next rule switch</div> | <div class="k">next rule change</div> |
|
||||
| index.html | Prove: segments row | whole segments this machine proved | whole runs of blocks this machine proved |
|
||||
| index.html | Prove: shard program id label | <div class="k">shard program id</div> | <div class="k">proof program id</div> |
|
||||
| index.html | Prove: aggregator id label | <div class="k">aggregator id</div> | <div class="k">combiner program id</div> |
|
||||
| index.html | Prove: lead sentence | Every block is turned into a short proof. The chain hands pieces to your cards; each piece proven pays IGN. | Every block is turned into a short proof. Your cards prove pieces of it and earn IGN for each one. |
|
||||
| index.html | Settings: Ember Tune switch | Tunes every card for hashes per watt: once after install, then every 7 days, and after a driver or program change. | Tunes every card for the most hashes per watt. Once after install, then every 7 days, and after a driver or program change. |
|
||||
| index.html | Settings: Hill climb switch | <b>Hill climb</b> <span class="dim">Searches around the best point instead of walking the ladders.</span> | <b>Fine tuning</b> <span class="dim">Searches around the best point instead of stepping down a ladder.</span> |
|
||||
| index.html | Settings: vote switch | <b>Vote on finality checkpoints</b> <span class="dim">Signs a checkpoint every 30 s. Votes lock the chain; leave it on.</span> | <b>Vote on lock points</b> <span class="dim">Signs a lock point every 30 s. Votes are what lock the chain. Leave it on.</span> |
|
||||
| index.html | Settings: trust switch | <b>Trust proof records without verifying them</b> <span class="dim">Devnet only. Without a verifier the node includes records it never checked. Changing this restarts the | <b>Trust proofs without checking them</b> <span class="dim">Devnet only. The node includes proofs it never checked. Changing this restarts the node.</span> |
|
||||
| index.html | Settings: remote jobs switch | Signed jobs (a benchmark, a script, logs to collect) run here once and report back. | Signed jobs from the team (a benchmark, a script, logs to collect) run here once and report back. |
|
||||
| index.html | Settings: Power control | Off, NVIDIA cards are measured only. | Off, NVIDIA cards are only measured. |
|
||||
| index.html | Earnings: dev fee switch | Dev fee: 1 block in 100 pays the miner software's author | Dev fee: 1 block in 100 pays the people who make this app |
|
||||
| index.html | Setup: address lead | 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. |
|
||||
| index.html | Setup: GPU note | An integrated GPU is off by default: it is slow and shares the machine's memory. | A built-in GPU starts off. It is slow and shares the machine's memory. |
|
||||
| index.html | Settings: price | pence per kWh. Every £ figure uses it. | pence per kWh. Every £ figure uses this. |
|
||||
| index.html | Settings: name help | A label for you only. | A name for you only. |
|
||||
| index.html | Prove: setup note | About 20 minutes, once. | Takes about 20 minutes, once. |
|
||||
| index.html | Cards: the strip title | <div class="card tune-strip" id="tune-card"> <div class="ts-row"> | <div class="card tune-strip" id="tune-card"> <div class="card-head"><h3>Ember Tune</h3><span class="eyebrow">tunes every card for hashes per watt</span></di |
|
||||
| index.html | Cards: the shared goal options with their meanings | <button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false">Efficiency</button> <button class="seg-b" data-goal="balanced" role | <button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false" title="most hashes per watt, a little rate traded">Efficiency</button> |
|
||||
| index.html | Settings: the shared goal options with their meanings | <button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false">Efficiency</button> <button class="seg-b" data-goal="balanced" role | <button class="seg-b" data-goal="efficiency" role="radio" aria-checked="false" title="most hashes per watt, a little rate traded">Efficiency</button> |
|
||||
| app.js | Cards: the per-card goal options | [['', 'As the fleet'], ['efficiency', 'Efficiency'], ['balanced', 'Balanced'], ['rate', 'Maximum']].map(function (o) { return '<button class="seg-b' + (g === o[0] ? ' on' | [['', 'Default', 'the goal set for all cards'], ['efficiency', 'Efficiency', 'most hashes per watt, a little rate traded'], ['balanced', 'Balanced', 'a little rate for mo |
|
||||
| app.js | Cards: the fleet saving line | ' over ' + n + ' tuned card' | ' across ' + n + ' tuned card' |
|
||||
| app.js | Cards: the Ember line from a prior | ' from the fleet prior' | ' from what other miners found' |
|
||||
| app.js | Cards: the Ember line, measured sub | 'Apple silicon sets its own clocks' | 'Apple silicon sets its own power' |
|
||||
| app.js | Cards: the tune line, prior | ' from the fleet prior, confirmed' | ' from what other miners found, confirmed' |
|
||||
| app.js | Cards: the schedule, off | 'Ember Tune is off. Tune and Tune all still work by hand.' | 'Ember Tune is off. Tune and Tune all still work when you press them.' |
|
||||
| app.js | Cards: the floor note | ' (the ladder’s floor)' | ' (the lowest clock Ember will set)' |
|
||||
| app.js | Cards: identities label | <span class="k">Identities</span><p class="help"> | <span class="k">Miners on this card</span><p class="help"> |
|
||||
| app.js | Cards: identities toast | 'Identities set; that card’s worker restarts' | 'Set. The card restarts' |
|
||||
| app.js | Cards: cap help, Ember may move it | 'Ember Tune may move it.' | 'Default: Ember may move it.' |
|
||||
| app.js | Cards: cap help, pinned | 'Set by hand, so the tune leaves it. <button class="btn tiny ghost" data-pin="0">Unpin</button>' | 'Set by hand, so Ember leaves it. <button class="btn tiny ghost" data-pin="0">Back to Default</button>' |
|
||||
| app.js | Cards: cap applied | '<div class="line ok">cap applied: <b>' + want + ' W</b>' | '<div class="line ok">Cap in force: <b>' + want + ' W</b>' |
|
||||
| app.js | Cards: cap not applied | '<div class="line hot">cap not applied: Windows needs the administrator prompt <button class="btn tiny" data-power-retry="1">Retry</button></div>' | '<div class="line hot">Cap not set yet. Windows needs the administrator prompt. <button class="btn tiny" data-power-retry="1">Retry</button></div>' |
|
||||
| app.js | Cards: chosen by the tune pin | ' <span class="pin">chosen by the tune</span>' | ' <span class="pin">Ember’s choice</span>' |
|
||||
| app.js | Cards: Apple cap help | '<p class="help">Apple silicon manages its own power; there is no cap to set.</p>' | '<p class="help">Apple silicon manages its own power. There is no cap to set.</p>' |
|
||||
| app.js | Cards: facts line worker | cd.worker ? 'worker ' + cd.worker : '' | cd.worker ? 'runs on ' + cd.worker : '' |
|
||||
| app.js | Row word: held | 'held for a remote job' | 'paused for a job from the team' |
|
||||
| app.js | Row word: faulted | 'stopped after repeated faults' | 'stopped after repeated errors' |
|
||||
| app.js | Big button: held | 'a remote job holds the cards; mining restarts when it ends' | 'a job from the team is using the cards. Mining restarts when it ends' |
|
||||
| app.js | Hero sub: no GPU worker | m.state === 'idle' ? 'no GPU worker' | m.state === 'idle' ? 'no card mining' |
|
||||
| app.js | Hero sub: worker starting | (n.synced ? 'worker starting' : 'waiting for the node') | (n.synced ? 'card starting' : 'waiting for the node') |
|
||||
| app.js | Big button: worker starting | sub: n && n.synced ? 'worker starting' : 'waiting for the node to sync' | sub: n && n.synced ? 'card starting' : 'waiting for the node to sync' |
|
||||
| app.js | Big button: switch a GPU on | 'switch a GPU on below first' | 'switch a card on in Cards first' |
|
||||
| app.js | Big button: no GPU | 'no GPU this app can drive' | 'no card this app can use' |
|
||||
| app.js | Strip fallback: mining | 'mining: a block this machine finds appears here' | 'Mining. A block this machine finds appears here' |
|
||||
| app.js | Strip fallback: paused | 'paused: no blocks while mining is stopped' | 'Paused. No blocks while mining is stopped' |
|
||||
| app.js | Strip fallback: worker | state.node.synced ? 'waiting for the worker' : 'waiting for the node to sync' | state.node.synced ? 'waiting for the card' : 'waiting for the node to sync' |
|
||||
| app.js | Chain scene: the note | 'The full chain view needs the next engine update; this is the last 10 minutes of your own blocks.' | 'The full chain view needs the next update. This is the last 10 minutes of your own blocks.' |
|
||||
| app.js | Chain scene: the state word | st === 'failed' ? 'your blocks' | st === 'failed' ? 'your blocks only' |
|
||||
| app.js | Node: next program sub | 'until the next hourly mining program' | 'until the next hourly program' |
|
||||
| app.js | Node: next program unknown | 'known once the node runs' | 'known once the node is running' |
|
||||
| app.js | Node: finality note | 'a point the miners agreed can never be undone; locks at 2/3 of the 30-day weight' | 'a point the miners agreed can never be undone. It locks at two thirds of the last 30 days of mining' |
|
||||
| app.js | Node: finality none | 'locks appear once the miner votes on checkpoints' | 'locks appear once this machine votes' |
|
||||
| app.js | Node: switch line | 'A switch is a planned rule change. The node applies it by itself when the chain reaches that height.' | 'A rule change is planned ahead. The node applies it by itself when the chain reaches that height.' |
|
||||
| app.js | Node: switch line, all behind | 'Every planned switch is behind this node. ' | 'Every planned rule change is behind this node. ' |
|
||||
| app.js | Node: switch line, at DAA | sw.name + ' at DAA ' + withCommas(sw.daa) + ': ' + blocks | sw.name + ' at block ' + withCommas(sw.daa) + ': ' + blocks |
|
||||
| app.js | Node: manifest switch line | 'The update manifest announces a switch at DAA ' + withCommas(n.consensus_switch_daa) | 'A rule change is announced at block ' + withCommas(n.consensus_switch_daa) |
|
||||
| app.js | Node: digest not printed | 'not printed yet' | 'not read yet' |
|
||||
| app.js | Prove: the words off | 'Off. Switch it on and this machine proves the pieces the chain assigns to its keys.' | 'Off. Switch it on and this machine proves the pieces it is given.' |
|
||||
| app.js | Prove: proving sub | sub: pv.current \|\| 'a shard' | sub: pv.current \|\| 'a piece' |
|
||||
| app.js | Prove: idle note | 'On. Nothing is assigned to your keys right now; the next shard arrives within a minute.' | 'On. Nothing to prove right now. The next piece arrives within a minute.' |
|
||||
| app.js | Prove: proving note | 'A proof takes minutes on a CPU and seconds on a CUDA card.' | 'A proof takes minutes on a CPU and seconds on an NVIDIA card.' |
|
||||
| app.js | Prove: tier, cannot prove yet | ': cannot prove yet (the prover is CUDA only).' | ': cannot prove yet (proving needs an NVIDIA card).' |
|
||||
| app.js | Prove: tier, proves while it mines | ': proves while it mines (' + gb + ' GB; a full shard needs ' + SHARD_GB + ' GB).' | ': proves while it mines (' + gb + ' GB; a full piece needs ' + SHARD_GB + ' GB).' |
|
||||
| app.js | Prove: tier, cannot prove a full shard | ': cannot prove a full shard (' + gb + ' GB; a full shard needs ' + SHARD_GB + ' GB).' | ': cannot prove a full piece (' + gb + ' GB; a full piece needs ' + SHARD_GB + ' GB).' |
|
||||
| app.js | Prove: toast on | 'Proving on; the first piece arrives within a minute' | 'Proving on. The first piece arrives within a minute' |
|
||||
| app.js | Prove: verifier words | { command: 'verifying', trust: 'trusting (devnet only)', off: 'off: relay only', unknown: 'not read yet' } | { command: 'checking proofs', trust: 'trusting without checking (devnet only)', off: 'off, passing proofs along', unknown: 'not read yet' } |
|
||||
| app.js | Prove: segments line | 'whole segments this machine proved' | 'whole runs of blocks this machine proved' |
|
||||
| app.js | Prove: segments held | ', ' + pv.segments_held + ' held for the chain rule' | ', ' + pv.segments_held + ' waiting for the chain' |
|
||||
| app.js | Prove: segments last | (pv.segment_last_s ? ', the last in ' | (pv.segment_last_s ? ', the last took ' |
|
||||
| app.js | Earnings: dev fee off | 'Dev fee off: every block pays your address' | 'Dev fee off. Every block pays your address' |
|
||||
| app.js | Earnings: dev fee on | 'Dev fee: 1 block in 100 pays the miner software’s author' | 'Dev fee: 1 block in 100 pays the people who make this app' |
|
||||
| app.js | Earnings: dev fee line | 'The protocol takes nothing; this is the miner software’s fee, the same way every GPU miner takes one' | 'The network itself takes nothing. This is the app’s fee, the same way every mining app takes one' |
|
||||
| app.js | Earnings: dev fee line off | 'The protocol takes nothing either.' | 'The network itself takes nothing either.' |
|
||||
| app.js | Earnings: 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.' | '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.' |
|
||||
| app.js | Settings: jobs note off | 'Off: nothing runs here until the switch above allows remote jobs.' | 'Off. Nothing runs here until the switch above allows jobs from the team.' |
|
||||
| app.js | Settings: jobs note no address | 'No jobs address in this build.' | 'This build has no jobs address.' |
|
||||
| app.js | Settings: jobs note running | 'Running ' + jobTitle(j) + ' (' + j.id + ').' | 'Running: ' + jobTitle(j) + '.' |
|
||||
| app.js | Settings: update note current | 'Up to date' + (u.checked_at ? ', checked ' + rel(state.now - u.checked_at) : '') + '.' | 'Up to date' + (u.checked_at ? '. Checked ' + rel(state.now - u.checked_at) : '') + '.' |
|
||||
| app.js | Settings: update note never | 'Not checked yet. The engine checks by itself every hour.' | 'Not checked yet. The app checks by itself every hour.' |
|
||||
| app.js | Settings: update note rule change | 'A rule switch comes at height ' + withCommas(u.activation_height) + '.' | 'A rule change comes at block ' + withCommas(u.activation_height) + '.' |
|
||||
| app.js | Settings: price toast | 'A price in pence per kWh, for example 28' | 'Type a price in pence per kWh, for example 28' |
|
||||
| app.js | Settings: Ember toast | 'Ember Tune on: once after install, then every 7 days' | 'Ember Tune on. Once after install, then every 7 days' |
|
||||
| app.js | Settings: Power control on toast | 'Power control on: Windows asks for administrator rights once' | 'Power control on. Windows asks for administrator rights once' |
|
||||
| app.js | Settings: Power control off toast | 'Power control off; nothing asks' | 'Power control off. Nothing asks' |
|
||||
| app.js | Settings: trust on toast | 'Trust mode on (devnet only); the node restarts' | 'Trust mode on (devnet only). The node restarts' |
|
||||
| app.js | Settings: trust off toast | 'Trust mode off; the node restarts' | 'Trust mode off. The node restarts' |
|
||||
| app.js | Settings: applied toasts | 'Applied; the miner restarts' | 'Applied. Mining restarts' |
|
||||
| app.js | Cards: switch on toast | 'Worker starting on that card' | 'Starting that card' |
|
||||
| app.js | Cards: switch off toast | 'Worker stopped on that card' | 'That card is off' |
|
||||
| app.js | Cards: cap toast | 'Power cap set; pinned until you unpin it' | 'Power cap set. It stays until you choose Default again' |
|
||||
| app.js | Cards: sendCards default toast | 'Applied; only the changed worker restarts' | 'Applied. Only that card restarts' |
|
||||
| app.js | Cards: stop toast | 'Tune stopped; the card is back where it was' | 'Tune stopped. The card is back where it was' |
|
||||
| app.js | Cards: pin toast | 'The tune chooses the cap again' | 'Default: Ember chooses the cap again' |
|
||||
| app.js | Cards: retry toast | 'Administrator prompt: allow it to set the cap' | 'Allow the administrator prompt to set the cap' |
|
||||
| app.js | Cards: Tune all toast | 'Tune queued for ' + n + ' cards, one at a time' | 'Tune queued for ' + n + ' cards, one after another' |
|
||||
| app.js | Overview: toggle toast | 'Mining stops; the node keeps running' | 'Mining stops. The node keeps running' |
|
||||
| app.js | Address ask | ' from the next block? The miner restarts.' | ' from the next block? Mining restarts.' |
|
||||
| app.js | Trust ask on | 'Include proof records the node never checked? Devnet only; the node restarts.' | 'Include proofs the node never checked? Devnet only. The node restarts.' |
|
||||
| app.js | Trust ask off | 'Verify proof records again? The node restarts.' | 'Check proofs again? The node restarts.' |
|
||||
| app.js | Pill: engine away | 'Engine not answering' | 'App not answering' |
|
||||
| app.js | Node line: engine away | 'the engine is not answering; the window reconnects by itself' | 'the app is not answering. The window reconnects by itself' |
|
||||
| app.js | Button: engine away | 'the engine is not answering' | 'the app is not answering' |
|
||||
| app.js | Hero: engine away | 'no answer from the engine' | 'no answer from the app' |
|
||||
| app.js | Toast: engine did not answer | 'The engine did not answer' | 'The app did not answer' |
|
||||
| app.js | Settings: goal toast | 'The goal needs the Ember Tune update' | 'The goal needs the next Ember Tune update' |
|
||||
| app.js | Cards: card goal toast | 'A per-card goal needs the Ember Tune update' | 'A goal for one card needs the next Ember Tune update' |
|
||||
| app.js | Cards: card goal set toast | 'This card follows the fleet goal' | 'Default: this card follows the goal set for all cards' |
|
||||
| app.js | Activity: identity events | m[1] + ': ' + m[2] + ' identit' + m[3] + '; its worker restarts' | m[1] + ': ' + m[2] + ' miner' + (m[2] === '1' ? '' : 's') + ' on it. The card restarts' |
|
||||
| app.js | Activity: worker ready | m[1] + ': worker ready' | m[1] + ': ready' |
|
||||
| app.js | Activity: worker fault | m[1] + ': worker fault; restarting it' | m[1] + ': an error. Restarting it' |
|
||||
| app.js | Activity: worker stopped | 'A worker stopped (code ' + m[2] + '); restarting in ' + m[3] + ' s' | 'A card stopped (code ' + m[2] + '). Restarting in ' + m[3] + ' s' |
|
||||
| app.js | Activity: worker built | 'Worker built' | 'Mining program built' |
|
||||
| app.js | Activity: program variant | m[1] + ': program variant chosen, ' + m[3] + ' MH/s' | m[1] + ': fastest program chosen, ' + m[3] + ' MH/s' |
|
||||
| app.js | Activity: segment proven pieces | 'Segment proven: ' + m[3] + ' pieces in ' + m[4] + ' s' | 'Run of blocks proven: ' + m[3] + ' pieces in ' + m[4] + ' s' |
|
||||
| app.js | Activity: segment proven | 'Segment proven in ' + m[3] + ' s' | 'Run of blocks proven in ' + m[3] + ' s' |
|
||||
| app.js | Activity: segment paid | 'Segment paid: ' + m[1] + ' IGN' | 'Run of blocks paid: ' + m[1] + ' IGN' |
|
||||
| app.js | Activity: segment held | 'Segment proven: ' + m[3] + ' pieces in ' + m[4] + ' s; its record waits for the chain rule' | 'Run of blocks proven: ' + m[3] + ' pieces in ' + m[4] + ' s. Its record waits for the chain' |
|
||||
| app.js | Activity: segment accepted | 'Segment record accepted' | 'Run of blocks accepted' |
|
||||
| app.js | Activity: node external | 'The team asked for a node restart; this node is not the app’s, so nothing was done' | 'The team asked for a node restart. This node is not the app’s, so nothing was done' |
|
||||
| app.js | Activity: no peer | 'No other node found after 90 s; still looking' | 'No other node found after 90 s. Still looking' |
|
||||
| app.js | Activity: node stopped restarting | 'The node stopped (code ' + m[1] + '); restarting' | 'The node stopped (code ' + m[1] + '). Restarting' |
|
||||
| app.js | Activity: no reading | 'No reading from the node for ' + m[1] + ' s; restarting it' | 'No reading from the node for ' + m[1] + ' s. Restarting it' |
|
||||
| app.js | Activity: mining paused | 'Mining paused; the node keeps running' | 'Mining paused. The node keeps running' |
|
||||
| app.js | Activity: settings changed | 'Settings changed (' + m[1] + '); mining restarts' | 'Settings changed (' + m[1] + '). Mining restarts' |
|
||||
| app.js | Activity: tune best point | m[1] + ': best point ' + m[2] + ' (' + m[3] + '); your cap stays pinned' | m[1] + ': best point ' + m[2] + ' (' + m[3] + '). Your cap stays pinned' |
|
||||
| app.js | Activity: next program compiles | m[1] + ': the next program compiles at the hour boundary instead' | m[1] + ': the next program is built at the top of the hour instead' |
|
||||
| app.js | Activity: new program worker | 'New hourly program; the worker restarts on it' | 'New hourly program. The card restarts on it' |
|
||||
| app.js | Activity: pack | 'Rebuilding the program pack' | 'Rebuilding the mining programs' |
|
||||
| app.js | Activity: trust | 'Devnet trust mode: proof records are not verified (Settings)' | 'Devnet trust mode: proofs are not checked (Settings)' |
|
||||
| app.js | Activity: sweep caps | 'A tune is setting the caps now; retry when it is done' | 'A tune is setting the caps now. Retry when it is done' |
|
||||
| app.js | Activity: updates off engine | 'Updates are off for this engine (a measurement run)' | 'Updates are off for this copy (a measurement run)' |
|
||||
| app.js | Activity: power cap not applied | 'Power cap not applied: the administrator prompt was refused (Retry on the card)' | 'Power cap not set. The administrator prompt was refused (Retry on the card)' |
|
||||
| app.js | Activity: node too old | 'This node is too old for the team’s rule settings; an app update carries the newer node' | 'This node is too old for the team’s rule settings. An app update carries the newer node' |
|
||||
| app.js | Row word: unplugged | 'unplugged; its worker stopped. The row goes in five minutes.' | 'unplugged. Its row goes in five minutes.' |
|
||||
| app.js | Details: removed | '<p class="help">The card was unplugged; its worker stopped. The row goes in five minutes.</p>' | '<p class="help">The card was unplugged. Its row goes in five minutes.</p>' |
|
||||
| app.js | Details: problem | 'No worker can drive this card.' | 'This app cannot use this card.' |
|
||||
| app.js | Setup: no cards | 'No graphics card this app can drive was found.' | 'No graphics card this app can use was found.' |
|
||||
| app.js | Overview: no cards | 'No graphics card this app can drive was found. The node runs on its own.' | 'No graphics card this app can use was found. The node runs on its own.' |
|
||||
| app.js | Pages: overview sub | sub: 'the rate, what it costs, the chain as it grows' | sub: 'your rate, what it costs, the chain as it grows' |
|
||||
| app.js | Pages: prove sub | sub: 'turn blocks into proofs, paid in IGN' | sub: 'prove blocks, earn IGN' |
|
||||
| app.js | Pages: earnings sub | sub: 'what it made, where it goes' | sub: 'what you made, where it goes' |
|
||||
| app.js | Goal names for the Default option | 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 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_NAME |
|
||||
| app.js | Export GOAL_NAMES | GOALS: GOALS, goalWords: goalWords, | GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, |
|
||||
|
|
@ -55,3 +55,14 @@ The chain scene in the shots is the mock's `/api/live` (four lanes, one block a
|
|||
| Re-copy `site/live-dag.js` over `app/igneum-app/ui/live-dag.js` when the site agent's audit commit lands, so the two stay identical | the shipper at merge |
|
||||
|
||||
Tests: `node --test app/igneum-app/ui/*.test.mjs`: 37 pass.
|
||||
|
||||
## 6. The second tip (20:0x UK): /api/live on the engine, the copy sweep, the project lead's goal rulings
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| `GET /api/live` on the engine: the observer's `/api/live` (the URL `ota.rs` already polls for the identity count) through curl, cached 2 s per window, this machine's blocks marked as the lane "you" by matching the block's miner id (the first 8 hex of its vote key hash) against every card's `ids`; `state.you_blocks` and `state.source: "observer"` added; `{ok:false, error}` when the observer is unreachable or the build has no observer address, which the UI meets with its own blocks strip. Four unit tests on the shaping from a fixture in the observer's shape (`live::tests`). The local node speaks gRPC and wRPC only and the engine carries no client for either (no TLS, no websocket stack), so a local-node source is OWED (a JSON endpoint on igneumd, or a wRPC client here); the reply shape stays the same when it comes. | `src/live.rs`, `src/server.rs`, `src/main.rs`, `ota::live_api_from` made `pub` |
|
||||
| The copy sweep: 150 strings, every one listed in `docs/plans/miner-ui-4-copy.md` | `ui/index.html`, `ui/app.js`, the tests |
|
||||
| the project lead's rulings: the per-card inherit option is "Default · Balanced" (grey, following the shared goal); "Default" wherever a per-card setting follows the shared one (the cap's "Back to Default"); the Cards strip is titled "Ember Tune"; every goal option carries its meaning as a tooltip (Efficiency "most hashes per watt, a little rate traded", Balanced "a little rate for most of the saving", Maximum "most hashes, watts second", Default "the goal set for all cards") | `ui/index.html`, `ui/app.js` |
|
||||
|
||||
The Rust tests did not run on this Mac (its cargo predates the lock file's version 4); the box runs them at the
|
||||
merge, as the rule says. UI tests: 37 pass. Screenshots `u01`, `u03`, `u04`, `u06` reshot after the sweep.
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 541 KiB After Width: | Height: | Size: 537 KiB |
|
Before Width: | Height: | Size: 476 KiB After Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 294 KiB After Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 160 KiB After Width: | Height: | Size: 160 KiB |