miner-ui-4: GET /api/live on the engine (observer-backed, this machine as 'you', cached 2 s, 4 tests); the copy sweep (150 strings); the project lead's goal rulings (Default · <shared goal>, Ember Tune strip, meanings on hover)

src/live.rs reads the observer's /api/live through curl (the URL ota.rs already polls), caches it 2 s per window,
marks this machine's blocks as the lane 'you' by matching the miner id against every card's vote-key ids, and
answers {ok:false} when the observer is unreachable so the UI draws its own strip; four unit tests shape a fixture
in the observer's reply shape. The local node speaks gRPC and wRPC only, so a local-node source stays owed.
The copy sweep: no fleet, lane, identities, prior, hill climb, sweep, DAA, digest, manifest, shard, segment,
aggregator, verifier, worker or engine on a user surface; every toast and event line one sentence; the full list
in docs/plans/miner-ui-4-copy.md. The per-card goal's inherit option reads 'Default · Balanced' and follows the
shared goal; the cap's unpin is 'Back to Default'; the Cards strip is titled Ember Tune; each goal carries its
meaning as a tooltip. 37 UI tests pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-06 18:57:56 +00:00
parent 3b54f24f18
commit 4710d7b4bd
13 changed files with 508 additions and 160 deletions

140
app/igneum-app/src/live.rs Normal file
View 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");
}
}

View file

@ -37,6 +37,7 @@ mod sweep;
mod ember;
mod powertask;
mod watchdog;
mod live;
use std::io::{BufRead, Write};
use std::sync::mpsc::channel;

View file

@ -954,7 +954,7 @@ impl Updater {
// ---- the threads ---------------------------------------------------------------------------------------------------
/// https://igneum.network/live -> https://igneum.network/api/live; "" when the build carries no live page.
fn live_api_from(live_page: &str) -> String {
pub fn live_api_from(live_page: &str) -> String {
let Some(rest) = live_page.strip_prefix("https://") else { return String::new() };
let host = rest.split('/').next().unwrap_or("");
if host.is_empty() { String::new() } else { format!("https://{host}/api/live") }

View file

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

View file

@ -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">&minus;</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">&minus;</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');
}
}
});

View file

@ -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>

View file

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

View 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, |

View file

@ -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.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 KiB

After

Width:  |  Height:  |  Size: 537 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

After

Width:  |  Height:  |  Size: 487 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 294 KiB

After

Width:  |  Height:  |  Size: 304 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB