Compare commits
86 commits
master
...
release-0.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6084a7ea64 | ||
|
|
708b1e0fc8 | ||
|
|
d33fa30f89 | ||
|
|
1e8bcf7cb1 | ||
|
|
b3cd2d21f4 | ||
|
|
c4d29c58fd | ||
|
|
9c9a09ff09 | ||
|
|
ea76eac0f8 | ||
|
|
30511ab700 | ||
|
|
60e78a56ba | ||
|
|
10112d1df7 | ||
|
|
9601749fb2 | ||
|
|
3559537130 | ||
|
|
e976b344f3 | ||
|
|
ea7c30d799 | ||
|
|
c07d259323 | ||
|
|
de058a4af5 | ||
|
|
0969bedb2e | ||
|
|
e49c7a058a | ||
|
|
3ef7f901c6 | ||
|
|
fddb02d016 | ||
|
|
359dd11d00 | ||
|
|
ebe27aae69 | ||
|
|
3be454f128 | ||
|
|
93b05c9b7b | ||
|
|
54cc0bd6aa | ||
|
|
12f4fdbde3 | ||
|
|
f35816ddfb | ||
|
|
ff320d9fe3 | ||
|
|
5baaa32273 | ||
|
|
3fd96e4882 | ||
|
|
547b3323d2 | ||
|
|
3dd010bca8 | ||
|
|
f242646a01 | ||
|
|
91c3ed747d | ||
|
|
62736cecb5 | ||
|
|
19b802a17b | ||
|
|
418801fac2 | ||
|
|
0aef24f747 | ||
|
|
7ee76db53c | ||
|
|
4bf858f3db | ||
|
|
8321127385 | ||
|
|
f0660d59c3 | ||
|
|
6f6d058c9f | ||
|
|
9201a07faf | ||
|
|
7996240f6f | ||
|
|
2c2859253d | ||
|
|
e8244a7853 | ||
|
|
17e1eb4d12 | ||
|
|
f7d5db67ad | ||
|
|
98b686059a | ||
|
|
54bcdbe510 | ||
|
|
64e70bb27d | ||
|
|
8aa8f0a048 | ||
|
|
05a2d0599a | ||
|
|
b371bf2bf4 | ||
|
|
0ac8d097b2 | ||
|
|
5b4cb56a27 | ||
|
|
fa20b72fea | ||
|
|
4af9ab4d0a | ||
|
|
409cb0f76c | ||
|
|
7a69c03135 | ||
|
|
7ce5965e46 | ||
|
|
18b154de9c | ||
|
|
7943f83c17 | ||
|
|
4f6b7810d5 | ||
|
|
fc94277775 | ||
|
|
02627f3d9f | ||
|
|
441d1eaf88 | ||
|
|
53d99b17f9 | ||
|
|
3cc1b9ef11 | ||
|
|
0b48c028f3 | ||
|
|
95707125c5 | ||
|
|
f57c5293bc | ||
|
|
3d0a6458da | ||
|
|
2b244d732e | ||
|
|
0659fab5f2 | ||
|
|
0477f8857d | ||
|
|
6e9bd00dbb | ||
|
|
98bdeeb88b | ||
|
|
5143cd2ed6 | ||
|
|
9a45ea3785 | ||
|
|
75a6364384 | ||
|
|
bd3fb7a809 | ||
|
|
ecfffa019a | ||
|
|
14dc093549 |
4
.gitignore
vendored
|
|
@ -32,6 +32,10 @@ vendor/igneum-node-ship/
|
|||
brand/trademark/pbip-pack/
|
||||
brand/trademark/*.zip
|
||||
|
||||
# cargo target dirs of the shipper's local test runs and the build box's fetched artefacts (6 October 2026: a stray add -A staged 450 of them)
|
||||
app/igneum-app/target-tests/
|
||||
proving/igneum-prove/target-remote/
|
||||
**/target-remote/
|
||||
# the Discord bot and reddit bot dry-run renders
|
||||
tools/community/out/
|
||||
# the GPU workers built on igneum-build-1 (tools/workers-remote.sh); binaries, never committed
|
||||
|
|
|
|||
2
app/igneum-app/Cargo.lock
generated
|
|
@ -219,7 +219,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "igneum-app"
|
||||
version = "0.3.14"
|
||||
version = "0.3.16"
|
||||
dependencies = [
|
||||
"ed25519-dalek",
|
||||
"getrandom",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "igneum-app"
|
||||
version = "0.3.14"
|
||||
version = "0.3.16"
|
||||
edition = "2021"
|
||||
description = "Igneum Miner engine: supervises the node, the miner and the GPU workers, and serves the dashboard on 127.0.0.1"
|
||||
license = "MIT"
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
1 ICON "igneum.ico"
|
||||
|
||||
1 VERSIONINFO
|
||||
FILEVERSION 0,3,14,0
|
||||
PRODUCTVERSION 0,3,14,0
|
||||
FILEVERSION 0,3,16,0
|
||||
PRODUCTVERSION 0,3,16,0
|
||||
FILEFLAGSMASK 0x3fL
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
|
|
@ -20,12 +20,12 @@ BEGIN
|
|||
BEGIN
|
||||
VALUE "CompanyName", "Igneum"
|
||||
VALUE "FileDescription", "Igneum Miner engine"
|
||||
VALUE "FileVersion", "0.3.14"
|
||||
VALUE "FileVersion", "0.3.16"
|
||||
VALUE "InternalName", "igneum-app"
|
||||
VALUE "LegalCopyright", "Igneum contributors"
|
||||
VALUE "OriginalFilename", "igneum-app.exe"
|
||||
VALUE "ProductName", "Igneum Miner"
|
||||
VALUE "ProductVersion", "0.3.14"
|
||||
VALUE "ProductVersion", "0.3.16"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,15 @@ pub struct CardPref {
|
|||
pub sweep_class: String,
|
||||
#[serde(default)]
|
||||
pub sweep_source: String,
|
||||
/// Miner UI 4 (6 October 2026): this card's goal (efficiency | balanced | rate); empty = the global tune_goal
|
||||
#[serde(default)]
|
||||
pub tune_goal: String,
|
||||
/// the untuned point the last FULL plan measured first (its step 0), kept across confirm plans so the row can
|
||||
/// read "saves 84 W, 0.15% of rate"; 0 = never measured
|
||||
#[serde(default)]
|
||||
pub sweep_before_watts: f64,
|
||||
#[serde(default)]
|
||||
pub sweep_before_mhs: f64,
|
||||
/// Ember 2: the memory clock the last tune chose (0 = the driver's default)
|
||||
#[serde(default)]
|
||||
pub sweep_mem_mhz: u32,
|
||||
|
|
@ -357,8 +366,9 @@ impl Runtime {
|
|||
let p2p_port = env("IGNEUM_APP_P2P_PORT").and_then(|v| v.parse().ok()).unwrap_or(26611);
|
||||
let peers = match std::env::var("IGNEUM_APP_PEERS") {
|
||||
Ok(v) => v.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(),
|
||||
// the public seed node first, then the project lead's Mac on the house LAN (devnet only)
|
||||
Err(_) if network == "devnet" => vec!["188.245.5.161:26611".to_string(), "192.168.68.64:26611".to_string()],
|
||||
// the public seed node first, then the build box's hand nodes (main's decision (b), 6 October 2026, 19:1x UTC:
|
||||
// the hands move off the project lead's Mac to igneum-build-1), then the project lead's Mac on the house LAN (devnet only)
|
||||
Err(_) if network == "devnet" => vec!["188.245.5.161:26611".to_string(), "188.40.146.49:26611".to_string(), "192.168.68.64:26611".to_string()],
|
||||
Err(_) => vec![],
|
||||
};
|
||||
let unsynced_mining = env("IGNEUM_APP_UNSYNCED").map(|v| v == "1").unwrap_or(false);
|
||||
|
|
|
|||
|
|
@ -906,6 +906,33 @@ pub fn tuned_line(mhs: f64, watts: f64, eff: f64) -> String {
|
|||
format!("Tuned: {mhs:.1} MH/s at {watts:.0} W ({eff:.3} MH/W)")
|
||||
}
|
||||
|
||||
/// Miner UI 4 (6 October 2026): a card's own goal when set, else the global one.
|
||||
pub fn goal_for(card_goal: &str, global: &str) -> Goal {
|
||||
Goal::parse(if card_goal.trim().is_empty() { global } else { card_goal })
|
||||
}
|
||||
|
||||
/// (D) May a step be SET on this card? Measure-only (no control: Apple, NVIDIA without Power control, an AMD card
|
||||
/// whose probe gave no tune line) sets nothing: the run notices the missing acknowledgement and measures. An AMD
|
||||
/// card also needs its ADLX ordinal. 6 October 2026, PC 1 17:57Z: a measure-only 9070 XT was sent a set, refused.
|
||||
pub fn may_set(vendor: &str, tune_control: bool, amd_ordinal: i64) -> bool {
|
||||
match vendor {
|
||||
"nvidia" => tune_control,
|
||||
"amd" => tune_control && amd_ordinal >= 0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// (E) The row's words while a back-off from a failed tune holds: when the retry comes and why it stopped.
|
||||
pub fn retry_note(remaining_s: u64, last_note: &str) -> String {
|
||||
let mins = (remaining_s + 59) / 60;
|
||||
let why = last_note.trim_start_matches("tuning stopped: ").trim_start_matches("tuning: ").trim();
|
||||
if why.is_empty() {
|
||||
format!("tuning: retry in {mins} min")
|
||||
} else {
|
||||
format!("tuning: retry in {mins} min ({why})")
|
||||
}
|
||||
}
|
||||
|
||||
/// The row's line for a baseline (measure-only) result: the card was measured as it runs, nothing was set.
|
||||
/// (6 October 2026: PC 1's rows read "Tuned: 114.2 MH/s at 305 W" for a baseline, which is not a tune.)
|
||||
pub fn measured_line(mhs: f64, watts: f64, eff: f64) -> String {
|
||||
|
|
@ -1007,6 +1034,26 @@ mod tests {
|
|||
assert!(Run::applied(&step, Readback { limit_w: 90.0, acked: false }, 100.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_card_goal_overrides_the_global_goal_and_empty_follows_it() {
|
||||
assert_eq!(goal_for("", "balanced"), Goal::Balanced);
|
||||
assert_eq!(goal_for("rate", "balanced"), Goal::MaxRate);
|
||||
assert_eq!(goal_for("efficiency", "rate"), Goal::Efficiency);
|
||||
assert_eq!(goal_for(" ", "efficiency"), Goal::Efficiency);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_measure_only_card_is_never_set_and_a_back_off_says_when_the_retry_comes() {
|
||||
assert!(!may_set("amd", false, 1), "the probe gave no tune line: measure only");
|
||||
assert!(may_set("amd", true, 1));
|
||||
assert!(!may_set("amd", true, -1), "no ADLX ordinal");
|
||||
assert!(!may_set("nvidia", false, -1), "Power control off: measure only");
|
||||
assert!(may_set("nvidia", true, -1));
|
||||
assert!(!may_set("apple", true, -1));
|
||||
assert_eq!(retry_note(3599, "tuning stopped: the card refused the setting (see the log)"), "tuning: retry in 60 min (the card refused the setting (see the log))");
|
||||
assert_eq!(retry_note(30, ""), "tuning: retry in 1 min");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_baseline_result_reads_measured_and_a_tune_reads_tuned() {
|
||||
assert_eq!(result_line(PlanKind::Baseline, 114.2, 305.0, 0.3744), "Measured: 114.2 MH/s at 305 W (0.374 MH/W)");
|
||||
|
|
|
|||
|
|
@ -77,6 +77,8 @@ pub enum Cmd {
|
|||
TuneProgress(Value),
|
||||
/// Ember 2: Settings > goal, electricity price, the hill-climb switch
|
||||
TuneGoal(Option<String>, Option<f64>, Option<bool>),
|
||||
/// one card's goal override (key, goal; "" = back to the global goal)
|
||||
TuneCardGoal(String, String),
|
||||
/// restart the node with the verifier decided again (src/verifier.rs): the trust setting changed, or the
|
||||
/// prover found a host that was not there when the node started
|
||||
RestartNode(String),
|
||||
|
|
@ -1147,6 +1149,26 @@ impl Engine {
|
|||
}
|
||||
}
|
||||
Cmd::TuneProgress(v) => self.tune_progress(&v),
|
||||
Cmd::TuneCardGoal(key, goal) => {
|
||||
{
|
||||
let mut s = self.shared.settings.lock().unwrap();
|
||||
let (enabled, identities) = self.st().mining.cards.iter().find(|c| c.key == key).map(|c| (c.enabled, c.identities)).unwrap_or((true, 1));
|
||||
let e = s.cards.entry(key.clone()).or_insert_with(|| crate::config::CardPref { enabled, identities, ..Default::default() });
|
||||
e.tune_goal = goal.clone();
|
||||
}
|
||||
self.shared.save_settings();
|
||||
let name = {
|
||||
let mut st = self.st();
|
||||
let c = st.mining.cards.iter_mut().find(|c| c.key == key);
|
||||
c.map(|c| {
|
||||
c.tune_goal = goal.clone();
|
||||
c.name.clone()
|
||||
})
|
||||
};
|
||||
if let Some(name) = name {
|
||||
self.shared.event("info", &format!("{name}: tune goal {}", if goal.is_empty() { "follows the global setting".to_string() } else { goal.clone() }));
|
||||
}
|
||||
}
|
||||
Cmd::TuneGoal(goal, price, climb) => {
|
||||
{
|
||||
let mut s = self.shared.settings.lock().unwrap();
|
||||
|
|
@ -2219,9 +2241,24 @@ impl Engine {
|
|||
if c.tune_control && c.vendor == "nvidia" && !power_control && !forced {
|
||||
// measure-only until Power control is on; the baseline plan covers it below
|
||||
}
|
||||
if self.sweep_retry.get(&c.index).map(|t| now < *t).unwrap_or(false) {
|
||||
// (E) 6 October 2026, PC 1 18:18Z: three "Tune now" requests sat behind the hour's back-off from the
|
||||
// 17:55 failures and the rows said "queued" with no start; an explicit request now clears the back-off,
|
||||
// and a held card's row says when the retry comes
|
||||
if let Some(t) = self.sweep_retry.get(&c.index).copied() {
|
||||
if now < t {
|
||||
if forced {
|
||||
self.sweep_retry.remove(&c.index);
|
||||
} else {
|
||||
if c.sweep_pct == 0 || c.sweep_note.starts_with("tuning stopped") {
|
||||
let note = crate::ember::retry_note((t - now).as_secs(), &c.sweep_note);
|
||||
if let Some(cc) = self.st().mining.cards.get_mut(c.index) {
|
||||
cc.sweep_note = note;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
let stable = self.miners.iter().find(|m| m.card == c.index).map(|m| m.proc.as_ref().map(|p| p.started.elapsed() >= Duration::from_secs(crate::ember::STABLE_S)).unwrap_or(false) && m.last_status.is_some()).unwrap_or(false);
|
||||
let wait = if c.state != "mining" {
|
||||
Some(format!("tuning: waits for the card to mine ({})", c.state))
|
||||
|
|
@ -2399,7 +2436,8 @@ impl Engine {
|
|||
let full_due = self.tune_full_due.remove(&idx);
|
||||
let (goal, climb_on) = {
|
||||
let s = self.shared.settings.lock().unwrap();
|
||||
(crate::ember::Goal::parse(&s.tune_goal), s.tune_climb)
|
||||
// the card's own goal when set, else the global one
|
||||
(crate::ember::goal_for(s.cards.get(&c.key).map(|p| p.tune_goal.as_str()).unwrap_or(""), &s.tune_goal), s.tune_climb)
|
||||
};
|
||||
let plan = if let Some(why) = control.as_ref() {
|
||||
if let Some(cc) = self.st().mining.cards.get_mut(idx) {
|
||||
|
|
@ -2567,16 +2605,47 @@ impl Engine {
|
|||
}
|
||||
let dev = device.to_string();
|
||||
let cmd = format!("{seq}0 dev {dev}\n{seq}1 pl {w}\n{seq}2 {}\n{seq}3 {}\n", if clock > 0 { format!("lgc {clock}") } else { "rgc".to_string() }, if mem > 0 { format!("lmc {mem}") } else { "rmc".to_string() });
|
||||
let _ = std::fs::write(self.helper_cmd_dir().join("cmd.txt"), cmd);
|
||||
// the helper polls twice a second and nvidia-smi answers within a second or two
|
||||
let dir = self.helper_cmd_dir();
|
||||
let is_task = self.sweep_helper_is_task;
|
||||
// 6 October 2026, PC 1: (B) the task's helper had exited while the engine's flag still said it ran,
|
||||
// so commands went to nobody and a blind 4-second sleep called them acknowledged; (C) the first
|
||||
// request of the first tune was written in the same second the helper started, and the helper
|
||||
// skipped it as stale. Now, in the task case: the helper's heartbeat says whether it runs; the task
|
||||
// is started and a fresh heartbeat awaited BEFORE the command is written; and the acknowledgement
|
||||
// is the helper's own log line for this sequence (`<seq>3 nvidia-smi ...`), read for up to 15 s;
|
||||
// a missing line is a failure with that reason
|
||||
let want = format!(" {seq}3 nvidia-smi");
|
||||
let log_file = dir.join("helper.log");
|
||||
std::thread::spawn(move || {
|
||||
std::thread::sleep(Duration::from_secs(4));
|
||||
shared.send(Cmd::TuneSet(seq, Ok("helper".into())));
|
||||
if is_task {
|
||||
if let Err(e) = crate::powertask::ensure_running(&dir, Duration::from_secs(12)) {
|
||||
shared.send(Cmd::TuneSet(seq, Err(e)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let _ = std::fs::write(dir.join("cmd.txt"), cmd);
|
||||
let until = Instant::now() + Duration::from_secs(if is_task { 15 } else { 6 });
|
||||
loop {
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
let text = std::fs::read_to_string(&log_file).unwrap_or_default();
|
||||
if let Some(line) = text.lines().rev().find(|l| l.contains(&want)) {
|
||||
shared.send(Cmd::TuneSet(seq, Ok(format!("helper: {}", line.trim()))));
|
||||
return;
|
||||
}
|
||||
if Instant::now() >= until {
|
||||
shared.send(Cmd::TuneSet(seq, Err(format!("the helper did not run sequence {seq} within {} s (no line in {})", if is_task { 15 } else { 6 }, log_file.display()))));
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
"amd" => {
|
||||
let Some(exe) = self.bins.telemetry.clone() else { return };
|
||||
if c.amd_ordinal < 0 {
|
||||
// (D) 6 October 2026, PC 1 17:57Z: a measure-only card (the probe gave no tune line) was still sent a
|
||||
// set, which the helper refused and the tune stopped; measure only means nothing is set, the run
|
||||
// notices the missing acknowledgement and measures
|
||||
if !crate::ember::may_set("amd", c.tune_control, c.amd_ordinal) {
|
||||
return;
|
||||
}
|
||||
let n = c.amd_ordinal.to_string();
|
||||
|
|
@ -2792,6 +2861,14 @@ impl Engine {
|
|||
e.sweep_driver = c.driver.clone();
|
||||
e.sweep_class = c.program_class.clone();
|
||||
e.sweep_source = kind.name().into();
|
||||
if kind == crate::ember::PlanKind::Full {
|
||||
if let Some(b) = run.rows.first() {
|
||||
e.sweep_before_watts = b.watts;
|
||||
e.sweep_before_mhs = b.mhs;
|
||||
c.tune_before_watts = b.watts;
|
||||
c.tune_before_mhs = b.mhs;
|
||||
}
|
||||
}
|
||||
if !c.pinned && kind != crate::ember::PlanKind::Baseline {
|
||||
e.power_pct = row.point.power_pct;
|
||||
}
|
||||
|
|
@ -2977,7 +3054,9 @@ impl Engine {
|
|||
node_synced: st.node.synced && st.clock.severity != "block",
|
||||
boundary_eta_s: if st.node.daa > 0 && st.program.boundary_daa > 0 { Some(st.program.eta_s) } else { None },
|
||||
// a remote job in progress counts as busy: no update applies under it (src/jobrun.rs)
|
||||
miner_busy: self.miners.iter().any(|m| m.building) || st.mining.cards.iter().any(|c| c.enabled && c.state == "starting") || self.jobs.active(),
|
||||
miner_busy: self.miners.iter().any(|m| m.building) || st.mining.cards.iter().any(|c| c.enabled && c.state == "starting"),
|
||||
// a remote job in progress holds the update, urgent or not (src/manifest.rs safe_to_apply; PC 1, 6 October 2026)
|
||||
job_active: self.jobs.active(),
|
||||
daa: st.node.daa,
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -167,6 +167,9 @@ pub fn apply_pref(c: &mut CardState, p: &CardPref) {
|
|||
c.mem_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_mem_mhz };
|
||||
c.clock_cap_mhz = if p.pinned || p.sweep_source == "baseline" { 0 } else { p.sweep_clock_mhz };
|
||||
c.tune_source = p.sweep_source.clone();
|
||||
c.tune_goal = p.tune_goal.clone();
|
||||
c.tune_before_watts = p.sweep_before_watts;
|
||||
c.tune_before_mhs = p.sweep_before_mhs;
|
||||
if p.sweep_mhs > 0.0 && p.sweep_watts > 0.0 {
|
||||
c.tune_line = crate::ember::tuned_line(p.sweep_mhs, p.sweep_watts, p.sweep_eff);
|
||||
}
|
||||
|
|
|
|||
140
app/igneum-app/src/live.rs
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
//! GET /api/live for the dashboard's chain scene (ui/live-dag.js, the site's module). The scene wants the network's
|
||||
//! blocks over the last minutes with lanes, parents, colours, the selected chain, checkpoints and proof state. The
|
||||
//! local node speaks gRPC and wRPC only and the engine carries no client for either (no TLS, no websocket stack), so
|
||||
//! this reads the observer's /api/live (the same URL ota.rs already polls for the identity count) through curl, caches
|
||||
//! it for 2 s, and marks this machine's blocks: the observer names a block's miner by the first 8 hex of its vote key
|
||||
//! hash, and every card's `ids` carry the same 8 characters for this machine's keys, so a match becomes the lane "you".
|
||||
//! Read-only; one upstream call per 2 s whatever the window asks. A local-node source is owed (a JSON RPC on igneumd or
|
||||
//! a wRPC client here); the shape stays the same when it comes.
|
||||
use serde_json::{json, Value};
|
||||
use std::collections::HashSet;
|
||||
use std::process::Command;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
static CACHE: Mutex<Option<(Instant, u32, Value)>> = Mutex::new(None);
|
||||
const TTL: Duration = Duration::from_secs(2);
|
||||
|
||||
/// The observer's reply with this machine's lane marked. `ids` are the 8-character vote key ids of this machine's
|
||||
/// cards. Blocks whose `miner` is one of them read `miner: "you"`; `state.you_blocks` counts them; `state.source` names
|
||||
/// where the data came from. Anything else passes through untouched, so the module's contract holds.
|
||||
pub fn shape(mut reply: Value, ids: &HashSet<String>) -> Value {
|
||||
let mut yours = 0u64;
|
||||
if let Some(blocks) = reply.get_mut("blocks").and_then(|b| b.as_array_mut()) {
|
||||
for b in blocks.iter_mut() {
|
||||
let mine = b.get("miner").and_then(|m| m.as_str()).map(|m| ids.contains(m)).unwrap_or(false);
|
||||
if mine {
|
||||
yours += 1;
|
||||
if let Some(o) = b.as_object_mut() { o.insert("miner".into(), json!("you")); }
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(st) = reply.get_mut("state").and_then(|s| s.as_object_mut()) {
|
||||
st.insert("you_blocks".into(), json!(yours));
|
||||
st.insert("source".into(), json!("observer"));
|
||||
}
|
||||
reply
|
||||
}
|
||||
|
||||
/// The observer URL for a window: `<live_api>?window=<s>`, the window clamped to the module's 30 to 300 s.
|
||||
pub fn url_for(live_api: &str, window_s: u32) -> String {
|
||||
format!("{live_api}?window={}", window_s.clamp(30, 300))
|
||||
}
|
||||
|
||||
/// The reply for the dashboard: cached 2 s per window; `{ok:false, error}` when the observer is unreachable or the
|
||||
/// build has no observer address (the UI then draws its own blocks strip).
|
||||
pub fn fetch(live_api: &str, window_s: u32, ids: &HashSet<String>) -> Value {
|
||||
if live_api.is_empty() {
|
||||
return json!({ "ok": false, "error": "no observer address in this build" });
|
||||
}
|
||||
let window_s = window_s.clamp(30, 300);
|
||||
if let Some((at, w, v)) = CACHE.lock().unwrap().as_ref() {
|
||||
if *w == window_s && at.elapsed() < TTL { return v.clone(); }
|
||||
}
|
||||
let url = url_for(live_api, window_s);
|
||||
let out = crate::detect::run_timeout(
|
||||
Command::new(crate::platform::tool("curl")).args(["-s", "--max-time", "4", &url]),
|
||||
None,
|
||||
Duration::from_secs(6),
|
||||
);
|
||||
let reply = match out.and_then(|t| serde_json::from_str::<Value>(&t).ok()) {
|
||||
Some(v) if v.get("ok").and_then(|o| o.as_bool()).unwrap_or(false) && v.get("blocks").map(|b| b.is_array()).unwrap_or(false) => shape(v, ids),
|
||||
_ => json!({ "ok": false, "error": "the observer did not answer" }),
|
||||
};
|
||||
if reply.get("ok").and_then(|o| o.as_bool()).unwrap_or(false) {
|
||||
*CACHE.lock().unwrap() = Some((Instant::now(), window_s, reply.clone()));
|
||||
}
|
||||
reply
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// a fixture in the observer's shape (site/api/live.mjs): three miners, this machine is 8fafda27, one of its blocks on
|
||||
// the selected chain and proven, one excluded; a locked and a pending checkpoint
|
||||
fn fixture() -> Value {
|
||||
json!({
|
||||
"ok": true, "now": 1791301670,
|
||||
"state": { "stale": false, "age_s": 1, "height": 198490 },
|
||||
"blocks": [
|
||||
{ "hash": "a1", "ts": 1791301600000i64, "blue_score": 194690, "daa": 198486, "parents": [], "chain": true, "color": "blue", "miner": "8fafda27", "locked": true, "final": true, "shards": [{ "state": "paid" }], "proven": true },
|
||||
{ "hash": "a2", "ts": 1791301601000i64, "blue_score": 194691, "daa": 198487, "parents": ["a1"], "chain": true, "color": "blue", "miner": "1b2c3d4e", "locked": false, "final": false, "shards": [{ "state": "proving" }], "proven": false },
|
||||
{ "hash": "a3", "ts": 1791301601500i64, "blue_score": null, "daa": 198487, "parents": ["a1"], "chain": false, "color": "red", "miner": "8fafda27", "locked": false, "final": false, "shards": [], "proven": false },
|
||||
{ "hash": "a4", "ts": 1791301602000i64, "blue_score": 194692, "daa": 198488, "parents": ["a2", "a3"], "chain": true, "color": "pending", "miner": "deadbeef", "locked": false, "final": false, "shards": [], "proven": false }
|
||||
],
|
||||
"finality": { "checkpoints": [
|
||||
{ "index": 6490, "blue_score": 194690, "daa": 198486, "state": "locked", "fraction_total": 0.71 },
|
||||
{ "index": 6491, "blue_score": 194720, "daa": 198516, "state": "pending", "fraction_total": 0.44 }
|
||||
] }
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn this_machines_blocks_become_the_lane_you_and_nothing_else_moves() {
|
||||
let ids: HashSet<String> = ["8fafda27".to_string(), "9a8b7c6d".to_string()].into_iter().collect();
|
||||
let out = shape(fixture(), &ids);
|
||||
let blocks = out["blocks"].as_array().unwrap();
|
||||
assert_eq!(blocks.len(), 4);
|
||||
assert_eq!(blocks[0]["miner"], "you");
|
||||
assert_eq!(blocks[2]["miner"], "you");
|
||||
assert_eq!(blocks[1]["miner"], "1b2c3d4e");
|
||||
assert_eq!(blocks[3]["miner"], "deadbeef");
|
||||
// the rest of the contract passes through: parents, chain, colour, proof state, checkpoints
|
||||
assert_eq!(blocks[3]["parents"], json!(["a2", "a3"]));
|
||||
assert_eq!(blocks[0]["chain"], true);
|
||||
assert_eq!(blocks[2]["color"], "red");
|
||||
assert_eq!(blocks[0]["proven"], true);
|
||||
assert_eq!(blocks[1]["shards"][0]["state"], "proving");
|
||||
assert_eq!(out["finality"]["checkpoints"].as_array().unwrap().len(), 2);
|
||||
assert_eq!(out["finality"]["checkpoints"][0]["state"], "locked");
|
||||
assert_eq!(out["state"]["you_blocks"], 2);
|
||||
assert_eq!(out["state"]["source"], "observer");
|
||||
assert_eq!(out["state"]["height"], 198490);
|
||||
assert_eq!(out["ok"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_ids_means_no_lane_is_yours_and_a_bare_reply_survives() {
|
||||
let out = shape(fixture(), &HashSet::new());
|
||||
assert!(out["blocks"].as_array().unwrap().iter().all(|b| b["miner"] != "you"));
|
||||
assert_eq!(out["state"]["you_blocks"], 0);
|
||||
let bare = shape(json!({ "ok": true, "blocks": [] }), &HashSet::new());
|
||||
assert_eq!(bare["blocks"].as_array().unwrap().len(), 0);
|
||||
assert!(bare.get("state").is_none(), "no state object is invented");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_window_is_clamped_to_the_modules_range() {
|
||||
assert_eq!(url_for("https://igneum.network/api/live", 120), "https://igneum.network/api/live?window=120");
|
||||
assert_eq!(url_for("https://igneum.network/api/live", 5), "https://igneum.network/api/live?window=30");
|
||||
assert_eq!(url_for("https://igneum.network/api/live", 9000), "https://igneum.network/api/live?window=300");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_build_without_an_observer_address_says_so() {
|
||||
let out = fetch("", 120, &HashSet::new());
|
||||
assert_eq!(out["ok"], false);
|
||||
assert_eq!(out["error"], "no observer address in this build");
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,7 @@ mod sweep;
|
|||
mod ember;
|
||||
mod powertask;
|
||||
mod watchdog;
|
||||
mod live;
|
||||
|
||||
use std::io::{BufRead, Write};
|
||||
use std::sync::mpsc::channel;
|
||||
|
|
|
|||
|
|
@ -300,6 +300,10 @@ pub struct Moment {
|
|||
pub boundary_eta_s: Option<i64>,
|
||||
/// A worker is starting, exporting a pack or being built: let it finish.
|
||||
pub miner_busy: bool,
|
||||
/// A remote job is running on this engine (src/jobrun.rs). It holds even an urgent install: a job is bounded by its
|
||||
/// cap, and an install under it kills its process tree (PC 1, 6 October 2026, 17:52:54Z: 0.3.14 went in during a
|
||||
/// measurement job because install_asked from a two-hour-old update-now still counted as urgent).
|
||||
pub job_active: bool,
|
||||
/// How long the update has been ready and waiting.
|
||||
pub ready_for_s: u64,
|
||||
/// A consensus activation is close, or this version is below min_supported_version: now beats later.
|
||||
|
|
@ -320,6 +324,9 @@ pub fn slot_minute(id8: &str) -> u64 {
|
|||
|
||||
/// Ok when the update may be applied now; Err carries the reason to wait, in the words the dashboard shows.
|
||||
pub fn safe_to_apply(m: &Moment) -> Result<(), String> {
|
||||
if m.job_active {
|
||||
return Err("a remote job is running; installing when it closes".into());
|
||||
}
|
||||
if m.urgent {
|
||||
return Ok(());
|
||||
}
|
||||
|
|
@ -492,7 +499,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn safe_moments() {
|
||||
let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, ready_for_s: 60, urgent: false, slot_ok: true, network_drop_pct: 0.0 };
|
||||
let base = Moment { node_synced: true, boundary_eta_s: Some(1800), miner_busy: false, job_active: false, ready_for_s: 60, urgent: false, slot_ok: true, network_drop_pct: 0.0 };
|
||||
assert!(safe_to_apply(&base).is_ok());
|
||||
assert_eq!(safe_to_apply(&Moment { node_synced: false, ..base.clone() }).unwrap_err(), "waiting for the node to sync");
|
||||
assert!(safe_to_apply(&Moment { boundary_eta_s: Some(120), ..base.clone() }).unwrap_err().contains("boundary in 120 s"));
|
||||
|
|
@ -502,6 +509,11 @@ mod tests {
|
|||
assert!(safe_to_apply(&Moment { miner_busy: true, ..base.clone() }).unwrap_err().contains("worker"));
|
||||
// urgent beats every wait
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, boundary_eta_s: Some(5), miner_busy: true, urgent: true, ..base.clone() }).is_ok());
|
||||
// PC 1, 6 October 2026, 17:52:54Z: a scheduled update arriving mid-job is deferred to the job's close, urgent or not
|
||||
assert!(safe_to_apply(&Moment { job_active: true, ..base.clone() }).unwrap_err().contains("remote job"));
|
||||
assert!(safe_to_apply(&Moment { job_active: true, urgent: true, ..base.clone() }).unwrap_err().contains("remote job"));
|
||||
assert!(safe_to_apply(&Moment { job_active: true, urgent: true, slot_ok: false, ..base.clone() }).is_err());
|
||||
assert!(safe_to_apply(&Moment { job_active: false, urgent: true, slot_ok: false, ..base.clone() }).is_ok());
|
||||
// patience: an unsynced node for 6 h applies anyway
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S, ..base.clone() }).is_ok());
|
||||
assert!(safe_to_apply(&Moment { node_synced: false, ready_for_s: SAFE_MOMENT_PATIENCE_S - 1, ..base.clone() }).is_err());
|
||||
|
|
|
|||
|
|
@ -70,6 +70,8 @@ pub struct Ctx {
|
|||
pub node_synced: bool,
|
||||
pub boundary_eta_s: Option<i64>,
|
||||
pub miner_busy: bool,
|
||||
/// A remote job is running (src/jobrun.rs): the install holds, urgent or not.
|
||||
pub job_active: bool,
|
||||
pub daa: u64,
|
||||
}
|
||||
|
||||
|
|
@ -539,7 +541,7 @@ impl Updater {
|
|||
}
|
||||
let slot_ok = minute == self.slot || catch_up || std::env::var("IGNEUM_APP_UPDATE_NO_SLOT").map(|v| v == "1").unwrap_or(false);
|
||||
let ready_for = self.ready_since.map(|t| now.duration_since(t).as_secs()).unwrap_or(0);
|
||||
let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct };
|
||||
let moment = Moment { node_synced: ctx.node_synced, boundary_eta_s: ctx.boundary_eta_s, miner_busy: ctx.miner_busy, job_active: ctx.job_active, ready_for_s: ready_for, urgent: urgent || self.install_asked, slot_ok, network_drop_pct };
|
||||
if !self.auto && !urgent && !self.install_asked {
|
||||
shared.state.lock().unwrap().update.wait = "waiting for Install now (automatic updates are off)".into();
|
||||
return None;
|
||||
|
|
@ -668,6 +670,12 @@ impl Updater {
|
|||
shared.log(&format!("update check: {} is published but has no {} build yet", m.version, manifest::platform_name()));
|
||||
} else {
|
||||
shared.log(&format!("update check: {} is current (manifest {})", self.current, m.version));
|
||||
// an asked install (update-now) covers this one check: nothing newer, so the ask is spent.
|
||||
// Left true it made the NEXT manifest urgent hours later (PC 1, 6 October 2026, 17:52:54Z).
|
||||
if self.install_asked {
|
||||
self.install_asked = false;
|
||||
shared.log("update check: Install now asked and nothing newer is published; the ask is spent (the next manifest takes the usual slot)");
|
||||
}
|
||||
}
|
||||
self.entry = None;
|
||||
self.file = None;
|
||||
|
|
@ -954,7 +962,7 @@ impl Updater {
|
|||
// ---- the threads ---------------------------------------------------------------------------------------------------
|
||||
|
||||
/// https://igneum.network/live -> https://igneum.network/api/live; "" when the build carries no live page.
|
||||
fn live_api_from(live_page: &str) -> String {
|
||||
pub fn live_api_from(live_page: &str) -> String {
|
||||
let Some(rest) = live_page.strip_prefix("https://") else { return String::new() };
|
||||
let host = rest.split('/').next().unwrap_or("");
|
||||
if host.is_empty() { String::new() } else { format!("https://{host}/api/live") }
|
||||
|
|
|
|||
|
|
@ -35,6 +35,46 @@ use std::time::{Duration, Instant};
|
|||
pub const TASK_NAME: &str = "Igneum Power Helper";
|
||||
/// The helper ends after this long without a new command.
|
||||
pub const IDLE_S: u64 = 20 * 60;
|
||||
/// The heartbeat file the helper refreshes every poll; an engine judges "the helper runs" by it, never by a flag
|
||||
/// of its own (6 October 2026, PC 1: the flag said yes after the helper's idle exit, and commands went to nobody).
|
||||
pub const ALIVE_FILE: &str = "helper.alive";
|
||||
/// A heartbeat older than this is a dead helper.
|
||||
pub const ALIVE_MAX_S: u64 = 4;
|
||||
|
||||
/// Writes the heartbeat: the unix time, as text.
|
||||
pub fn beat(dir: &Path, now: u64) {
|
||||
let _ = std::fs::write(dir.join(ALIVE_FILE), now.to_string());
|
||||
}
|
||||
|
||||
/// Reads a heartbeat: is the helper alive at `now`?
|
||||
pub fn alive_at(text: &str, now: u64) -> bool {
|
||||
text.trim().parse::<u64>().map(|t| now.saturating_sub(t) <= ALIVE_MAX_S).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Is the helper that reads `dir` alive now?
|
||||
pub fn alive(dir: &Path) -> bool {
|
||||
std::fs::read_to_string(dir.join(ALIVE_FILE)).map(|t| alive_at(&t, crate::platform::unix_now())).unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Starts the task unless the helper already runs, then waits for a fresh heartbeat (up to `wait`). The engine
|
||||
/// writes a command only after this says Ok: the helper skips what the file held before its start (a stale quit
|
||||
/// is not a command), so a command written before the start would be skipped with it (6 October 2026, 17:55:25Z
|
||||
/// on PC 1: the first request of the first tune).
|
||||
pub fn ensure_running(dir: &Path, wait: Duration) -> Result<(), String> {
|
||||
if alive(dir) {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
start()?;
|
||||
let until = Instant::now() + wait;
|
||||
while Instant::now() < until {
|
||||
std::thread::sleep(Duration::from_millis(250));
|
||||
if alive(dir) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(format!("the Igneum Power Helper task gave no heartbeat within {} s of its start", wait.as_secs()))
|
||||
}
|
||||
|
||||
/// One parsed command from cmd.txt.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
|
|
@ -79,6 +119,13 @@ pub fn parse_line(line: &str) -> Option<(u64, HelperCmd)> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The commands a helper acts on: the lines added after its start (`skip` = the line count at the start, 0 again
|
||||
/// when the file shrank). A stale `quit` or `remove` from an earlier engine is never a command.
|
||||
pub fn commands_after(text: &str, skip: usize) -> Vec<(u64, HelperCmd)> {
|
||||
let skip = if text.lines().count() < skip { 0 } else { skip };
|
||||
text.lines().skip(skip).filter_map(parse_line).collect()
|
||||
}
|
||||
|
||||
/// The nvidia-smi arguments a command becomes (None for the verbs that run nothing).
|
||||
pub fn smi_args(dev: &str, c: &HelperCmd) -> Option<Vec<String>> {
|
||||
match c {
|
||||
|
|
@ -195,20 +242,30 @@ pub fn run_helper(dir: &Path) -> i32 {
|
|||
}
|
||||
};
|
||||
log("helper started (scheduled task, elevated)");
|
||||
// a stale file from an earlier run is not a command: only lines after the start count
|
||||
let mut last_seq: u64 = std::fs::read_to_string(&cmd_file).ok().and_then(|t| t.lines().filter_map(parse_line).map(|(s, _)| s).max()).unwrap_or(0);
|
||||
let mut last_text = String::new();
|
||||
beat(dir, crate::platform::unix_now());
|
||||
// a stale file from an earlier run is not a command: only lines ADDED after the start count. 6 October 2026,
|
||||
// PC 1 17:55:55Z: a helper that started after an engine had written `quit` read that line and exited in the
|
||||
// same second, and every later start did the same; so the lines present at the start are skipped whole
|
||||
// (quit and remove included), and a file that shrinks starts the count again
|
||||
let initial = std::fs::read_to_string(&cmd_file).unwrap_or_default();
|
||||
let mut last_seq: u64 = initial.lines().filter_map(parse_line).map(|(s, _)| s).max().unwrap_or(0);
|
||||
let mut skip = initial.lines().count();
|
||||
let mut last_text = initial;
|
||||
let mut dev = "0".to_string();
|
||||
let mut idle = Instant::now();
|
||||
let smi = crate::platform::tool("nvidia-smi");
|
||||
loop {
|
||||
let text = std::fs::read_to_string(&cmd_file).unwrap_or_default();
|
||||
if text != last_text {
|
||||
if text.lines().count() < skip {
|
||||
skip = 0;
|
||||
}
|
||||
last_text = text.clone();
|
||||
for (seq, c) in text.lines().filter_map(parse_line) {
|
||||
for (seq, c) in commands_after(&text, skip) {
|
||||
match c {
|
||||
HelperCmd::Quit => {
|
||||
log("quit");
|
||||
let _ = std::fs::remove_file(dir.join(ALIVE_FILE));
|
||||
return 0;
|
||||
}
|
||||
HelperCmd::Remove => {
|
||||
|
|
@ -261,8 +318,10 @@ pub fn run_helper(dir: &Path) -> i32 {
|
|||
}
|
||||
if idle.elapsed() >= Duration::from_secs(IDLE_S) {
|
||||
log("idle 20 min: exit (the engine starts the task again when it needs it)");
|
||||
let _ = std::fs::remove_file(dir.join(ALIVE_FILE));
|
||||
return 0;
|
||||
}
|
||||
beat(dir, crate::platform::unix_now());
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
|
|
@ -296,6 +355,15 @@ mod tests {
|
|||
assert_eq!(parse_line("quit"), Some((0, HelperCmd::Quit)));
|
||||
assert_eq!(parse_line("remove"), Some((0, HelperCmd::Remove)));
|
||||
assert_eq!(parse_line("12 reregister"), Some((12, HelperCmd::Reregister)));
|
||||
// a stale quit present at the start is skipped; a quit added later counts; a rewritten (shorter) file counts whole
|
||||
assert_eq!(commands_after("5 dev 0\n6 pl 460\nquit\n", 3), vec![]);
|
||||
assert_eq!(commands_after("5 dev 0\n6 pl 460\nquit\n7 dev 1\n", 3), vec![(7, HelperCmd::Dev("1".into()))]);
|
||||
assert_eq!(commands_after("quit\n", 3), vec![(0, HelperCmd::Quit)]);
|
||||
// the heartbeat: fresh within ALIVE_MAX_S, dead after, dead when unreadable
|
||||
assert!(alive_at("1791309325", 1791309325 + ALIVE_MAX_S));
|
||||
assert!(!alive_at("1791309325", 1791309325 + ALIVE_MAX_S + 1));
|
||||
assert!(!alive_at("", 1791309325));
|
||||
assert!(!alive_at("soon", 1791309325));
|
||||
assert_eq!(parse_line("12 reregister C:\\evil.exe"), None, "no path argument: the helper picks the install folder itself");
|
||||
assert_eq!(smi_args("0", &HelperCmd::Reregister), None);
|
||||
assert!(readback_command().contains("Actions[0].Execute"));
|
||||
|
|
|
|||
|
|
@ -135,6 +135,22 @@ struct Tools {
|
|||
/// The node's re-derivation boundary (0.3.14, 6 October 2026): the chain block its EVM restarted at after the
|
||||
/// bodies below the pruning point were gone (`igneum_getExecStatus.restartNumber`; on a 0.3.13 node the
|
||||
/// `startedFrom` text "restart at chain block N"), else 0. The prover never claims work below it (no bodies, no
|
||||
/// The exporter's failure line. When the node's export carries the account dump (0.3.14) and the exporter's output
|
||||
/// never mentions it, the exporter at `path` predates 0.3.14: it replays the dump's own segment from the restart
|
||||
/// state and fails there with "port state root differs" (PC 1, 6 October 2026 18:16Z, block 140,662: the installer
|
||||
/// had not replaced igneum-prove-export); the line names the stale binary instead of the misleading root error.
|
||||
fn exporter_failure(export_has_dump: bool, out: &str, path: &Path, block: Option<u64>) -> String {
|
||||
let last = out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed");
|
||||
let what = match block {
|
||||
Some(b) => format!("exporter, block {b}"),
|
||||
None => "exporter".to_string(),
|
||||
};
|
||||
if export_has_dump && !out.contains("account dump") {
|
||||
return format!("{what}: stale igneum-prove-export at {}: the node's export carries the account dump and this exporter does not read it (it predates 0.3.14); install this release's exporter there ({last})", path.display());
|
||||
}
|
||||
format!("{what}: {last}")
|
||||
}
|
||||
|
||||
/// state: unprovable on every node).
|
||||
fn exec_boundary(shared: &Shared) -> u64 {
|
||||
let st = match evm_rpc(shared, "igneum_getExecStatus", json!([]), Duration::from_secs(5)) {
|
||||
|
|
@ -665,7 +681,7 @@ fn loop_forever(shared: Arc<Shared>, bin_dir: PathBuf) {
|
|||
let (seq_p, fix_p, res_p) = if t.wsl { (wsl_path(&seq), wsl_path(&fixture), wsl_path(&results)) } else { (seq.display().to_string(), fixture.display().to_string(), results.display().to_string()) };
|
||||
let (ok, out) = run_tool(&shared, t, &t.export, &[seq_p, w.number.to_string(), fix_p.clone()], &[], Duration::from_secs(600), &dir.join(format!("export-{}.log", w.number)));
|
||||
if !ok || !fixture.exists() {
|
||||
return Err(format!("exporter: {}", out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed")));
|
||||
return Err(exporter_failure(export["preState"].is_array(), &out, &t.export, None));
|
||||
}
|
||||
set(&shared, |p| p.message = if t.cuda { "proving on the GPU".into() } else { "CPU prover: about five minutes a shard, 30 GB of RAM, paid only when no card proves first".into() });
|
||||
let prover_env = if t.cuda { "cuda" } else { "cpu" };
|
||||
|
|
@ -854,7 +870,7 @@ fn prove_segment(shared: &Shared, t: &Tools, seg: &crate::segments::SegmentWork,
|
|||
let fixture = dir.join(format!("block-{b}.json"));
|
||||
let (ok, out) = run_tool(shared, t, &t.export, &[as_host_path(&seq), b.to_string(), as_host_path(&fixture)], &[], Duration::from_secs(600), &dir.join(format!("export-{b}.log")));
|
||||
if !ok || !fixture.exists() {
|
||||
return Err(format!("exporter, block {b}: {}", out.lines().rev().find(|l| !l.trim().is_empty()).unwrap_or("failed")));
|
||||
return Err(exporter_failure(export["preState"].is_array(), &out, &t.export, Some(b)));
|
||||
}
|
||||
fixtures.push(as_host_path(&fixture));
|
||||
}
|
||||
|
|
@ -1092,6 +1108,21 @@ pub fn setup(shared: &Shared) -> Result<Value, String> {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_stale_exporter_is_named_when_the_export_carries_the_dump() {
|
||||
let p = Path::new("C:/x/igneum-prove-export.exe");
|
||||
let old_out = "Error: segment 140661: port state root 0xe45c differs from the node's 0xddd7; the port is not the node's executor, stop\n";
|
||||
let line = exporter_failure(true, old_out, p, Some(140662));
|
||||
assert!(line.contains("stale igneum-prove-export at C:/x/igneum-prove-export.exe"), "{line}");
|
||||
assert!(line.starts_with("exporter, block 140662:"));
|
||||
// the 0.3.14 exporter read the dump and failed later: the real line, not the stale one
|
||||
let new_out = "account dump: 79 accounts after chain block 140661, state root 0x1 (equals the node's)\nError: segment 140662: port state root 0x2 differs from the node's 0x3\n";
|
||||
let line = exporter_failure(true, new_out, p, None);
|
||||
assert_eq!(line, "exporter: Error: segment 140662: port state root 0x2 differs from the node's 0x3");
|
||||
// an export without a dump (an older node): never the stale line
|
||||
assert_eq!(exporter_failure(false, old_out, p, None), format!("exporter: {}", old_out.trim()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pinned_ids_come_out_of_the_hosts_id_line() {
|
||||
let out = "RESULT id: pinned guests: shard program id 0x2b1a81cb413236cf063077b46ed3111628f6c41036bcf6e23ee4cbbf5679ef7a (2832504 bytes, sha256 0x150f4c05a2951fc5) aggregator id 0x474678f35f7545db28055d5e5bbc308231d84a5a072202087a2a8d5b09123896 (319744 bytes), pinned 2026-10-05T16:20:38Z on Darwin, SP1 5.0 circuit v5\n";
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ const INDEX: &str = include_str!("../ui/index.html");
|
|||
const CSS: &str = include_str!("../ui/app.css");
|
||||
const JS: &str = include_str!("../ui/app.js");
|
||||
const MARK: &str = include_str!("../ui/mark.svg");
|
||||
const DAG_JS: &str = include_str!("../ui/live-dag.js");
|
||||
const COIN: &[u8] = include_bytes!("../../../brand/igneum-coin-1024.png");
|
||||
const FONT_MONO_400: &[u8] = include_bytes!("../ui/fonts/IBMPlexMono-400.woff2");
|
||||
const FONT_MONO_500: &[u8] = include_bytes!("../ui/fonts/IBMPlexMono-500.woff2");
|
||||
|
|
@ -165,6 +166,7 @@ fn handle(mut stream: TcpStream, shared: Arc<Shared>) {
|
|||
("GET", "/app.css") => respond(&mut stream, 200, "text/css; charset=utf-8", CSS.as_bytes(), false),
|
||||
("GET", "/app.js") => respond(&mut stream, 200, "application/javascript; charset=utf-8", JS.as_bytes(), false),
|
||||
("GET", "/mark.svg") => respond(&mut stream, 200, "image/svg+xml", MARK.as_bytes(), true),
|
||||
("GET", "/live-dag.js") => respond(&mut stream, 200, "application/javascript; charset=utf-8", DAG_JS.as_bytes(), false),
|
||||
("GET", "/coin.png") => respond(&mut stream, 200, "image/png", COIN, true),
|
||||
("GET", "/fonts/IBMPlexMono-400.woff2") => respond(&mut stream, 200, "font/woff2", FONT_MONO_400, true),
|
||||
("GET", "/fonts/IBMPlexMono-500.woff2") => respond(&mut stream, 200, "font/woff2", FONT_MONO_500, true),
|
||||
|
|
@ -178,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);
|
||||
|
|
@ -256,6 +265,14 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
|
|||
let goal = body.get("goal").and_then(|v| v.as_str()).map(|g| crate::ember::Goal::parse(g).name().to_string());
|
||||
let price = body.get("price_pence").and_then(|v| v.as_f64()).filter(|p| (0.0..=500.0).contains(p));
|
||||
let climb = body.get("climb").and_then(|v| v.as_bool());
|
||||
// with "key": that card's goal override ("" or "global" clears it); without: the global goal
|
||||
let key = body.get("key").and_then(|v| v.as_str()).map(|k| k.to_string());
|
||||
if let Some(k) = key {
|
||||
let g = body.get("goal").and_then(|v| v.as_str()).unwrap_or("");
|
||||
let g = if g.is_empty() || g == "global" { String::new() } else { crate::ember::Goal::parse(g).name().to_string() };
|
||||
shared.send(Cmd::TuneCardGoal(k, g));
|
||||
return Ok(json!({ "ok": true }));
|
||||
}
|
||||
shared.send(Cmd::TuneGoal(goal, price, climb));
|
||||
Ok(json!({ "ok": true }))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -126,6 +126,9 @@ pub struct CardState {
|
|||
pub tune_clock_mhz: u32, // the clock cap the last tune chose (0 = unlocked)
|
||||
pub tune_source: String, // full | confirm | baseline
|
||||
pub tune_line: String, // "Tuned: 122.3 MH/s at 290 W (0.422 MH/W)" once tuned
|
||||
pub tune_goal: String, // this card's goal override (efficiency | balanced | rate); "" = the global goal
|
||||
pub tune_before_watts: f64, // the untuned point of the last full plan (step 0); 0 = never measured
|
||||
pub tune_before_mhs: f64,
|
||||
// a tune in progress on this card, by this engine or by a measurement engine posting /api/tune-progress
|
||||
// (the project lead, 6 October 2026: "don't we need to show in the app that tuning is in progress?")
|
||||
pub tune_step: u32,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ input,textarea{font-variant-numeric:tabular-nums}
|
|||
.btn.tiny{min-height:26px;padding:2px 10px;font-size:var(--t-sm);border-radius:7px;font-family:var(--mono);font-weight:500}
|
||||
.btn.ghost{border-color:transparent;color:var(--ink-2)}
|
||||
.btn.ghost:hover{border-color:var(--line-2);color:var(--bone)}
|
||||
.btn.ghost.on{color:var(--molten);border-color:var(--molten-40);background:var(--molten-10)}
|
||||
.btn.ghost.on{color:var(--molten);border-color:var(--molten-40)}
|
||||
.btn.danger:hover{color:var(--ember);border-color:var(--ember)}
|
||||
:focus-visible{outline:2px solid var(--ember);outline-offset:3px;border-radius:6px}
|
||||
@media (prefers-reduced-motion:reduce){.btn{transition:none}}
|
||||
|
|
@ -77,7 +77,7 @@ body.mac .rail-brand{padding-top:30px}
|
|||
.nav svg{width:19px;height:19px;flex:0 0 19px;fill:none;stroke:currentColor;stroke-width:1.9;stroke-linecap:round;stroke-linejoin:round;color:var(--ash);transition:color .15s ease}
|
||||
.nav:hover{background:var(--hover);color:var(--bone)}
|
||||
.nav:hover svg{color:var(--ink-2)}
|
||||
.nav.on{background:var(--ember-12);border-color:var(--ember-40);color:var(--bone);font-weight:600}
|
||||
.nav.on{background:var(--hover);border-color:var(--line);color:var(--bone);font-weight:600}
|
||||
.nav.on svg{color:var(--ember)}
|
||||
.nav.on::before{content:"";position:absolute;left:-13px;top:10px;bottom:10px;width:3px;border-radius:0 3px 3px 0;background:var(--ember)}
|
||||
.nav.small{min-height:36px;font-size:var(--t-base);color:var(--ash)}
|
||||
|
|
@ -99,13 +99,15 @@ body.has-rail .top{left:var(--rail)}
|
|||
.brand{display:flex;align-items:center;gap:10px;min-width:0}
|
||||
.brand .word{font-family:var(--head);font-weight:900;font-size:20px;letter-spacing:.06em}
|
||||
.brand .miner{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.22em;color:var(--ash);margin-left:var(--s-1);padding-top:3px}
|
||||
body.has-rail .top{align-items:baseline;padding-top:19px}
|
||||
.page-title{display:flex;align-items:baseline;gap:12px;min-width:0}
|
||||
.page-title h1{font-size:19px;font-weight:700;letter-spacing:0;white-space:nowrap}
|
||||
.page-sub{font-size:var(--t-base);color:var(--ash);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
|
||||
.top-right{display:flex;align-items:center;gap:var(--s-3);flex:0 0 auto;min-width:0}
|
||||
.pill{display:inline-flex;align-items:center;gap:var(--s-2);font-family:var(--mono);font-size:var(--t-sm);letter-spacing:.08em;text-transform:uppercase;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:6px 12px 6px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;min-width:112px;justify-content:center}
|
||||
.pill{display:inline-flex;align-items:baseline;gap:var(--s-2);font-family:var(--sans);font-size:var(--t-base);font-weight:600;color:var(--ash);border:1px solid var(--line);border-radius:999px;padding:5px 12px 5px 10px;background:var(--graphite);white-space:nowrap;font-variant-numeric:tabular-nums;justify-content:center;line-height:1.3}
|
||||
.pill .dot{position:relative;top:-1px}
|
||||
.pill.on{color:var(--molten);border-color:var(--molten-40)}
|
||||
.pill.warn{color:var(--ember)}
|
||||
.pill.warn{color:var(--ember);border-color:var(--ember-40)}
|
||||
.dot{width:8px;height:8px;border-radius:50%;background:var(--ash);display:inline-block;flex:0 0 8px}
|
||||
.on .dot,.dot.live{background:var(--molten);animation:pulse 2s ease-in-out infinite}
|
||||
.warn .dot,.dot.bad{background:var(--ember);animation:none}
|
||||
|
|
@ -116,26 +118,30 @@ body.has-rail .top{left:var(--rail)}
|
|||
top moves once per change with a 150 ms transition (layoutStrip), never per poll. */
|
||||
.notices{position:fixed;top:var(--top);left:0;right:0;z-index:19}
|
||||
body.has-rail .notices{left:var(--rail)}
|
||||
.notice{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-4);padding:8px calc(var(--gutter) - 6px) 8px var(--gutter);background:var(--molten-10);border-bottom:1px solid var(--molten-40);font-size:var(--t-base);line-height:1.4;color:var(--bone)}
|
||||
.notice.bad{background:var(--ember-12);border-bottom-color:var(--ember-40)}
|
||||
.notice.warn{background:var(--molten-10);border-bottom-color:var(--molten-40)}
|
||||
.notice.update-urgent{background:rgba(242,84,27,.55);border-bottom-color:var(--ember);color:#fff;font-weight:600}
|
||||
.notice{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-2) var(--s-3);padding:9px calc(var(--gutter) - 6px) 9px var(--gutter);background:var(--row);border-bottom:1px solid var(--line);box-shadow:inset 0 1px 0 var(--ember);font-size:var(--t-base);line-height:1.4;color:var(--ash)}
|
||||
.notice-dot{width:7px;height:7px;border-radius:50%;background:var(--ember);flex:0 0 7px;display:inline-block}
|
||||
.notice.bad .notice-text,.notice.update-urgent .notice-text{color:var(--bone)}
|
||||
.notice.update-urgent .notice-text{font-weight:600}
|
||||
.notice .notice-text b{color:var(--bone);font-weight:600}
|
||||
.notice-text{flex:1 1 320px;min-width:0}
|
||||
.notice-actions{display:flex;align-items:center;gap:var(--s-2);flex:0 0 auto}
|
||||
.notice-actions:empty{display:none}
|
||||
.notice-close{flex:0 0 auto;width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;color:var(--ash);font-size:20px;line-height:1;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}
|
||||
.notice-close:hover{color:var(--bone);border-color:var(--line-2)}
|
||||
.notice.update-urgent .notice-close{color:#fff}
|
||||
.notice-detail{flex-basis:100%;font-size:var(--t-sm);color:var(--ink-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;margin-top:-3px}
|
||||
.notice .prog{flex-basis:100%;height:3px;background:rgba(127,127,127,.2);border-radius:2px;overflow:hidden;margin-top:-3px}
|
||||
.notice-more{flex:0 0 auto;width:30px;height:30px;border-radius:8px;border:1px solid transparent;background:transparent;color:var(--ash);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;padding:0}
|
||||
.notice-more:hover{color:var(--bone);border-color:var(--line-2)}
|
||||
.notice-more .chev{width:8px;height:8px;transform:rotate(45deg) translateY(-2px)}
|
||||
.notice-more.open .chev{transform:rotate(225deg) translateY(-2px)}
|
||||
.notice-detail{flex-basis:100%;font-family:var(--mono);font-size:var(--t-sm);color:var(--ink-2);white-space:normal;word-break:break-word;margin-top:-2px;user-select:text;-webkit-user-select:text}
|
||||
.notice .prog{flex-basis:100%;height:2px;background:var(--line);border-radius:2px;overflow:hidden;margin-top:-3px}
|
||||
.notice .prog i{display:block;height:100%;width:0;background:var(--ember);transition:width .5s linear}
|
||||
|
||||
/* a question in place of a dialog: the quit strip over the page, the inline asks inside a card */
|
||||
.ask-wrap{position:fixed;left:0;right:0;bottom:0;z-index:36;display:flex;justify-content:center;padding:0 var(--s-4) var(--s-4);pointer-events:none}
|
||||
body.has-rail .ask-wrap{left:var(--rail)}
|
||||
.ask{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;background:var(--graphite);border:1px solid var(--ember-40);border-radius:14px;padding:12px 16px;box-shadow:0 14px 40px var(--shadow);pointer-events:auto;max-width:720px;animation:rise .2s ease}
|
||||
.ask{display:flex;align-items:center;gap:var(--s-3);flex-wrap:wrap;background:var(--row);border:1px solid var(--line);border-radius:14px;padding:12px 16px;box-shadow:inset 0 1px 0 var(--ember),0 14px 40px var(--shadow);pointer-events:auto;max-width:720px;animation:rise .2s ease}
|
||||
.ask-text{flex:1 1 260px;font-size:var(--t-md);min-width:0}
|
||||
.ask.inline{box-shadow:none;background:var(--ember-12);margin-top:var(--s-3);animation:none}
|
||||
.ask.inline{box-shadow:inset 0 1px 0 var(--ember);background:var(--row);margin-top:var(--s-3);animation:none}
|
||||
|
||||
/* screens and pages */
|
||||
main{position:absolute;top:var(--top);bottom:var(--bottom);left:0;right:0;overflow:auto;padding:0 var(--gutter);overscroll-behavior:contain;transition:top .15s ease}
|
||||
|
|
@ -214,7 +220,7 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.toggle-big .ts{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.06em;opacity:.8;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:100%}
|
||||
.toggle-big.stop{background:var(--graphite);border-color:var(--line-2);color:var(--bone)}
|
||||
.toggle-big.stop:hover{border-color:var(--ash);background:var(--row)}
|
||||
.toggle-big.stop .ring{background:var(--ember-12);color:var(--ember)}
|
||||
.toggle-big.stop .ring{background:var(--obsidian);border:1px solid var(--line-2);color:var(--ember)}
|
||||
.toggle-big.stop .ts{color:var(--molten);opacity:1}
|
||||
.totals{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--s-3);background:var(--graphite);border:1px solid var(--line);border-radius:14px;padding:18px 20px;min-width:0}
|
||||
.tot{display:flex;flex-direction:column;min-width:0;padding-left:var(--s-4);border-left:1px solid var(--line)}
|
||||
|
|
@ -246,9 +252,10 @@ body[data-phase="welcome"] #screen-welcome,body[data-phase="cards"] #screen-card
|
|||
.feed>div:last-child{border-bottom:0}
|
||||
.feed>div>span:first-child{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.feed .t{color:var(--ash);flex:0 0 auto;font-size:var(--t-xs)}
|
||||
.feed .k{color:var(--molten);margin-right:6px}
|
||||
.feed .k.error,.feed .k.warn,.feed .k.block{color:var(--ember)}
|
||||
.feed .k.build,.feed .k.info{color:var(--ash)}
|
||||
.feed .k{display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--molten);margin-right:10px;position:relative;top:-1px}
|
||||
.feed .k.error,.feed .k.warn,.feed .k.block{background:var(--ember)}
|
||||
.feed .k.build,.feed .k.info{background:var(--line-2)}
|
||||
.feed .k.proving{background:var(--nvidia)}
|
||||
|
||||
/* the card rows on Mine (the hero object): badge, name and state, the numbers, Tune, the switch, the chevron;
|
||||
the tune line under; the details under that */
|
||||
|
|
@ -349,8 +356,8 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
#s-jobs-history td{padding:6px 6px 6px 0;border-top:1px solid var(--line);vertical-align:top}
|
||||
#s-jobs-history tr.failed td,#s-jobs-history tr.timeout td,#s-jobs-history tr.aborted td{color:var(--ember)}
|
||||
#s-jobs-history tr.running td{color:var(--molten)}
|
||||
.clock-card{border:1px solid var(--ember-40);background:var(--ember-12);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.clock-card.warn{border-color:var(--molten-40);background:var(--molten-10)}
|
||||
.clock-card{border:1px solid var(--line);background:var(--row);box-shadow:inset 0 1px 0 var(--ember);border-radius:12px;padding:12px 14px;display:flex;flex-direction:column;gap:var(--s-2)}
|
||||
.clock-card.warn{box-shadow:inset 0 1px 0 var(--molten)}
|
||||
.clock-msg{font-size:var(--t-md);color:var(--bone);line-height:1.45}
|
||||
.clock-card .note{margin-top:0}
|
||||
|
||||
|
|
@ -372,7 +379,7 @@ table{border-collapse:collapse;width:100%;font-size:var(--t-base)}
|
|||
.options{display:flex;flex-direction:column;gap:var(--s-3);margin-top:6px}
|
||||
.option{display:flex;gap:var(--s-4);align-items:flex-start;background:var(--graphite);border:1px solid var(--line);border-radius:var(--card-r);padding:20px 22px;cursor:pointer;transition:border-color .15s ease,background .15s ease}
|
||||
.option:hover{border-color:var(--line-2)}
|
||||
.option.on{border-color:var(--ember);background:var(--ember-12)}
|
||||
.option.on{border-color:var(--ember);background:var(--row)}
|
||||
.option input[type=radio]{position:absolute;opacity:0;width:0;height:0}
|
||||
.option .radio{width:20px;height:20px;border-radius:50%;border:2px solid var(--line-2);flex:0 0 20px;margin-top:2px;position:relative}
|
||||
.option.on .radio{border-color:var(--ember)}
|
||||
|
|
@ -506,8 +513,8 @@ body.drawer-drag main{pointer-events:none}
|
|||
.log .src.app{color:var(--ember)}
|
||||
.log .src.watch{color:var(--ash)}
|
||||
.log .e,.log .e .src{color:var(--ember)}
|
||||
.log .ln.hit{background:rgba(255,179,92,.18);box-shadow:inset 3px 0 0 var(--molten);margin:0 calc(-1 * var(--gutter));padding:0 var(--gutter)}
|
||||
.log mark{background:rgba(255,179,92,.3);color:var(--bone);border-radius:2px;padding:0 1px}
|
||||
.log .ln.hit{background:var(--hover);box-shadow:inset 3px 0 0 var(--molten);margin:0 calc(-1 * var(--gutter));padding:0 var(--gutter)}
|
||||
.log mark{background:transparent;color:var(--bone);text-decoration:underline;text-decoration-color:var(--ember);text-underline-offset:2px}
|
||||
.log-empty{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;color:var(--ash);font-size:var(--t-sm);padding:0 var(--gutter);text-align:center}
|
||||
.log-jump{position:absolute;right:calc(var(--gutter) + 14px);bottom:14px;background:var(--graphite);border-color:var(--molten-40);color:var(--molten);box-shadow:0 8px 24px var(--shadow);z-index:2;gap:6px;padding:6px 12px}
|
||||
.log-jump:hover{border-color:var(--molten)}
|
||||
|
|
@ -590,6 +597,8 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)}
|
|||
.step{padding:32px 0}
|
||||
.money-row .v{font-size:var(--t-num)}
|
||||
.drawer-head{padding-left:var(--s-4);padding-right:var(--s-4)}
|
||||
.notice{padding-left:var(--s-4);padding-right:var(--s-2);gap:var(--s-2)}
|
||||
.notice-text{flex-basis:160px}
|
||||
.log,.log-ruler .t0,.log-ruler .t1{padding-left:var(--s-4);padding-right:var(--s-4)}
|
||||
}
|
||||
/* short windows (the 600 px floor): tighter paddings, a smaller canvas, so the drawer always has room */
|
||||
|
|
@ -605,3 +614,81 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)}
|
|||
.drawer-head{padding-bottom:6px}
|
||||
.toggle-big{min-height:96px}
|
||||
}
|
||||
|
||||
/* miner-ui-4 (6 October 2026): Overview and Cards. The Overview hero (the project lead's pick, C): the fleet rate at 84 px on a
|
||||
graphite-to-obsidian fade with W, £ a day and blocks beside it, Start/Stop as the full-width bar under it. */
|
||||
.hero-row{grid-template-columns:1fr}
|
||||
.totals{order:1;grid-template-columns:2fr 1fr 1fr 1fr;align-items:end;padding:28px 28px 24px;border-color:transparent;background:linear-gradient(180deg,var(--graphite),var(--obsidian))}
|
||||
.tot{border-left:0;padding-left:0}
|
||||
.tot:first-child .v{font-size:84px;line-height:.95;letter-spacing:-.03em}
|
||||
.tot:first-child .k{font-size:var(--t-sm);margin-top:10px}
|
||||
.tot:first-child .s{font-size:var(--t-md)}
|
||||
.tot .v{font-size:30px}
|
||||
.tot .v.ask-price{font-size:var(--t-md);padding:9px 0 8px}
|
||||
.toggle-big{order:2;min-height:64px;flex-direction:row;align-items:center;gap:14px;padding:12px 20px}
|
||||
.toggle-big .ring{margin:0}
|
||||
.toggle-big .ts{margin-left:auto}
|
||||
/* the chain scene: the site's live-dag.js in a 220 px box, the legend words under it, the blocks strip as the fallback */
|
||||
.dag-wrap{position:relative}
|
||||
#dag-live{display:block;width:100%;height:220px;border-radius:10px;background:var(--obsidian);border:1px solid var(--line)}
|
||||
#dag{height:120px;margin-bottom:0}
|
||||
.dag-tip{position:absolute;left:0;top:0;pointer-events:none;background:var(--graphite);border:1px solid var(--line-2);border-radius:8px;padding:8px 10px;font-size:var(--t-xs);color:var(--ink-2);display:flex;flex-direction:column;gap:2px;max-width:320px;box-shadow:0 8px 24px var(--shadow);z-index:3}
|
||||
.dag-tip b{color:var(--bone);font-weight:500;word-break:break-all}
|
||||
.dag-chip{position:absolute;right:10px;top:10px;font-size:var(--t-xs);color:var(--ash);background:var(--graphite);border:1px solid var(--line-2);border-radius:999px;padding:3px 10px;pointer-events:none;z-index:2}
|
||||
.legend{display:flex;flex-wrap:wrap;gap:8px 16px;margin-top:var(--s-3);font-size:var(--t-sm);color:var(--ash);align-items:center}
|
||||
.legend span{display:inline-flex;align-items:center;gap:7px}
|
||||
.lg{width:10px;height:10px;border-radius:3px;display:inline-block;border:1.5px solid var(--ash);background:var(--row)}
|
||||
.lg.you{border-color:var(--molten);box-shadow:0 0 0 2px var(--molten-40)}
|
||||
.lg.pending{border-color:var(--ash)}
|
||||
.lg.included{border-color:var(--ember)}
|
||||
.lg.excluded{border-color:var(--ash);opacity:.45}
|
||||
.lg.proven{background:var(--ember);border-color:var(--ember)}
|
||||
.lg.locked{border-color:var(--ember);border-radius:50%;box-shadow:0 0 0 2px var(--ember-40)}
|
||||
/* Cards: the Tuning strip above the list */
|
||||
.tune-strip{padding:var(--s-4) var(--card-pad)}
|
||||
.ts-row{display:flex;align-items:center;gap:var(--s-4);flex-wrap:wrap}
|
||||
.ts-row .goal-line{flex:0 1 auto}
|
||||
.ts-saving{font-size:var(--t-base);color:var(--molten);font-weight:500;margin-left:auto}
|
||||
.ts-saving:empty{display:none}
|
||||
.tune-strip .line-text{margin-top:var(--s-2);font-size:var(--t-base);color:var(--ash)}
|
||||
.tune-strip .switch{padding-bottom:0}
|
||||
/* the Ember layer on a row: the flame mark, the saving, the sparkline, the curve */
|
||||
.gl-tune{align-items:center}
|
||||
.flame{flex:0 0 16px;display:inline-block}
|
||||
.flame .fo{fill:none;stroke:var(--ash);stroke-width:7}
|
||||
.flame .ff{fill:var(--ember)}
|
||||
.flame.running .ff{fill:var(--molten)}
|
||||
.flame.measured .ff{fill:var(--ash)}
|
||||
.flame.idle .ff,.flame.paused .ff,.flame.stopped .ff{fill:none}
|
||||
.gl-tune.tuned .t{color:var(--ink-2)}
|
||||
.gl-tune .save,.gl-details .save{color:var(--molten);white-space:nowrap}
|
||||
.num .k.spark{display:inline-flex;align-items:center;gap:6px}
|
||||
.spark{display:inline-block;vertical-align:middle}
|
||||
.spark path{fill:none;stroke:var(--ash);stroke-width:1.2}
|
||||
.spark circle{fill:var(--ember)}
|
||||
.curve-wrap{display:flex;flex-direction:column;gap:6px;max-width:420px}
|
||||
.curve-svg{width:100%;height:auto;background:var(--obsidian);border:1px solid var(--line);border-radius:10px}
|
||||
.curve-svg .ln{fill:none;stroke:var(--ash);stroke-width:1.2}
|
||||
.curve-svg circle{fill:var(--ember)}
|
||||
.curve-svg circle.chosen{fill:none;stroke:var(--ember);stroke-width:2}
|
||||
.curve-svg circle.marked{fill:none;stroke:var(--ash);stroke-width:1.2}
|
||||
.curve-svg text{font-family:var(--mono);font-size:9px;fill:var(--ash)}
|
||||
.seg.small .seg-b{padding:4px 10px;font-size:var(--t-sm)}
|
||||
.gl-details .ctl .seg{justify-self:start}
|
||||
@media (max-width:1180px){
|
||||
.totals{grid-template-columns:1fr 1fr 1fr 1fr}
|
||||
.tot:first-child .v{font-size:64px}
|
||||
}
|
||||
@media (max-width:860px){
|
||||
.totals{grid-template-columns:1fr 1fr;row-gap:var(--s-4)}
|
||||
.tot:first-child{grid-column:1 / -1}
|
||||
.tot:nth-child(3){padding-left:0}
|
||||
}
|
||||
@media (max-width:720px){
|
||||
.tot:first-child .v{font-size:56px}
|
||||
.toggle-big .ts{display:none}
|
||||
#dag-live{height:170px}
|
||||
.ts-saving{margin-left:0}
|
||||
.legend{gap:6px 12px}
|
||||
.rail-foot .nav.small{padding:6px 4px}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,12 +49,27 @@ var Notices = (function () {
|
|||
}
|
||||
if (u.updated_from && s.uptime_s >= 0 && s.uptime_s < INSTALLED_S) {
|
||||
var now = s.version || ver;
|
||||
return notice('update-installed', 'update:installed:' + now, 'Updated to Igneum Miner ' + now + ' from ' + u.updated_from + '.');
|
||||
var at = s.now > 0 && s.uptime_s >= 0 ? ' at ' + hhmm(s.now - s.uptime_s) : '';
|
||||
return notice('update-installed', 'update:installed:' + now, 'Updated to ' + now + at + '.', { title: 'Igneum Miner ' + now + ', from ' + u.updated_from });
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function jobTitle(j) { return j.title && j.title !== j.kind ? j.title : (j.kind === 'shard-benchmark' ? 'shard benchmark' : j.kind === 'build' ? 'build (mining continues)' : j.kind); }
|
||||
// the job in a user's words (miner-ui-3 polish, 6 October 2026): from its kind, else from its id's prefix
|
||||
function jobWord(kind, id) {
|
||||
var k = String(kind || ''), i = String(id || '');
|
||||
var t = k || i;
|
||||
if (/^update-now/.test(t) || /^update-now/.test(i)) return 'update check';
|
||||
if (/^restart/.test(t) || /^restart/.test(i)) return 'restart';
|
||||
if (/^collect/.test(t) || /^collect/.test(i)) return 'log collection';
|
||||
if (/shard-benchmark|^shard/.test(t) || /shard/.test(i)) return 'shard benchmark';
|
||||
if (/^build/.test(t) || /^build/.test(i)) return 'build';
|
||||
if (/ember|tune|sweep/.test(t) || /ember|tune|sweep/.test(i)) return 'tuning check';
|
||||
if (/script|playbook|run/.test(t)) return 'a script';
|
||||
return k || 'a task';
|
||||
}
|
||||
function hhmm(t) { var d = new Date(t * 1000); return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0'); }
|
||||
function firstErrorLine(results, summary) {
|
||||
for (var i = 0; i < results.length; i++) if (/BUILD FAILED|error|failed|panic/i.test(results[i])) return firstLine(results[i]);
|
||||
return firstLine(summary);
|
||||
|
|
@ -65,18 +80,20 @@ var Notices = (function () {
|
|||
if (j.active) {
|
||||
var m = Math.max(0, Math.floor((now - j.started_at) / 60));
|
||||
var results = (j.results || []).filter(function (x) { return x.indexOf('RESULT') === 0; });
|
||||
return notice('job-running', 'job:run:' + j.id, 'Job: ' + jobTitle(j) + ' running, ' + m + ' min.' + (j.stage ? ' ' + endDot(cap(j.stage)) : ''), { detail: results.length ? results[results.length - 1] : '', title: j.message || '' });
|
||||
var tech = [jobTitle(j), results.length ? results[results.length - 1] : ''].filter(Boolean).join(' · ');
|
||||
return notice('job-running', 'job:run:' + j.id, 'A job from the team is running: ' + jobWord(j.kind, j.id) + ', ' + m + ' min.' + (j.stage ? ' ' + endDot(cap(j.stage)) : ''), { detail: tech, title: j.message || '' });
|
||||
}
|
||||
var l = j.last;
|
||||
if (!l || !l.id) return null;
|
||||
var mins = Math.max(0, Math.round((l.finished_at - l.started_at) / 60)), t = l.title || l.kind;
|
||||
var report = l.uploaded ? 'Report uploaded.' : 'Report not uploaded.';
|
||||
var report = l.uploaded ? '' : ' The report did not upload.', word = jobWord(l.kind, l.id), tech = t + (l.summary ? ': ' + l.summary : '');
|
||||
if (l.status === 'done') {
|
||||
if (l.finished_at && now - l.finished_at > JOB_DONE_S) return null;
|
||||
return notice('job-done', 'job:done:' + l.id, 'Job: ' + t + ' done after ' + mins + ' min. ' + report, { title: l.summary || '' });
|
||||
return notice('job-done', 'job:done:' + l.id, 'A job from the team ran: ' + word + ', ' + mins + ' min.' + report, { detail: tech, title: l.summary || '' });
|
||||
}
|
||||
var word = l.status === 'timeout' ? 'hit its time cap' : l.status === 'aborted' ? 'was stopped' : 'failed';
|
||||
return notice('job-failed', 'job:failed:' + l.id, 'Job: ' + t + ' ' + word + ' after ' + mins + ' min, exit ' + l.exit + '. ' + report, { tone: 'bad', detail: firstErrorLine(j.last_results || [], l.summary), title: l.summary || '' });
|
||||
var verb = l.status === 'timeout' ? 'ran out of time' : l.status === 'aborted' ? 'was stopped' : 'failed';
|
||||
var cause = firstErrorLine(j.last_results || [], l.summary);
|
||||
return notice('job-failed', 'job:failed:' + l.id, 'A job from the team ' + verb + ': ' + word + ', after ' + mins + ' min.' + report, { tone: 'bad', detail: [tech, cause !== l.summary ? cause : ''].filter(Boolean).join(' · ') + ' · exit ' + l.exit, title: l.summary || '' });
|
||||
}
|
||||
|
||||
// c = state.clock: the engine's words, Sync clock, the manual hint underneath
|
||||
|
|
@ -136,7 +153,7 @@ var Notices = (function () {
|
|||
}
|
||||
return best;
|
||||
}
|
||||
return { tuneNotice: tuneNotice, LEVEL: LEVEL, JOB_DONE_S: JOB_DONE_S, INSTALLED_S: INSTALLED_S, CARD_S: CARD_S, updateNotice: updateNotice, jobNotice: jobNotice, clockNotice: clockNotice, cardNotices: cardNotices, gather: gather, pick: pick, jobTitle: jobTitle, cap: cap, endDot: endDot, kindWord: kindWord };
|
||||
return { tuneNotice: tuneNotice, jobWord: jobWord, hhmm: hhmm, LEVEL: LEVEL, JOB_DONE_S: JOB_DONE_S, INSTALLED_S: INSTALLED_S, CARD_S: CARD_S, updateNotice: updateNotice, jobNotice: jobNotice, clockNotice: clockNotice, cardNotices: cardNotices, gather: gather, pick: pick, jobTitle: jobTitle, cap: cap, endDot: endDot, kindWord: kindWord };
|
||||
})();
|
||||
|
||||
/* ---------- the update card (pure; update-card.test.mjs loads this block) ----------
|
||||
|
|
@ -249,9 +266,10 @@ var UpdateCard = (function () {
|
|||
var View = (function () {
|
||||
'use strict';
|
||||
var PAGES = [
|
||||
{ id: 'mine', title: 'Mine', sub: 'your cards, the rate, what it costs' },
|
||||
{ id: 'earnings', title: 'Earnings', sub: 'what it made, where it goes' },
|
||||
{ id: 'prove', title: 'Prove', sub: 'turn blocks into proofs, paid in IGN' },
|
||||
{ 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 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'];
|
||||
|
|
@ -293,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'));
|
||||
|
|
@ -379,9 +397,9 @@ var View = (function () {
|
|||
var when = cd.sweep_at > 0 && now ? ago(now - cd.sweep_at) : '';
|
||||
var nc = nextCheck(cd.sweep_at, st.tune_period_s, now);
|
||||
if (cd.tune_line) {
|
||||
var nums = String(cd.tune_line).replace(/^Tuned:\s*/, '');
|
||||
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) : '';
|
||||
|
|
@ -402,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;
|
||||
|
|
@ -427,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
|
||||
|
|
@ -472,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 shards 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 shard', 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 shard 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: 'verifying', trust: 'trusting without verifying (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) || '') };
|
||||
}
|
||||
|
|
@ -500,7 +519,7 @@ var View = (function () {
|
|||
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).';
|
||||
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) {
|
||||
|
|
@ -525,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(' ');
|
||||
}
|
||||
|
|
@ -539,10 +558,157 @@ var View = (function () {
|
|||
if (text.length > max) { var cut = text.lastIndexOf(' ', max - 2); text = text.slice(0, cut > 40 ? cut : max - 2) + '…'; }
|
||||
return text;
|
||||
}
|
||||
// the engine's event line in a user's voice (src/engine.rs event strings, 6 October 2026): {text, tech}; tech is
|
||||
// the engine's own line when it differs, for the tooltip. Unknown lines keep their words with a JSON tail cut.
|
||||
var EVENT_MAP = [
|
||||
[/^updated to Igneum Miner (\S+) from (\S+)/, function (m) { return 'Updated to ' + m[1]; }],
|
||||
[/^Igneum Miner (\S+) is available: downloading/, function (m) { return 'Downloading ' + m[1]; }],
|
||||
[/^Igneum Miner (\S+) is ready/, function (m) { return m[1] + ' is ready to install'; }],
|
||||
[/^installing Igneum Miner (\S+)/, function (m) { return 'Installing ' + m[1]; }],
|
||||
[/^update to (\S+) failed/, function (m) { return 'The update to ' + m[1] + ' failed'; }],
|
||||
[/^the update to (\S+) did not take/, function (m) { return 'The update to ' + m[1] + ' did not take'; }],
|
||||
[/^Igneum Miner (\S+) did not stay up twice; restoring (\S+)/, function (m) { return m[1] + ' did not stay up; going back to ' + m[2]; }],
|
||||
[/^update downloaded/, function () { return 'Update downloaded'; }],
|
||||
[/^OTA: administrator approval not given/, function () { return 'The update waits for the next time someone is at this PC'; }],
|
||||
[/^job (\S+) \((.+)\) starts: /, function (m) { return 'A job from the team started: ' + Notices.jobWord(m[2], m[1]); }],
|
||||
[/^job (\S+) \((.+)\) (done|failed|timeout|aborted) after (\d+) min/, function (m) { return 'A job from the team ' + (m[3] === 'done' ? 'ran' : m[3] === 'timeout' ? 'ran out of time' : m[3] === 'aborted' ? 'was stopped' : 'failed') + ': ' + Notices.jobWord('', m[1]) + ', ' + m[4] + ' min'; }],
|
||||
[/^(\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 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'; }],
|
||||
[/^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 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 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 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 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'; }],
|
||||
[/^(.+): 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'; }],
|
||||
[/^(.+): 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] + ': 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] + ' 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 card restarts on it'; }],
|
||||
[/^hourly program changed: /, function () { return 'New hourly program'; }],
|
||||
[/^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] + ' shards 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] + ' shards in ' + m[4] + ' s. Its record waits for the chain'; }],
|
||||
[/^segment (\S+)\.\.(\S+) record accepted on retry/, function () { return 'Segment record 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: 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'; }]
|
||||
];
|
||||
function plainEvent(text) {
|
||||
text = String(text || '');
|
||||
for (var i = 0; i < EVENT_MAP.length; i++) { var m = EVENT_MAP[i][0].exec(text); if (m) { var out = EVENT_MAP[i][1](m); return { text: out, tech: out === text ? '' : text }; } }
|
||||
var cut = eventLine(text);
|
||||
return { text: cap(cut), tech: cut === text ? '' : text };
|
||||
}
|
||||
// the pill in the top bar: a verb a user understands, and its tone (on = molten, bad = ember, '' = grey)
|
||||
function pill(s) {
|
||||
if (s.quitting) return { text: 'Stopping', tone: '' };
|
||||
if (!s.setup_done) return { text: s.detecting ? 'Detecting cards' : 'Setting up', tone: '' };
|
||||
var m = s.mining || {}, n = s.node || {};
|
||||
if (tuningCard(m.cards)) return { text: 'Tuning', tone: 'on' };
|
||||
if (m.state === 'mining') return { text: 'Mining · ' + (m.hash_total >= 100 ? m.hash_total.toFixed(0) : (m.hash_total || 0).toFixed(1)) + ' MH/s', tone: 'on' };
|
||||
if (m.state === 'paused') return { text: 'Paused', tone: '' };
|
||||
if (n.state === 'syncing') return { text: 'Syncing the node', tone: '' };
|
||||
if (n.state === 'starting') return { text: 'Starting the node', tone: '' };
|
||||
if (n.state === 'restarting') return { text: 'Node restarting', tone: 'bad' };
|
||||
if (n.state === 'failed') return { text: 'Node failed', tone: 'bad' };
|
||||
if (n.state === 'stopped') return { text: 'Node stopped', tone: 'bad' };
|
||||
if (m.state === 'waiting') return { text: 'Starting the worker', tone: '' };
|
||||
if (m.state === 'idle') return { text: 'Not mining', tone: '' };
|
||||
return { text: cap(m.state || 'starting'), tone: '' };
|
||||
}
|
||||
// ---- Ember on the card row (miner-ui-4, 6 October 2026): the flame mark's fill, the before -> after watts, the saving,
|
||||
// the sparkline points and the plain label. cd = the card, s = state, now = unix s ----
|
||||
// the "before" point (Ember, 6 October 2026): card.tune_before_watts / tune_before_mhs, step 0 of the last FULL plan,
|
||||
// kept across confirm plans; until a build carries them, the full plan's step 0 in tune_curve (unlocked, 100%); a
|
||||
// confirm or baseline plan without the fields has no before, so no saving is claimed
|
||||
function beforeOf(cd) {
|
||||
if (cd.tune_before_watts > 0) return { watts: cd.tune_before_watts, mhs: cd.tune_before_mhs || 0 };
|
||||
if (cd.tune_before_watts !== undefined) return null;
|
||||
var r = (cd.tune_curve || [])[0];
|
||||
if (cd.tune_source === 'full' && r && r.watts > 0 && (r.power_pct || 0) >= 100 && !(r.clock_mhz > 0)) return { watts: r.watts, mhs: r.mhs || 0 };
|
||||
return null;
|
||||
}
|
||||
// {kind: none|idle|measured|running|tuned|stopped|paused, fill: 0..1, label, sub, before, after, savingW, savingPct, savingText, next, points}
|
||||
function ember(cd, s, now) {
|
||||
var tw = tuneWords(cd, s, now), out = { kind: tw.kind, fill: 0, label: '', sub: '', before: 0, after: 0, savingW: 0, savingPct: 0, savingText: '', next: '', points: [] };
|
||||
if (tw.kind === 'none') return out;
|
||||
var st = (s && s.settings) || {};
|
||||
out.next = nextCheck(cd.sweep_at, st.tune_period_s, now).replace(/^next check /, '');
|
||||
out.points = (cd.tune_curve || []).filter(function (r) { return r.watts > 0 && r.mhs > 0; }).map(function (r) { return { w: r.watts, mhs: r.mhs, eff: r.eff || (r.mhs / r.watts), marked: !!r.mark, chosen: cd.sweep_watts > 0 && Math.abs(r.watts - cd.sweep_watts) < 0.5 && Math.abs((r.mhs || 0) - (cd.sweep_mhs || 0)) < 0.05 }; });
|
||||
if (cd.tune_floor) out.floor = true;
|
||||
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 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 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);
|
||||
out.savingPct = b.mhs > 0 && cd.sweep_mhs > 0 ? Math.max(0, (b.mhs - cd.sweep_mhs) / b.mhs * 100) : 0;
|
||||
out.savingText = 'saves ' + out.savingW + ' W' + (b.mhs > 0 ? ', ' + (out.savingPct < 0.05 ? 'no' : out.savingPct.toFixed(2) + '% of') + ' rate' : '');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
out.label = st.sweep === false ? 'Not tuned: Ember Tune is off' : 'Not tuned yet'; out.sub = 'starts after 2 min of steady mining'; return out;
|
||||
}
|
||||
// the Tuning strip's fleet saving: watts and £ a day summed over the tuned cards
|
||||
function fleetSaving(cards, s, now, pence) {
|
||||
var w = 0, n = 0;
|
||||
(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' : '') + ' 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, 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
|
||||
|
|
@ -585,7 +751,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var state = null, phase = 'welcome', page = 'mine', forced = null, cardsSeen = false, keyShownThisRun = false;
|
||||
var params = new URLSearchParams(location.search);
|
||||
if (params.get('screen')) forced = params.get('screen');
|
||||
if (params.get('page')) page = params.get('page'); // ?page=mine|earnings|prove|settings (screenshots)
|
||||
if (params.get('page')) page = params.get('page'); // ?page=overview|cards|earnings|prove|settings (screenshots)
|
||||
var forcedUpdate = params.get('update'); // a sample update state for screenshots: available|downloading|ready|waiting|applying|urgent|manual|error|updated
|
||||
var forcedJob = params.get('job'); // a sample remote job for screenshots: running|done|failed
|
||||
if (params.get('host') === 'mac') document.body.classList.add('mac');
|
||||
|
|
@ -659,7 +825,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
$('page-title').hidden = !dash;
|
||||
if (!dash && drawerOpen()) setDrawer(false);
|
||||
if (p === 'cards' && !cardsSeen) { cardsSeen = true; api('api/detect', {}); }
|
||||
if (dash) showPage(page);
|
||||
if (dash) { showPage(page); mountDag(); }
|
||||
api('api/phase', { phase: p });
|
||||
}
|
||||
function showPage(id) {
|
||||
|
|
@ -674,7 +840,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
});
|
||||
setText('page-title-text', meta.title); setText('page-sub', meta.sub);
|
||||
$('main').scrollTop = 0;
|
||||
if (state) { renderPage(state); if (page === 'mine') size(); }
|
||||
if (state) { renderPage(state); if (page === 'overview') size(); }
|
||||
}
|
||||
$('rail-nav').addEventListener('click', function (e) { var b = e.target.closest('.nav[data-page]'); if (b) showPage(b.dataset.page); });
|
||||
// the rail answers the arrow keys and Home/End, so the sections are reachable without a mouse
|
||||
|
|
@ -713,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;
|
||||
|
|
@ -743,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)
|
||||
|
|
@ -752,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) {
|
||||
|
|
@ -761,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) {
|
||||
|
|
@ -776,11 +942,14 @@ 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] : '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); }
|
||||
function cardOf(key) { return state ? state.mining.cards.filter(function (c) { return c.key === key; })[0] : null; }
|
||||
|
|
@ -789,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 () {
|
||||
|
|
@ -826,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 shard 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 ----------
|
||||
|
|
@ -838,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); });
|
||||
}
|
||||
|
|
@ -852,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); });
|
||||
|
|
@ -1121,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;
|
||||
|
|
@ -1131,7 +1300,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
drawRaf = 0; clearTimeout(drawTimer); drawTimer = null;
|
||||
if (hiddenNow()) return;
|
||||
var fast = false;
|
||||
if (phase === 'dashboard' && page === 'mine') { var a = performance.now(); fast = draw(now); perf.draw += performance.now() - a; perf.frames++; }
|
||||
if (phase === 'dashboard' && page === 'overview' && !$('dag').hidden) { var a = performance.now(); fast = draw(now); perf.draw += performance.now() - a; perf.frames++; }
|
||||
if (fast) drawRaf = requestAnimationFrame(frame);
|
||||
else drawTimer = setTimeout(function () { drawTimer = null; drawRaf = requestAnimationFrame(frame); }, reduce ? 1000 : 500);
|
||||
}
|
||||
|
|
@ -1143,6 +1312,20 @@ if (typeof document !== 'undefined') (function () {
|
|||
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', function () { size(); });
|
||||
requestDraw();
|
||||
|
||||
// ---------- the chain scene (live-dag.js, the site's module, copied in): the engine's own api/live, this machine's
|
||||
// lane as "you"; until the engine serves it (owed) the module reports failed and the blocks strip draws instead ----------
|
||||
var dag = null, dagMode = '';
|
||||
function mountDag() {
|
||||
if (dag || typeof IgneumDag === 'undefined' || !$('dag-live')) return;
|
||||
try {
|
||||
dag = IgneumDag.mount($('dag-live'), { compact: window.innerWidth < 720, window: 120, url: 'api/live', mine: 'you', tooltip: $('dag-tip'), chip: $('dag-chip'), 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 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(); }
|
||||
} });
|
||||
} catch (e) { dag = null; $('dag-live').hidden = true; $('dag').hidden = false; size(); }
|
||||
}
|
||||
// ---------- render ----------
|
||||
function badgeHtml(cd) {
|
||||
if (cd.vendor === 'apple') return '<svg viewBox="0 0 24 24" width="22" height="22" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.5 2-3.6 2.1-3.7-1.2-1.7-3-1.9-3.6-2-1.5-.2-3 .9-3.8.9-.8 0-2-.9-3.3-.8-1.7 0-3.2 1-4.1 2.5-1.8 3-.5 7.6 1.3 10.1.9 1.2 1.9 2.6 3.2 2.5 1.3 0 1.8-.8 3.3-.8 1.6 0 2 .8 3.3.8 1.4 0 2.3-1.2 3.1-2.5 1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.9-4.1zM13.9 5.3c.7-.8 1.2-2 1-3.2-1 0-2.2.7-2.9 1.5-.6.7-1.2 1.9-1 3 1.1.1 2.2-.5 2.9-1.3z"/></svg>';
|
||||
|
|
@ -1192,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;
|
||||
|
|
@ -1229,13 +1412,16 @@ if (typeof document !== 'undefined') (function () {
|
|||
if (cells.money) nums += numCell('money', cells.money, '', 'a day', '');
|
||||
else if (cells.needsPrice) nums += '<div class="num money"><button class="btn tiny ghost" data-price="1">Set a price</button><span class="k">£ a day</span></div>';
|
||||
if (r.temp) nums += numCell('temp', r.temp.replace(' °C', ''), '°C', 'GPU', r.tempTone);
|
||||
var tuneBtn = tw.button === 'stop' ? '<button class="btn small" data-stop="1">Stop</button>' : tw.button === 'tune' ? '<button class="btn small ghost" data-tune="1">Tune</button>' : '';
|
||||
var em = View.ember(cd, s, s.now);
|
||||
if (em.kind === 'tuned' && em.before > 0 && em.after > 0 && cells.power) nums = nums.replace(numCell('power', cells.power.replace(' W', ''), 'W', cells.eff, ''), numCell('power', Math.round(em.before) + ' → ' + cells.power.replace(' W', ''), 'W', cells.eff, ''));
|
||||
if (em.points.length > 1 && cells.power) nums = nums.replace('<span class="k">' + esc(cells.eff) + '</span>', '<span class="k spark">' + esc(cells.eff) + sparkHtml(em.points) + '</span>');
|
||||
var tuneBtn = tw.button === 'stop' ? '<button class="btn small" data-stop="1">Stop</button>' : tw.button === 'tune' ? '<button class="btn small ghost" data-tune="1">' + (em.kind === 'tuned' ? 'Retune' : 'Tune') + '</button>' : '';
|
||||
var sw = '<label class="switch" title="' + (r.on ? 'mining on this card' : 'not mining on this card') + '"><input type="checkbox" aria-label="Mine on ' + esc(r.name) + '"' + (r.on ? ' checked' : '') + (r.canToggle ? '' : ' disabled') + '><span class="track"></span></label>';
|
||||
var chev = '<button class="chev-btn" data-open="1" aria-expanded="false" aria-label="Details for ' + esc(r.name) + '"><span class="chev"></span></button>';
|
||||
var subs = [];
|
||||
if (r.sub) subs.push('<div class="msg' + (r.tone === 'bad' ? '' : ' dim') + '">' + esc(r.sub) + '</div>');
|
||||
if (cells.noReading && r.on) subs.push('<div class="msg dim">' + esc(cells.noReading) + '</div>');
|
||||
var tune = tw.kind === 'none' ? '' : '<div class="gl-tune ' + tw.kind + '"><span class="t">' + esc(tw.text) + '</span>' + (tw.note ? '<span class="n dim">' + esc(tw.note) + '</span>' : '') + (tw.progress >= 0 ? '<i class="bar"><b style="width:' + Math.round(tw.progress * 100) + '%"></b></i>' : '') + '</div>';
|
||||
var tune = em.kind === 'none' ? '' : '<div class="gl-tune ' + em.kind + '">' + flameHtml(em.fill, em.kind) + '<span class="t">' + esc(em.label) + '</span>' + (em.sub ? '<span class="n dim">' + esc(em.sub) + '</span>' : '') + (em.savingText ? '<span class="n save">' + esc(em.savingText) + '</span>' : '') + (em.kind === 'running' ? '<i class="bar"><b style="width:' + Math.round(em.fill * 100) + '%"></b></i>' : '') + '</div>';
|
||||
return '<div class="gpu-line' + (r.on ? ' on' : ' off') + (r.removed ? ' removed' : '') + (r.unusable ? ' unusable' : '') + '" data-key="' + esc(r.key) + '">' +
|
||||
'<div class="gl-main"><div class="badge ' + esc(r.vendor) + '">' + badgeHtml({ vendor: r.vendor }) + '</div>' +
|
||||
'<div class="who"><div class="name" title="' + esc(r.title) + '">' + esc(r.name) + ' <span class="kind ' + esc(r.kind) + '">' + esc(r.kindWord) + '</span></div>' +
|
||||
|
|
@ -1244,17 +1430,46 @@ if (typeof document !== 'undefined') (function () {
|
|||
'<div class="acts">' + tuneBtn + sw + chev + '</div></div>' + tune +
|
||||
'<div class="gl-details" hidden>' + detailsHtml(cd, s) + '</div></div>';
|
||||
}
|
||||
// the flame mark (the brand mark's two polygons) filled to a fraction: outline when not tuned, filling while tuning, solid when tuned
|
||||
var flameN = 0;
|
||||
function flameHtml(fill, kind) {
|
||||
var id = 'fl' + (++flameN), h = Math.round(100 * (1 - Math.max(0, Math.min(1, fill))));
|
||||
return '<svg class="flame ' + kind + '" viewBox="0 0 100 100" width="16" height="16" aria-hidden="true"><defs><clipPath id="' + id + '"><rect x="0" y="' + h + '" width="100" height="' + (100 - h) + '"/></clipPath></defs>' +
|
||||
'<path class="fo" d="M50 4 L78 50 L50 96 L22 50 Z M50 34 L62 50 L50 66 L38 50 Z" fill-rule="evenodd"/>' +
|
||||
'<path class="ff" clip-path="url(#' + id + ')" d="M50 4 L78 50 L50 96 L22 50 Z M50 34 L62 50 L50 66 L38 50 Z" fill-rule="evenodd"/></svg>';
|
||||
}
|
||||
// a tiny sparkline of the tune's ladder: MH/W per step, the chosen step as a dot
|
||||
function sparkHtml(points) {
|
||||
var effs = points.map(function (p) { return p.eff; }), lo = Math.min.apply(null, effs), hi = Math.max.apply(null, effs), n = points.length, W = 44, H = 12;
|
||||
var pts = points.map(function (p, i) { var x = n > 1 ? i / (n - 1) * (W - 2) + 1 : W / 2, y = hi > lo ? H - 1 - (p.eff - lo) / (hi - lo) * (H - 2) : H / 2; return { x: x, y: y, c: p.chosen, m: p.marked }; });
|
||||
var d = pts.map(function (p, i) { return (i ? 'L' : 'M') + p.x.toFixed(1) + ' ' + p.y.toFixed(1); }).join(' ');
|
||||
var dot = pts.filter(function (p) { return p.c; })[0];
|
||||
return '<svg class="spark" viewBox="0 0 ' + W + ' ' + H + '" width="' + W + '" height="' + H + '" aria-hidden="true"><path d="' + d + '"/>' + (dot ? '<circle cx="' + dot.x.toFixed(1) + '" cy="' + dot.y.toFixed(1) + '" r="2"/>' : '') + '</svg>';
|
||||
}
|
||||
// the tune's curve behind the chevron: W across, MH/s up, every step a dot, the chosen one ringed, a marked one hollow
|
||||
function curveHtml(points) {
|
||||
if (points.length < 2) return '';
|
||||
var ws = points.map(function (p) { return p.w; }), ms = points.map(function (p) { return p.mhs; });
|
||||
var w0 = Math.min.apply(null, ws), w1 = Math.max.apply(null, ws), m0 = Math.min.apply(null, ms), m1 = Math.max.apply(null, ms), W = 320, H = 110, L = 34, B = 18;
|
||||
var X = function (w) { return L + (w1 > w0 ? (w - w0) / (w1 - w0) : 0.5) * (W - L - 8); }, Y = function (m) { return 6 + (m1 > m0 ? 1 - (m - m0) / (m1 - m0) : 0.5) * (H - B - 6); };
|
||||
var sorted = points.slice().sort(function (a, b) { return a.w - b.w; });
|
||||
var path = sorted.map(function (p, i) { return (i ? 'L' : 'M') + X(p.w).toFixed(1) + ' ' + Y(p.mhs).toFixed(1); }).join(' ');
|
||||
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 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 + ')' : ''));
|
||||
|
|
@ -1262,13 +1477,15 @@ if (typeof document !== 'undefined') (function () {
|
|||
if (cd.fan_pct > 0) tl.push('fan <b>' + Math.round(cd.fan_pct) + '%</b>');
|
||||
if (cd.util_pct > 0) tl.push('busy <b>' + Math.round(cd.util_pct) + '%</b>');
|
||||
if (tl.length) h += '<div class="line">' + tl.join('<span class="sep"></span>') + '</div>';
|
||||
var tm = TuneLine.model(cd, s.now);
|
||||
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>' : '') + '</div>';
|
||||
if (cd.tune_curve && cd.tune_curve.length) {
|
||||
h += '<table class="curve"><thead><tr><th>step</th><th>clock</th><th>power</th><th>MH/s</th><th>W</th><th>MH/W</th><th>note</th></tr></thead><tbody>' + cd.tune_curve.map(function (r, i) { return '<tr' + (r.mark ? ' class="marked"' : '') + '><td>' + (i + 1) + '</td><td>' + (r.clock_mhz ? r.clock_mhz + ' MHz' : 'unlocked') + '</td><td>' + (r.power_pct || 0) + '%</td><td>' + (r.mhs || 0).toFixed(1) + '</td><td>' + Math.round(r.watts || 0) + '</td><td>' + (r.eff || 0).toFixed(3) + '</td><td>' + esc(r.mark || '') + '</td></tr>'; }).join('') + '</tbody></table>';
|
||||
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) + '">' + [['', '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>';
|
||||
}
|
||||
h += '<div class="ctl"><span class="k">Identities</span><p class="help">Each identity votes and is paid on its own. 8 suits a big card, 2 a small one, 1 an integrated GPU. Changing it restarts that card’s worker.</p><div class="ids"><button type="button" data-d="-1" aria-label="fewer identities">−</button><input type="number" min="1" max="64" value="' + (cd.identities || 1) + '" aria-label="Identities on ' + esc(cd.name) + '"><button type="button" data-d="1" aria-label="more identities">+</button></div></div>';
|
||||
var facts = [cd.worker ? 'worker ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean);
|
||||
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">Miners on this card</span><p class="help">How many miners this card runs as. Each one votes and is paid on its own. 8 suits a big card, 2 a small one, 1 a built-in GPU. Changing it restarts the card.</p><div class="ids"><button type="button" data-d="-1" aria-label="fewer identities">−</button><input type="number" min="1" max="64" value="' + (cd.identities || 1) + '" aria-label="Identities on ' + esc(cd.name) + '"><button type="button" data-d="1" aria-label="more identities">+</button></div></div>';
|
||||
var facts = [cd.worker ? 'runs on ' + cd.worker : '', cd.detail || '', cardTitle(cd), cd.driver ? 'driver ' + cd.driver : ''].filter(Boolean);
|
||||
if (facts.length) h += '<p class="note mono small">' + esc(facts.join(' · ')) + '</p>';
|
||||
return h;
|
||||
}
|
||||
|
|
@ -1281,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);
|
||||
|
|
@ -1293,12 +1510,12 @@ if (typeof document !== 'undefined') (function () {
|
|||
var on = rows.filter(function (r) { return r.on; }).length;
|
||||
setText('d-cards-eyebrow', s.detecting ? 'detecting' : !shown.length ? 'none found' : on + ' of ' + presentCards.length + ' on');
|
||||
var tunables = presentCards.filter(function (c) { return View.tunable(c) && c.enabled; });
|
||||
setHidden('btn-tune-all', !(tunables.length > 1) || !!(s.settings && s.settings.tuning_off) || !!View.tuningCard(presentCards));
|
||||
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]; })]);
|
||||
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 : '';
|
||||
|
|
@ -1311,7 +1528,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var key = s.events.length ? s.events[0].t + ':' + s.events.length : '';
|
||||
if (key !== lastEventsKey) {
|
||||
lastEventsKey = key;
|
||||
$('d-events').innerHTML = s.events.length ? s.events.slice(0, 8).map(function (e) { return '<div><span><span class="k ' + esc(e.kind) + '">' + esc(e.kind) + '</span>' + esc(View.eventLine(e.text)) + '</span><span class="t">' + clock(e.t) + '</span></div>'; }).join('') : '<div class="empty">No events yet. The node and the miner report here as they start.</div>';
|
||||
$('d-events').innerHTML = s.events.length ? s.events.slice(0, 8).map(function (e) { var pe = View.plainEvent(e.text); return '<div' + (pe.tech ? ' title="' + esc(pe.tech) + '"' : '') + '><span><i class="k ' + esc(e.kind) + '"></i>' + esc(pe.text) + '</span><span class="t">' + clock(e.t) + '</span></div>'; }).join('') : '<div class="empty">No events yet. The node and the miner report here as they start.</div>';
|
||||
}
|
||||
}
|
||||
// the Tuning card on Mine and the Tuning rows on Settings share the goal, the schedule and the Power control switch
|
||||
|
|
@ -1321,7 +1538,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
setText('goal-line', gw.consequence); setText('goal-line-2', gw.consequence);
|
||||
var sched = View.schedule(s.mining.cards, s, s.now);
|
||||
setText('tune-schedule', sched); setText('s-tune-schedule', sched);
|
||||
setText('tune-eyebrow', st.tuning_off ? 'paused by Igneum' : st.sweep === false ? 'Ember Tune off' : 'Ember Tune');
|
||||
setText('fleet-saving', View.fleetSaving(s.mining.cards, s, s.now, priceNow()));
|
||||
var nvOff = cards.some(function (c) { return c.vendor === 'nvidia' && View.present(c); }) && !st.power_control;
|
||||
setHidden('m-power-row', !nvOff);
|
||||
if (document.activeElement !== $('m-power-control')) $('m-power-control').checked = !!st.power_control;
|
||||
|
|
@ -1341,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 || 'checkpoints lock once this machine and the others vote'); }
|
||||
setText('f-votes', withCommas(f.votes));
|
||||
}
|
||||
|
||||
|
|
@ -1368,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 : '');
|
||||
}
|
||||
|
||||
|
|
@ -1393,10 +1610,10 @@ if (typeof document !== 'undefined') (function () {
|
|||
setText('pv-note', (!enabled && pv.default_note) ? pv.default_note + '.' : w.note);
|
||||
var v = View.verifierWords(pv);
|
||||
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');
|
||||
setText('pv-verifier-help', pv.verifier_note || 'the verifier: 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 segments (runs of 8 blocks) this machine proved');
|
||||
setText('pv-program', pv.program_id || 'not read yet');
|
||||
setText('pv-aggregator', pv.aggregator_id || 'not read yet');
|
||||
}
|
||||
|
|
@ -1436,24 +1653,16 @@ if (typeof document !== 'undefined') (function () {
|
|||
}
|
||||
|
||||
function renderPage(s) {
|
||||
if (page === 'mine') renderMine(s);
|
||||
if (page === 'overview' || page === 'cards') renderMine(s);
|
||||
else if (page === 'earnings') renderEarnings(s);
|
||||
else if (page === 'prove') renderProve(s);
|
||||
else if (page === 'settings') renderSettings(s);
|
||||
}
|
||||
function renderPill(s) {
|
||||
var pill = $('pill'), text;
|
||||
var pill = $('pill'), pw = View.pill(s);
|
||||
pill.classList.remove('on', 'warn');
|
||||
if (s.quitting) text = 'stopping';
|
||||
else if (!s.setup_done) text = s.detecting ? 'detecting' : 'set up';
|
||||
else if (View.tuningCard(s.mining.cards)) { text = 'tuning'; pill.classList.add('on'); }
|
||||
else if (s.mining.state === 'mining') { text = s.mining.hash_total.toFixed(1) + ' MH/s'; pill.classList.add('on'); }
|
||||
else if (s.mining.state === 'paused') text = 'paused';
|
||||
else if (s.node.state === 'syncing' || s.node.state === 'starting') text = 'node ' + s.node.state;
|
||||
else if (s.node.state === 'failed' || s.node.state === 'restarting') { text = 'node ' + s.node.state; pill.classList.add('warn'); }
|
||||
else if (s.mining.state === 'idle' && s.node.state === 'synced') { text = 'node synced'; pill.classList.add('on'); }
|
||||
else text = s.mining.state;
|
||||
$('pill-text').textContent = text;
|
||||
if (pw.tone === 'on') pill.classList.add('on'); else if (pw.tone === 'bad') pill.classList.add('warn');
|
||||
$('pill-text').textContent = pw.text;
|
||||
var foot = (s.display_name ? '<b title="' + esc(s.display_name) + '">' + esc(s.display_name) + '</b><br>' : '') + (s.address.display ? 'pays ' + esc(s.address.display.slice(0, 6) + '…' + s.address.display.slice(-4)) : 'no address yet') + '<br>up ' + uptime(s.uptime_s);
|
||||
if (foot !== lastFoot) { lastFoot = foot; $('rail-status').innerHTML = foot; }
|
||||
var fv = 'v' + esc(s.version) + '<span class="chain"> · ' + esc(s.chain || s.network || '') + '</span>';
|
||||
|
|
@ -1492,7 +1701,8 @@ if (typeof document !== 'undefined') (function () {
|
|||
el.className = 'notice ' + n.kind + (n.tone ? ' ' + n.tone : '');
|
||||
el.dataset.key = n.key;
|
||||
$('notice-text').textContent = n.text; $('notice-text').title = n.title || '';
|
||||
$('notice-detail').textContent = n.detail; $('notice-detail').hidden = !n.detail; $('notice-detail').title = n.detail;
|
||||
$('notice-detail').textContent = n.detail; $('notice-detail').hidden = true; $('notice-detail').title = n.detail;
|
||||
$('notice-more').hidden = !n.detail; $('notice-more').setAttribute('aria-expanded', 'false'); $('notice-more').classList.remove('open');
|
||||
$('notice-actions').innerHTML = n.actions.map(function (a) { return '<button class="btn small ' + (a.primary ? 'primary' : 'ghost') + '" data-act="' + esc(a.act) + '"' + (a.disabled ? ' disabled' : '') + '>' + esc(a.label) + '</button>'; }).join('');
|
||||
$('notice-prog').hidden = !(n.progress >= 0);
|
||||
strip.hidden = false;
|
||||
|
|
@ -1514,6 +1724,7 @@ if (typeof document !== 'undefined') (function () {
|
|||
var b = e.target.closest('[data-act]'); if (!b || b.disabled) return;
|
||||
var act = b.dataset.act, key = $('notice').dataset.key;
|
||||
if (act === 'close') { dismissed[key] = true; renderNotices(state || {}); }
|
||||
else if (act === 'more') { var open = $('notice-detail').hidden; $('notice-detail').hidden = !open; b.setAttribute('aria-expanded', open ? 'true' : 'false'); b.classList.toggle('open', open); layoutStrip(); }
|
||||
else if (act === 'install') { api('api/update/install', {}); toast('Installing at once'); }
|
||||
else if (act === 'retry') { api('api/update/install', {}); toast('Trying the update again'); }
|
||||
else if (act === 'open') api('api/update/open', {});
|
||||
|
|
@ -1584,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) {
|
||||
|
|
@ -1620,7 +1831,9 @@ if (typeof document !== 'undefined') (function () {
|
|||
function sampleTune(s, kind) {
|
||||
var c = s.mining.cards[0]; if (!c) return;
|
||||
if (kind === 'running') { c.state = 'tuning'; c.sweep_state = 'running'; c.tune_step = 7; c.tune_steps = 9; c.tune_eta_s = 230; c.tune_plan = 'full'; c.sweep_note = 'tuning: holding 2472 MHz · 100% · 41 s (step 7 of 9)'; if (!(c.power_w > 0)) c.power_w = 219.6; if (!(c.hash_now > 0)) c.hash_now = 123.4; }
|
||||
if (kind === 'tuned') { c.tune_line = 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)'; c.tune_source = 'full'; c.tune_control = true; c.tune_clock_mhz = 2470; c.sweep_pct = 100; c.sweep_at = s.now - 7200; c.sweep_watts = 290; }
|
||||
var curve = [{ clock_mhz: 0, power_pct: 100, watts: 374, mhs: 122.5, eff: 0.328 }, { clock_mhz: 0, power_pct: 90, watts: 340, mhs: 122.4, eff: 0.36 }, { clock_mhz: 0, power_pct: 80, watts: 302, mhs: 122.3, eff: 0.405 }, { clock_mhz: 0, power_pct: 70, watts: 264, mhs: 118.1, eff: 0.447 }, { clock_mhz: 0, power_pct: 60, watts: 228, mhs: 104.9, eff: 0.46, mark: 'hot' }, { clock_mhz: 2780, power_pct: 80, watts: 298, mhs: 122.4, eff: 0.411 }, { clock_mhz: 2470, power_pct: 80, watts: 290, mhs: 122.3, eff: 0.422 }, { clock_mhz: 2160, power_pct: 80, watts: 281, mhs: 117.0, eff: 0.416 }, { clock_mhz: 1850, power_pct: 80, watts: 270, mhs: 109.2, eff: 0.404 }];
|
||||
if (kind === 'tuned' || kind === 'mix') { c.tune_line = 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)'; c.tune_source = 'full'; c.tune_control = true; c.tune_clock_mhz = 2470; c.sweep_pct = 80; c.sweep_at = s.now - 7200; c.sweep_watts = 290; c.sweep_mhs = 122.3; c.tune_curve = curve; c.power_default_w = c.power_default_w || 374; }
|
||||
if (kind === 'mix' && s.mining.cards[1]) { var d = s.mining.cards[1]; d.state = 'tuning'; d.sweep_state = 'running'; d.tune_step = 4; d.tune_steps = 9; d.tune_eta_s = 410; d.tune_plan = 'full'; d.tune_control = true; d.hash_now = d.hash_now || 98.1; }
|
||||
if (kind === 'measured') { c.tune_line = 'Tuned: 26.7 MH/s at 38 W (0.703 MH/W)'; c.tune_source = 'baseline'; c.tune_control = false; c.sweep_at = s.now - 600; c.sweep_note = 'measure only on Apple silicon: the system sets the clocks and the power; no control exposed'; }
|
||||
}
|
||||
function render(s) {
|
||||
|
|
@ -1659,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 away';
|
||||
$('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');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<link rel="icon" href="mark.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="app.css">
|
||||
</head>
|
||||
<body class="phase-welcome" data-phase="welcome" data-page="mine">
|
||||
<body class="phase-welcome" data-phase="welcome" data-page="overview">
|
||||
|
||||
<!-- the rail: four sections (miner-ui-3: Mine, Earnings, Prove, Settings), Logs and Quit in the foot. Under 720 px it
|
||||
is a bottom tab bar. The setup screens have no rail. -->
|
||||
|
|
@ -18,7 +18,8 @@
|
|||
<span class="word">IGNEUM</span>
|
||||
</div>
|
||||
<nav class="rail-nav" id="rail-nav" aria-label="Sections">
|
||||
<button class="nav on" data-page="mine" aria-current="page"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/></svg><span>Mine</span></button>
|
||||
<button class="nav on" data-page="overview" aria-current="page"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 2 4 14h7l-1 8 9-12h-7l1-8z"/></svg><span>Overview</span></button>
|
||||
<button class="nav" data-page="cards"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="11" rx="2"/><path d="M7 20h10M9 16v4M15 16v4M7 10h3M14 10h3"/></svg><span>Cards</span></button>
|
||||
<button class="nav" data-page="earnings"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="6" width="18" height="13" rx="2"/><path d="M3 10h18M16 15h2"/></svg><span>Earnings</span></button>
|
||||
<button class="nav" data-page="prove"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2 4 5v6c0 5 3.4 9.4 8 11 4.6-1.6 8-6 8-11V5l-8-3z"/><path d="m9 12 2 2 4-4"/></svg><span>Prove</span></button>
|
||||
<button class="nav" data-page="settings"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h10M18 7h2M4 17h4M12 17h8"/><circle cx="16" cy="7" r="2"/><circle cx="10" cy="17" r="2"/></svg><span>Settings</span><i class="nav-dot" id="nav-updates-dot" hidden></i></button>
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
<span class="word">IGNEUM</span><span class="miner">MINER</span>
|
||||
</div>
|
||||
<div class="page-title" id="page-title" hidden>
|
||||
<h1 id="page-title-text">Mine</h1>
|
||||
<h1 id="page-title-text">Overview</h1>
|
||||
<span class="page-sub" id="page-sub"></span>
|
||||
</div>
|
||||
<div class="top-right">
|
||||
|
|
@ -48,8 +49,10 @@
|
|||
<!-- the status strip: one notice at a time (app.js, Notices) -->
|
||||
<div class="notices" id="notices" hidden>
|
||||
<div class="notice" id="notice" role="status" aria-live="polite">
|
||||
<i class="notice-dot" aria-hidden="true"></i>
|
||||
<span class="notice-text" id="notice-text"></span>
|
||||
<span class="notice-actions" id="notice-actions"></span>
|
||||
<button class="notice-more" id="notice-more" data-act="more" title="The technical line" aria-label="Show the technical line" aria-expanded="false" hidden><span class="chev"></span></button>
|
||||
<button class="notice-close" id="notice-close" data-act="close" title="Close" aria-label="Close">×</button>
|
||||
<span class="notice-detail mono" id="notice-detail" hidden></span>
|
||||
<span class="prog" id="notice-prog" hidden><i></i></span>
|
||||
|
|
@ -115,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>
|
||||
|
|
@ -143,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>
|
||||
|
|
@ -160,8 +163,8 @@
|
|||
<!-- 4. the dashboard: four pages behind the rail -->
|
||||
<section class="screen" id="screen-dashboard">
|
||||
|
||||
<!-- Mine -->
|
||||
<section class="page" id="page-mine" data-page="mine">
|
||||
<!-- Overview: the fleet hero, the chain scene, the node line, activity -->
|
||||
<section class="page" id="page-overview" data-page="overview">
|
||||
<div class="clock-card" id="m-clock" hidden>
|
||||
<p class="clock-msg" id="m-clock-msg"></p>
|
||||
<div class="row"><button class="btn small primary" id="m-clock-sync">Sync clock</button><span class="note mono small" id="m-clock-result"></span></div>
|
||||
|
|
@ -182,24 +185,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Your cards</h3><div class="head-right"><span class="eyebrow" id="d-cards-eyebrow">detecting</span><button class="btn small" id="btn-tune-all" hidden>Tune all</button></div></div>
|
||||
<div class="gpu-list" id="d-cards"><div class="empty">Waiting for the engine.</div></div>
|
||||
<p class="note" id="d-cards-note" hidden></p>
|
||||
<div 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: your row and the other miners, the selected chain as a lit path, checkpoints as bands"></canvas>
|
||||
<div class="dag-tip mono" id="dag-tip" hidden></div>
|
||||
<span class="dag-chip mono" id="dag-chip" hidden>scroll to zoom · Esc to release</span>
|
||||
<canvas id="dag" aria-hidden="true" hidden></canvas>
|
||||
</div>
|
||||
|
||||
<div class="card" id="tune-card">
|
||||
<div class="card-head"><h3>Tuning</h3><span class="eyebrow" id="tune-eyebrow">Ember Tune</span></div>
|
||||
<div class="goal-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>
|
||||
</div>
|
||||
<span class="goal-line" id="goal-line"></span>
|
||||
</div>
|
||||
<p class="line-text" id="tune-schedule"></p>
|
||||
<label class="switch" id="m-power-row" hidden><input type="checkbox" id="m-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.</span></span></label>
|
||||
<div class="legend"><span><i class="lg you"></i>yours</span><span><i class="lg pending"></i>pending</span><span><i class="lg included"></i>included</span><span><i class="lg excluded"></i>excluded</span><span><i class="lg proven"></i>proven</span><span><i class="lg locked"></i>locked checkpoint</span><span class="dim" id="dag-note"></span></div>
|
||||
</div>
|
||||
|
||||
<div class="card node-card" id="node-card">
|
||||
|
|
@ -214,21 +208,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">last checkpoint</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 (a point the miners agree can never be undone)</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 +232,35 @@
|
|||
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Activity</h3><div class="head-right"><span class="stats mono" id="d-stats"></span><button class="btn small ghost" id="btn-open-log">Open the log</button></div></div>
|
||||
<canvas id="dag" aria-hidden="true"></canvas>
|
||||
<div class="feed" id="d-events"><div class="empty">No events yet.</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 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" 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>
|
||||
<button class="btn small" id="btn-tune-all" hidden>Tune all</button>
|
||||
</div>
|
||||
<p class="line-text" id="tune-schedule"></p>
|
||||
<label class="switch" id="m-power-row" hidden><input type="checkbox" id="m-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.</span></span></label>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-head"><h3>Your cards</h3><div class="head-right"><span class="eyebrow" id="d-cards-eyebrow">detecting</span></div></div>
|
||||
<div class="gpu-list" id="d-cards"><div class="empty">Waiting for the engine.</div></div>
|
||||
<p class="note" id="d-cards-note" hidden></p>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<!-- Earnings -->
|
||||
<section class="page" id="page-earnings" data-page="earnings" hidden>
|
||||
<div class="card">
|
||||
|
|
@ -252,7 +270,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>
|
||||
|
||||
|
|
@ -297,19 +315,19 @@
|
|||
<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, in pieces called shards. Your cards prove shards 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 segments (runs of 8 blocks) this machine proved</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="k">shard program id</div>
|
||||
|
|
@ -318,7 +336,7 @@
|
|||
<div class="field">
|
||||
<div class="k">aggregator 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>
|
||||
<p class="help">Every proof names the program that made it: the shard program for one shard, the aggregator for a whole segment. Other nodes accept a proof only from these two ids.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -330,19 +348,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>
|
||||
|
|
@ -352,7 +370,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>
|
||||
|
|
@ -362,7 +380,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>
|
||||
|
|
@ -401,8 +419,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 checkpoints</b> <span class="dim">Signs a checkpoint 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>
|
||||
|
|
@ -494,6 +512,7 @@
|
|||
</div>
|
||||
|
||||
<div class="toast" id="toast" hidden></div>
|
||||
<script src="live-dag.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
236
app/igneum-app/ui/live-dag.js
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
/* Igneum live DAG view (docs/plans/site-ui-3.md section 6, 6 October 2026).
|
||||
One module for /live and the home page. Reads /api/live only; every pixel is a block, edge or checkpoint the observer
|
||||
stored. Blocks are placed by header time on x (the axis is labelled in DAA score) and by miner lane on y. Parent edges
|
||||
are drawn; the edge between two selected-chain blocks is heavier and the chain glows as one path. Colour words are the
|
||||
ledger's: pending, included (blue), excluded (red), proven (every shard verified or paid), locked (a checkpoint block).
|
||||
Checkpoints are vertical bands at their blue score with the lock weight; a locked band is solid, a pending one dashed.
|
||||
The view scrolls at chain speed, new blocks arrive from the right with a brief glow. The page's scroll passes through the
|
||||
scene; zoom takes ctrl or cmd with the wheel, a pinch, or a click into the scene (engaged, released by Escape or a click
|
||||
outside; opts.chip is the "scroll to zoom, Esc to release" element); window 30 to 300 s (the API's ?window=); a pause button;
|
||||
hover and tap show a block's details.
|
||||
|
||||
Usage: var dag = IgneumDag.mount(canvas, { compact, window, url, poll, mine, onState, onData, onWindow, onEngage, pauseButton, tooltip, chip })
|
||||
- url: the /api/live endpoint (default '/api/live'); poll: false stops the module's own 2 s fetch, the host then calls
|
||||
dag.push(reply) with a reply of the same shape ({ok, state:{stale, age_s, height, ...}, blocks:[...], finality:{checkpoints:[...]}, proving})
|
||||
whenever it has new data (every poll or every new block, either is fine; blocks already on screen are updated in place).
|
||||
- block record (the /api/live shape): hash (string, 16 hex), ts (ms, header time), blue_score, daa (numbers), parents ([hash]),
|
||||
chain (bool, selected chain), color ('blue' | 'red' | 'pending'), miner (string lane key, 8 hex of the vote key hash),
|
||||
locked (bool, a locked checkpoint block), final (bool, at or before the newest lock), shards ([{state: 'planned' | 'proving' |
|
||||
'verified' | 'paid'}]), proven (bool, every shard verified or paid). Checkpoints: {index, blue_score, state: 'locked' | 'pending', fraction_total}.
|
||||
- mine: a miner lane key (string) or function(block) -> bool; the user's own blocks take a molten ring and the lane is labelled "you".
|
||||
- The legend is the host's; the module draws no HTML. Colours come from the CSS custom properties on :root (--ember, --ember-hi,
|
||||
--molten, --bone, --ash, --line, --line-2, --row or --obsidian), re-read on data-theme and prefers-color-scheme changes.
|
||||
- Sizing: the canvas fills its CSS box (width and height from the stylesheet); 280 px wide works, 600 or more shows lane labels well.
|
||||
onState(state, reason): 'connecting' | 'live' | 'stale' | 'failed'; onData(d) with every fresh reply. */
|
||||
(function (root) {
|
||||
'use strict';
|
||||
var COL = {
|
||||
ember: '#F2541B', emberHi: '#FF6A2B', molten: '#FFB35C', bone: '#F4F1EC', ash: '#9A9A9E', line: '#2A2A30', line2: '#3A3A42', obsidian: '#0C0C0E',
|
||||
red: '#B0362B'
|
||||
};
|
||||
function cssVar(name, fallback) { try { var v = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); return v || fallback; } catch (e) { return fallback; } }
|
||||
function readTheme() {
|
||||
COL.ember = cssVar('--ember', COL.ember); COL.emberHi = cssVar('--ember-hi', COL.emberHi); COL.molten = cssVar('--molten', COL.molten);
|
||||
COL.bone = cssVar('--bone', COL.bone); COL.ash = cssVar('--ash', COL.ash); COL.line = cssVar('--line', COL.line); COL.line2 = cssVar('--line-2', COL.line2);
|
||||
COL.obsidian = cssVar('--row', cssVar('--obsidian', COL.obsidian));
|
||||
}
|
||||
function rgba(hex, a) { var h = hex.replace('#', ''); if (h.length !== 6) return hex; return 'rgba(' + parseInt(h.slice(0, 2), 16) + ',' + parseInt(h.slice(2, 4), 16) + ',' + parseInt(h.slice(4, 6), 16) + ',' + a + ')'; }
|
||||
function fmt(n) { return n === null || n === undefined ? 'pending' : Number(n).toLocaleString('en-GB'); }
|
||||
function fmtAge(sec) { sec = Math.round(sec); return sec < 90 ? sec + ' s' : sec < 5400 ? Math.round(sec / 60) + ' min' : Math.round(sec / 3600) + ' h'; }
|
||||
|
||||
function mount(canvas, opts) {
|
||||
opts = opts || {};
|
||||
var compact = !!opts.compact, windowS = Math.max(30, Math.min(300, opts.window || (compact ? 90 : 120)));
|
||||
var reduce = root.matchMedia && root.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
var isMine = typeof opts.mine === 'function' ? opts.mine : opts.mine ? function (b) { return b.miner === opts.mine; } : null;
|
||||
var ctx = canvas.getContext('2d'), dpr = Math.min(root.devicePixelRatio || 1, 2), W = 0, H = 0;
|
||||
var blocks = [], byHash = {}, lanes = [], laneOf = {}, cps = [], data = null, state = 'connecting', reason = 'connecting';
|
||||
var paused = false, offsetMs = 0, hover = null, pinned = null, lastPoll = 0, failures = 0, running = false, inView = true, lastT = 0;
|
||||
var vnow = function () { return Date.now() - 2500 - offsetMs; }; // the right edge sits 2.5 s behind the clock so a block has arrived before it is drawn
|
||||
var padL = compact ? 10 : 56, padR = 14, padT = compact ? 12 : 22, padB = compact ? 10 : 26;
|
||||
|
||||
function size() { W = canvas.clientWidth; H = canvas.clientHeight; canvas.width = Math.round(W * dpr); canvas.height = Math.round(H * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); layout(); }
|
||||
root.addEventListener('resize', size);
|
||||
|
||||
// ---- data ----
|
||||
function apply(d) {
|
||||
data = d;
|
||||
var seen = {}, now = performance.now();
|
||||
d.blocks.forEach(function (b) {
|
||||
seen[b.hash] = 1;
|
||||
var e = byHash[b.hash];
|
||||
if (e) { e.chain = !!b.chain; e.color = b.color || 'pending'; e.locked = !!b.locked; e.final = !!b.final; e.shards = b.shards || []; e.proven = !!b.proven; e.blue = b.blue_score; e.daa = b.daa; return; }
|
||||
e = { hash: b.hash, ts: b.ts, blue: b.blue_score, daa: b.daa, parents: b.parents || [], chain: !!b.chain, color: b.color || 'pending', miner: b.miner || '?', locked: !!b.locked, final: !!b.final, shards: b.shards || [], proven: !!b.proven, born: now, x: 0, y: 0 };
|
||||
byHash[b.hash] = e; blocks.push(e);
|
||||
});
|
||||
var cut = vnow() - windowS * 1000 - 20000;
|
||||
blocks = blocks.filter(function (b) { if (b.ts < cut && !seen[b.hash]) { delete byHash[b.hash]; return false; } return true; });
|
||||
blocks.sort(function (a, b) { return a.ts - b.ts; });
|
||||
cps = (d.finality && d.finality.checkpoints) || [];
|
||||
layout();
|
||||
}
|
||||
function layout() {
|
||||
// lanes: miners of the visible window ordered by blocks, as many as fit at 22 px, the rest in one "others" lane
|
||||
var vis = vnow() - windowS * 1000, count = {};
|
||||
blocks.forEach(function (b) { if (b.ts >= vis) count[b.miner] = (count[b.miner] || 0) + 1; });
|
||||
var ids = Object.keys(count).sort(function (a, b) { return count[b] - count[a] || (a < b ? -1 : 1); });
|
||||
var usable = H - padT - padB, laneH = compact ? 26 : 22, maxLanes = Math.max(2, Math.floor(usable / laneH));
|
||||
var shown = ids.length > maxLanes ? ids.slice(0, maxLanes - 1) : ids;
|
||||
lanes = shown.slice(); if (ids.length > shown.length) lanes.push('others');
|
||||
laneOf = {}; shown.forEach(function (id, i) { laneOf[id] = i; });
|
||||
}
|
||||
function laneY(b) { var i = b.miner in laneOf ? laneOf[b.miner] : lanes.length - 1; var n = Math.max(1, lanes.length); return padT + (i + 0.5) * ((H - padT - padB) / n); }
|
||||
function xOf(ts) { return padL + (W - padL - padR) * (1 - (vnow() - ts) / (windowS * 1000)); }
|
||||
|
||||
// the host can feed replies itself (poll: false); the same path the fetch takes
|
||||
function push(d) {
|
||||
if (!d || !d.ok || !d.state || !d.blocks) { setState('failed', 'live feed unavailable'); return; }
|
||||
failures = 0;
|
||||
if (d.state.stale) setState('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown'));
|
||||
else setState('live', null);
|
||||
apply(d); if (opts.onData) opts.onData(d); kick();
|
||||
}
|
||||
function poll() {
|
||||
if (opts.poll === false || document.hidden) return;
|
||||
var u = (opts.url || '/api/live') + '?window=' + windowS;
|
||||
fetch(u, { cache: 'no-store' }).then(function (r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }).then(function (d) {
|
||||
if (!d || !d.ok || !d.state || !d.blocks) throw new Error('bad reply');
|
||||
failures = 0;
|
||||
if (d.state.stale) { setState('stale', 'observer offline, last update ' + (d.state.age_s !== null && d.state.age_s !== undefined ? fmtAge(d.state.age_s) + ' ago' : 'unknown')); }
|
||||
else setState('live', null);
|
||||
apply(d); if (opts.onData) opts.onData(d);
|
||||
kick();
|
||||
}).catch(function () { failures++; if (failures >= 2) setState('failed', 'live feed unavailable'); });
|
||||
}
|
||||
function setState(s, r) { if (s === state && r === reason) return; state = s; reason = r; if (opts.onState) opts.onState(s, r); }
|
||||
|
||||
// ---- drawing ----
|
||||
function draw(t) {
|
||||
if (!W || !H) return;
|
||||
var dt = lastT ? Math.min(50, t - lastT) : 16; lastT = t;
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
var vis = vnow() - windowS * 1000, n = Math.max(1, lanes.length), lh = (H - padT - padB) / n;
|
||||
// lane rails and labels
|
||||
ctx.lineWidth = 1;
|
||||
for (var i = 0; i < n; i++) {
|
||||
var y = padT + (i + 0.5) * lh;
|
||||
ctx.strokeStyle = rgba(COL.line, 0.9); ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(W - padR, y); ctx.stroke();
|
||||
if (!compact) { var mineLane = isMine && typeof opts.mine === 'string' && lanes[i] === opts.mine; ctx.fillStyle = mineLane ? COL.molten : COL.ash; ctx.font = (mineLane ? '500' : '400') + ' 10px IBM Plex Mono, monospace'; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; ctx.fillText(mineLane ? 'you' : lanes[i] === 'others' ? 'others' : String(lanes[i]).slice(0, 8), 6, y); }
|
||||
}
|
||||
// checkpoint bands at their blue score (x from the chain block carrying that score, else the nearest)
|
||||
cps.forEach(function (c) {
|
||||
var ref = null, best = Infinity;
|
||||
for (var k = blocks.length - 1; k >= 0; k--) { var b = blocks[k]; if (b.blue === null || b.blue === undefined) continue; var dd = Math.abs(b.blue - c.blue_score); if (dd < best) { best = dd; ref = b; } }
|
||||
if (!ref || best > 3 || ref.ts < vis) return;
|
||||
var x = xOf(ref.ts), locked = c.state === 'locked';
|
||||
ctx.strokeStyle = rgba(COL.ember, locked ? 0.55 : 0.3); ctx.lineWidth = locked ? 1.5 : 1; ctx.setLineDash(locked ? [] : [4, 6]);
|
||||
ctx.beginPath(); ctx.moveTo(x, padT - 6); ctx.lineTo(x, H - padB + 6); ctx.stroke(); ctx.setLineDash([]);
|
||||
if (!compact) { ctx.fillStyle = rgba(COL.ember, 0.9); ctx.font = '500 10px IBM Plex Mono, monospace'; ctx.textAlign = 'center'; ctx.textBaseline = 'alphabetic'; ctx.fillText((locked ? 'locked ' : 'pending ') + (c.fraction_total !== null && c.fraction_total !== undefined ? Math.round(c.fraction_total * 100) + '%' : ''), x, H - 8); }
|
||||
});
|
||||
// positions
|
||||
blocks.forEach(function (b) { b.x = xOf(b.ts); b.y = laneY(b); b.on = b.ts >= vis && b.x >= padL - 10; });
|
||||
// edges: merge edges thin, chain-to-chain heavier; the chain as one glowing path
|
||||
blocks.forEach(function (b) {
|
||||
if (!b.on) return;
|
||||
b.parents.forEach(function (h) {
|
||||
var p = byHash[h]; if (!p || !p.on) return;
|
||||
var chainEdge = b.chain && p.chain;
|
||||
ctx.strokeStyle = chainEdge ? rgba(COL.ember, 0.75) : rgba(COL.ash, b.color === 'red' ? 0.12 : 0.22);
|
||||
ctx.lineWidth = chainEdge ? 2 : 1;
|
||||
ctx.beginPath(); ctx.moveTo(b.x, b.y); var mx = (b.x + p.x) / 2; ctx.bezierCurveTo(mx, b.y, mx, p.y, p.x, p.y); ctx.stroke();
|
||||
if (chainEdge) { ctx.strokeStyle = rgba(COL.ember, 0.18); ctx.lineWidth = 7; ctx.beginPath(); ctx.moveTo(b.x, b.y); ctx.bezierCurveTo(mx, b.y, mx, p.y, p.x, p.y); ctx.stroke(); }
|
||||
});
|
||||
});
|
||||
// blocks
|
||||
var S = compact ? 9 : 8, now = performance.now();
|
||||
blocks.forEach(function (b) {
|
||||
if (!b.on) return;
|
||||
var age = now - b.born, glow = age < 1600 ? 1 - age / 1600 : 0, ex = b.color === 'red';
|
||||
var r = b.chain ? S + 1 : S;
|
||||
if (glow > 0) { var g = ctx.createRadialGradient(b.x, b.y, 0, b.x, b.y, r * 3); g.addColorStop(0, rgba(COL.ember, 0.4 * glow)); g.addColorStop(1, rgba(COL.ember, 0)); ctx.fillStyle = g; ctx.beginPath(); ctx.arc(b.x, b.y, r * 3, 0, Math.PI * 2); ctx.fill(); }
|
||||
ctx.globalAlpha = ex ? 0.45 : 1;
|
||||
// fill: proven = ember, otherwise the row surface; outline: included (blue or chain) = ember, pending = grey, excluded = dim
|
||||
var proving = !b.proven && b.shards.some(function (s) { return s.state === 'proving' || s.state === 'verified' || s.state === 'paid'; });
|
||||
ctx.beginPath(); rr(b.x - r, b.y - r, 2 * r, 2 * r, 3);
|
||||
ctx.fillStyle = b.proven ? COL.ember : COL.obsidian; ctx.fill();
|
||||
ctx.lineWidth = b.chain ? 2 : 1.5;
|
||||
// chain blocks in ember (the path), included blocks lit in bone, pending in ash, excluded dim
|
||||
ctx.strokeStyle = b.proven ? COL.ember : proving ? COL.molten : b.chain ? rgba(COL.ember, 0.9) : b.color === 'blue' ? rgba(COL.bone, 0.8) : ex ? rgba(COL.ash, 0.5) : rgba(COL.ash, 0.75);
|
||||
ctx.stroke();
|
||||
if (b.locked) { ctx.strokeStyle = rgba(COL.ember, 0.8 + 0.2 * Math.sin(t / 350)); ctx.lineWidth = 2; ctx.beginPath(); ctx.arc(b.x, b.y, r * 1.9, 0, Math.PI * 2); ctx.stroke(); }
|
||||
if (isMine && isMine(b)) { ctx.strokeStyle = rgba(COL.molten, 0.9); ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(b.x, b.y, r * 1.5, 0, Math.PI * 2); ctx.stroke(); }
|
||||
ctx.globalAlpha = 1;
|
||||
});
|
||||
// hover or pinned ring
|
||||
var f = pinned || hover; if (f && f.on) { ctx.strokeStyle = COL.bone; ctx.lineWidth = 1; ctx.setLineDash([2, 3]); ctx.beginPath(); ctx.arc(f.x, f.y, S * 2.4, 0, Math.PI * 2); ctx.stroke(); ctx.setLineDash([]); }
|
||||
// axis in DAA at the left and right edges, and the lock line label
|
||||
if (!compact) {
|
||||
ctx.fillStyle = COL.ash; ctx.font = '400 10px IBM Plex Mono, monospace'; ctx.textBaseline = 'alphabetic';
|
||||
var first = null, last = null; blocks.forEach(function (b) { if (!b.on) return; if (!first || b.ts < first.ts) first = b; if (!last || b.ts > last.ts) last = b; });
|
||||
if (first && last) { ctx.textAlign = 'left'; ctx.fillText('daa ' + fmt(first.daa), padL, padT - 8); ctx.textAlign = 'right'; ctx.fillText('daa ' + fmt(last.daa) + (paused ? ' paused' : ''), W - padR, padT - 8); }
|
||||
}
|
||||
if (paused) { ctx.fillStyle = rgba(COL.obsidian, 0.0); }
|
||||
}
|
||||
function rr(x, y, w, h, r) { ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath(); }
|
||||
|
||||
function frame(t) { if (!running) return; if (!paused) draw(t); if (reduce || document.hidden || !inView) { running = false; return; } requestAnimationFrame(frame); }
|
||||
function start() { if (running) return; running = true; requestAnimationFrame(frame); }
|
||||
function kick() { if (!running) requestAnimationFrame(function (t) { draw(t); if (!reduce && !document.hidden && inView) start(); }); }
|
||||
|
||||
// ---- interaction ----
|
||||
function hit(ev) {
|
||||
var rect = canvas.getBoundingClientRect(), px = ev.clientX - rect.left, py = ev.clientY - rect.top, best = null, bd = 14 * 14;
|
||||
blocks.forEach(function (b) { if (!b.on) return; var dx = b.x - px, dy = b.y - py, d = dx * dx + dy * dy; if (d < bd) { bd = d; best = b; } });
|
||||
return best;
|
||||
}
|
||||
function words(b) {
|
||||
var state = b.locked ? 'locked checkpoint' : b.proven ? 'proven' : b.chain ? 'included, selected chain' : b.color === 'blue' ? 'included' : b.color === 'red' ? 'excluded' : 'pending';
|
||||
var sh = b.shards.length ? b.shards.map(function (s) { return s.state; }).join(', ') : 'no shard plan yet';
|
||||
return { title: b.hash + ' ' + state, lines: [(isMine && isMine(b) ? 'your block, miner ' : 'miner ') + b.miner + (b.miner in laneOf ? '' : ' (others lane)'), 'blue score ' + fmt(b.blue) + ' daa ' + fmt(b.daa), b.parents.length + (b.parents.length === 1 ? ' parent' : ' parents'), 'proof: ' + sh + (b.final ? ' final' : '')] };
|
||||
}
|
||||
var tip = opts.tooltip || null;
|
||||
function showTip(b, ev) {
|
||||
if (!tip) return;
|
||||
if (!b) { tip.hidden = true; return; }
|
||||
var w = words(b); tip.hidden = false; tip.innerHTML = '<b>' + w.title + '</b>' + w.lines.map(function (l) { return '<span>' + l + '</span>'; }).join('');
|
||||
var rect = canvas.getBoundingClientRect(); var x = Math.min(rect.width - tip.offsetWidth - 8, Math.max(8, b.x + 12)), y = Math.max(8, b.y - tip.offsetHeight - 10); if (y < 8) y = b.y + 14;
|
||||
tip.style.transform = 'translate(' + Math.round(x) + 'px,' + Math.round(y) + 'px)';
|
||||
}
|
||||
canvas.addEventListener('mousemove', function (ev) { hover = hit(ev); canvas.style.cursor = hover ? 'pointer' : 'default'; if (!pinned) showTip(hover, ev); kick(); });
|
||||
canvas.addEventListener('mouseleave', function () { hover = null; if (!pinned) showTip(null); kick(); });
|
||||
canvas.addEventListener('click', function (ev) { engage(true); var b = hit(ev); pinned = (pinned && b && pinned.hash === b.hash) ? null : b; showTip(pinned || hover); kick(); });
|
||||
// the wheel is never captured by default (6 Oct 2026, the owner: scrolling past the scene zoomed it): the page scrolls through the
|
||||
// scene; zoom takes ctrl or cmd with the wheel, a pinch, or a click into the scene, which engages it (opts.chip shows
|
||||
// "scroll to zoom, Esc to release"); Escape, a click outside or leaving the scene releases it
|
||||
var engaged = false, chip = opts.chip || null;
|
||||
function engage(on) { if (on === engaged) return; engaged = on; canvas.classList.toggle('engaged', on); if (chip) chip.hidden = !on; if (opts.onEngage) opts.onEngage(on); }
|
||||
canvas.addEventListener('wheel', function (ev) {
|
||||
var zoomKey = ev.ctrlKey || ev.metaKey;
|
||||
if (!zoomKey && !engaged) return; // a plain wheel or a two-finger scroll passes through to the page
|
||||
if (Math.abs(ev.deltaY) < 1) return;
|
||||
ev.preventDefault(); zoom(ev.deltaY > 0 ? 1.15 : 1 / 1.15);
|
||||
}, { passive: false });
|
||||
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape' && engaged) engage(false); });
|
||||
document.addEventListener('pointerdown', function (ev) { if (engaged && !canvas.contains(ev.target) && !(chip && chip.contains(ev.target))) engage(false); });
|
||||
var pinch = null;
|
||||
canvas.addEventListener('touchstart', function (ev) { if (ev.touches.length === 2) pinch = dist(ev.touches); }, { passive: true });
|
||||
canvas.addEventListener('touchmove', function (ev) { if (ev.touches.length === 2 && pinch) { var d = dist(ev.touches); if (Math.abs(d - pinch) > 12) { zoom(d > pinch ? 1 / 1.1 : 1.1); pinch = d; } } }, { passive: true });
|
||||
canvas.addEventListener('touchend', function () { pinch = null; }, { passive: true });
|
||||
function dist(t) { var dx = t[0].clientX - t[1].clientX, dy = t[0].clientY - t[1].clientY; return Math.sqrt(dx * dx + dy * dy); }
|
||||
function zoom(f) { var w = Math.round(Math.max(30, Math.min(300, windowS * f))); if (w === windowS) return; windowS = w; layout(); if (opts.onWindow) opts.onWindow(w); poll(); kick(); }
|
||||
function setPaused(p) { paused = !!p; if (paused) { offsetMs = 0; pausedAt = Date.now(); } else { offsetMs = 0; } if (opts.pauseButton) { opts.pauseButton.setAttribute('aria-pressed', paused ? 'true' : 'false'); opts.pauseButton.textContent = paused ? 'Resume' : 'Pause'; } kick(); }
|
||||
var pausedAt = 0;
|
||||
if (opts.pauseButton) opts.pauseButton.addEventListener('click', function () { setPaused(!paused); });
|
||||
// while paused the clock stops: offsetMs grows so the scene holds still but keeps its blocks
|
||||
setInterval(function () { if (paused) offsetMs = Date.now() - pausedAt; }, 250);
|
||||
|
||||
if ('IntersectionObserver' in root) { new IntersectionObserver(function (es) { es.forEach(function (e) { inView = e.isIntersecting; if (inView) kick(); }); }, { threshold: 0.05 }).observe(canvas); }
|
||||
document.addEventListener('visibilitychange', function () { if (!document.hidden) { poll(); kick(); } });
|
||||
if (root.matchMedia) { try { root.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () { readTheme(); kick(); }); } catch (e) { } }
|
||||
new MutationObserver(function () { readTheme(); kick(); }).observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
|
||||
|
||||
readTheme(); size(); if (opts.poll !== false) { poll(); setInterval(poll, 2000); } kick();
|
||||
return { push: push, setPaused: setPaused, zoom: zoom, engage: engage, getWindow: function () { return windowS; }, getState: function () { return { state: state, reason: reason }; }, redraw: kick, words: words };
|
||||
}
|
||||
root.IgneumDag = { mount: mount };
|
||||
})(window);
|
||||
|
|
@ -96,7 +96,7 @@ test('update wording: available, downloading with percent, installing, failed wi
|
|||
assert.equal(updateNotice(upd({ status: 'deferred' }), s()).text, 'Igneum Miner 0.3.6 is waiting for permission. It installs the next time someone is at this PC. Mining continues.');
|
||||
// updated: gone within 60 s of the new version starting
|
||||
const cur = upd({ status: 'current', available: false, ready: false, downloaded: false, updated_from: '0.3.4' });
|
||||
assert.equal(updateNotice(cur, s({ version: '0.3.6', uptime_s: 59 })).text, 'Updated to Igneum Miner 0.3.6 from 0.3.4.');
|
||||
assert.match(updateNotice(cur, s({ version: '0.3.6', uptime_s: 59, now: 1_800_000_000 })).text, /^Updated to 0\.3\.6 at \d\d:\d\d\.$/);
|
||||
assert.equal(updateNotice(cur, s({ version: '0.3.6', uptime_s: 60 })), null);
|
||||
assert.equal(updateNotice(upd({ status: 'current', available: false }), s()), null);
|
||||
// a build with no manifest reports an error the user cannot act on: no notice (the Settings note still says it)
|
||||
|
|
@ -106,22 +106,26 @@ test('update wording: available, downloading with percent, installing, failed wi
|
|||
|
||||
test('job wording: running with minutes and stage, done goes after 5 minutes, failed stays with the first error line', () => {
|
||||
const r = jobNotice(run(), NOW);
|
||||
assert.equal(r.text, 'Job: shard benchmark running, 6 min. Proving shard 3 of 8.');
|
||||
assert.equal(r.detail, 'RESULT shard=2 prove_s=39.8');
|
||||
assert.equal(jobNotice(run({ stage: '' }), NOW).text, 'Job: shard benchmark running, 6 min.');
|
||||
assert.equal(r.text, 'A job from the team is running: shard benchmark, 6 min. Proving shard 3 of 8.');
|
||||
assert.equal(r.detail, 'shard benchmark · RESULT shard=2 prove_s=39.8', 'the technical line goes behind the chevron');
|
||||
assert.equal(jobNotice(run({ stage: '' }), NOW).text, 'A job from the team is running: shard benchmark, 6 min.');
|
||||
const d = jobNotice(fin('done'), NOW);
|
||||
assert.equal(d.text, 'Job: build done after 32 min. Report uploaded.');
|
||||
assert.equal(d.text, 'A job from the team ran: build, 32 min.');
|
||||
assert.equal(d.key, 'job:done:job-6');
|
||||
assert.ok(jobNotice(fin('done'), NOW - 60 + N.JOB_DONE_S));
|
||||
assert.equal(jobNotice(fin('done'), NOW - 60 + N.JOB_DONE_S + 1), null);
|
||||
const f = jobNotice(fin('failed'), NOW);
|
||||
assert.equal(f.text, 'Job: build failed after 32 min, exit 1. Report not uploaded.');
|
||||
assert.equal(f.detail, 'BUILD FAILED: error[E0425]: cannot find value `foo`');
|
||||
assert.equal(f.text, 'A job from the team failed: build, after 32 min. The report did not upload.');
|
||||
assert.match(f.detail, /BUILD FAILED: error\[E0425\]: cannot find value `foo` · exit 1$/);
|
||||
assert.equal(f.tone, 'bad');
|
||||
assert.ok(jobNotice(fin('failed'), NOW + 86400 * 7), 'a failed job stays until closed');
|
||||
// no error line among the results: the summary is the cause
|
||||
assert.equal(jobNotice(fin('failed', { last_results: ['STAGE build'] }), NOW).detail, 'build exited with code 1');
|
||||
assert.equal(jobNotice(fin('timeout'), NOW).text, 'Job: build hit its time cap after 32 min, exit 1. Report not uploaded.');
|
||||
assert.match(jobNotice(fin('failed', { last_results: ['STAGE build'] }), NOW).detail, /^build: build exited with code 1 · exit 1$/);
|
||||
assert.equal(jobNotice(fin('timeout'), NOW).text, 'A job from the team ran out of time: build, after 32 min. The report did not upload.');
|
||||
assert.equal(N.jobWord('', 'update-now-0313-switch-d937c69d'), 'update check');
|
||||
assert.equal(N.jobWord('restart', 'restart-miners-0312'), 'restart');
|
||||
assert.equal(N.jobWord('collect', ''), 'log collection');
|
||||
assert.equal(N.jobWord('', 'ember-tune-pc1-5'), 'tuning check');
|
||||
assert.equal(jobNotice({ active: false, last: {} }, NOW), null);
|
||||
assert.equal(jobNotice(null, NOW), null);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -14,10 +14,11 @@ const V = mod.exports.View;
|
|||
|
||||
const card = (over) => ({ key: 'nvidia:0:RTX 5090', name: 'NVIDIA GeForce RTX 5090', vendor: 'nvidia', kind: 'discrete', worker: 'CUDA', vram_mb: 32768, enabled: true, state: 'mining', hash_now: 124.3, hash_avg: 120, accepted: 3, rejected: 0, identities: 8, ids: [], prepared: true, restart_in_s: 0, message: '', reason: '', power_w: 410.2, power_limit_w: 460, power_default_w: 575, power_pct: 80, power_applied: true, temp_gpu: 61, temp_mem: 72, telemetry_at: 1, ...over });
|
||||
|
||||
test('the four sections and their order (miner-ui-3)', () => {
|
||||
assert.deepEqual(V.PAGES.map((p) => p.id), ['mine', 'earnings', 'prove', 'settings']);
|
||||
test('the five sections and their order (miner-ui-4)', () => {
|
||||
assert.deepEqual(V.PAGES.map((p) => p.id), ['overview', 'cards', 'earnings', 'prove', 'settings']);
|
||||
assert.equal(V.page('cards').title, 'Cards');
|
||||
assert.equal(V.page('earnings').title, 'Earnings');
|
||||
assert.equal(V.page('nonsense').id, 'mine');
|
||||
assert.equal(V.page('nonsense').id, 'overview');
|
||||
});
|
||||
|
||||
test('a GPU row: name, kind, the numbers where known, the switch', () => {
|
||||
|
|
@ -72,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);
|
||||
});
|
||||
|
|
@ -106,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);
|
||||
});
|
||||
|
||||
|
|
@ -139,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');
|
||||
|
|
@ -159,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);
|
||||
|
|
@ -194,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');
|
||||
});
|
||||
|
|
@ -249,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');
|
||||
|
|
@ -279,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.');
|
||||
|
|
@ -296,7 +297,7 @@ 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({ 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');
|
||||
});
|
||||
|
|
@ -310,8 +311,67 @@ 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');
|
||||
});
|
||||
|
||||
test('the activity feed and the pill speak in the user\'s voice; the engine line is kept as the tooltip', () => {
|
||||
const pe = V.plainEvent('job update-now-0313-switch-d937c69d (Exec restart: re-read the manifest (exec_restart_number 27276) and restart the node with it) done after 0 min, exit 0, report uploaded: update check and install asked');
|
||||
assert.equal(pe.text, 'A job from the team ran: update check, 0 min');
|
||||
assert.match(pe.tech, /^job update-now/);
|
||||
assert.equal(V.plainEvent('updated to Igneum Miner 0.3.14 from 0.3.13').text, 'Updated to 0.3.14');
|
||||
assert.equal(V.plainEvent('consensus parameters from the signed manifest: {"difficulty_v2_activation_daa":33000}').text, 'Rule settings read from the team');
|
||||
assert.equal(V.plainEvent('node synced: 151512 blocks, 6 peer(s)').text, 'Node synced: 151,512 blocks, 6 peers');
|
||||
assert.equal(V.plainEvent('block accepted by the node (Apple M5 Max)').text, 'Block found by Apple M5 Max');
|
||||
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 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');
|
||||
assert.equal(V.plainEvent('something new the engine says').text, 'Something new the engine says');
|
||||
assert.equal(V.plainEvent('something new the engine says').tech, '');
|
||||
const base = { setup_done: true, quitting: false, mining: { state: 'mining', hash_total: 510.6, cards: [] }, node: { state: 'synced' } };
|
||||
assert.deepEqual(V.pill(base), { text: 'Mining · 511 MH/s', tone: 'on' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'mining', hash_total: 17.04, cards: [] } }), { text: 'Mining · 17.0 MH/s', tone: 'on' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'paused', hash_total: 0, cards: [] } }), { text: 'Paused', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'syncing' } }), { text: 'Syncing the node', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'waiting', hash_total: 0, cards: [] }, node: { state: 'restarting' } }), { text: 'Node restarting', tone: 'bad' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'mining', hash_total: 1, cards: [{ state: 'tuning' }] } }), { text: 'Tuning', tone: 'on' });
|
||||
assert.deepEqual(V.pill({ ...base, setup_done: false, detecting: true }), { text: 'Detecting cards', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, quitting: true }), { text: 'Stopping', tone: '' });
|
||||
assert.deepEqual(V.pill({ ...base, mining: { state: 'idle', hash_total: 0, cards: [] } }), { text: 'Not mining', tone: '' });
|
||||
});
|
||||
|
||||
test('Ember on the row (miner-ui-4): the flame fill, before -> after, the saving, the sparkline points, the plain label', () => {
|
||||
const curve = [{ clock_mhz: 0, power_pct: 100, watts: 374, mhs: 122.5, eff: 0.328 }, { clock_mhz: 0, power_pct: 80, watts: 302, mhs: 122.3, eff: 0.405 }, { clock_mhz: 0, power_pct: 60, watts: 228, mhs: 104.9, eff: 0.46, mark: 'hot' }, { clock_mhz: 2470, power_pct: 80, watts: 290, mhs: 122.3, eff: 0.422 }];
|
||||
const tuned = card({ tune_line: 'Tuned: 122.3 MH/s at 290 W (0.422 MH/W)', tune_source: 'full', tune_control: true, tune_clock_mhz: 2470, sweep_pct: 80, sweep_at: NOW - 7200, sweep_watts: 290, sweep_mhs: 122.3, tune_curve: curve, sweep_state: 'idle', sweep_note: '' });
|
||||
const e = V.ember(tuned, settings(), NOW);
|
||||
assert.equal(e.kind, 'tuned'); assert.equal(e.fill, 1);
|
||||
assert.equal(e.label, 'Tuned 2 h ago');
|
||||
assert.match(e.sub, /^2470 MHz at 80% · next check /);
|
||||
assert.equal(e.before, 374); assert.equal(e.after, 290); assert.equal(e.savingW, 84);
|
||||
assert.equal(e.savingText, 'saves 84 W, 0.16% of rate');
|
||||
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 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');
|
||||
const running = V.ember(card({ state: 'tuning', tune_step: 4, tune_steps: 9, tune_eta_s: 410, sweep_state: 'running' }), settings(), NOW);
|
||||
assert.equal(running.kind, 'running'); assert.equal(running.fill.toFixed(2), '0.33'); assert.equal(running.label, 'Tuning: step 4 of 9'); assert.equal(running.sub, 'about 7 min left');
|
||||
const measured = V.ember(card({ tune_line: 'Measured: 122.3 MH/s at 410 W (0.298 MH/W)', tune_source: 'baseline', tune_control: false, sweep_at: NOW - 600, sweep_note: 'measure only until Power control is on in Settings (Windows asks for administrator rights once)', sweep_state: 'idle' }), settings({ power_control: false }), NOW);
|
||||
assert.equal(measured.kind, 'measured'); assert.equal(measured.label, 'Measured, not tuned'); assert.equal(measured.sub, 'Power control is off'); assert.equal(measured.fill, 0.5);
|
||||
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 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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
// packaging/windows/Igneum-Miner.iss when the app version moves. Include guards, not #pragma once: rc.exe reads it too.
|
||||
#ifndef IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_H
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.14"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,14,0
|
||||
#define IGNEUM_HOST_VERSION_STR "0.3.16"
|
||||
#define IGNEUM_HOST_VERSION_RC 0,3,16,0
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -2544,6 +2544,35 @@ task now runs ...Programs\Igneum Miner\igneum-app.exe --power-helper"), then the
|
|||
task with no prompt ("-pl 460: set to 460.00 W from 575.00 W", "-pl 160: set to 160.00 W from 100.00 W"). Task
|
||||
Running, Highest, user Admin. Cards: 5090 221 W at 1845 MHz, 4070 75.8 W at 1860 MHz, 9070 XT 202 W, all mining
|
||||
through the installed app. Ember closed 16:53Z.
|
||||
### 6 October 2026, 17:55Z to 17:57Z, the first segment above the restart cut from a snapshot-restored node on the new export (Mac)
|
||||
|
||||
A copy of node 1's data dir started on the shipper's 0.3.14 Mac binary (target-0314, release-0.3.14-node 4c6b129d) with
|
||||
`--igneum-exec-snapshot=node1-copy-snapshot.bin,0xac101f13...` (the hands' recovery file, tip 130,272) and node 1 as its
|
||||
peer: IBD of 8,049 blocks in 29 s, the executor resumed from the file and reached the tip 140,023 with no restart replay.
|
||||
`igneum_exportSegments [139951, 139959]` answered in under 0.1 s with 9 segments, `preState` 79 accounts, `execRestart`
|
||||
27,276. The branch exporter (exec-app-0314 bfebff1) cut all 8 blocks of segment 139,952..139,959 in under 0.1 s each:
|
||||
"replayed 8 segments from the account dump; every state root equals the node's". The first block's fixture carries
|
||||
pre-state root 0x64304441... and post-state root 0x60d60bba..., equal to node 1's eth_getBlockByNumber roots at 139,951
|
||||
and 139,952 (an independent node). Consequence: a prover on a snapshot-restored node (every hand, nine fleet boxes) cuts a
|
||||
provable fixture again with the 0.3.14 app and exporter; what is left for a paid record is the proof itself (the PCs or
|
||||
the fleet, not this Mac).
|
||||
|
||||
### 6 October 2026, 18:11Z to 18:13Z, the PC shape over p2p (Mac, `tools/lock/with-lock.sh run`)
|
||||
|
||||
Fork 1255c0f9. `node tools/exec-sync/net.mjs`: 18/18 in 117.5 s. Case 7: a node whose data dir holds the tip-0 pair the 0.3.13
|
||||
genesis replay left behind, with `--igneum-exec-snapshot=peer`, sets the pair aside (.bin.tip0), asks A over p2p and loads
|
||||
A's state with no hand action; its state root at 60 equals A's. `reorg.mjs` 18/18 in 262.1 s on the same binary; the exec
|
||||
suite 20. Consequence: a PC or a box left with the tip-0 pair recovers on its own once a hand serves (the 5-minute
|
||||
no-progress rule fires the ask even when the error text is new); until 0.3.14.1 the recovery file by hand is the way.
|
||||
|
||||
## 6 October 2026, 18:18Z: the shipped 0.3.14 app asked to tune itself on PC 1 (ember-installed-pc1-1): FAILED, no tune ran
|
||||
|
||||
Gate ok (0.3.14, Power control on, the task on the installed exe). Every card's "after" was its 17:55Z failure from
|
||||
the app's own first tune: 5090 and 4070 "did not take within 30 s (card reports 460 / 160 W, acknowledged true)",
|
||||
9070 XT "the card refused the setting"; the three "Tune now" requests sat queued behind the hour's back-off. Five
|
||||
causes (A to E) and their fixes in the plan's 0.3.14 window section; the window repeats on 0.3.15. The cards kept
|
||||
mining at the driver's held locks: 5090 222.9 W at 1845 MHz, 4070 75.9 W at 1860 MHz, 9070 XT 201 W. Lever-2 TUNING
|
||||
records during the window: 5090 135.6 MH/s at 236 W (0.574 MH/W), 4070 31.1 MH/s at 79 W (0.393).
|
||||
|
||||
## Prover tiers on real cards: the rented fleet, 6 October 2026 (branch gpu-fleet)
|
||||
|
||||
|
|
|
|||
|
|
@ -296,8 +296,38 @@ record. Known in 0.3.13 on PC 1 until 0.3.14: the rows show the app's own earlie
|
|||
to receive the run's result), and the 80% caps are the app's setting; the draw is the tuned one regardless because
|
||||
the driver holds the clock locks and the caps do not bind.
|
||||
|
||||
### The 0.3.14 window (6 October 2026, 18:18 to 18:27Z): no tune ran; five causes
|
||||
|
||||
The shipped 0.3.14 app, Power control on, the task registered, asked over its own API to tune each card
|
||||
(`relay/playbooks/ember-installed-tune-pc1.ps1`). From its log and helper.log:
|
||||
|
||||
| | When (UTC) | What | Cause | Fix (0.3.15) |
|
||||
|---|---|---|---|---|
|
||||
| C | 17:55:25 | the app's own first 5090 tune wrote its first request in the second the helper started; the helper skipped it as "present at my start"; 30 s later "did not take (card reports 460 W, acknowledged true)" | the engine wrote before the helper was up | the helper writes a heartbeat (`helper.alive`) every poll; the engine starts the task and waits for a fresh heartbeat BEFORE writing (`powertask::ensure_running`) |
|
||||
| A | 17:55:55 | the 4070's helper start read the stale `quit` from the 5090's abort and exited in the same second | quit/remove matched before the sequence check | the helper acts only on lines added after its start (`commands_after`) |
|
||||
| D | 17:57:27 | the 9070 XT, measure only ("igneum-gpu-telemetry gave no tune line": the 3.0 coordinator's job was on the ADLX helper), was still sent a set; refused; "tuning stopped" | a measure-only card was set | `ember::may_set`: measure only sets nothing |
|
||||
| E | 18:18 to 18:27 | three "Tune now" requests sat "queued" behind the hour's back-off from the 17:55 failures; none started | `sweep_retry` blocked a forced request silently | a forced request clears the back-off; a held row says "tuning: retry in N min (why)" (`ember::retry_note`) |
|
||||
| B | (latent) | the engine's helper flag stayed true after the helper's idle exit; a later step would have been acknowledged after a blind 4 s sleep | a flag instead of a fact | the heartbeat decides; the acknowledgement is the helper's own log line for the sequence (up to 15 s), a missing line fails the step with that reason |
|
||||
|
||||
What a user on 0.3.14 sees: Power control on, no prompt, and no card tunes (the rows keep the baseline or
|
||||
"tuning stopped"); the lever-2 TUNING records still flow. 0.3.15 closes it; the window repeats there.
|
||||
|
||||
## 8a. Next-cut notes (for the 0.3.12 shipper)
|
||||
|
||||
For 0.3.16 (Miner UI 4's Cards tab, Ember as its second layer), on ember-tune past the 0.3.15 tip 7fadd4c:
|
||||
|
||||
| Commit | What | Where |
|
||||
|---|---|---|
|
||||
| (past 7fadd4c) | a per-card goal: `POST /api/tune/goal {key, goal}` (efficiency, balanced, rate; "" or "global" clears), persisted as `CardPref.tune_goal`, echoed as `card.tune_goal` ("" = the global goal), resolved at the tune's start (`ember::goal_for`); the untuned point of the last full plan on the row: `card.tune_before_watts`, `card.tune_before_mhs` (its step 0, kept across confirm plans; `CardPref.sweep_before_*`), so the row can read "saves 84 W, 0.15% of rate" against `sweep_watts` / `sweep_mhs`; Retune = `POST /api/sweep/start {key}` (forced: clears a back-off since 0.3.15) | ember.rs, engine.rs, server.rs, config.rs, state.rs, hotplug.rs + test |
|
||||
|
||||
For the 0.3.15 cut (6 October 2026 evening), on ember-tune:
|
||||
|
||||
| Commit | What | Where |
|
||||
|---|---|---|
|
||||
| a8cab23 | (A) the helper acts only on lines added after its start; the acknowledgement is the helper's log line for the sequence | powertask.rs `commands_after` + test, engine.rs |
|
||||
| (tip) | (B, C) the heartbeat, `ensure_running` before every task-path write; (D) `may_set`; (E) the back-off yields to a forced request, `retry_note`; the window playbook's gate at 0.3.15; `ember-tune-pc1.ps1` deleted (superseded by the installed-tune playbook) | powertask.rs, engine.rs, ember.rs + tests, relay/playbooks |
|
||||
| e70ac5e | every task command goes to the helper's fixed folder | platform.rs, powertask.rs, engine.rs, main.rs + test |
|
||||
|
||||
For the 0.3.14 cut (6 October 2026, after run 6), on ember-tune:
|
||||
|
||||
| Commit | What | Where |
|
||||
|
|
|
|||
|
|
@ -275,3 +275,35 @@ the quit ask, paused, syncing, the clock, the update card, the job strip, the lo
|
|||
| A Windows and a PC screenshot of the row with a real NVIDIA card (the telemetry cells, the cap slider, Retry) | the 0.3.14 cut, first thing to look at |
|
||||
| The row's state word while tuning carries the meta after it ("tuning: step 7 of 9 · measuring 441 W · 32 GB · ...") and is cut on a narrow row | a later pass: drop the meta while tuning |
|
||||
| The merge with `ember-tune`: `app.js` takes this branch's file (it already carries ember-tune's UI words and test); the engine files merge by themselves | the 0.3.14 shipper |
|
||||
|
||||
## 14. Polish round (6 October 2026, after the 0.3.14 cut)
|
||||
|
||||
the project lead, on the live Mac app: "I do think this could look better", at the job strip reading "Job: 0.3.14 is published:
|
||||
re-read the manifest and install now (...) done after 0 min. Report uploaded." Branch rebased on master 630b537.
|
||||
|
||||
| What | Where |
|
||||
|---|---|
|
||||
| No raw job, manifest or relay text on a user surface. The strip reads "A job from the team ran: update check, 0 min." / "A job from the team is running: shard benchmark, 6 min. Proving shard 3 of 8." / "A job from the team failed: build, after 32 min." / "Updated to 0.3.14 at 18:52."; the technical line (the job's title, summary, RESULT line, exit code) sits behind a chevron; the × stays. `Notices.jobWord` maps a kind or an id prefix to a user's word (update check, restart, log collection, shard benchmark, build, tuning check, a script). | app.js Notices |
|
||||
| The Activity feed speaks in the app's voice: `View.plainEvent` maps every engine event string (`src/engine.rs` et al., 80 patterns) to a sentence ("Block found by Apple M5 Max", "Rule settings read from the team", "The node stopped (code 101); restarting", "A job from the team ran: update check, 0 min"); the engine's own line is the row's tooltip; the kind word becomes a coloured dot. Unknown lines keep their words with a JSON tail cut. | app.js View, renderMine |
|
||||
| The header: title, grey subtitle and the pill share one baseline; the pill is a sentence with a coloured dot: "Mining · 511 MH/s" (molten), "Tuning" (molten), "Paused", "Syncing the node", "Starting the worker", "Not mining" (grey), "Node restarting", "Node failed", "Engine not answering" (ember), "Stopping", "Detecting cards", "Setting up". `View.pill`. | app.js, app.css |
|
||||
| Three Mine layouts were offered behind `?variant=a|b|c` (quieter, denser, hero number); the project lead picked C, then ruled the page stays as shipped (section 15). The switch is gone. | app.css |
|
||||
| 390 px: the strip's text, chevron and × on one line. Light and dark checked. | app.css |
|
||||
|
||||
Screenshots: `p04`/`p05` the plain job strip closed and open, `p06` the updated strip,
|
||||
`p07` the header paused, `p08` the activity feed, `p09`/`p12` light, `p10`/`p11` 390 px. Tests: 36 pass.
|
||||
|
||||
## 15. the project lead's ruling: the banner only (6 October 2026, evening)
|
||||
|
||||
the project lead: "lets not actually change the miner page just sort the banner". Variant C and the Earnings reshaping are
|
||||
reverted; the Mine and Earnings screens are exactly as shipped in 0.3.14. The three variant screenshots are gone with
|
||||
them. What stays from the polish round:
|
||||
|
||||
| Kept | Where |
|
||||
|---|---|
|
||||
| The strip's look: the row surface (one step lighter than the page), a 1 px ember hairline along the top, ash text, ember only on the state dot and the chevron; the update strip and the job strip share the one `.notice`; the asks and the clock card carry the same hairline. No filled brown or mustard field anywhere (the rail's active item, the pill, the ghost-on button, the option, the stop ring, the log hit and `mark` lost their tints; only `::selection` keeps a colour). | app.css |
|
||||
| The strip's words: "A job from the team ran: update check, 0 min." / "Updated to 0.3.14 at 18:52.", the technical line behind the chevron, the × kept | app.js Notices |
|
||||
| The Activity feed in the app's voice (`View.plainEvent`, the engine line as the tooltip, a kind dot) | app.js View, renderMine |
|
||||
| The header: title, subtitle and pill on one baseline; the pill a sentence with a coloured dot (`View.pill`) | app.js, app.css |
|
||||
|
||||
Screenshots: `r01-mine-strip-dark.png`, `r02-mine-strip-light.png` (Mine as shipped, the job strip showing),
|
||||
`r03-earnings-dark.png`. Tests: 36 pass.
|
||||
|
|
|
|||
BIN
docs/plans/miner-ui-3/p04-strip-job-done-plain.png
Normal file
|
After Width: | Height: | Size: 195 KiB |
BIN
docs/plans/miner-ui-3/p05-strip-job-done-open.png
Normal file
|
After Width: | Height: | Size: 211 KiB |
BIN
docs/plans/miner-ui-3/p06-strip-updated.png
Normal file
|
After Width: | Height: | Size: 226 KiB |
BIN
docs/plans/miner-ui-3/p07-header-paused.png
Normal file
|
After Width: | Height: | Size: 215 KiB |
BIN
docs/plans/miner-ui-3/p08-activity-plain.png
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
docs/plans/miner-ui-3/p09-light-mine.png
Normal file
|
After Width: | Height: | Size: 302 KiB |
BIN
docs/plans/miner-ui-3/p10-phone-mine.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
BIN
docs/plans/miner-ui-3/p11-phone-earnings.png
Normal file
|
After Width: | Height: | Size: 133 KiB |
BIN
docs/plans/miner-ui-3/p12-light-earnings.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
BIN
docs/plans/miner-ui-3/r01-mine-strip-dark.png
Normal file
|
After Width: | Height: | Size: 282 KiB |
BIN
docs/plans/miner-ui-3/r02-mine-strip-light.png
Normal file
|
After Width: | Height: | Size: 284 KiB |
BIN
docs/plans/miner-ui-3/r03-earnings-dark.png
Normal file
|
After Width: | Height: | Size: 197 KiB |
190
docs/plans/miner-ui-4-copy.md
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
# 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). Main's correction (20:0x UK): the chain's own objects keep the words of the site, the litepaper and the ledger,
|
||||
with a grey explanation where they first appear: shard (not piece), segment (not run of blocks), checkpoint and
|
||||
"locked checkpoint" (not lock point), aggregator (not combiner), verifying (not checking proofs); "network blocks (the
|
||||
DAA score)" stays. The rows below that renamed those words are superseded by this correction. 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 | off: relay only / Every block is turned into a short proof. The chain hands pieces to your cards | off, passing proofs along / Every block is turned into a short proof, in pieces called shards. Your cards prove shards and earn IGN for each one (shard, segment and aggregator keep their names, each explained once) |
|
||||
| Node details | DAA score / rules digest / next rule switch / last lock | network blocks (the DAA score) / rules fingerprint / next rule change / last checkpoint (a point the miners agree can never be undone) |
|
||||
| Settings | Vote on finality checkpoints / Trust proof records without verifying them / Hill climb | Vote on checkpoints / 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"; "fleet prior" became "what other miners found". Shard, segment, checkpoint and aggregator stay (the
|
||||
chain's own words), each with a grey explanation where it first appears. 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, |
|
||||
68
docs/plans/miner-ui-4.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Igneum Miner UI 4: Overview, Cards, Ember on the row
|
||||
|
||||
6 October 2026, evening. the project lead: "add Cards under its own tab, and have Overview and the cool mining demo on the main
|
||||
page with the revamped C option", then "incorporate Ember into the Cards tab beautifully", then "I would like the new
|
||||
miner UI to go into .15". Branch `miner-ui-4` from the 0.3.15 tip `6ea9497` (untouched), worktree
|
||||
`../igneum-wt-miner-ui-4`. Rust: one route (`GET /live-dag.js`, `server.rs`), nothing else.
|
||||
|
||||
## 1. The rail
|
||||
|
||||
Overview, Cards, Earnings, Prove, Settings; Logs and Quit in the foot; a bottom tab bar under 720 px.
|
||||
|
||||
## 2. Overview
|
||||
|
||||
| Block | What |
|
||||
|---|---|
|
||||
| The hero (the project lead's pick, C) | the fleet rate at 84 px on a graphite-to-obsidian fade, W with MH/W, £ a day at the user's price, blocks (this run, lifetime) beside it; Start/Stop as the full-width bar under it with its state sentence at the right |
|
||||
| The chain, live | the site's `live-dag.js` (a byte copy at `app/igneum-app/ui/live-dag.js`, served by the engine at `/live-dag.js`), mounted on the engine's own `api/live` with `mine: 'you'` so this machine's lane reads "you" and its blocks take a molten ring; the legend words in HTML under the canvas: yours, pending, included, excluded, proven, locked; Pause; a tooltip per block. The engine's `/api/live` is OWED (it maps the local node's RPC into the site's reply shape: `{ok, state, blocks[{hash, ts, blue_score, daa, parents, chain, color, miner, locked, final, shards, proven}], finality.checkpoints}`); until it exists the module reports `failed` and the app's own 10-minute blocks strip draws in its place with the note "The full chain view needs the next engine update". |
|
||||
| The node line | as 0.3.15, Details under it |
|
||||
| Activity | the event feed in the app's voice (the strip canvas moved into the chain card as the fallback) |
|
||||
|
||||
## 3. Cards
|
||||
|
||||
| Block | What |
|
||||
|---|---|
|
||||
| The Tuning strip | the goal control (Efficiency, Balanced, Maximum) with "about £X a day at Y W"; the fleet saving ("Ember saves 84 W, £0.56 a day over 1 tuned card", `View.fleetSaving`); Tune all; the schedule line; Power control only when an NVIDIA card has it off |
|
||||
| The rows | as 0.3.15 (state word with its reason, MH/s, W with MH/W, £ a day, °C, the switch, the chevron) with Ember as the second layer |
|
||||
| The flame mark | the brand mark's two polygons, 16 px, outline when not tuned, filling bottom-up with step k of n while tuning (molten), solid ember when tuned, half grey when measured only |
|
||||
| The Ember line | "Tuned 2 h ago · 2470 MHz at 80% · next check Sunday · saves 84 W, 0.16% of rate"; "Tuning: step 4 of 9 · about 7 min left" with the bar; "Measured, not tuned · Power control is off"; "Not tuned yet · starts after 2 min of steady mining"; the fleet pause and a stop in the same place |
|
||||
| The W cell | before → after when tuned (`tune_before_watts` → the live draw) |
|
||||
| The MH/W cell | a 44 px sparkline of the ladder's MH/W per step, the chosen step as a dot |
|
||||
| Tune / Retune / Stop | the row's button; Retune once tuned |
|
||||
| The chevron | the cap slider, the applied or Retry line, memory and clocks, the card's own goal (As the fleet, Efficiency, Balanced, Maximum: `POST api/tune/goal {key, goal}`, "" clears; a build without it gets a toast), Retune, the Tuned line, the curve (W across, MH/s up, every step a dot, the chosen ringed, a rejected one hollow), identities, the facts |
|
||||
|
||||
Fields read (Ember Tune 5429e82, on the 0.3.16 lane): `tune_before_watts`, `tune_before_mhs` (step 0 of the last full
|
||||
plan; 0 = never; the UI falls back to `tune_curve[0]` only for a full plan on an older build and claims no saving for a
|
||||
confirm or baseline plan), `sweep_watts`, `sweep_mhs`, `sweep_eff`, `tune_curve`, `tune_goal`, `tune_step`,
|
||||
`tune_steps`, `tune_eta_s`, `sweep_state`, `sweep_note`, `tune_floor` (asked for). `View.ember` and `View.beforeOf`
|
||||
carry the rules; `view.test.mjs` covers every state.
|
||||
|
||||
## 4. Screenshots (the mock on port 4318 with the rig state, the 5090 tuned, the 4070 at step 4 of 9)
|
||||
|
||||
`docs/plans/miner-ui-4/u01-overview-dark-1440.png`, `u02-overview-light-1440.png`, `u03-cards-dark-1440.png` (the
|
||||
5090's details open: cap, goal, Retune, the curve), `u04-cards-light-1440.png`, `u05-overview-dark-390.png`,
|
||||
`u06-cards-dark-390.png`, `u07-overview-light-390.png`, `u08-cards-light-390.png`, `u09-overview-mac-dark.png`.
|
||||
The chain scene in the shots is the mock's `/api/live` (four lanes, one block a second, "you" ringed); on a real
|
||||
0.3.15 engine the fallback strip shows until `/api/live` lands.
|
||||
|
||||
## 5. Owed
|
||||
|
||||
| What | Who |
|
||||
|---|---|
|
||||
| `GET /api/live` on the engine: the local node's blocks over the window, mapped into the site's shape, this machine's vote keys as the lane "you" | engine |
|
||||
| `card.tune_floor` | Ember (said yes) |
|
||||
| `watts_total`, `pounds_per_day` for the tray tip; `address.balance_wei`; `price_gbp_per_ign` | engine (from miner-ui-3) |
|
||||
| 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.
|
||||
BIN
docs/plans/miner-ui-4/u01-overview-dark-1440.png
Normal file
|
After Width: | Height: | Size: 537 KiB |
BIN
docs/plans/miner-ui-4/u02-overview-light-1440.png
Normal file
|
After Width: | Height: | Size: 561 KiB |
BIN
docs/plans/miner-ui-4/u03-cards-dark-1440.png
Normal file
|
After Width: | Height: | Size: 487 KiB |
BIN
docs/plans/miner-ui-4/u04-cards-light-1440.png
Normal file
|
After Width: | Height: | Size: 304 KiB |
BIN
docs/plans/miner-ui-4/u05-overview-dark-390.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
docs/plans/miner-ui-4/u06-cards-dark-390.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
docs/plans/miner-ui-4/u07-overview-light-390.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
docs/plans/miner-ui-4/u08-cards-light-390.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
BIN
docs/plans/miner-ui-4/u09-overview-mac-dark.png
Normal file
|
After Width: | Height: | Size: 460 KiB |
167
docs/plans/release-0.3.15.md
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
# Igneum Miner 0.3.15: class v4 signalling, generator-4 workers, miner-ui-4
|
||||
|
||||
Cut 6 October 2026, staged by the release engineer under the coordinator; the live cut waits for the Devnet 2 gate's PASS and the project lead's go. Worktree `igneum-wt-ship0315` (branch `release-0.3.15`), node `release-0.3.15-node` 713ef876 in `vendor/igneum-node-0315`.
|
||||
|
||||
## 1. Why
|
||||
|
||||
- The class v4 program (`mx8+sh256x27`, Counter ASIC 3.0) can now be signalled: the node reads `program_class_v4_activation_daa` and `program_class_v4_signal_window_daa`, flips the class at the first epoch whose 86,400-DAA window has 95 percent of blue blocks signalling, or at the floor. Both fields enter the consensus digest only once set, so this binary peers with 0.3.14 on today's file.
|
||||
- Every GPU worker is the generator-4 one, built from this tree: a worker from 0.3.14 refuses a generator-4 pack and stops dead at the flip (the rehearsal of 6 October showed it on 52 boxes).
|
||||
- The miner app's new Overview and Cards (miner-ui-4): the big-number hero, the chain scene, Ember Tune woven into every card row, the chain's own words with one grey explanation each.
|
||||
- Ember Tune tunes through the Power Helper: the helper acts only on commands after its start, heartbeats before any command, acknowledges by its own log line.
|
||||
- The Windows payload carries the Linux prover pair for WSL2 and refuses to build without it; every PC had run a stale host and exporter since 0.3.5.
|
||||
- An update never installs while a remote job is active, urgent or not; an asked install is spent by the check it asked for (PC 1, 17:52:54Z).
|
||||
- The exec layer names a stale exporter by path; the node says at start when the verifier host is older than proving v1 (`igneum_getProvingStatus.verifierStale`); a tip-0 snapshot pair is never resumed from, and a blocked follower asks a peer for its state within five minutes.
|
||||
- The devnet hand nodes run under launchd; the app's default devnet peers include the build box's hand nodes.
|
||||
|
||||
## 2. The order (two publishes, the worker rule)
|
||||
|
||||
1. **Publish 1** (binaries, the live THIRTEEN-field object unchanged, digest b18ed271): the fleet's miners by script, PC 1, PC 2 and the Mac by `update-now`, the hands and the seed last. No window: a 0.3.15 node on the thirteen-field file peers with 0.3.14 (measured: same digest).
|
||||
2. **Publish 2** (the SIXTEEN-field object: the thirteen plus `exec_restart_state_root`, `program_class_v4_activation_daa` = the floor at publish DAA + 14,400 rounded up to the epoch (at least 10,800 ahead), `program_class_v4_signal_window_daa` = 86,400): only when every NODE reports 0.3.15 AND every WORKER is the 0.3.15 one. A 0.3.14 node exits at parse on this file ("unknown field program_class_v4_activation_daa"); a 0.3.14 worker stops at the flip. Miners first, the hands and the seed last. The digest moves once, read from the 0.3.15 node on the exact file at the publish (2c1162e2 at floor 226,800; 1dddfa55 at 219,600; another floor moves it).
|
||||
3. The hands move from the Mac to igneum-build-1 on the "0.3.15 live" line (the build-server agent's move-hand.sh, observer first, one hand down at a time).
|
||||
|
||||
## 3. Digests (0.3.15 Mac binary, no suffix, no peers)
|
||||
|
||||
| Object | Digest |
|
||||
|---|---|
|
||||
| the live thirteen-field file | b18ed271f75dd46406d230f4156c37472127415a4c32c558bac662f6f840e61c (equal to 0.3.14) |
|
||||
| fourteen fields (the exec pin 0xed27bb2d...) | 23e7693634a9b8b3c0643f16c8a01e4cf057985c7e0e0e044f70bc919a3c0fef |
|
||||
| sixteen fields, floor 219,600, window 86,400 | 1dddfa5574d5536109a5ae68dc415b55e954bd11208608440845e19670505871 (the node lane's pinned test value) |
|
||||
| sixteen fields, floor 226,800 (DAA 209,458 at 19:47Z) | 2c1162e2ccf34531bf3af131d21c36e857e3e99e265969e4b094bd7421a1d74e |
|
||||
| the 0.3.14 binary on the sixteen-field file | refused at parse, exits |
|
||||
|
||||
## 4. Builds
|
||||
|
||||
| Piece | Where | Commit | sha256 | Size |
|
||||
|---|---|---|---|---|
|
||||
| Linux igneumd (fleet, seed) | igneum-build-1, glibc 2.39 | 713ef876 | 06211d55b8cce1c19b31b218f5b61f42389a5ceaf68e0d27781b2e10e33b3f2a | 49,719,776 |
|
||||
| Linux igneumd (HiveOS, glibc 2.36) | Mac zigbuild | 713ef876 | 1e51bfb6401e2d86022eeaddf03750a72d6eb200fa879b7c58fb5c3afbf38a50 | 48,434,344 |
|
||||
| Linux igneum-miner (HiveOS) | Mac zigbuild | 713ef876 | c5b489105d933b01e4dceff8dc31e2db8e9aa53de06dddafc6eb12ee85b8f7a6 | 9,998,576 |
|
||||
| Windows igneumd.exe | igneum-build-1 cross | 713ef876 | d08be1a72082880e91dceb0f34e4337663c9494ccc62eb108d7c3a274801e928 | 51,846,656 |
|
||||
| Windows igneum-miner.exe | igneum-build-1 cross | 713ef876 | e251bd2b3146830ae8c22627b6b37eba3428d1ba64fbcbd938aa0a36c7916fe3 | 11,129,344 |
|
||||
| Mac igneumd | Mac | 713ef876 | 7f0948d95996eb00f8f574dcaabc25b3ad8a407e08eac3b85055fd15abd0139f | 41,819,824 |
|
||||
| igneum-worker-cuda.exe (generator 4) | Mac mingw | 563485b tree | 14b6637e1d1c9e83311f2cf4e8ca76fb5ff8e42336b5e09b2b89165da55ea00e | 1,537,024 |
|
||||
| igneum-worker-opencl.exe (generator 4) | Mac mingw | 563485b tree | 0cbb65f0cadaa560037175d81d829ad7d50fcbdcf8a510d382681468f32f7ce8 | 479,232 |
|
||||
| igneum-gpu-telemetry.exe | Mac mingw | 563485b tree | f7d924a7ba9ccd6f66fd7eee99f04835f52903456c4789f2c599e88fb02e2d37 | 387,584 |
|
||||
| hive igneum-worker-cuda (generator 4) | Mac zig | 563485b tree | 97e036e23f4ace669b68b85980701ffe4e32017cdfbf28603bf992802a8b9828 | 6,756,480 |
|
||||
| hive igneum-worker-opencl (generator 4) | Mac zig | 563485b tree | ef9fbd03e7ce14d1b2b9b85964397b244dc153edee0607d71f26dbc640a4c5e6 | 307,328 |
|
||||
| igneum-prove-host (Linux, cuda, WSL2) | igneum-build-1 | master e1b5bc9 | 71bc2438856bb141f6cad3d18489f708568144fad5a06a002fbadefb9ce256f9 | 73,161,528 |
|
||||
| igneum-prove-export (Linux, WSL2) | igneum-build-1 | master e1b5bc9 | 263bf4cef70af4a13a45b2e79b8dbab373282ea4c571f624d02ddb5791935361 | 3,609,360 |
|
||||
| Igneum-Miner-0.3.15.dmg (miner-ui-4 872f1c9) | Mac | af36aaa | 90bdf8c8de965961e6b365ee8664bce3efe7873f45f7bc27793b692804fe42f5 | 41,961,932 |
|
||||
| igneum-hive-0.3.15.tar.gz | Mac | | 1715e58ea1d4a1ed1b733881de51b8754ca975f0359b3b29ecb49061d02124c7 | 24,713,232 |
|
||||
|
||||
Rebuilt on the node lane's version-gate commit 17c60367 (the signal stamp and read gated on publish 2's object), 20:0xZ: Linux igneumd 0f1c0ddec8e81256... 49,720,288, igneum-miner ebc392fc4be5f873... 9,979,224 (box); Windows igneumd.exe f764d2080b9b12bde6163abf0252fb8cf195d72fbb0e77e180eb6ee8f5d91a35 51,854,336, igneum-miner.exe 4cf260160722787df29e569dcb66f3743f4c493a7a97dab530e0db990bd5599e 11,129,344 (cross); Mac igneumd 7966152f027c65a66f2573f7e05e78c781be44bd8c52884d7ed9af6d4e2c9d22 41,819,904. Digests unchanged (b18ed271 / 23e76936 / 1dddfa55 at 219,600). Suite: kaspa-consensus 97 + the recorded flake alone, igneum-exec 20, igneum-miner 18, kaspa-pow 15, p2p-flows 33. Superseded by the combined commit (version gate + the sync-panic fix) when it lands; the DMG, the hive package and the inputs rebuild on that one.
|
||||
|
||||
**Final node 7961c5f1** (17c60367 the version gate + 7961c5f1 the pruned-node sync fix), built 20:10Z to 20:15Z with master's reproducibility exports: Linux igneumd 6615571ced35f694fa016e9b36439b29d83cd6e7c1df49fc11a9d4bd0236cba6 49,719,840, igneum-miner 3976c1b270761218298105dd256123381894c6ec7b380a43c4a702e486fe7bf4 9,979,992 (box); Windows igneumd.exe 30b14d174eb5615989e2117425c59a8e59d64e89a7298cab2b3fda15c778534e 51,894,272, igneum-miner.exe 16e9991f27e67995b416e0860f732f354f4d8b2982f46a8268bf91512778aaa1 11,130,368 (cross); Mac igneumd c0a29bcadd782c65770c1d3942a85629158b4178605c2ae3c68ca33e39cd18e2 41,803,088, igneum-miner 414720ed...; HiveOS igneum-hive-0.3.15.tar.gz 79aa96fb12d34948d568af6a2785ee6fff090f216e8df318cfc328d71181e886 (glibc 2.36 node from this commit); Igneum-Miner-0.3.15.dmg ab925203c4e0615f4de9c75f284590366f942ed848505e4338524ad48a315436 41,940,411 (miner-ui-4 872f1c9); Igneum-Wallet-0.1.5.dmg bf37a4ec1b682de86032b24ed2f271149e794177e80880f74dffed54d9a8da71 20,132,824. Digests b18ed271 / 23e76936 / 1dddfa55 at 219,600. Suite: kaspa-consensus 99 pass 0 fail, consensus-core 111, igneum-exec 20, igneum-miner 18, kaspa-pow 15, p2p-flows 33. Inputs pushed 20:13:40Z (node_source_commit 7961c5f1); windows.yml run 37525228879 on c25a3ca (after the Windows-path rename).
|
||||
|
||||
**Final node f1ea7a38** (the five fix commits: 791ff22c, 713ef876, 17c60367, 7961c5f1, f1ea7a38 = the header-version rule on the receive side gated like the stamp), rebuilt 20:42Z to 20:46Z: Linux igneumd 7f0bde70cf2e72a3a9479ba6421f2b37162c3dd4391bf413717c99e6f168668d 49,711,008, igneum-miner 3976c1b2... (box); Mac igneumd and igneum-miner in the log; Igneum-Miner-0.3.15.dmg 041099c2a77f3f7ff145c9a88afc0160e6f5e2951bda0ae46882fbc0259370f5 41,936,114; igneum-hive-0.3.15.tar.gz b710ac596c31d811...; Igneum-Wallet-0.1.5.dmg daf259f272934f0c1a8155ef68762c344164ae8a77c8d0621d0fceed0fb163fa 20,122,390. Digests unchanged (b18ed271 / 23e76936 / 1dddfa55 at 219,600). Node lane's box line: kaspa-consensus 99/0, consensus-core 111 + 7. The compat gate with a POISONED peer (node-compat.mjs --poisoned, 20:42:55 to 20:45:42Z): PASS, the new node refused the poisoned blocks 12 times with 0.3.14's exact line and relayed nothing, the old hub's chain holds no 1026 block, 180/180/180/180/180 on every clean node, the new node mined 72 accepted blocks. Windows cross on f1ea7a38: igneumd.exe 181e10fd4b1ad0c259122d45abac018082a054c3836431f6802f0532a8ee1a43 51,894,272 (carries f1ea7a38), igneum-miner.exe 16e9991f... unchanged; inputs pushed 20:59:15Z; windows.yml run 37530680431 on 24132fb. Two earlier dispatches (37529276665, 37529597068) were cancelled: master's tools/cross-remote.sh had its defaults line swallowed by a trailing comment since the slots merge, so a default cross-build died under bash 3.2 ("CARGO_ARGS: unbound variable", then "OUT") and the chain silently kept 7961c5f1's exes while the inputs said f1ea7a38 (fixed on master 36e4ee7 with tools/ci/defaults-line-check.sh; merged here). Rule for me: an artefact's commit string is read before it is pushed anywhere (the inputs step now prints it; the installer check too). The Mac igneumd on f1ea7a38: 85ac04df27b342f5cd225762a940400bb6142401f33b41fc7ea8ea462508ded1 41,819,648. Suite on the box on f1ea7a38: kaspa-consensus 99/0, consensus-core 111 + 7, igneum-exec 20, igneum-miner 18, kaspa-pow 15, p2p-flows 33.
|
||||
|
||||
**Final Windows on f1ea7a38** (windows.yml run 37530680431 on 24132fb, 18 of 18 steps, 21:07Z): Igneum-Miner-Setup-0.3.15.exe 99ec575ccbeef2e0fd1ff5c4ce1646905cb5e88efe31f71eb3058337641b3ec7 62,652,179; igneum-windows-app.zip a379bc446d55b8d659d72e7fd403749cb9d6d751dd9a39a8be2672cbfd1c7ecd 90,511,038 (its igneumd.exe 181e10fd carries f1ea7a38; wsl2/bin/SHA256SUMS names the prover pair 71bc2438 / 263bf4ce). Final manifest 0.3.15: mac 041099c2 + windows 99ec575c, the thirteen-field object, signed and verified locally; the site index Mac/Windows/HiveOS at 0.3.15 (hive b710ac59). Deploy on the retry's PASS.
|
||||
|
||||
Earlier Windows (windows.yml run 37525228879 on c25a3ca, 18 of 18 steps, 20:24:00Z; superseded): Igneum-Miner-Setup-0.3.15.exe 9db842fce524b2a45978f0de6060923ed67144204131724d517b9e1a063695c9 62,663,008; igneum-windows-app.zip 65d463c305d23128896e33c10cca9c6dc6ccac12ba829ed1890d3d4a2acd9240 90,518,322 (the Linux prover pair inside: wsl2/bin/igneum-prove-host 73,161,528 with SHA256SUMS). Manifest 0.3.15: mac ab925203 + windows 9db842fc, the thirteen-field object, signed and verified locally (the deploy on main's line).
|
||||
|
||||
Installer, zip and manifest rows above (`tools/ship-app.mjs 0.3.15 ... --until manifest`).
|
||||
|
||||
## 5. Gate
|
||||
|
||||
| Line | Result |
|
||||
|---|---|
|
||||
| node suite on igneum-build-1 (713ef876, six crates, --no-fail-fast) | kaspa-consensus 97 + the recorded flake `ban_is_decided...` 1/1 alone, consensus-core 111 + 7, igneum-exec 20, kaspa-pow 15, igneum-miner 18, p2p-flows 33 |
|
||||
| app crate tests (release-0.3.15) | 162 + 28 + 8; UI 37 |
|
||||
| worker smoke, Mac (Metal, serve mode, epoch-2 v4 pack 24304f0788ea9408) | 1,024 of 1,024, sha256 7bf77506... equal to igneum-pow's derivation (19:16Z) |
|
||||
| worker smoke, Linux (the hive package's igneum-worker-cuda 97e036e2 on the fleet's p1-4090, RTX 4090) | 1,024 of 1,024, sha256 7bf77506... equal to the Mac's; done g2 in 2.44 s, self-test PASS (19:2xZ; rechecked from this tree) |
|
||||
| worker smoke, Windows (the kit's igneum-worker-cuda.exe 14b6637e on PC 2's RTX 5090, job v4-smoke-1ccfe586) | 1,024 of 1,024, sha256 7bf77506... equal to the Mac's; done g2 in 2.71 s (19:24:25Z) |
|
||||
| Live retry on 7f0bde70, clean form (p2-4090-3 wiped and mining beside the 0.3.14 peers, 21:27Z to 21:37Z) | PASS: 0 version-1026 headers accepted or relayed through a 129,000-block IBD and the window, 0 rejects from any peer, never disconnected, the 0.3.14 hub holding 3 blocks by its key mined in the window; verifierStale null |
|
||||
| Live forced poisoned-peer confirmation on f1ea7a38 (22:18Z to 22:24Z, after publish 1: the 713ef876 miner into the 6615571c relay pool-1, the target p2-4090-3) | PASS on the receive side: 6 relayed 1026 blocks refused with "wrong block version: got 1026 but expected 2, disconnecting from peer 213.173.98.36:18451", none accepted or relayed, the hands and the hub never rejected the target, its mined blocks kept landing in the hub; both boxes restored with wiped datadirs |
|
||||
| Devnet 2: P1 rehearsal | PASS (19:33Z): the flip by signal on 52 nodes, three epochs' ids, zero rejected on 16 nodes, the stale box refused both ways, blocks on v4, the floor crossed with v4 in force |
|
||||
| Devnet 2: 0.3.15 canary (run 4, 19:41Z: p2-3090-1, p2-4090-3, p1-3080, p1-a5000 on 713ef876, the thirteen-field file) | FAIL. The handshake peers (digest b18ed271) but the node writes block version 1026 (the class v4 signal bit) from its first block with no window and no floor set; every 0.3.14 node answers "wrong block version: got 1026 but expected 2" and disconnects (the hub: 45 disconnects since 19:41Z, 146 today); p2-3090-1 fell behind and could not re-sync (peers 0); the siblings lost every block they mined. Rolled back to 0.3.14. |
|
||||
|
||||
## 6. Incidents of the day that shaped this cut
|
||||
|
||||
- 17:52:54Z, PC 1: 0.3.14 installed under a measurement job; cause install_asked from a two-hour-old update-now, which counts as urgent (section 1, the updater rule).
|
||||
- 18:2x UK: the desktop app crashed and both hand nodes (nohup children of agent shells) died for about 70 minutes; now LaunchAgents (`infra/devnet/hands-launchd.sh`).
|
||||
- 18:3xZ: every PC ran a stale igneum-prove-host and igneum-prove-export (the payload never carried the Linux pair); placed by job tonight (zigbuild pair 75166ad8 / 24d34cb9), required in the payload from 0.3.15.
|
||||
- 18:58Z: `restart --what app` jobs to both PCs quit the apps and the relaunch helper did not bring them back (the engine exits under the stale window host); retired until fixed with a watcher. Recovery through the relay agent on PC 2; PC 1's agent was down.
|
||||
- 17:47Z onward: the pre-push hook's site build wrote 0.3.14's download rows into five worktrees; fixed on master (SITE_DOWNLOADS_REFRESH, tools/ci/no-foreign-tree-writes.sh).
|
||||
- 18:5xZ: the P1 rehearsal's flip stopped every GPU miner on the 0.3.14 worker (generator-4 refusal); hence section 2's worker rule.
|
||||
|
||||
## 6a. The evening's two blocks on the ship itself
|
||||
|
||||
- 19:24Z onward: every GitHub-hosted Actions job dies at start ("recent account payments have failed or your spending limit needs to be increased"); the Windows installer and payload zip are made only by windows.yml on windows-latest, so the Windows side waits for the org's Billing & plans. The Mac DMG, the HiveOS package and the Linux binaries do not depend on it.
|
||||
- 19:30Z: tools/ship-app.mjs's commit step pushed the release tree's HEAD to origin/master although the run had --branch release-0.3.15 (the step pushes master by design); master carries the 0.3.15 tree from 272e30a. Row: the tool honours --branch for the push, and the after-rollout merge is thereby already done.
|
||||
- The inputs step compared the state's short fork commit with the manifest's full one (`!==`): two needless re-pushes and a false failure; fixed (a prefix match, the manifest read bypasses the CDN cache).
|
||||
- `--until <step>` added to the ship tool: a cut is staged (built, signed, verified locally) with nothing deployed.
|
||||
|
||||
## 6c. The canary's FAIL and the gate line that was missing
|
||||
|
||||
The digest readings were on a throwaway node with no peers; the node lane's compat harness peered a new and an old node on the thirteen-field file but neither mined; the rehearsal ran the signalling binary on every node. None of the three put a BLOCK from a new node in front of an old node. The line every node cut needs from here: a mining new node beside an old node on the live file, the old node accepting the new node's blocks, plus a fresh join of a clean new node against an old peer. Proposed for CLAUDE.md's Devnet 2 rules (main's call).
|
||||
|
||||
The fix (node lane): the signal stamp and the signal read gated on publish 2's object (both v4 fields set); a template on the thirteen-field file has header version 2, on the sixteen-field file 1026; the digest tests unchanged; the harness with the new node mining. Then the node rebuilds (Linux, Windows cross, Mac), the digests are re-read, the canary runs again. The app tree needs nothing.
|
||||
|
||||
## 6d. The second canary FAIL (20:36Z): the receive side
|
||||
|
||||
On 7961c5f1 (the stamp gated), two boxes with clean datadirs got "wrong block version: got 1026 but expected 2" from both hands within three minutes: a version-1026 block from the poisoned boxes' branch reached them via relay through pool-1 (a 7961c5f1 node poisoned by p1-a5000), the new node ACCEPTED and RELAYED it, the 0.3.14 nodes rejected the relay and disconnected the sender. One poisoned peer poisons every new node it touches. The fix (node lane): the header-version RULE gated like the stamp (window or floor absent: version 2 only, 1026 rejected with 0.3.14's line, no relay; publish 2's object: both accepted); unit tests both ways; the compat harness with a poisoned peer. The retry form adds the poisoned peer relaying into the new node. Fleet: pool-1 and the three retry boxes held on 6615571c for the next cut; the 0.3.14 poisoned boxes (p1-3080, p2-3090-1, p1-a5000) wiped and fresh-joined from the hub.
|
||||
|
||||
Lesson for every node cut: the compat gate needs the receive side as well as the mining side: a peer feeding the new node the headers the rule must refuse, with the old node watching the new node's relays.
|
||||
|
||||
## 6e. Publish 1 as run (21:39Z to 21:47Z, 22:39 to 22:47 UK)
|
||||
|
||||
- 21:40Z deploy: the public folder verified file by file (200 with the local sizes); the live manifest 0.3.15 with mac 041099c2 and windows 99ec575c, the thirteen-field object (13 fields, exec_restart_number 27276), digest b18ed271.
|
||||
- 21:41:33Z update-now to PC 1 (ae432dc7), PC 2 (1ccfe586) and the Mac (d937c69d). PC 2 took it at 21:42:26Z ("updated to Igneum Miner 0.3.15 from 0.3.14", node igneumd 2.1.0-f1ea7a38); the Mac and PC 1 show app 0.3.15 on the console within the minute.
|
||||
- 21:42Z the hands (announced), through hands-launchd.sh on the Mac igneumd 85ac04df: observer pid 60931 and node 1 pid 61387, both igneumd/2.1.0-f1ea7a38, digest b18ed271, each resumed from its own exec snapshot (node 1 at tip 147,215), eth_blockNumber 147,233 on 26790 and 26791 a minute later.
|
||||
- 21:43Z the seed: the box's Linux igneumd 7f0bde70 (glibc 2.39) does not run on the seed (Debian 12, glibc 2.36: "version GLIBC_2.38 not found", 14 restarts in a loop, about three minutes down); re-run at 21:46Z on the zigbuild igneumd ede4a61ac9ab0993c644f82d61077c34fce11d7f8ffc642e8052887b978deeb3 (48,421,224, glibc 2.34 max, carries f1ea7a38): active, exec resumed from its own snapshot at tip 147,247. Rule: the seed and every Debian 12 host take the zigbuild (glibc 2.36) binary, the fleet (Ubuntu 24.04) takes the box's; restart-seed.sh now has to refuse a binary whose GLIBC need exceeds the host's (row).
|
||||
- The fleet's standing boxes: by the fleet agent's script on 7f0bde70 (table owed).
|
||||
|
||||
## 6f. The leak: the Mac and PC 1 took earlier 0.3.15 builds before the publish
|
||||
|
||||
The downloads folder (~/Projects/igneum-dlsite) is shared disk state and every `publish-jobs.sh --deploy` by any agent deploys the whole of it; the dl project deployed every few minutes all evening (other lanes' job publishes). So the manifests and DMGs I wrote there "undeployed" at 19:4xZ (DMG on node 713ef876) and 20:24Z (installer on node 7961c5f1) went live within minutes. The Mac applied 0.3.15 at 20:12:25Z (ota-apply.log; its installed igneumd carries 713ef876, its igneum-bench is the 19:41Z DMG's); PC 1 took 0.3.15 at its 20:35Z slot (installer 9db842fc, node 7961c5f1). Neither took the 21:40Z publish: the updater compares version strings only, and the version scheme is three-part, so a same-version republish cannot reach them. Harm so far none (the Mac's miner is paused; PC 1's node writes version 2; no 1026 header can be created any more). The move: a version bump with the same tree (main's number), update-now to both; or hand installs.
|
||||
|
||||
Rule rows: nothing is staged in the live downloads folder (a scratch copy, or publish-manifest.sh writes to a staging folder, and the live folder changes only in the deploy step); `publish-jobs.sh --deploy` deploys a jobs-only change, never the whole folder; a box's installed build is read back by its node commit string (not the version) after every publish.
|
||||
|
||||
## 6g. 0.3.16, the corrective cut (21:55Z to 22:13Z): every machine on f1ea7a38
|
||||
|
||||
The same tree under a new number (8bf922d; node pin unchanged at f1ea7a38), because the Mac and PC 1 held earlier 0.3.15 builds and the updater compares version strings only. DMG Igneum-Miner-0.3.16.dmg 0a542bad002c252bc7a80c5810781e9ec8ea31173ce2bd597a9e29892c50b6c3 (41,936,050); windows.yml run 37537291393 (18 of 18, 22:03:42Z): Igneum-Miner-Setup-0.3.16.exe a1eaa1dfaa0cd428378fd1ab86245e146c180070861dc4643d62773a92239a0c (62,671,852), igneum-windows-app.zip b47939c1... (90,520,492), the payload's igneumd.exe carrying f1ea7a38 (read before the push). Manifest 0.3.16 with both rows written and deployed in one step at 22:04Z. update-now 22:08Z. Read-back: the Mac applied 22:12:27Z ("0.3.16 is running", installed igneumd carries f1ea7a38, api/state 0.3.16 current, miner paused); PC 1 "updated to 0.3.16 from 0.3.15" 22:12:33Z, node 2.1.0-f1ea7a38; PC 2 the same at 22:11:24Z, 95.2 MH/s. Hands, seed and the fleet's 14 boxes as in 6e; the hub's last 1,000 blocks at 22:10Z hold blocks by 11 of 14 standing keys (the other three in IBD or in the confirmation). HiveOS renamed igneum-hive-0.3.16.tar.gz (the same bytes, b710ac59) at 22:17Z. The jobs-only deploy guard fired once on its first use (the index's "updated" stamp alone) and now ignores that field. The Discord release card for 0.3.16 posted 22:18Z (message 1557154778415505432).
|
||||
|
||||
0.3.17 is the planned tree (the lanes renumbered: node ca3-v4-0316 plus the proving lane's exec-sync-0313 tip past da2d17ec with the override's proving ids as the statement's ids when set, Ember 8f581de, miner-ui-4 past e31acab, proving 38b65e7 / fc08246 with X34, the finality LEAVE item, relay 28c028b, the testing-integration crate fixed). Devnet 2 nodes on 4c6b129d need IGNEUM_PROOF_PROGRAM_IDS=0x2b1a81cb...,0x474678f3... in their environment until then (the fleet has the line).
|
||||
|
||||
## 6h. Wallet 0.1.5 published (22:27Z) and publish 2's floor rule
|
||||
|
||||
Igneum Wallet 0.1.5: DMG daf259f272934f0c1a8155ef68762c344164ae8a77c8d0621d0fceed0fb163fa (20,122,390; node f1ea7a38, the live thirteen-field object bundled; wallet-ui-3 ab8dfdc). No wallet publisher script exists (0.1.4's manifest was written by hand): igneum-wallet-latest.json written in the app manifest's canonical form, signed with igneum-ota-sign and verified, then publish-public.sh --app --wallet --deploy; live at 22:27Z, the site's wallet row 0.1.5. The fresh-joiner gate line could not run on this Mac (the installed 0.1.4 wallet's protocol-12 node holds 26620/26800; it is the hub's "level is 0" source); the installed wallet's own OTA to 0.1.5 is the live check, read back below. Row: a wallet publisher (publish-manifest.sh --product wallet).
|
||||
|
||||
Publish 2's floor: the node flips the class at the floor unconditionally (class_signal.rs: the floor rule when the floor has passed), so the object carries program_class_v4_activation_daa a week past the publish (DAA + 604,800 rounded up to the epoch; 828,000 at 22:24Z), the window 86,400, the exec pin; the digest with that object read 0f9e26fe on f1ea7a38 and is recomputed at the publish. The vote opens with publish 2; the flip comes by the 95 percent signal; the floor is the backstop a week out (the 0.3.17 P2 rule in advance).
|
||||
|
||||
## 6i. Publish 2 as run (22:42Z to 22:57Z, 23:42 to 23:57 UK): the vote is open
|
||||
|
||||
- The object from the live DAA at 22:42:52Z (223,667): the thirteen + exec_restart_state_root 0xed27bb2d... + program_class_v4_activation_daa 831,600 (epoch 231, a week out, the unconditional floor's backstop) + program_class_v4_signal_window_daa 86,400. Digest on f1ea7a38: eada4bda8aa8368c2b2c3d17744bc7a70a0ff0e996dad681884d3ac5de1207eb. Manifest 0.3.16 with the sixteen-field override written and deployed in one step at 22:43Z (live read 22:46Z: 16 fields, floor 831600, window 86400).
|
||||
- 22:46Z update-now to the Mac, PC 1, PC 2; the PCs' nodes restarted on the object ("consensus override changed", "Program class v4 from the override file: active from epoch 231", "Consensus params digest: eada4bda") by 22:53Z. The Mac's app held its node restart "for a safe moment"; a `restart --what node` job at 22:59Z restarted it 23:01Z (synced, 7 peers); its own digest line is not in the app's log (the node's stdout is not logged there): row.
|
||||
- The fleet by the fleet agent 22:47Z to 22:54Z: 14 of 14 standing boxes print eada4bda on f1ea7a38 with the sixteen-field override.json (the thirteen-field file kept as override-13.json), the two program-class lines on every node, miners up (p2-3090-3 and pool-1 finishing fresh IBDs on the object); the five-minute check reads eada4bda as the live digest and posts on any box left behind. Expected during the sweep: every moved box logged 3 to 12 digest-mismatch refusals from the not-yet-moved hands and seed.
|
||||
- 22:56Z the hands (announced): observer pid 13170 and node 1 pid 13188, eada4bda, "active from epoch 231", exec resumed from their own snapshots (node 1 at 149,729), eth_blockNumber 149,742; the seed 22:56:04Z: eada4bda, igneumd/2.1.0-f1ea7a38, 16 fields in /etc/igneum/override-v3.json, active.
|
||||
- The window: 86,400 DAA ending at each epoch's seed block, 9500 bps of blue blocks signalling flips the class at the next epoch; the flip comes by miner signal; the floor 831,600 is the backstop (about 7 days at 1 DAA/s).
|
||||
|
||||
## 6b. Publish 1 as staged (the downloads folder, local until the line)
|
||||
|
||||
- Manifest 0.3.15, Mac row only (Igneum-Miner-0.3.15.dmg 90bdf8c8, 41,961,932), the thirteen-field object, signed; a Windows box reads "no file for this platform" and waits. Site rows: Mac 0.3.15, HiveOS 0.3.15 (1715e58e), Windows carried on 0.3.14 (publish-public.sh keeps a platform's previous file when the manifest lacks it: packaging/ota/aliases.py), wallet 0.1.4.
|
||||
- Sequence (scratchpad r0315/publish1.sh): deploy the folder; update-now to the Mac (d937c69d, miner paused per the project lead); the fleet's standing boxes by the fleet agent's script (06211d55, generator-4 workers); the hands on the Mac binary 7f0948d9 through hands-launchd.sh (announced); the seed on 06211d55; every version and digest line read back. The Windows row joins through `ship-app.mjs --from ci` when GitHub's billing block lifts. The Discord card waits for the full set.
|
||||
- Trigger: the fleet's canary PASS (run 4: three of four boxes met every criterion at 19:49Z; p2-3090-1's re-sync is the open verdict) or main's call at 20:00Z.
|
||||
|
||||
## 7. Owed
|
||||
|
||||
- Publish 2 after every node and every worker reports 0.3.15; the hands' move; the first locked checkpoint after the flip.
|
||||
- Pool-v0 fork commits (48c8163b, b8070476) for 0.3.16 (b8070476 conflicts with the 0.3.15 miner).
|
||||
- Ember 995530b (card.tune_floor) for 0.3.16.
|
||||
- The prover thread drops its cached tool paths when a stale binary is named (0.3.16).
|
||||
- The relaunch helper fixed with a watcher, or `restart --what app` stays retired.
|
||||
- Env-only defaults for the eight scripts that name the shared checkout by absolute path.
|
||||
- The Mac app's own igneumd never runs while hand node 1 holds 26610/26611 on this Mac: the app reads the hand node as its own ("node1" watch lines), api/state's digest field reads empty. A port collision must be loud in the app, and the hands' move to the box frees the ports.
|
||||
- The app's log does not carry its node's stdout, so the node's digest line is unreadable from api/log: the app should log the node's "Consensus params digest" and "Program class" lines (or expose consensus_digest from the node's RPC).
|
||||
- The build box's 188.40.146.49:26611 answers "Network mismatch: igneum-devnet-2" to the 0.3.16 apps' default peer dial; the live-devnet hand must own it before the hands move.
|
||||
- Publish 2's "safe moment" hold: with the miner paused the Mac's engine held the override restart until a restart job; the hold's condition wants reading (an idle engine should restart its node at once).
|
||||
- 0.3.17 node tree (ca3-v4-0316; the planned tree moved one number: 0.3.16 is the corrective cut of 6 October, the same release as 0.3.15 under a new number): 8e2f5cbe (the unwrap class), 0760b844 (the seven-window signalling rule, gate PASSED four cases), b2e21447 (finality cause fields), eec34ac3 (igneum_getRecentBlocks), 325f8b98 (difficulty rule v3 behind difficulty_v3_activation_daa at u64::MAX, digest unchanged until set). Pre-existing breakage to fix in that cut: `cargo check -p kaspa-testing-integration --tests` fails on the fork (the Header initialiser lacks vote_key_hash; the KaspadPayloadOps match is non-exhaustive on the finality ops), broken since the finality fields landed; the box suite then compiles EVERY test crate (`cargo test --no-run --workspace` or the crate list widened) so a test crate cannot rot silently again.
|
||||
- 0.3.17 node tree (was 0.3.16), proving agent: exec-sync-0313 da2d17ec (on 2e5d3d30) = consensus proof verification in-process (sp1-sdk 6.8.1 linked, 169 crates; three override fields in the digest once set: proving_consensus_verify_daa, proving_shard_program_id 0x2b1a81cb..., proving_aggregator_id 0x474678f3...; p2p payload 78 IgneumRequestProofRecords; a node with other embedded keys exits 3 at start); never by default, a digest move when switched, miners first. App side exec-app-0314 c00e608 (the proving-dir cap, forged.mjs harness 8/8, ledger P21 round 4 and its export-disk row labelled X31, which the site lane already used on master (X31 the testnet date, X32, X33): renumber the proving row X34 at the merge and rerun the ledger-text check). The fork's proof fixture is untracked at igneum/exec/testdata/ (the test skips without it): a build row.
|
||||
- Wallet 0.1.5 (Igneum-Wallet-0.1.5.dmg daf259f2, node f1ea7a38, wallet-ui-3 ab8dfdc): the gate and the publish straight after "0.3.15 live" (the wallet in the wild bundles a 5 October igneumd at protocol 12, the hub's 172 "level is 0" rejects); not held.
|
||||
- A standalone igneum-miner does not recover its template subscription after a node restart (fleet, 20:24Z): ledger row, fix = resubscribe on reconnect (0.3.16, the miner's owner); the fleet's restart script restarts the miner with the node until then.
|
||||
- 0.3.17 node tree (the finality lane's branch notes still say 0.3.16; the lane is closed, the number here is the one that counts): the finality lane's LEAVE item (finality-pause-node 766e70ca on the box mirror /srv/igneum-node.git, one commit on 4c6b129d, rebases onto the 0.3.15 node with only params.rs overlapping; docs on origin finality-pause 27f82ec) is a digest move (the devnet digest becomes 63703f5b..., protocol 17) gated by finality_leave_activation_daa (never on devnet until the 95 percent signal); its Devnet 2 gate is tools/finality-attacks/leave.mjs both ways with the new binaries, before it merges.
|
||||
- With the after-rollout merge: the relay security fixes X23 to X28 (28c028b, on fud-close and the ledger-* branches only; the intake key gets 403 on console writes while master lacks them).
|
||||
- DONE 20:3xZ: packaging/ota/publish-manifest.sh refuses an --activation-height at or below the live DAA (the observer's executedTipDaa; IGNEUM_MANIFEST_ALLOW_PAST_HEIGHT=1 for a replay; --self-test-height), the engine half with the Ember agent for 0.3.16.
|
||||
- 0.3.17's app merge (was 0.3.16), as the lanes handed it: app/igneum-app/ui/ whole from miner-ui-4 57bb4f7 (the UI pinned to the engine fields, the Ember off-state copy, N4's app half: sync_decision_v2, is_stall_exit, NodeState.tip_age_s / sync_cause / stall_exits in engine.rs and state.rs, which want the node lane's miner half (exit 45, the STALLED line) in the same cut); src/live.rs, ui/app.js, the /api/live route and packaging/ota/publish-manifest.sh from ember-tune da11c6f (cbd4d51: one monotonic wire sequence space for every helper writer, powertask::wire_seq; without it a 0.3.16 user with Power control on gets the caps and no tune; pinned to the node lane's ca3-v4-0316 b2e21447 finality-pause fields and eec34ac3 igneum_getRecentBlocks, reading the absent-field path on an older node; the node source in the site's row shape, the finality-paused state, the activation-height guard with --self-test-height; 652f6fc's guard is dropped at that merge), not miner-ui-4's 0.3.15 files; Ember's 995530b and aa73005 fields (watts_total, pounds_per_day, balance_wei, price null with the manifest rule, tune_floor, the finality pause rule fd03f35).
|
||||
- The publish side owes a way to set the manifest's top-level "urgent": true for an emergency rollout through a finality pause (fd03f35 reads it; igneum-ota-sign passes the document through): a publish-manifest.sh flag.
|
||||
- The gate line for every node cut from here (main writes it into CLAUDE.md): a mining new node beside an old node on the live file for ten minutes, the old node's reject count read before and after, and one new node restarted mid-window re-syncing from an old peer.
|
||||
75
infra/devnet/hands-launchd.sh
Executable file
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env bash
|
||||
# The two hand-run devnet nodes on this Mac (the observer and node 1) run under launchd, never as children of a shell.
|
||||
# 6 October 2026, 18:2x UK: the Claude desktop app crashed and both hands died with it (they were nohup children of
|
||||
# agent shells); the devnet lost its two hand nodes for about 70 minutes. From here a LaunchAgent per hand keeps them up
|
||||
# (KeepAlive, RunAtLoad), and every restart goes through launchctl, so a closed terminal or a crashed app changes nothing.
|
||||
#
|
||||
# infra/devnet/hands-launchd.sh install <igneumd> [<snapshot-path>,<sha256>] write both plists for this binary (and
|
||||
# the exec recovery flag), (re)start the
|
||||
# observer, wait for it, then node 1
|
||||
# infra/devnet/hands-launchd.sh restart kickstart both as they are, observer first
|
||||
# infra/devnet/hands-launchd.sh status pids, parents, versions, digests, exec tips
|
||||
#
|
||||
# The override object stays in /tmp/igneum-devnet/override-v3.json (written by restart-hand-nodes.sh, which calls `restart`
|
||||
# here after it). Plists: ~/Library/LaunchAgents/network.igneum.devnet.observer.plist and network.igneum.devnet.node1.plist.
|
||||
# Logs: ~/Library/Logs/Igneum/observer.out and node1.out. Ports as before: observer rpc 26640 / p2p 26641 / evm 26790 (the
|
||||
# default), node 1 rpc 26610 / p2p 26611 / evm 26791. The two hands never share a port.
|
||||
set -euo pipefail
|
||||
UID_="$(id -u)"; LA="$HOME/Library/LaunchAgents"; LOGS="$HOME/Library/Logs/Igneum"; OV=/tmp/igneum-devnet/override-v3.json
|
||||
LABELS=(network.igneum.devnet.observer network.igneum.devnet.node1)
|
||||
mkdir -p "$LA" "$LOGS"
|
||||
|
||||
write_plists() {
|
||||
local bin="$1" snap="${2:-}"
|
||||
[ -x "$bin" ] || { echo "no binary at $bin"; exit 1; }
|
||||
python3 - "$bin" "$snap" "$OV" "$LA" "$LOGS" <<'PY'
|
||||
import sys, plistlib
|
||||
bin_, snap, ov, la, logs = sys.argv[1:6]
|
||||
common = ["--devnet", "--nodnsseed", "--disable-upnp", f"--override-params-file={ov}"]
|
||||
if snap: common.append(f"--igneum-exec-snapshot={snap}")
|
||||
common += ["--nologfiles", "--yes"]
|
||||
hands = {
|
||||
"network.igneum.devnet.observer": [bin_, "--appdir=/tmp/igneum-devnet/observer-v4", "--rpclisten=127.0.0.1:26640", "--rpclisten-json=127.0.0.1:28640",
|
||||
"--listen=127.0.0.1:26641", "--addpeer=127.0.0.1:26611", "--addpeer=188.245.5.161:26611", "--addpeer=192.168.68.67:26611"] + common,
|
||||
"network.igneum.devnet.node1": ["/usr/bin/caffeinate", "-dims", bin_, "--enable-unsynced-mining", "--appdir=/tmp/igneum-devnet/node1", "--rpclisten=0.0.0.0:26610",
|
||||
"--listen=0.0.0.0:26611", "--evm-rpclisten=127.0.0.1:26791", "--addpeer=188.245.5.161:26611", "--addpeer=192.168.68.67:26611"] + common,
|
||||
}
|
||||
for label, args in hands.items():
|
||||
name = label.rsplit(".", 1)[1]
|
||||
d = {"Label": label, "ProgramArguments": args, "RunAtLoad": True, "KeepAlive": True, "ThrottleInterval": 10,
|
||||
"StandardOutPath": f"{logs}/{name}.out", "StandardErrorPath": f"{logs}/{name}.out", "WorkingDirectory": "/tmp/igneum-devnet",
|
||||
"ProcessType": "Background", "EnvironmentVariables": {"PATH": "/usr/bin:/bin:/usr/sbin:/sbin"}}
|
||||
with open(f"{la}/{label}.plist", "wb") as f: plistlib.dump(d, f)
|
||||
print(f"wrote {la}/{label}.plist ({len(args)} args)")
|
||||
PY
|
||||
plutil -lint "$LA"/network.igneum.devnet.*.plist >/dev/null
|
||||
}
|
||||
|
||||
# (re)start one hand: bootstrap if launchd does not know it, else kickstart -k (stop and start); wait for its rpc port
|
||||
start_one() {
|
||||
local label="$1" port="$2" i
|
||||
if launchctl print "gui/$UID_/$label" >/dev/null 2>&1; then launchctl kickstart -k "gui/$UID_/$label"; else launchctl bootstrap "gui/$UID_" "$LA/$label.plist"; fi
|
||||
for i in $(seq 1 90); do
|
||||
if curl -s -m 2 -X POST -H 'content-type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' "http://127.0.0.1:$port" | grep -q result; then echo "$label: evm port $port answers after $i s"; return 0; fi
|
||||
sleep 1
|
||||
done
|
||||
echo "$label: evm port $port silent after 90 s"; return 1
|
||||
}
|
||||
|
||||
status() {
|
||||
local label pid
|
||||
for label in "${LABELS[@]}"; do
|
||||
pid="$(launchctl print "gui/$UID_/$label" 2>/dev/null | sed -n 's/^[[:space:]]*pid = \([0-9]*\).*/\1/p' | head -1)"
|
||||
echo "== $label: launchd pid ${pid:-none}, parent $( [ -n "$pid" ] && ps -o ppid= -p "$pid" | tr -d ' ' ), state $(launchctl print "gui/$UID_/$label" 2>/dev/null | sed -n 's/^[[:space:]]*state = //p' | head -1)"
|
||||
local name="${label##*.}"
|
||||
grep -E "igneumd/|Consensus params digest|exec sync: resumed" "$LOGS/$name.out" 2>/dev/null | tail -3 | sed -E 's/^.*\[INFO \] //' | cut -c1-150
|
||||
done
|
||||
for port in 26790 26791; do printf ":%s eth_blockNumber " "$port"; curl -s -m 3 -X POST -H 'content-type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}' "http://127.0.0.1:$port" | python3 -c "import json,sys; print(int(json.load(sys.stdin)['result'],16))" 2>/dev/null || echo "no answer"; done
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
install) write_plists "$2" "${3:-}"; start_one network.igneum.devnet.observer 26790; start_one network.igneum.devnet.node1 26791; status ;;
|
||||
restart) start_one network.igneum.devnet.observer 26790; start_one network.igneum.devnet.node1 26791; status ;;
|
||||
status) status ;;
|
||||
*) sed -n 2,16p "$0"; exit 2 ;;
|
||||
esac
|
||||
|
|
@ -27,16 +27,18 @@ printf '%s\n' "$OVJSON" > "$OV"
|
|||
# caller's own zsh -c "..." and the stop waited 11 minutes on it)
|
||||
stop() { local p; p=$( { for c in $(pgrep -f "$1" || true); do if [ "$(ps -o comm= -p "$c" 2>/dev/null | xargs basename 2>/dev/null)" = igneumd ]; then echo "$c"; fi; done; true; } | head -1 ); if [ -n "$p" ]; then kill -INT "$p"; while kill -0 "$p" 2>/dev/null; do sleep 1; done; fi; } # the filter never fails the pipeline (17:43Z: a trailing caffeinate pid made the loop exit 1 and set -e ended the script before node 1)
|
||||
lines() { sleep 10; grep -E "Calibrated v1 fees from the override file|Fees on igneum-devnet|Consensus params digest" "$1" | tail -3 || true; }
|
||||
# the observer first
|
||||
# 6 October 2026, 19:5x UK: the hands run under launchd (infra/devnet/hands-launchd.sh; a LaunchAgent per hand, KeepAlive), never
|
||||
# as children of this shell: the desktop app's crash at 18:2x UK took both nohup children down for about 70 minutes. This
|
||||
# script keeps the object check, the override file and the binary pin, then hands the start to launchd: each hand is booted
|
||||
# out (and any stray igneumd outside launchd stopped), its exec files reset if asked, and `hands-launchd.sh install` writes
|
||||
# the plists for THIS binary, starts the observer, waits for its evm port, then node 1, and reads both back.
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
for label in network.igneum.devnet.observer network.igneum.devnet.node1; do launchctl bootout "gui/$(id -u)/$label" 2>/dev/null || true; done
|
||||
stop "igneumd --devnet.*observer-v4"
|
||||
reset_exec /tmp/igneum-devnet/observer-v4
|
||||
nohup "$BIN" --devnet --nodnsseed --disable-upnp --appdir=/tmp/igneum-devnet/observer-v4 --rpclisten=127.0.0.1:26640 --rpclisten-json=127.0.0.1:28640 --listen=127.0.0.1:26641 \
|
||||
--addpeer=127.0.0.1:26611 --addpeer=188.245.5.161:26611 --addpeer=192.168.68.67:26611 --override-params-file="$OV" $SNAPARG --nologfiles --yes >> /tmp/igneum-devnet/observer-v4.out 2>&1 &
|
||||
lines /tmp/igneum-devnet/observer-v4.out
|
||||
# node 1, under caffeinate as it runs today
|
||||
stop "igneumd --devnet.*appdir=/tmp/igneum-devnet/node1 "
|
||||
reset_exec /tmp/igneum-devnet/observer-v4
|
||||
reset_exec /tmp/igneum-devnet/node1
|
||||
nohup caffeinate -dims "$BIN" --devnet --nodnsseed --disable-upnp --enable-unsynced-mining --appdir=/tmp/igneum-devnet/node1 --rpclisten=0.0.0.0:26610 --listen=0.0.0.0:26611 --evm-rpclisten=127.0.0.1:26791 \
|
||||
--addpeer=188.245.5.161:26611 --addpeer=192.168.68.67:26611 --override-params-file="$OV" $SNAPARG --nologfiles --yes >> /tmp/igneum-devnet/node1.out 2>&1 &
|
||||
lines /tmp/igneum-devnet/node1.out
|
||||
echo "running now:"; ps -o pid=,lstart=,command= -p "$(pgrep -f 'igneumd --devnet' | tr '\n' ',' | sed 's/,$//')" | cut -c1-160
|
||||
"$HERE/hands-launchd.sh" install "$BIN" "$SNAP"
|
||||
lines "$HOME/Library/Logs/Igneum/observer.out"
|
||||
lines "$HOME/Library/Logs/Igneum/node1.out"
|
||||
echo "running now (launchd children, parent pid 1):"; ps -o pid=,ppid=,lstart=,command= -p "$(pgrep -f 'igneumd --devnet' | tr '\n' ',' | sed 's/,$//')" | cut -c1-160
|
||||
|
|
|
|||
|
|
@ -13,6 +13,15 @@ SRC="${IGNEUMD_LINUX:-/Users/joshm/Projects/igneum/infra/cross/out-039/igneumd}"
|
|||
WANT="${IGNEUMD_LINUX_SHA256:-7e26e374586ad8ba539e371c6cbbd74f0f77aee1e2e97cd25f0182fcbda458a5}"
|
||||
echo "$OVJSON" | python3 -c 'import json,sys; o=json.load(sys.stdin); assert isinstance(o, dict) and "fees_v1_activation_daa" in o; print("override:", json.dumps(o, sort_keys=True))'
|
||||
test "$(shasum -a 256 "$SRC" | cut -c1-64)" = "$WANT" || { echo "$SRC is not the 2b6d23ef Linux node ($WANT)"; exit 1; }
|
||||
# the seed is Debian 12 (glibc 2.36): a binary built on igneum-build-1 (Ubuntu 24.04, glibc 2.39) does not start there
|
||||
# ("version GLIBC_2.38 not found", a restart loop; 6 October 2026, 21:43Z, three minutes down). The zigbuild from
|
||||
# infra/cross/build-linux.sh (glibc 2.36 target) is the seed's binary. Refused here before anything is copied.
|
||||
NEED="$(strings "$SRC" | grep -oE 'GLIBC_2\.[0-9]+' | sort -t. -k2 -n | tail -1 | sed 's/GLIBC_//')"
|
||||
HOST_GLIBC="$(ssh -i "$HOME/.ssh/igneum_ed25519" -o ConnectTimeout=8 root@188.245.5.161 'ldd --version 2>/dev/null | head -1 | grep -oE "[0-9]+\.[0-9]+$"' 2>/dev/null || echo 2.36)"
|
||||
if [ -n "$NEED" ] && [ "$(printf '%s\n%s\n' "$NEED" "$HOST_GLIBC" | sort -t. -k1,1n -k2,2n | tail -1)" != "$HOST_GLIBC" ]; then
|
||||
echo "$SRC needs GLIBC $NEED but the seed has $HOST_GLIBC: build it with infra/cross/build-linux.sh (zigbuild, glibc 2.36) and pass that one"; exit 1
|
||||
fi
|
||||
echo "binary needs GLIBC $NEED, the seed has $HOST_GLIBC"
|
||||
SSH="ssh -i $HOME/.ssh/igneum_ed25519 -o ConnectTimeout=20 root@188.245.5.161"
|
||||
# 6 October 2026 (the exec recovery after the 15:44Z reorg): IGNEUMD_EXEC_SNAPSHOT_FILE=<local file> copies that exec snapshot to the
|
||||
# seed (/root/v4/exec-snapshot.bin) and adds --igneum-exec-snapshot=<path>,0x<sha256> to EXTRA_ARGS in seed-v4.env (replacing an
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
"exec_restart_number": 18446744073709551615,
|
||||
"exec_restart_hash": "",
|
||||
"exec_restart_trust_daa": 18446744073709551615,
|
||||
"exec_restart_state_root": "",
|
||||
"pow_genesis_dataset_log2": 28,
|
||||
"proving_v1_activation_daa": 18446744073709551615,
|
||||
"proving_v1_segment_blocks": 8,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
<key>CFBundleVersion</key>
|
||||
<string>VERSION_STAMP</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.3.14</string>
|
||||
<string>0.3.16</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
|
|
|
|||
31
packaging/ota/aliases.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env python3
|
||||
# The public aliases: what the two manifests and the newest HiveOS package in dl/public/ name (publish-public.sh calls
|
||||
# this with <pub dir> <dry 0|1>; a separate file because bash 3.2 mis-parses a heredoc with parentheses inside $( )).
|
||||
import json, os, re, sys
|
||||
pub, dry = sys.argv[1], sys.argv[2] == "1"
|
||||
# a platform the current manifest lacks keeps the newest versioned file of that platform already in dl/public/ (6 October
|
||||
# 2026, 0.3.15: the Mac row moved first while the Windows installer waited on the GitHub billing block; the Windows link of the site
|
||||
# must stay on the version that exists, never go dark, and never name a version that is not there). The index below
|
||||
# reads such a file"s version from its name, so the site stamps the carried version honestly.
|
||||
CARRY = {"windows": r"Igneum-Miner-Setup-(\d+\.\d+\.\d+)\.exe", "mac": r"Igneum-Miner-(\d+\.\d+\.\d+)\.dmg"}
|
||||
def newest(pattern):
|
||||
fs = [f for f in os.listdir(pub) if re.fullmatch(pattern, f)] if os.path.isdir(pub) else []
|
||||
fs.sort(key=lambda f: tuple(int(x) for x in re.findall(r"\d+", f)[:3]))
|
||||
return fs[-1] if fs else None
|
||||
def entry(name, plat):
|
||||
p = os.path.join(pub, name)
|
||||
if not os.path.exists(p): return None
|
||||
e = json.load(open(p)).get("platforms", {}).get(plat)
|
||||
if not e:
|
||||
return newest(CARRY[plat]) if name == "igneum-app-latest.json" and plat in CARRY else None
|
||||
f = e["url"].rsplit("/", 1)[1]
|
||||
return f if os.path.exists(os.path.join(pub, f)) or dry else None
|
||||
hive = sorted([f for f in os.listdir(pub) if re.fullmatch(r"igneum-hive-\d+\.\d+\.\d+\.tar\.gz", f)],
|
||||
key=lambda f: tuple(int(x) for x in re.findall(r"\d+", f)[:3])) if os.path.isdir(pub) else []
|
||||
aliases = {
|
||||
"igneum-miner-windows.exe": entry("igneum-app-latest.json", "windows"),
|
||||
"igneum-miner-mac.dmg": entry("igneum-app-latest.json", "mac"),
|
||||
"igneum-miner-hive.tar.gz": hive[-1] if hive else None,
|
||||
"igneum-wallet-mac.dmg": entry("igneum-wallet-latest.json", "mac"),
|
||||
}
|
||||
print(json.dumps(aliases))
|
||||
|
|
@ -404,6 +404,35 @@ if [ -n "$NEXT_FOLDER" ]; then
|
|||
cp "$JOBS" "$JOBS.sig" "$SIGNED" "$NEXT_FOLDER/" && echo "jobs file, signature and envelope mirrored to the next folder"
|
||||
fi
|
||||
|
||||
# --deploy ships the JOBS FILE ONLY (6 October 2026, 22:0x UK rule): the downloads folder is shared disk state and a deploy
|
||||
# ships all of it, so a manifest or a package staged there by another lane went live with the next job publish (the Mac and
|
||||
# PC 1 took earlier 0.3.15 builds that way). Before deploying, every file in the live token folder and dl/public/ apart from
|
||||
# the jobs file and its signature must equal what is live (size and sha256 against the served copy); anything else differing
|
||||
# refuses the deploy and names the files. The release's own deploy goes through packaging/ota/publish-manifest.sh --deploy or
|
||||
# tools/ship-app.mjs, never through here. IGNEUM_JOBS_DEPLOY_ALL=1 overrides, knowingly, for a full-folder deploy.
|
||||
jobs_only_check() {
|
||||
[ "${IGNEUM_JOBS_DEPLOY_ALL:-0}" = 1 ] && { echo "IGNEUM_JOBS_DEPLOY_ALL=1: deploying the whole folder" >&2; return 0; }
|
||||
local base="https://dl.igneum.network" bad=0 f n live_size local_size local_sha live_sha
|
||||
for f in "$DEST"/* "$DLSITE"/dl/public/*; do
|
||||
[ -f "$f" ] || continue
|
||||
n="$(basename "$f")"
|
||||
case "$n" in igneum-jobs.json|igneum-jobs.signed.json|igneum-jobs.json.sig) continue ;; esac
|
||||
local rel; rel="${f#"$DLSITE"}"
|
||||
local_size="$(stat -f %z "$f" 2>/dev/null || stat -c %s "$f")"
|
||||
live_size="$(curl -sI -m 10 -H 'Cache-Control: no-cache' "$base$rel?x=$RANDOM" | awk 'tolower($1)=="content-length:"{print $2}' | tr -d '\r' | tail -1)"
|
||||
if [ -z "$live_size" ]; then echo " not live yet: $rel (a new file; the release step deploys it)" >&2; bad=1; continue; fi
|
||||
if [ "$live_size" != "$local_size" ]; then echo " differs from the live copy: $rel (local $local_size B, live $live_size B)" >&2; bad=1; continue; fi
|
||||
case "$n" in *.json|*.sig|*.sha256) # small: compare the bytes
|
||||
# the downloads index carries an "updated" stamp rewritten after every alias pass; the stamp is not a change
|
||||
norm() { if [ "$n" = igneum-downloads.json ]; then sed -E 's/"updated": *"[^"]*"/"updated": ""/'; else cat; fi; }
|
||||
local_sha="$(norm < "$f" | shasum -a 256 | cut -c1-64)"; live_sha="$(curl -s -m 15 -H 'Cache-Control: no-cache' "$base$rel?x=$RANDOM" | norm | shasum -a 256 | cut -c1-64)"
|
||||
[ "$local_sha" = "$live_sha" ] || { echo " differs from the live copy: $rel (content)" >&2; bad=1; } ;;
|
||||
esac
|
||||
done
|
||||
if [ "$bad" = 1 ]; then echo "publish-jobs: the folder holds changes besides the jobs file (above); a job publish never deploys them. Deploy the release through publish-manifest.sh --deploy or ship-app.mjs, or stage elsewhere. Not deployed." >&2; return 1; fi
|
||||
return 0
|
||||
}
|
||||
if [ "$DEPLOY" = 1 ] && ! jobs_only_check; then DEPLOY=0; echo "the jobs file is written and signed locally; the deploy was refused" >&2; exit 1; fi
|
||||
if [ "$DEPLOY" = 1 ]; then
|
||||
[ -n "$DLSITE" ] || { echo "--deploy needs the real downloads folder (no --dest)" >&2; exit 1; }
|
||||
echo "deploying $DLSITE"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,26 @@ TOKEN_FILE="$HOME/.config/igneum/dl-token"
|
|||
SIGNER="$ROOT/app/igneum-app/target/release/igneum-ota-sign"
|
||||
|
||||
VERSION="" MAC="" WIN="" NOTES="" ACTIVATION="" DEADLINE="" MIN_SUPPORTED="" CHANNEL="devnet" BASE="" DEST="" DEPLOY=0 VERIFY_ONLY=0 TRIES=12
|
||||
|
||||
# An activation height at or below the live DAA makes every app treat the update as urgent (manifest::fork_is_close counts
|
||||
# any height already passed as "close"), which skips the slot, the patience and the busy rule; PC 1 took an install under a
|
||||
# running job on 6 October 2026 on that path. The live DAA comes from the observer's exec status (igneum_getExecStatus,
|
||||
# executedTipDaa) at 127.0.0.1:26790; a height at or below it is refused. IGNEUM_MANIFEST_ALLOW_PAST_HEIGHT=1 overrides for a
|
||||
# deliberate replay. `--self-test-height` exercises the rule on a known-bad and a known-good value against DAA 1000.
|
||||
height_rule() { # <height> <live daa> -> 0 ok, 1 refused
|
||||
local h="$1" daa="$2"
|
||||
[ -z "$h" ] && return 0
|
||||
[[ "$h" =~ ^[0-9]+$ ]] || { echo "activation height $h is not a number" >&2; return 1; }
|
||||
if [ "$h" -le "$daa" ]; then echo "activation height $h is at or below the live DAA $daa: every app would treat the update as urgent (fork_is_close); refused (IGNEUM_MANIFEST_ALLOW_PAST_HEIGHT=1 overrides a deliberate replay)" >&2; return 1; fi
|
||||
return 0
|
||||
}
|
||||
if [ "${1:-}" = "--self-test-height" ]; then
|
||||
height_rule 900 1000 2>/dev/null && { echo "self-test failed: a passed height was accepted"; exit 1; }
|
||||
height_rule 1000 1000 2>/dev/null && { echo "self-test failed: the live height was accepted"; exit 1; }
|
||||
height_rule 1001 1000 || { echo "self-test failed: a future height was refused"; exit 1; }
|
||||
height_rule "" 1000 || { echo "self-test failed: no height was refused"; exit 1; }
|
||||
echo "self-test passed: a height at or below the live DAA is refused, a future one and none pass"; exit 0
|
||||
fi
|
||||
OVERRIDE="" TUNING_FILE="" NO_TUNING=0 PUBLIC=0
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
|
|
@ -63,6 +83,13 @@ while [ $# -gt 0 ]; do
|
|||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -n "$ACTIVATION" ] && [ "${IGNEUM_MANIFEST_ALLOW_PAST_HEIGHT:-0}" != "1" ]; then
|
||||
LIVE_DAA="$(curl -s -m 5 -X POST -H 'content-type: application/json' --data '{"jsonrpc":"2.0","id":1,"method":"igneum_getExecStatus","params":[]}' http://127.0.0.1:26790 2>/dev/null | python3 -c 'import json,sys; print(int(json.load(sys.stdin)["result"]["executedTipDaa"],16))' 2>/dev/null || true)"
|
||||
[ -n "$LIVE_DAA" ] || { echo "cannot read the live DAA from the observer (127.0.0.1:26790) to check --activation-height $ACTIVATION; start it or set IGNEUM_MANIFEST_ALLOW_PAST_HEIGHT=1 knowingly" >&2; exit 1; }
|
||||
height_rule "$ACTIVATION" "$LIVE_DAA" || exit 1
|
||||
echo "activation height $ACTIVATION is above the live DAA $LIVE_DAA ($((ACTIVATION - LIVE_DAA)) ahead)"
|
||||
fi
|
||||
[ -n "$VERSION" ] || [ "$VERIFY_ONLY" = 1 ] || { echo "--version is required" >&2; exit 2; }
|
||||
[ -n "$VERSION" ] || VERSION="(the folder's)"
|
||||
[ "$VERIFY_ONLY" = 1 ] || case "$VERSION" in [0-9]*.[0-9]*.[0-9]*) ;; *) echo "--version must be major.minor.patch" >&2; exit 2 ;; esac
|
||||
|
|
|
|||
|
|
@ -120,27 +120,7 @@ if [ -n "$HIVE" ]; then
|
|||
fi
|
||||
|
||||
# the aliases: what the two manifests and the newest HiveOS package in dl/public/ name, nothing else
|
||||
ALIASES_JSON="$(python3 - "$PUB" "$DRY" <<'PY'
|
||||
import json, os, re, sys
|
||||
pub, dry = sys.argv[1], sys.argv[2] == "1"
|
||||
def entry(name, plat):
|
||||
p = os.path.join(pub, name)
|
||||
if not os.path.exists(p): return None
|
||||
e = json.load(open(p)).get("platforms", {}).get(plat)
|
||||
if not e: return None
|
||||
f = e["url"].rsplit("/", 1)[1]
|
||||
return f if os.path.exists(os.path.join(pub, f)) or dry else None
|
||||
hive = sorted([f for f in os.listdir(pub) if re.fullmatch(r"igneum-hive-\d+\.\d+\.\d+\.tar\.gz", f)],
|
||||
key=lambda f: tuple(int(x) for x in re.findall(r"\d+", f)[:3])) if os.path.isdir(pub) else []
|
||||
aliases = {
|
||||
"igneum-miner-windows.exe": entry("igneum-app-latest.json", "windows"),
|
||||
"igneum-miner-mac.dmg": entry("igneum-app-latest.json", "mac"),
|
||||
"igneum-miner-hive.tar.gz": hive[-1] if hive else None,
|
||||
"igneum-wallet-mac.dmg": entry("igneum-wallet-latest.json", "mac"),
|
||||
}
|
||||
print(json.dumps(aliases))
|
||||
PY
|
||||
)"
|
||||
ALIASES_JSON="$(python3 "$HERE/aliases.py" "$PUB" "$DRY")"
|
||||
KEEP="$(python3 -c 'import json,sys; a=json.loads(sys.argv[1]); print(" ".join(v for v in a.values() if v))' "$ALIASES_JSON")"
|
||||
log "aliases (vercel.json rewrites under /public/):"
|
||||
python3 -c 'import json,sys; a=json.loads(sys.argv[1]); [print(" /public/%s -> %s" % (k, ("/dl/public/" + v) if v else "(no file yet; alias left out)")) for k,v in a.items()]' "$ALIASES_JSON" | scrub
|
||||
|
|
@ -178,6 +158,9 @@ def sha(p):
|
|||
def version_of(name, f):
|
||||
if name.startswith("igneum-miner-hive"): return ".".join(re.findall(r"\d+", f)[:3])
|
||||
m = json.load(open(os.path.join(pub, "igneum-wallet-latest.json" if name.startswith("igneum-wallet") else "igneum-app-latest.json")))
|
||||
plat = "windows" if name == "igneum-miner-windows.exe" else "mac"
|
||||
if name.startswith("igneum-miner") and not m.get("platforms", {}).get(plat):
|
||||
return ".".join(re.findall(r"\d+", f)[:3]) # a carried file: the version in its name, not the one of the manifest
|
||||
return m.get("version")
|
||||
keys = {"igneum-miner-windows.exe": "miner-windows", "igneum-miner-mac.dmg": "miner-mac", "igneum-miner-hive.tar.gz": "miner-hive", "igneum-wallet-mac.dmg": "wallet-mac"}
|
||||
files = {}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
#define ArtDir "..\..\brand\icons"
|
||||
#endif
|
||||
#ifndef AppVersion
|
||||
#define AppVersion "0.3.14"
|
||||
#define AppVersion "0.3.16"
|
||||
#endif
|
||||
#define AppName "Igneum Miner"
|
||||
#define Publisher "Igneum"
|
||||
|
|
|
|||
|
|
@ -93,10 +93,18 @@ cp "$HERE/stop-igneum.ps1" "$STAGE/stop-igneum.ps1"
|
|||
# them as /mnt/<drive>/.../wsl2/bin/igneum-prove-host from Ubuntu-24.04; Set up runs wsl2\setup-wsl.sh.
|
||||
PROVE_LINUX="${IGNEUM_PROVE_LINUX:-$ROOT/proving/igneum-prove/target-linux/x86_64-unknown-linux-gnu/release}"
|
||||
mkdir -p "$STAGE/wsl2/bin" "$STAGE/wsl2/fixtures"
|
||||
if [ -f "$PROVE_LINUX/igneum-prove-host" ] && [ -f "$PROVE_LINUX/igneum-prove-export" ]; then
|
||||
cp "$PROVE_LINUX/igneum-prove-host" "$PROVE_LINUX/igneum-prove-export" "$STAGE/wsl2/bin/"
|
||||
echo "prover (WSL2): igneum-prove-host $(stat -f %z "$PROVE_LINUX/igneum-prove-host") bytes, igneum-prove-export $(stat -f %z "$PROVE_LINUX/igneum-prove-export") bytes"
|
||||
else echo "warning: no Linux igneum-prove-host/igneum-prove-export in $PROVE_LINUX (cargo zigbuild --target x86_64-unknown-linux-gnu.2.36 --features igneum-prove-host/cuda); the Proving tile will ask for the WSL2 setup, which builds them"; fi
|
||||
# 6 October 2026 (0.3.15): the pair is REQUIRED. First from the inputs next to the exes (push-inputs.sh ships them flat as
|
||||
# igneum-prove-host.linux-x86_64 and igneum-prove-export.linux-x86_64, which is what CI has), else from PROVE_LINUX (a Mac
|
||||
# build); a payload without them shipped every PC a stale prover from 0.3.5 to 0.3.14 (no peer-proof verification, no exports).
|
||||
for b in igneum-prove-host igneum-prove-export; do
|
||||
if [ -f "$REL/$b.linux-x86_64" ]; then cp "$REL/$b.linux-x86_64" "$STAGE/wsl2/bin/$b"; src="the inputs ($REL)"
|
||||
elif [ -f "$PROVE_LINUX/$b" ]; then cp "$PROVE_LINUX/$b" "$STAGE/wsl2/bin/$b"; src="$PROVE_LINUX"
|
||||
else echo "no Linux $b (looked at $REL/$b.linux-x86_64 and $PROVE_LINUX/$b); the payload never ships without the WSL2 prover pair again" >&2; exit 1; fi
|
||||
chmod +x "$STAGE/wsl2/bin/$b"
|
||||
echo "prover (WSL2): $b $(stat -f %z "$STAGE/wsl2/bin/$b" 2>/dev/null || stat -c %s "$STAGE/wsl2/bin/$b") bytes from $src"
|
||||
done
|
||||
# the shas the app and a job can check the installed pair against (a stale host is loud at the node's start from 0.3.15)
|
||||
(cd "$STAGE/wsl2/bin" && { shasum -a 256 igneum-prove-host igneum-prove-export 2>/dev/null || sha256sum igneum-prove-host igneum-prove-export; } > SHA256SUMS)
|
||||
cp "$ROOT"/proving/windows-wsl2/*.sh "$ROOT"/proving/windows-wsl2/*.ps1 "$ROOT"/proving/windows-wsl2/*.bat "$ROOT/proving/windows-wsl2/README.txt" "$STAGE/wsl2/"
|
||||
cp "$ROOT"/proving/fixtures/*.json "$STAGE/wsl2/fixtures/"
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
4c6b129d75c3d77a3689d22f1e1dc721b556aebb
|
||||
f1ea7a38e820b40c6c930b966af4f3cf8d7f8ab9
|
||||
|
|
|
|||
|
|
@ -62,6 +62,19 @@ else echo "warning: no $NVRTC_DIR/igneum-worker-cuda.exe (run $NVRTC_DIR/build-w
|
|||
[ -f "$CL_WORKER" ] && cp "$CL_WORKER" "$STAGE/" || echo "warning: no $CL_WORKER"
|
||||
[ -f "$TELEMETRY" ] && cp "$TELEMETRY" "$STAGE/" || echo "warning: no $TELEMETRY (AMD cards show no draw or temperature)"
|
||||
|
||||
# the Linux x86_64 prover pair for the PCs' WSL2 (6 October 2026: the CI payload had never carried it, so every PC ran a stale
|
||||
# host and exporter built by setup-wsl.sh from an old package, and no PC verified peer proofs or exported segments after
|
||||
# 0.3.14). The pair travels flat in the zip under distinct names (inputs.rs allows plain file names only); make-payload.sh
|
||||
# puts them at wsl2\bin\igneum-prove-host and igneum-prove-export, candidate 1 of the WSL lookup (src/wslhost.rs). Refused
|
||||
# without them: IGNEUM_PROVE_LINUX names the folder (the box's target-remote/release or the Mac's zigbuild target-linux).
|
||||
PROVE_LINUX="${IGNEUM_PROVE_LINUX:-$ROOT/proving/igneum-prove/target-linux/x86_64-unknown-linux-gnu/release}"
|
||||
for b in igneum-prove-host igneum-prove-export; do
|
||||
[ -f "$PROVE_LINUX/$b" ] || { echo "no Linux $b in $PROVE_LINUX (build the pair on igneum-build-1 with tools/build-remote.sh from proving/igneum-prove, or cargo zigbuild --target x86_64-unknown-linux-gnu.2.36 --features igneum-prove-host/cuda); the PCs' provers would stay stale" >&2; exit 1; }
|
||||
file "$PROVE_LINUX/$b" | grep -q "ELF 64-bit.*x86-64" || { echo "$PROVE_LINUX/$b is not a Linux x86_64 ELF" >&2; exit 1; }
|
||||
cp "$PROVE_LINUX/$b" "$STAGE/$b.linux-x86_64"
|
||||
echo "$b.linux-x86_64: $(stat -f %z "$PROVE_LINUX/$b") bytes from $PROVE_LINUX"
|
||||
done
|
||||
|
||||
# the signer, built from the app crate (it includes src/manifest.rs and src/inputs.rs, so it signs what the runner verifies)
|
||||
KEY="$HOME/.config/igneum/ota-signing-key"
|
||||
PUB="$HOME/.config/igneum/ota-signing-key.pub"
|
||||
|
|
|
|||
|
|
@ -16,6 +16,11 @@ function Post([string] $path, [string] $json) { try { Invoke-WebRequest -Uri ($b
|
|||
$s = State
|
||||
if (-not $s) { Write-Output 'RESULT TUNE error=state_unreadable'; exit 1 }
|
||||
$ver = [string]$s.version
|
||||
# the first line: the version gate (main, 6 October 2026): 0.3.14 or later carries bd7fcf4 (AMD apply on ack) and
|
||||
# 5165de7 (api/tune/goal with the climb); 0.3.15 the helper fixes A to E (6 October 2026); an older app aborts here
|
||||
$vok = $false; try { $vok = ([version]($ver -replace '[^0-9.].*$', '')) -ge [version]'0.3.15' } catch { }
|
||||
Write-Output ('RESULT TUNE version=' + $ver + ' gate=' + $(if ($vok) { 'ok (0.3.15 or later)' } else { 'FAILED (needs 0.3.15: the helper fixes A to E)' }) + ' climb_field=' + $(if ($null -ne $s.settings.tune_climb) { 'present' } else { 'absent' }))
|
||||
if (-not $vok -or $null -eq $s.settings.tune_climb) { Write-Output 'RESULT TUNE error=version_gate'; exit 1 }
|
||||
Write-Output ('RESULT TUNE installed app ' + $ver + ' power_control=' + $s.settings.power_control + ' tuning_off=' + $s.settings.tuning_off + ' climb_before=' + $s.settings.tune_climb + ' goal_before=' + $s.settings.tune_goal)
|
||||
$t = Get-ScheduledTask -TaskName 'Igneum Power Helper' -ErrorAction SilentlyContinue
|
||||
Write-Output ('RESULT TUNE task ' + $(if ($t) { $t.Actions[0].Execute + ' state=' + $t.State } else { 'NOT REGISTERED' }))
|
||||
|
|
@ -30,10 +35,13 @@ if (-not (Post 'api/tune/goal' '{"goal":"balanced","climb":true}')) { Write-Outp
|
|||
Start-Sleep -Seconds 2
|
||||
$order = @($cards | Where-Object { $_.vendor -eq 'amd' }) + @($cards | Where-Object { $_.vendor -eq 'nvidia' })
|
||||
$deadline = (Get-Date).AddMinutes(27)
|
||||
$logs = Join-Path $appDir 'logs'
|
||||
$logs = Join-Path $env:LOCALAPPDATA 'igneum\logs' # the app's log folder is <data root>\logs (platform::log_dir), not app\logs (6 October 2026: the first window read no log at all)
|
||||
function LogMark() { $l = Get-ChildItem -Path $logs -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1; if ($l) { return @{ f = $l.FullName; n = (Get-Content -LiteralPath $l.FullName).Count } } else { return @{ f = ''; n = 0 } } }
|
||||
function LogSince($m) { if (-not $m.f) { return @() }; $all = @(Get-Content -LiteralPath $m.f -ErrorAction SilentlyContinue); if ($all.Count -le $m.n) { return @() }; return @($all[$m.n..($all.Count - 1)]) }
|
||||
$rows = 0
|
||||
$m0 = LogMark
|
||||
# run 6's ladder floors (6 October 2026, PC 1), what the climb has to beat
|
||||
$floor = @{ 'NVIDIA GeForce RTX 5090' = @{ eff = 0.5632; mhs = 127.71; watts = 226.8; point = '1854 MHz at 100%' }; 'NVIDIA GeForce RTX 4070' = @{ eff = 0.3809; mhs = 28.78; watts = 75.6; point = '1863 MHz at 50%' } }
|
||||
foreach ($c in $order) {
|
||||
if ((Get-Date) -gt $deadline) { Say 'budget spent before ' + $c.key; break }
|
||||
$m = LogMark
|
||||
|
|
@ -58,6 +66,24 @@ foreach ($c in $order) {
|
|||
$st = State
|
||||
if ($st) { $cc = $st.mining.cards | Where-Object { $_.key -eq $c.key } | Select-Object -First 1; if ($cc) { Write-Output ('RESULT TUNE after key=' + $c.key + ' power_w=' + $cc.power_w + ' power_pct=' + $cc.power_pct + ' clock_cap=' + $cc.clock_cap_mhz + ' line=' + $cc.tune_line + ' source=' + $cc.tune_source + ' note=' + $cc.sweep_note); if ($cc.tune_curve) { Write-Output ('RESULT TUNE curve key=' + $c.key + ' ' + (($cc.tune_curve | ForEach-Object { ($_.clock_mhz) + 'MHz/' + ($_.mem_mhz) + 'mem/' + ($_.power_pct) + '%:' + $_.mhs + 'MH/s ' + $_.watts + 'W ' + $_.eff + ' ' + $_.mark }) -join ' | ')) } } }
|
||||
}
|
||||
# the prompt count: the app's own log over the window; a task line says "(no prompt)", a prompted helper says
|
||||
# "administrator prompt" (the Security log's process audit is off on PC 1, so the app's log is the record)
|
||||
$win = @(LogSince $m0)
|
||||
$noPrompt = @($win | Where-Object { $_ -match 'Power Helper task \(no prompt\)' }).Count
|
||||
$prompted = @($win | Where-Object { $_ -match 'administrator prompt|runas|UAC' }).Count
|
||||
Write-Output ('RESULT TUNE prompts=' + $prompted + ' task_lines=' + $noPrompt + ' (from the app log over the window)')
|
||||
# the climb against run 6's ladder floor, per NVIDIA card
|
||||
$st = State
|
||||
if ($st) {
|
||||
foreach ($c in @($cards | Where-Object { $_.vendor -eq 'nvidia' })) {
|
||||
$cc = $st.mining.cards | Where-Object { $_.key -eq $c.key } | Select-Object -First 1
|
||||
$f = $floor[[string]$c.name]
|
||||
if ($cc -and $f -and $cc.sweep_eff -gt 0) {
|
||||
$d = [math]::Round(($cc.sweep_eff - $f.eff) / $f.eff * 100, 1)
|
||||
Write-Output ('RESULT TUNE versus_run6 key=' + $c.key + ' climb=' + $cc.sweep_eff + ' MH/W (' + $cc.sweep_mhs + ' MH/s at ' + $cc.sweep_watts + ' W) ladder_floor=' + $f.eff + ' (' + $f.point + ') delta=' + $d + '% ' + $(if ($cc.sweep_eff -gt $f.eff) { 'the climb beat the floor' } elseif ($cc.sweep_eff -eq $f.eff) { 'equal' } else { 'the ladder floor stands' }))
|
||||
} elseif ($cc -and $f) { Write-Output ('RESULT TUNE versus_run6 key=' + $c.key + ' no chosen point this run (' + $cc.sweep_note + ')') }
|
||||
}
|
||||
}
|
||||
# the climb switch back to what it was
|
||||
Post 'api/tune/goal' ('{"climb":' + $(if ($climbBefore) { 'true' } else { 'false' }) + '}') | Out-Null
|
||||
Write-Output ('RESULT TUNE climb_restored=' + $climbBefore + ' rows=' + $rows)
|
||||
|
|
|
|||
12
relay/playbooks/ember-log-grep-pc1.ps1
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Ember: reads only. The installed app's own tune lines (not the job echoes) from <data root>\logs, newest file:
|
||||
# the engine's "tune:" requests and answers, "tune helper", TUNE start/aborted/chosen/rows, and the helper.log tail.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$l = Get-ChildItem (Join-Path $env:LOCALAPPDATA 'igneum\logs') -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if (-not $l) { Write-Output 'RESULT GREP no app log'; exit 1 }
|
||||
Write-Output ('RESULT GREP log=' + $l.FullName)
|
||||
Get-Content -LiteralPath $l.FullName | Where-Object { ($_ -match '\] tune|^\d+ tune|tune helper|TUNE (start|aborted|chosen|card=|helper|probe)|refused|did not take|power cap|Power Helper|probe|telemetry|gfx1201|queued|tune due|not due') -and ($_ -notmatch 'job ember-|RESULT ') } | Select-Object -Last 80 | ForEach-Object { Write-Output ('RESULT GREP ' + $_) }
|
||||
$h = Join-Path $env:LOCALAPPDATA 'igneum\app\sweep\helper.log'
|
||||
if (Test-Path -LiteralPath $h) { Get-Content -LiteralPath $h | Select-Object -Last 12 | ForEach-Object { Write-Output ('RESULT GREP helper ' + $_) } }
|
||||
$c = Join-Path $env:LOCALAPPDATA 'igneum\app\sweep\cmd.txt'
|
||||
if (Test-Path -LiteralPath $c) { Write-Output ('RESULT GREP cmd.txt: ' + ((Get-Content -LiteralPath $c) -join ' / ')) }
|
||||
exit 0
|
||||
|
|
@ -3,10 +3,10 @@
|
|||
# last two hours from the Security log. Never writes, quits, pauses or resumes anything.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$a = Join-Path $env:LOCALAPPDATA 'igneum\app'
|
||||
$l = Get-ChildItem (Join-Path $a 'logs') -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
$l = Get-ChildItem (Join-Path $env:LOCALAPPDATA 'igneum\logs') -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if ($l) {
|
||||
Write-Output ('RESULT PROOF log=' + $l.FullName)
|
||||
Get-Content -LiteralPath $l.FullName | Where-Object { $_ -match 'power cap|Power Helper|elevat|UAC|runas|tune' } | Select-Object -Last 24 | ForEach-Object { Write-Output ('RESULT PROOF applog ' + $_) }
|
||||
Get-Content -LiteralPath $l.FullName | Where-Object { $_ -match 'power cap|Power Helper|elevat|UAC|runas|tune' } | Select-Object -Last 60 | ForEach-Object { Write-Output ('RESULT PROOF applog ' + $_) }
|
||||
}
|
||||
$h = Join-Path $a 'sweep\helper.log'
|
||||
if (Test-Path -LiteralPath $h) { Get-Content -LiteralPath $h | Select-Object -Last 15 | ForEach-Object { Write-Output ('RESULT PROOF helper ' + $_) } } else { Write-Output 'RESULT PROOF helper none' }
|
||||
|
|
|
|||
|
|
@ -1,265 +0,0 @@
|
|||
# Igneum run job: Ember Tune end to end on PC 1 (machine ae432dc7), unattended. 5 October 2026.
|
||||
# Published as a `run` job with --stop-miners (docs/plans/ember-tune.md): the installed app stops its miners and
|
||||
# holds them; this script takes the engine that carries src/ember.rs (the one the fetch job put in
|
||||
# <app dir>\jobs\ember-kit-1\igneum-app-ember.exe, else the installed one; the AMD helper with the --tune and --set
|
||||
# commands from the telemetry agent's fetch job, <app dir>\jobs\amd-kit-1\kit\igneum-gpu-telemetry.exe), copies the install folder to a scratch
|
||||
# folder beside it, swaps the engine in, and starts that SECOND engine with `--sweep` in a scratch data folder (the
|
||||
# real settings.json, machine-id and wallet.json copied in; remote jobs, auto-update and proving switched off
|
||||
# there). That engine finds the installed app's node on 127.0.0.1:26610, mines on every card with the live program,
|
||||
# tunes them one after the other (the full two-knob plan where the card can be controlled, the baseline measurement
|
||||
# where it cannot: NVIDIA without administrator rights, Apple), prints every TUNE line on stdout, uploads its log
|
||||
# (the TUNE {json} record reaches the intake) and quits. Every TUNE line is re-emitted as a RESULT line, so
|
||||
# `node tools/jobs.mjs <job id>` shows the table. Before and after, nvidia-smi's limits and clocks and the AMD
|
||||
# helper's `--tune` lines are printed, so the restore can be read. The installed app's miners restart when the job
|
||||
# ends. Not elevated: nothing asks for administrator rights (the project lead asleep, 5 October 2026); the NVIDIA card is
|
||||
# therefore measure only tonight unless the engine finds itself elevated.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$resultTag = 'TUNE'
|
||||
$budgetMinutes = 55
|
||||
if (-not ($budgetMinutes -is [int]) -or $budgetMinutes -lt 5) { $budgetMinutes = 35 } # a budget under 5 minutes is a bug, not a budget (C35)
|
||||
$started = Get-Date
|
||||
$deadline = $started.AddMinutes($budgetMinutes)
|
||||
function Say([string] $m) { Write-Host ("[" + (Get-Date -Format 'HH:mm:ss') + "] " + $m) }
|
||||
|
||||
# the installed engine: the per-user install (0.3.3+), else Program Files
|
||||
$installDir = $null
|
||||
foreach ($d in @((Join-Path $env:LOCALAPPDATA 'Programs\Igneum Miner'), (Join-Path $env:ProgramFiles 'Igneum Miner'))) {
|
||||
if (Test-Path (Join-Path $d 'igneum-app.exe')) { $installDir = $d; break }
|
||||
}
|
||||
if (-not $installDir) { Write-Output 'RESULT TUNE error=no_engine reason=igneum-app.exe_not_found'; exit 2 }
|
||||
$appData = $env:IGNEUM_APP_DATA
|
||||
if (-not $appData) { $appData = Join-Path $env:LOCALAPPDATA 'igneum' }
|
||||
$appDir = $env:IGNEUM_APP_DIR
|
||||
if (-not $appDir) { $appDir = Join-Path $appData 'app' }
|
||||
|
||||
# the scratch install: the whole folder (workers, node, helper, DLLs) with the Ember engine swapped in
|
||||
# a FRESH scratch root per run (run 3, 6 October 2026, 11:45Z: the folder an earlier elevated engine had locked to itself
|
||||
# refused the settings copy, the engine started on stale files and exited in 6 s); old roots are small and left alone
|
||||
$root = Join-Path $env:LOCALAPPDATA ('igneum-tune-' + (Get-Date -Format 'yyyyMMdd-HHmmss'))
|
||||
$bin = Join-Path $root 'bin'
|
||||
$sApp = Join-Path $root 'app'
|
||||
$sLogs = Join-Path $root 'logs'
|
||||
New-Item -ItemType Directory -Force -Path $root, $sApp, $sLogs | Out-Null
|
||||
if (Test-Path $bin) { Remove-Item -LiteralPath $bin -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
Copy-Item -LiteralPath $installDir -Destination $bin -Recurse -Force
|
||||
# the installed engine carries Ember Tune from 0.3.12 on: prefer it; the kit is for a PC still on an older app
|
||||
$installedVer = (& (Join-Path $installDir 'igneum-app.exe') --version 2>&1 | Out-String).Trim()
|
||||
$installedHasEmber = $false
|
||||
if ($installedVer -match 'igneum-app (\d+)\.(\d+)\.(\d+)') { $installedHasEmber = ([int]$Matches[1] -gt 0) -or ([int]$Matches[2] -gt 3) -or (([int]$Matches[2] -eq 3) -and ([int]$Matches[3] -ge 12)) }
|
||||
$ember = $null
|
||||
# a kit is used when the installed app predates Ember Tune (0.3.13), or when a run carries fixes the installed app
|
||||
# lacks (ember-kit-6: the cmd quote fix and the self-registering elevated engine of 6 October 2026); a kit engine
|
||||
# registers the Power Helper on the INSTALLED exe's path (powertask::task_exe), never its own
|
||||
$k7 = Join-Path $appDir 'jobs\ember-kit-7\igneum-app-ember.exe'
|
||||
$k6 = Join-Path $appDir 'jobs\ember-kit-6\igneum-app-ember.exe'
|
||||
if (Test-Path $k7) { $ember = $k7 }
|
||||
elseif (Test-Path $k6) { $ember = $k6 }
|
||||
elseif (-not $installedHasEmber) { foreach ($cand in @((Join-Path $appDir 'jobs\ember-kit-2\igneum-app-ember.exe'), (Join-Path $appDir 'jobs\ember-kit-1\igneum-app-ember.exe'))) { if (Test-Path $cand) { $ember = $cand; break } } }
|
||||
if ($ember) {
|
||||
Copy-Item -LiteralPath $ember -Destination (Join-Path $bin 'igneum-app.exe') -Force
|
||||
Say ("engine: the Ember build from " + $ember)
|
||||
} else {
|
||||
Say ('engine: the installed one (' + $installedVer + $(if ($installedHasEmber) { ', carries Ember Tune' } else { '; no kit found: an older engine ignores the tune and reports no_rows' }) + ')')
|
||||
}
|
||||
$helper = $null
|
||||
$found = Get-ChildItem -Path (Join-Path $appDir 'jobs') -Recurse -Filter 'igneum-gpu-telemetry.exe' -ErrorAction SilentlyContinue | Where-Object { $_.FullName -match 'amd-kit' } | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if ($found) { $helper = $found.FullName }
|
||||
if ($helper) {
|
||||
Copy-Item -LiteralPath $helper -Destination (Join-Path $bin 'igneum-gpu-telemetry.exe') -Force
|
||||
Say ("helper: the Ember build of igneum-gpu-telemetry from " + $helper + " sha256=" + (Get-FileHash -LiteralPath $helper -Algorithm SHA256).Hash.ToLower())
|
||||
} else { Say 'helper: the installed igneum-gpu-telemetry (no jobs\amd-kit-1\kit\igneum-gpu-telemetry.exe); without --tune the AMD card measures only' }
|
||||
$exe = Join-Path $bin 'igneum-app.exe'
|
||||
$ver = (& $exe --version 2>&1 | Out-String).Trim()
|
||||
Say ("engine: " + $exe + " (" + $ver + ")")
|
||||
Write-Output ("RESULT TUNE engine " + $ver + " sha256=" + (Get-FileHash -LiteralPath $exe -Algorithm SHA256).Hash.ToLower())
|
||||
if ($ver -notmatch 'igneum-app (\d+)\.(\d+)\.(\d+)') { Write-Output 'RESULT TUNE error=version_unknown'; exit 2 }
|
||||
|
||||
foreach ($f in @('settings.json', 'machine-id', 'wallet.json', 'tuning.json', 'firewall-rule.json')) { # the firewall flag too: an older kit then asks nothing
|
||||
$src = Join-Path $appDir $f
|
||||
if (Test-Path $src) { Copy-Item -LiteralPath $src -Destination (Join-Path $sApp $f) -Force }
|
||||
}
|
||||
# the copies are VERBATIM (run 4, 6 October 2026: a PowerShell ConvertFrom-Json | ConvertTo-Json round trip rewrote big
|
||||
# integers as doubles, the engine read the file as defaults, no payout address, every card off, 96 old jobs run in
|
||||
# the scratch root); the engine itself switches remote jobs, updates, proving and Power control off under --sweep
|
||||
# (Settings::for_measurement) and makes every card due. Only a report line is read here.
|
||||
$sj = Join-Path $sApp 'settings.json'
|
||||
if (-not (Test-Path -LiteralPath $sj)) { Write-Output 'RESULT TUNE error=no_settings reason=the_installed_app_has_no_settings.json'; exit 2 }
|
||||
$installedPowerControl = 'unknown'; $addr = ''; $ncards = 0
|
||||
try { $back = Get-Content -LiteralPath $sj -Raw | ConvertFrom-Json; $addr = [string]$back.address; if ($back.cards) { $ncards = @($back.cards.PSObject.Properties).Count }; if ($back.PSObject.Properties.Name -contains 'power_control') { $installedPowerControl = ([bool]$back.power_control).ToString().ToLower() } } catch { }
|
||||
$bom = (Get-Content -LiteralPath $sj -Encoding Byte -TotalCount 3 -ErrorAction SilentlyContinue) -join ','
|
||||
Write-Output ('RESULT TUNE installed_power_control=' + $installedPowerControl + ' (the tune engine runs with it off: no prompt unless the job itself is elevated)')
|
||||
Write-Output ('RESULT TUNE scratch settings (verbatim copy): address ' + $(if ($addr) { $addr.Substring(0, [Math]::Min(10, $addr.Length)) + '...' } else { 'EMPTY' }) + ', cards ' + $ncards + ', first bytes ' + $bom + ', ' + (Get-Item -LiteralPath $sj).Length + ' bytes')
|
||||
if (-not $addr -and -not (Test-Path (Join-Path $sApp 'wallet.json'))) { Write-Output 'RESULT TUNE error=no_address reason=the_copied_settings_carry_no_payout_address_and_no_wallet.json'; exit 2 }
|
||||
Remove-Item -LiteralPath (Join-Path $sApp 'app.url') -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# the state before, for the report
|
||||
$smi = Join-Path $env:ProgramFiles 'NVIDIA Corporation\NVSMI\nvidia-smi.exe'
|
||||
if (-not (Test-Path $smi)) { $smi = Join-Path $env:SystemRoot 'System32\nvidia-smi.exe' }
|
||||
$tele = Join-Path $bin 'igneum-gpu-telemetry.exe'
|
||||
function Snapshot([string] $tag) {
|
||||
if (Test-Path $smi) {
|
||||
$q = (& $smi --query-gpu=index,name,driver_version,power.draw,power.limit,power.default_limit,power.min_limit,power.max_limit,clocks.gr,clocks.max.gr,clocks.mem --format=csv,noheader 2>&1 | Out-String).Trim()
|
||||
Write-Output ("RESULT TUNE " + $tag + " nvidia " + ($q -replace "`r?`n", ' | '))
|
||||
}
|
||||
if (Test-Path $tele) {
|
||||
$t = (& $tele --tune 2>&1 | Out-String).Trim()
|
||||
Write-Output ("RESULT TUNE " + $tag + " amd " + ($t -replace "`r?`n", ' | '))
|
||||
} else { Write-Output ("RESULT TUNE " + $tag + " amd no_helper") }
|
||||
}
|
||||
Snapshot 'before'
|
||||
|
||||
# the tune engine: status every 10 s (6 rate samples per 60 s hold)
|
||||
$env:IGNEUM_APP_DATA = $root
|
||||
$env:IGNEUM_APP_LOGS = $sLogs
|
||||
$env:IGNEUM_APP_STATUS_SECS = '10'
|
||||
$env:IGNEUM_APP_NO_OTA = '1' # C35: a second engine never runs the updater (the installer would quit the installed app)
|
||||
# C35 (5 October 2026): the engine's output goes to a FILE, never a pipe. A pipe's write end is inherited by every
|
||||
# process the engine starts (its miners and workers), so after an abort the installed app's jobs runner waits for an
|
||||
# EOF that never comes and hangs in its own quit; and the engine's whole tree is killed at the end (nothing orphaned).
|
||||
$outFile = Join-Path $root 'engine-stdout.log'
|
||||
$errFile = Join-Path $root 'engine-stderr.log'
|
||||
Remove-Item -LiteralPath $outFile, $errFile -Force -ErrorAction SilentlyContinue
|
||||
$p = Start-Process -FilePath $exe -ArgumentList '--sweep' -WorkingDirectory (Split-Path $exe) -WindowStyle Hidden -PassThru -RedirectStandardOutput $outFile -RedirectStandardError $errFile
|
||||
Say ("engine started, pid " + $p.Id + ", data " + $root + ", stdout " + $outFile)
|
||||
function EndTree([int] $procId, [string] $why) {
|
||||
$before = @(Get-Process -Name 'igneum-app', 'igneum-miner', 'igneum-worker-cuda', 'igneum-worker-opencl', 'igneum-worker-metal' -ErrorAction SilentlyContinue).Count
|
||||
& taskkill /T /F /PID $procId 2>&1 | Out-Null
|
||||
Start-Sleep -Seconds 2
|
||||
$after = @(Get-Process -Name 'igneum-app', 'igneum-miner', 'igneum-worker-cuda', 'igneum-worker-opencl', 'igneum-worker-metal' -ErrorAction SilentlyContinue).Count
|
||||
Write-Output ('RESULT ' + $resultTag + ' tree ended (' + $why + '): igneum processes ' + $before + ' -> ' + $after + ' (the installed app''s own miners are stopped and held by the job)')
|
||||
}
|
||||
$seen = 0
|
||||
$rows = 0
|
||||
# the installed app shows the tune instead of a bare "off" (the project lead, 6 October 2026): every `TUNE progress` line and the
|
||||
# `TUNE chosen` line go to its /api/tune-progress (a POST of state, never quit, pause or resume: the 5 October rule)
|
||||
$installedUrl = ''
|
||||
try { $installedUrl = (Get-Content -LiteralPath (Join-Path $appDir 'app.url') -Raw -ErrorAction Stop).Trim() } catch { }
|
||||
function Forward([string] $line) {
|
||||
if (-not $installedUrl) { return }
|
||||
$kv = @{}
|
||||
foreach ($tok in ($line -split ' ')) { if ($tok -match '^([a-z_]+)=(.*)$') { $kv[$Matches[1]] = $Matches[2] } }
|
||||
$body = @{ key = ($kv['card'] -replace '_', ' '); done = ($line -match '^TUNE chosen ') }
|
||||
if ($kv['key']) { $body.key = ($kv['key'] -replace '_', ' ') }
|
||||
foreach ($n in @('step', 'of', 'eta_s', 'mhs', 'watts', 'clock', 'cap')) { if ($kv[$n] -ne $null) { $body[$n] = [double]$kv[$n] } }
|
||||
if ($kv['cap'] -ne $null) { $body['power_pct'] = [double]$kv['cap'] }
|
||||
if ($kv['clock'] -ne $null) { $body['clock_mhz'] = [double]$kv['clock'] }
|
||||
if ($kv['plan']) { $body.plan = $kv['plan'] }
|
||||
if ($kv['note']) { $body.note = ($kv['note'] -replace '_', ' ') }
|
||||
if ($body.done) { $body.note = ('tuned: ' + $kv['mhs'] + ' MH/s at ' + $kv['watts'] + ' W') }
|
||||
try { Invoke-WebRequest -Uri ($installedUrl + 'api/tune-progress') -Method POST -Body ($body | ConvertTo-Json -Compress) -ContentType 'application/json' -UseBasicParsing -TimeoutSec 3 | Out-Null } catch { }
|
||||
}
|
||||
# the watchdog (coordinator, 6 October 2026): a tune engine that mines nothing for 120 s after its first status line
|
||||
# (every card "waiting" or 0.00 MH/s: no payout address, no worker, no node) fails the job at once with the engine's
|
||||
# last log line in the RESULT, its tree ended, mining restored by the job runner; a job that cannot mine never burns
|
||||
# its budget silently again
|
||||
$firstStatusAt = $null
|
||||
$lastMining = $null
|
||||
$idleSince = $null
|
||||
$idleSamples = 0
|
||||
$lastIdleStatus = ''
|
||||
$lastEngineLine = ''
|
||||
function EngineLogDump([string] $why) {
|
||||
Write-Output ('===== engine log tail (' + $why + ')')
|
||||
$t = Get-ChildItem -Path $sLogs -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if ($t) { Get-Content -LiteralPath $t.FullName -ErrorAction SilentlyContinue | Where-Object { $_ -notmatch 'status: accepted 0 blocks' } | Select-Object -Last 80 | ForEach-Object { Write-Output (' ' + $_) } } else { Write-Output ' (no app-*.log in the scratch logs folder)' }
|
||||
if (Test-Path -LiteralPath $errFile) { Write-Output '===== engine stderr'; Get-Content -LiteralPath $errFile -ErrorAction SilentlyContinue | Select-Object -Last 20 | ForEach-Object { Write-Output (' ' + $_) } }
|
||||
}
|
||||
# the newest `status:` line among the engine log's last 60 lines (run 5, 15:39Z: a single sampled tail line missed the
|
||||
# status lines between TUNE and telemetry lines for 300 s and the watchdog killed a live three-card tune)
|
||||
function EngineStatus() { $t = Get-ChildItem -Path $sLogs -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1; if ($t) { $ls = @(Get-Content -LiteralPath $t.FullName -Tail 60 -ErrorAction SilentlyContinue); for ($i = $ls.Count - 1; $i -ge 0; $i--) { if ([string]$ls[$i] -match ' status: ') { return [string]$ls[$i] } } }; return '' }
|
||||
function EngineTail() { $t = Get-ChildItem -Path $sLogs -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1; if ($t) { $l = Get-Content -LiteralPath $t.FullName -Tail 1 -ErrorAction SilentlyContinue; if ($l) { return [string]$l } }; return '' }
|
||||
function Exit3([string] $why, [string] $reason) {
|
||||
Write-Output ('RESULT TUNE error=' + $reason + ' last_status_line=' + ((EngineStatus) -replace '\s+', '_') + ' last_log_line=' + ($lastEngineLine -replace '\s+', '_'))
|
||||
EngineLogDump $why
|
||||
EndTree $p.Id $why
|
||||
Snapshot 'after'
|
||||
Write-Output 'RESULT TUNE error=no_rows'
|
||||
exit 3
|
||||
}
|
||||
# the AMD watts source (coordinator, 6 October 2026 16:00Z): the 3.0 coordinator's job read ZERO watts for the 9070 XT
|
||||
# from igneum-gpu-telemetry.exe while the installed app had read 198.9 W the day before. The tune engine samples its
|
||||
# own helper; this script also samples the installed app's /api/state power_w for the AMD card every 10 s (a GET,
|
||||
# never a write) and, when the engine's AMD progress lines carry no watts, prints a fallback ladder from those
|
||||
# samples (median per step) and names the source it used
|
||||
$amdApp = @(); $amdStep = 0; $amdMhs = 0.0; $amdEngNZ = 0; $amdEngZ = 0; $tick = 0; $amdMhsByStep = @{}
|
||||
while (-not $p.HasExited) {
|
||||
Start-Sleep -Seconds 5
|
||||
$tick++
|
||||
if ($installedUrl -and ($tick % 2 -eq 0)) {
|
||||
try {
|
||||
$js = (Invoke-WebRequest -Uri ($installedUrl + 'api/state') -UseBasicParsing -TimeoutSec 3).Content | ConvertFrom-Json
|
||||
foreach ($c in @($js.mining.cards)) { if ($c.vendor -eq 'amd') { $amdApp += ,@{ step = $amdStep; w = [double]$c.power_w } } }
|
||||
} catch { }
|
||||
}
|
||||
$tailLine = EngineTail
|
||||
if ($tailLine) { $lastEngineLine = $tailLine }
|
||||
$statusLine = EngineStatus
|
||||
if ($statusLine) {
|
||||
if (-not $firstStatusAt) { $firstStatusAt = Get-Date }
|
||||
$rate = 0.0; if ($statusLine -match '(\d+\.\d+) MH/s') { $rate = [double]::Parse($Matches[1], [Globalization.CultureInfo]::InvariantCulture) }
|
||||
# idle = three consecutive 10-s samples whose newest status line reads 0.00 MH/s (never a missing match)
|
||||
if ($rate -gt 0) { $lastMining = Get-Date; $idleSamples = 0; $idleSince = $null }
|
||||
elseif ($statusLine -ne $lastIdleStatus) { $lastIdleStatus = $statusLine; $idleSamples++; if (-not $idleSince) { $idleSince = Get-Date } }
|
||||
}
|
||||
if ($firstStatusAt -and -not $lastMining -and ((Get-Date) - $firstStatusAt).TotalSeconds -gt 120) { Exit3 'watchdog: not mining' 'not_mining reason=no_card_mined_within_120_s_of_the_first_status_line' }
|
||||
if ($lastMining -and $idleSamples -ge 3 -and $idleSince -and ((Get-Date) - $idleSince).TotalSeconds -gt 300) { Exit3 'watchdog: stopped mining' 'stopped_mining reason=three_status_lines_read_0.00_MH/s_and_300_s_passed' }
|
||||
$all = @(); if (Test-Path -LiteralPath $outFile) { $all = @(Get-Content -LiteralPath $outFile -ErrorAction SilentlyContinue) }
|
||||
while ($seen -lt $all.Count) {
|
||||
$l = [string]$all[$seen]; $seen++
|
||||
if ($l -match '^TUNE progress ') {
|
||||
Forward $l
|
||||
if ($l -match ' key=amd' -and $l -match ' step=(\d+)') { $amdStep = [int]$Matches[1]
|
||||
if ($l -match ' mhs=([\d.]+)') { $amdMhsByStep[$amdStep] = [double]::Parse($Matches[1], [Globalization.CultureInfo]::InvariantCulture) }
|
||||
if ($l -match ' watts=([\d.]+)' -and [double]::Parse($Matches[1], [Globalization.CultureInfo]::InvariantCulture) -gt 0) { $amdEngNZ++ } else { $amdEngZ++ } }
|
||||
}
|
||||
elseif ($l -match '^TUNE ') { Write-Output ('RESULT ' + $l); if ($l -match '^TUNE card=') { $rows++ }; if ($l -match '^TUNE chosen ') { Forward $l } }
|
||||
elseif ($l -match '^SWEEP ') { Write-Output ('RESULT ' + $l) }
|
||||
elseif ($l -match '^(URL|STATE) ') { }
|
||||
else { Say $l }
|
||||
}
|
||||
if ((Get-Date) -gt $deadline) {
|
||||
Say ("budget of " + $budgetMinutes + " min spent; asking the tune engine to quit")
|
||||
# C35 (5 October 2026): the only quit this script may send goes to the TUNE engine's own URL file in the scratch
|
||||
# root, never to a file under the installed app's folder; the RESULT line names the file it used
|
||||
$u = Join-Path $sApp 'app.url'
|
||||
# the only URL file this script may quit is its own scratch root's; the installed app's folder is refused by name
|
||||
if ((Resolve-Path -LiteralPath $u -ErrorAction SilentlyContinue).Path -like (Join-Path $appDir '*') -or $u -like '*\igneum\app\*') {
|
||||
Write-Output ('RESULT TUNE quit refused: ' + $u + ' is the installed app''s URL file')
|
||||
} elseif (Test-Path -LiteralPath $u) {
|
||||
Write-Output ('RESULT TUNE quit asked of the tune engine through ' + $u + ' (pid ' + $p.Id + ')')
|
||||
try { Invoke-WebRequest -Uri ((Get-Content -LiteralPath $u -Raw).Trim() + 'api/quit') -Method POST -Body '{}' -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 | Out-Null } catch { }
|
||||
} else { Write-Output ('RESULT TUNE quit not sent: no URL file at ' + $u + '; killing pid ' + $p.Id) }
|
||||
Start-Sleep -Seconds 20
|
||||
if (-not $p.HasExited) { EndTree $p.Id 'budget' }
|
||||
EngineLogDump 'budget'
|
||||
Write-Output 'RESULT TUNE error=budget_exceeded'
|
||||
}
|
||||
}
|
||||
$all = @(); if (Test-Path -LiteralPath $outFile) { $all = @(Get-Content -LiteralPath $outFile -ErrorAction SilentlyContinue) }
|
||||
while ($seen -lt $all.Count) { $l = [string]$all[$seen]; $seen++; if ($l -match '^TUNE ') { Write-Output ('RESULT ' + $l); if ($l -match '^TUNE card=') { $rows++ } } }
|
||||
$amdNZ = @($amdApp | Where-Object { $_.w -gt 0 })
|
||||
Write-Output ('RESULT TUNE amd_watts engine_progress nonzero=' + $amdEngNZ + ' zero=' + $amdEngZ + ' app_state samples=' + $amdApp.Count + ' nonzero=' + $amdNZ.Count)
|
||||
$amdSource = 'none'
|
||||
if ($amdEngNZ -gt 0) { $amdSource = 'engine_telemetry' }
|
||||
elseif ($amdNZ.Count -gt 0) {
|
||||
$amdSource = 'app_state_fallback'
|
||||
foreach ($g in ($amdNZ | Group-Object { $_.step } | Sort-Object { [int]$_.Name })) {
|
||||
$ws = @($g.Group | ForEach-Object { $_.w } | Sort-Object); $med = $ws[[int][math]::Floor($ws.Count / 2)]
|
||||
$mh = 0.0; if ($amdMhsByStep.ContainsKey([int]$g.Name)) { $mh = $amdMhsByStep[[int]$g.Name] }
|
||||
$eff = 0.0; if ($med -gt 0) { $eff = $mh / $med }
|
||||
Write-Output ('RESULT TUNE amd_fallback source=app_state step=' + $g.Name + ' watts=' + ('{0:F1}' -f $med) + ' mhs=' + ('{0:F2}' -f $mh) + ' eff=' + ('{0:F4}' -f $eff) + ' samples=' + $ws.Count)
|
||||
}
|
||||
}
|
||||
Write-Output ('RESULT TUNE amd_watts_source=' + $amdSource)
|
||||
Say ("tune engine exited " + $p.ExitCode + " after " + [int]((Get-Date) - $started).TotalSeconds + " s, " + $rows + " table rows")
|
||||
EndTree $p.Id 'end of run'
|
||||
# the project lead, 6 October 2026, 07:25Z: both cards stay on their best MH/W points (the tune pins them); no factory reset here.
|
||||
Snapshot 'after'
|
||||
# the tune engine's own log: the TUNE lines and what happened around them
|
||||
$log = Get-ChildItem -Path $sLogs -Filter 'app-*.log' -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if ($log) {
|
||||
Say ("engine log " + $log.FullName + ":")
|
||||
Get-Content -LiteralPath $log.FullName | Where-Object { $_ -match 'TUNE|tune|power cap|GPUs:|worker ready|STATUS|exited|upload' } | Select-Object -Last 100 | ForEach-Object { Say (' ' + $_) }
|
||||
}
|
||||
if ($rows -eq 0) { Write-Output 'RESULT TUNE error=no_rows'; exit 1 }
|
||||
exit 0
|
||||
|
|
@ -49,17 +49,10 @@ fi
|
|||
# PC 2 family probe and derive job; and the two hand-restore scripts pc1-amd-identities.ps1 and pc1-amd-flag-amd1.ps1,
|
||||
# allowed by the rule owner (6 October 2026, 19:xx UTC) because they post enabled TRUE only (a restore, never a switch off)
|
||||
PRE_RULE='^(relay/playbooks/(mixer-x4-pc1-bench|mixer-x4-5090-bench|mixer-x4-9070-bench|readwidth-5090-bench|readwidth-5090|readwidth-9070-bench|readwidth-9070|ca2-era-pc1|ca2-hot-5090-bench|ca2-hot-9070-bench|dot4-probe|ca3-derive-pc2)\.ps1|tools/ca3-reserve/pc2-family-probe\.ps1|tools/ca3-pc1-amd/pc1-amd-identities\.ps1|tools/ca3-pc1-amd/pc1-amd-flag-amd1\.ps1)$'
|
||||
# 6 October 2026 (0.3.14 gate): ember-tune-pc1.ps1 and the two agg-cost scripts are allowed pending the agg-cost --stop-miners change
|
||||
# and the Ember playbook's rewrite (the coordinator's 15:30Z ruling); EXPIRES 0.3.15 (the check below fails the tree at 0.3.15 or later while they stand)
|
||||
ALLOW='^(packaging/windows/stop-igneum\.ps1|relay/playbooks/ember-tune-pc1\.ps1|tools/proving-v1/pc2-agg-cost\.ps1|tools/proving-v1/pc2-agg-cost-restore\.ps1)$'
|
||||
# 6 October 2026 (0.3.15): the dated allow entries are gone (ember-tune-pc1.ps1 deleted, the agg-cost scripts on the
|
||||
# runner's --cards-off); only the installer's own stop step remains.
|
||||
ALLOW='^(packaging/windows/stop-igneum\.ps1)$'
|
||||
fail=0
|
||||
while IFS= read -r f; do [[ "$f" =~ $ALLOW ]] && continue; [[ "$f" =~ $PRE_RULE ]] && continue; check_file "$f" || fail=1; done < <(git ls-files 'relay/playbooks/**' 'tools/**' 'packaging/**' | grep -E '\.(ps1|sh)$' | grep -v '^tools/ci/')
|
||||
# expiry of the dated allow entries (6 October 2026): at 0.3.15 or later the three must be gone from ALLOW
|
||||
ver="$(sed -n 's/^version = "\(.*\)"/\1/p' app/igneum-app/Cargo.toml 2>/dev/null | head -1)"
|
||||
if [ -n "$ver" ] && [ "$(printf '%s\n0.3.15\n' "$ver" | sort -V | head -1)" = "0.3.15" ]; then
|
||||
for e in relay/playbooks/ember-tune-pc1.ps1 tools/proving-v1/pc2-agg-cost.ps1 tools/proving-v1/pc2-agg-cost-restore.ps1; do
|
||||
case "$ALLOW" in *"${e//./\\.}"*) echo "playbook-quit: the allow entry for $e expired at 0.3.15 (the tree says $ver): the pause/resume rule applies to it now"; fail=1 ;; esac
|
||||
done
|
||||
fi
|
||||
[ "$fail" = 0 ] && echo "playbook-quit: no playbook quits, pauses, resumes or switches the cards of the installed app"
|
||||
exit $fail
|
||||
|
|
|
|||
|
|
@ -13,10 +13,11 @@
|
|||
// "unknown to consensus" (the file's tip is from another chain: a second simnet)
|
||||
// 5. known-failed: a node E whose flag names a file that cannot be read blocks loudly (0.3.13.1), never genesis
|
||||
// 6. the account dump (0.3.14): export [tip-1, tip] from B carries preState; the exporter seeds from it and cuts
|
||||
// 7. the PC shape (0.3.14.1): G with the tip-0 pair and --igneum-exec-snapshot=peer recovers from A over p2p
|
||||
// Usage: node tools/exec-sync/net.mjs [--blocks 60]
|
||||
// IGNEUM_EXEC_BIN=<dir with igneumd and igneum-miner> (default vendor/igneum-node/target-exec-sync/release)
|
||||
import { spawn, spawnSync } from 'node:child_process';
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, openSync } from 'node:fs';
|
||||
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync, openSync, copyFileSync } from 'node:fs';
|
||||
import { createHash } from 'node:crypto';
|
||||
|
||||
const ROOT = new URL('../../', import.meta.url).pathname;
|
||||
|
|
@ -81,10 +82,14 @@ function sha256(path) { return '0x' + createHash('sha256').update(readFileSync(p
|
|||
async function main() {
|
||||
const a = await new Node(0).start();
|
||||
log(`node A up on ${a.evm}`);
|
||||
const tip0File = `${TMP}/tip0.bin`;
|
||||
const miner = mine(a, 'exec-sync-a');
|
||||
await waitTip(a, BLOCKS, 900);
|
||||
const tipA = hexn(await a.eth('eth_blockNumber'));
|
||||
const st0 = await a.eth('igneum_getExecStatus');
|
||||
// a tip-0 snapshot (the genesis replay's leftover shape) for case 7: the state after chain block 0, from A's ring
|
||||
const t0x = await a.eth('igneum_exportExecSnapshot', [tip0File, '0x0']);
|
||||
if (!existsSync(tip0File) || hexn(t0x.tip) !== 0) throw new Error('no tip-0 export');
|
||||
check('A executes from genesis', st0.startedFrom === 'genesis' && st0.blocked === null && hexn(st0.executedTip) >= BLOCKS, st0);
|
||||
// case 1: the export, the stop, the resume
|
||||
const file = `${TMP}/snapshot.bin`;
|
||||
|
|
@ -151,6 +156,26 @@ async function main() {
|
|||
const out = (r.stdout || '') + (r.stderr || '');
|
||||
check('the exporter seeds from the dump and cuts the block (state root equals the node\'s)', r.status === 0 && existsSync(fix) && /account dump: \d+ accounts after chain block/.test(out) && !/differs from the node/.test(out), { status: r.status, line: (out.split('\n').find(l => /account dump/.test(l)) || out.slice(-200)).slice(0, 200) });
|
||||
}
|
||||
// 7. the PC shape (0.3.14.1): a node G whose data dir holds the tip-0 pair the 0.3.13 genesis replay left behind,
|
||||
// with --igneum-exec-snapshot=peer: G refuses the pair (set aside as .tip0), blocks, asks A over p2p and loads
|
||||
// A's state with no hand action; then it follows the chain. (A pruning point above the anchor is not reproducible
|
||||
// on a simnet in minutes; the pruned-node branch of the same classifier is unit-tested in service.rs.)
|
||||
{
|
||||
const g = new Node(6, [`127.0.0.1:${a.p2pPort}`], ['--igneum-exec-snapshot=peer']);
|
||||
const evmDir = `${g.dir}/igneum-devnet-${SUFFIX}/datadir/evm`;
|
||||
mkdirSync(evmDir, { recursive: true });
|
||||
copyFileSync(tip0File, `${evmDir}/exec-snapshot.bin`); copyFileSync(tip0File, `${evmDir}/exec-snapshot.prev.bin`);
|
||||
await g.start();
|
||||
let stG = null;
|
||||
for (let k = 0; k < 240; k++) { stG = await g.eth('igneum_getExecStatus'); if (stG.startedFrom.startsWith('peer') && hexn(stG.executedTip) > 0) break; await sleep(500); }
|
||||
const gline = g.logText().split('\n').find(l => /tip 0 \(a genesis state\) is never a resume/.test(l));
|
||||
check('G refused the tip-0 pair in its data dir and set it aside', !!gline && existsSync(`${evmDir}/exec-snapshot.bin.tip0`), { line: gline && gline.slice(-140) });
|
||||
check('G recovered over p2p from A with no hand action and follows the chain', stG.startedFrom.startsWith('peer') && hexn(stG.executedTip) > 0 && stG.blocked === null, { startedFrom: stG.startedFrom, tip: stG.executedTip });
|
||||
const h = hexn(stG.executedTip);
|
||||
const ra = await a.eth('eth_getBlockByNumber', ['0x' + h.toString(16), false]), rg = await g.eth('eth_getBlockByNumber', ['0x' + h.toString(16), false]);
|
||||
check('G\'s state root at its tip equals A\'s', ra.stateRoot === rg.stateRoot, { height: h });
|
||||
await g.stop();
|
||||
}
|
||||
// 5. known-failed: a node E whose flag names a file that does not exist (the seed, 6 October 2026: unreadable under
|
||||
// /root) blocks loudly, never runs as if the flag were unset, never executes genesis
|
||||
const e = await new Node(5, [`127.0.0.1:${a.p2pPort}`], ['--igneum-exec-snapshot=/nonexistent/exec-snapshot.bin,0x00']).start();
|
||||
|
|
|
|||
|
|
@ -40,17 +40,9 @@ foreach ($p in (Get-CimInstance Win32_Process | Where-Object { $_.Name -like 'ig
|
|||
}
|
||||
"RESULT strays_stopped $(Stamp) $killed"
|
||||
& wsl.exe -d Ubuntu-24.04 -u root -- bash -c 'pkill -x sp1-gpu-server; rm -f /tmp/sp1-cuda-*.sock; echo "RESULT socket_cleanup $(date -u +%Y-%m-%dT%H:%M:%SZ) servers=$(pgrep -x sp1-gpu-server | wc -l) sockets=$(ls /tmp/sp1-cuda-*.sock 2>/dev/null | wc -l)"' 2>&1 | ForEach-Object { ($_ -replace "`0", '') }
|
||||
function Card($st) { if ($null -eq $st) { return $null }; $st.mining.cards | Where-Object { $_.vendor -eq 'nvidia' } | Select-Object -First 1 }
|
||||
$c = Card (State)
|
||||
if ($c) {
|
||||
$stc = State
|
||||
"RESULT cards_before $(Stamp) $(($stc.mining.cards | ForEach-Object { "$($_.name):enabled=$($_.enabled):identities=$($_.identities):$($_.state)" }) -join ',')"
|
||||
$list = @(); foreach ($k in $stc.mining.cards) { $list += @{ key = $k.key; enabled = $(if ($k.key -eq $c.key) { $true } else { [bool]$k.enabled }); identities = [int]$k.identities } }
|
||||
"RESULT card_on $(Stamp) $(Post '/api/cards' @{ cards = $list })"
|
||||
}
|
||||
# 6 October 2026 (0.3.15): no card switch, resume or start from a script (CLAUDE.md job rule); the runner of the measurement
|
||||
# job restores the card and the miners on any exit, so this script only cleans up strays and reads the state back.
|
||||
"RESULT timeslice $(Stamp) $((& nvidia-smi compute-policy --set-timeslice=0 2>&1) -join ' ')"
|
||||
"RESULT resume $(Stamp) $(Post '/api/resume' @{})"
|
||||
"RESULT start $(Stamp) $(Post '/api/start' @{})"
|
||||
"RESULT prove_on $(Stamp) $(Post '/api/prove' @{on=$true})"
|
||||
Start-Sleep -Seconds 20
|
||||
$st = State
|
||||
|
|
|
|||
|
|
@ -92,12 +92,7 @@ function CardPrefs {
|
|||
return $prefs
|
||||
}
|
||||
function NvidiaKey { $p = CardPrefs; ($p.Keys | Where-Object { $_ -like 'nvidia:*' } | Select-Object -First 1) }
|
||||
function CardSwitch($on, $identities) {
|
||||
$p = CardPrefs; $nk = NvidiaKey
|
||||
if (-not $nk) { return "no nvidia card key in settings.json (keys: $($p.Keys -join ','))" }
|
||||
$list = @(); foreach ($k in $p.Keys) { $list += @{ key = $k; enabled = $(if ($k -eq $nk) { [bool]$on } else { [bool]$p[$k].enabled }); identities = $(if ($k -eq $nk -and $identities) { [int]$identities } else { [int]$p[$k].identities }) } }
|
||||
"prefs_before=$(($p.Keys | ForEach-Object { "$($_):enabled=$($p[$_].enabled):identities=$($p[$_].identities)" }) -join ',') " + (Post '/api/cards' @{ cards = $list })
|
||||
}
|
||||
# CardSwitch removed 6 October 2026: the runner's --cards-off owns the card (see the needIdle block)
|
||||
function CudaWorkers { (Get-Process -ErrorAction SilentlyContinue | Where-Object { $_.ProcessName -like 'igneum-worker-cuda*' } | Measure-Object).Count }
|
||||
$job = $env:IGNEUM_JOB_DIR
|
||||
if (-not $job) { $job = Join-Path $env:TEMP 'igneum-agg-cost' }
|
||||
|
|
@ -346,12 +341,15 @@ try {
|
|||
if ($PhaseSet -contains 'B') { RunPhase 'B' "SP1_WORKER_VERIFY_INTERMEDIATES=false SP1_PROVER=cuda RUST_LOG=off `$H --mode aggregate --proofs '$(AggGroups $srcW)' --parent $(SrcParent) --out `$JOB/results-B.json" }
|
||||
$needIdle = @('C','C1','C2','G0','D','E','E0','F','G','H') | Where-Object { $PhaseSet -contains $_ }
|
||||
if ($needIdle) {
|
||||
# the 5090 switched off through /api/cards (and on again in the finally block): /api/pause then /api/resume left
|
||||
# the worker off on 0.3.9 (21:25Z, an app defect in the resume path, fixed for 0.3.11)
|
||||
"RESULT card_off $(Stamp) $(CardSwitch $false $null)"; $paused = $true
|
||||
$w = 0; $left = 1
|
||||
# 6 October 2026 (0.3.15): the script never switches a card or pauses the installed app (CLAUDE.md job rule, the
|
||||
# playbook-quit check). The job is published with --cards-off <nvidia key>: the RUNNER switches the 5090 off through
|
||||
# the app's own card path before the script and puts it back on any exit. Here the script only confirms the worker
|
||||
# is gone, and fails loud if it is not (no pause fallback: a script that dies before its finally left the box
|
||||
# paused unattended).
|
||||
$w = 0; $left = CudaWorkers
|
||||
while ($w -lt 120 -and $left -gt 0) { Start-Sleep -Seconds 5; $w += 5; $left = CudaWorkers }
|
||||
if ($left -gt 0) { "RESULT card_off_failed $(Stamp) the app's CUDA worker is still running after $w s ($left); /api/pause as the fallback: $(Post '/api/pause' @{})"; $w2 = 0; while ($w2 -lt 60 -and (CudaWorkers) -gt 0) { Start-Sleep -Seconds 5; $w2 += 5 }; $left = CudaWorkers }
|
||||
if ($left -gt 0) { "RESULT card_off_failed $(Stamp) the app's CUDA worker is still running after $w s ($left): publish this job with --cards-off <nvidia card key> (the runner switches it off); nothing measured"; exit 3 }
|
||||
$paused = $true
|
||||
"RESULT paused $(Stamp) after $w s; CUDA workers left: $left; $(Smi)"
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
|
|
@ -423,11 +421,7 @@ try {
|
|||
} finally {
|
||||
StopMiner 'final'
|
||||
& wsl.exe -d Ubuntu-24.04 -u root -- bash -c 'pkill -x sp1-gpu-server; rm -f /tmp/sp1-cuda-*.sock; echo "RESULT socket_cleanup $(date -u +%Y-%m-%dT%H:%M:%SZ) servers=$(pgrep -x sp1-gpu-server | wc -l) sockets=$(ls /tmp/sp1-cuda-*.sock 2>/dev/null | wc -l)"' 2>&1 | ForEach-Object { ($_ -replace "`0", '') }
|
||||
if ($paused) {
|
||||
"RESULT card_on $(Stamp) $(CardSwitch $true $null)"; "RESULT resume $(Stamp) $(Post '/api/resume' @{})"
|
||||
$w3 = 0; while ($w3 -lt 90 -and (CudaWorkers) -eq 0) { Start-Sleep -Seconds 5; $w3 += 5 }
|
||||
if ((CudaWorkers) -eq 0) { "RESULT miner_back_failed $(Stamp) no CUDA worker after $w3 s; /api/start: $(Post '/api/start' @{})" } else { "RESULT miner_back $(Stamp) CUDA worker running after $w3 s" }
|
||||
}
|
||||
if ($paused) { "RESULT card_restore $(Stamp) left to the runner (--cards-off puts the card back on any exit; the miners follow)" }
|
||||
"RESULT prove_on $(Stamp) $(Post '/api/prove' @{on=$true})"
|
||||
"RESULT end $(Stamp) $(Smi)"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// Cuts an Igneum Miner app version from one command. the project lead, 4 October 2026: 0.3.1, 0.3.2 and 0.3.3 each took eight hand
|
||||
// steps and an hour; this is the one step. packaging/README-ship.md has the short version.
|
||||
//
|
||||
// node tools/ship-app.mjs 0.3.4 --node <fork worktree> [--notes "..."] [--dry-run] [--from <step>]
|
||||
// node tools/ship-app.mjs 0.3.4 --node <fork worktree> [--notes "..."] [--dry-run] [--from <step>] [--until <step>]
|
||||
// [--skip-windows | --skip-mac] [--node-commit <sha>] [--win-release <dir>] [--mac-release <dir>]
|
||||
// [--min-supported 0.3.0] [--activation-height N --deadline-note "..."] [--dl-both] [--public]
|
||||
// node tools/ship-app.mjs --check the six version files agree (exit 1 when they do not)
|
||||
|
|
@ -158,7 +158,10 @@ async function gh(args, { quiet = true } = {}) {
|
|||
async function ghJson(args) { const r = await gh(args); if (r.code !== 0) throw new Error(`gh ${args.slice(0, 2).join(' ')} failed: ${r.out.trim()}`); return JSON.parse(r.out); }
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
async function head(url) { const r = await fetch(url, { method: 'HEAD' }); return { status: r.status, length: r.headers.get('content-length') ? Number(r.headers.get('content-length')) : null }; }
|
||||
async function getJson(url) { const r = await fetch(url); if (!r.ok) return null; return r.json().catch(() => null); }
|
||||
async function getJson(url) { const r = await fetch(`${url}${url.includes('?') ? '&' : '?'}t=${Date.now()}`, { headers: { 'Cache-Control': 'no-cache' } }); if (!r.ok) return null; return r.json().catch(() => null); }
|
||||
// the state keeps the fork commit as the preflight read it (short with --branch, full with --node-commit); the inputs
|
||||
// manifest carries the full one (6 October 2026, 0.3.15: `!==` on short against full re-pushed the inputs twice and failed)
|
||||
const sameCommit = (a, b) => !!a && !!b && (String(a).startsWith(String(b)) || String(b).startsWith(String(a)));
|
||||
|
||||
// ---- arguments -----------------------------------------------------------------------------------------------------
|
||||
const argv = process.argv.slice(2);
|
||||
|
|
@ -182,6 +185,7 @@ if (flags.check) {
|
|||
const VERSION = pos[0];
|
||||
if (!VERSION || !isVersion(VERSION)) { console.error('usage: node tools/ship-app.mjs <major.minor.patch> --node <fork worktree> [--notes "..."] [--dry-run] [--from <step>] [--skip-windows|--skip-mac] [--dl-both]\n node tools/ship-app.mjs --check | --self-test'); process.exit(2); }
|
||||
if (!flags.node) { console.error('--node <fork worktree> is required (the igneum-node worktree the node and miner were built from)'); process.exit(2); }
|
||||
if (flags.until && !STEPS.includes(flags.until)) { console.error(`--until must be one of: ${STEPS.join(', ')}`); process.exit(2); }
|
||||
if (flags.from && !STEPS.includes(flags.from)) { console.error(`--from must be one of: ${STEPS.join(', ')}`); process.exit(2); }
|
||||
if (flags['skip-windows'] && flags['skip-mac']) { console.error('--skip-windows and --skip-mac together leave nothing to ship'); process.exit(2); }
|
||||
const DRY = !!flags['dry-run'];
|
||||
|
|
@ -275,8 +279,9 @@ async function preflight() {
|
|||
const otherDirty = dirty.filter(p => !versionPaths.has(p));
|
||||
if (otherDirty.length) problems.push(`this tree has ${otherDirty.length} modified tracked file(s) besides the version files; commit or stash them first:\n ${otherDirty.slice(0, 8).join('\n ')}`);
|
||||
if (!DRY) { const f = git(['fetch', 'origin', 'master', '--quiet']); if (f.code !== 0) problems.push(`git fetch origin failed: ${f.out}`); }
|
||||
const behind = git(['rev-list', '--count', 'HEAD..origin/master']).out;
|
||||
if (behind !== '0') problems.push(`this tree is ${behind} commit(s) behind origin/master; git pull first`);
|
||||
const upstream = `origin/${flags.branch || 'master'}`;
|
||||
const behind = git(['rev-parse', '--verify', '-q', upstream]).code === 0 ? git(['rev-list', '--count', `HEAD..${upstream}`]).out : '0';
|
||||
if (behind !== '0') problems.push(`this tree is ${behind} commit(s) behind ${upstream}; git pull first`);
|
||||
const headSha = git(['rev-parse', 'HEAD']).out;
|
||||
// the fork
|
||||
if (!existsSync(join(NODE_DIR, '.git'))) problems.push(`--node ${NODE_DIR} is not a git worktree`);
|
||||
|
|
@ -362,14 +367,14 @@ async function inputs() {
|
|||
if (!WIN) return done('inputs', 'skipped', '--skip-windows');
|
||||
const files = [...WIN_INPUTS, ...WORKERS.filter(existsSync)];
|
||||
const live = await getJson(`${BASE}/payload-inputs.json`);
|
||||
const same = live && live.node_source_commit === state.forkCommit && files.every(p => live.files && live.files[basename(p)] && live.files[basename(p)].sha256 === sha256(p));
|
||||
const same = live && sameCommit(live.node_source_commit, state.forkCommit) && files.every(p => live.files && live.files[basename(p)] && live.files[basename(p)].sha256 === sha256(p));
|
||||
if (same) return done('inputs', 'already', `payload-inputs.zip carries these files from fork ${state.forkCommit} (built ${live.built_at})`);
|
||||
const cmd = `IGNEUM_WIN_RELEASE=${WIN_RELEASE} IGNEUM_NODE_SRC=${NODE_DIR} packaging/windows/push-inputs.sh`;
|
||||
if (DRY) return done('inputs', 'would', `run ${cmd} (deploys the downloads folder)`);
|
||||
const r = await run('bash', [join(ROOT, 'packaging', 'windows', 'push-inputs.sh')], { env: { IGNEUM_WIN_RELEASE: WIN_RELEASE, IGNEUM_NODE_SRC: NODE_DIR } });
|
||||
if (r.code !== 0) throw new Error(`push-inputs.sh exited ${r.code}; retry by hand: ${cmd}`);
|
||||
const after = await getJson(`${BASE}/payload-inputs.json`);
|
||||
if (!after || after.node_source_commit !== state.forkCommit) throw new Error(`the live payload-inputs.json does not name fork ${state.forkCommit} after the push`);
|
||||
if (!after || !sameCommit(after.node_source_commit, state.forkCommit)) throw new Error(`the live payload-inputs.json does not name fork ${state.forkCommit} after the push`);
|
||||
done('inputs', 'ok', `payload-inputs.zip live, fork ${state.forkCommit}`);
|
||||
}
|
||||
|
||||
|
|
@ -378,21 +383,25 @@ async function commit() {
|
|||
const changed = git(['status', '--porcelain', '--', ...paths]).out.split('\n').filter(Boolean);
|
||||
const msg = `Igneum Miner ${VERSION}: ${NOTES}`;
|
||||
if (changed.length) {
|
||||
if (DRY) return done('commit', 'would', `git commit ${paths.length} files as "${msg}" and push origin master`);
|
||||
if (DRY) return done('commit', 'would', `git commit ${paths.length} files as "${msg}" and push origin ${flags.branch || 'master'}`);
|
||||
const a = git(['add', '--', ...paths]); if (a.code !== 0) throw new Error(`git add failed: ${a.out}`);
|
||||
const cm = git(['commit', '-m', msg]); if (cm.code !== 0) throw new Error(`git commit failed: ${cm.out}`);
|
||||
} else if (DRY) { done('commit', 'would', 'nothing to commit (the files are committed); push if origin/master lacks HEAD'); return; }
|
||||
} else if (DRY) { done('commit', 'would', `nothing to commit (the files are committed); push if origin/${flags.branch || 'master'} lacks HEAD`); return; }
|
||||
const sha = git(['rev-parse', 'HEAD']).out;
|
||||
const subject = git(['log', '-1', '--format=%s']).out;
|
||||
if (!subject.startsWith(`Igneum Miner ${VERSION}`)) say(` note: HEAD is "${subject.slice(0, 80)}", not the version commit; the Windows build runs on whatever master is`);
|
||||
const pushed = git(['merge-base', '--is-ancestor', sha, 'origin/master']).code === 0;
|
||||
// the push goes to the branch this run was given and only there (6 October 2026: `git push origin master` from a release
|
||||
// worktree pushed the shared checkout's local master ref, said "pushed to origin/master" from its own arithmetic, and the
|
||||
// Windows build was then looked for on master; master merges are main's)
|
||||
const target = flags.branch || 'master';
|
||||
const pushed = git(['rev-parse', '--verify', '-q', `origin/${target}`]).code === 0 && git(['merge-base', '--is-ancestor', sha, `origin/${target}`]).code === 0;
|
||||
if (!pushed) {
|
||||
const sw = runSync('gh', ['auth', 'switch', '--user', GH_USER]); if (sw.code !== 0) throw new Error(`gh auth switch failed: ${sw.out}`);
|
||||
const p = await run('git', ['push', 'origin', 'master']);
|
||||
if (p.code !== 0) throw new Error(`git push origin master exited ${p.code}; retry: gh auth switch --user ${GH_USER} && git push origin master`);
|
||||
const p = await run('git', ['push', 'origin', `HEAD:refs/heads/${target}`]);
|
||||
if (p.code !== 0) throw new Error(`git push origin HEAD:${target} exited ${p.code}; retry: gh auth switch --user ${GH_USER} && git push origin HEAD:refs/heads/${target}`);
|
||||
}
|
||||
state.sha = sha; saveState();
|
||||
done('commit', pushed && !changed.length ? 'already' : 'ok', `${sha.slice(0, 12)} ${pushed ? 'was on' : 'pushed to'} origin/master`);
|
||||
done('commit', pushed && !changed.length ? 'already' : 'ok', `${sha.slice(0, 12)} ${pushed ? 'was on' : 'pushed to'} origin/${target}`);
|
||||
}
|
||||
|
||||
async function ci() {
|
||||
|
|
@ -408,8 +417,8 @@ async function ci() {
|
|||
say(` no ${WORKFLOW} run for ${sha.slice(0, 12)} yet; waiting up to 3 minutes for the push to start one`);
|
||||
for (let i = 0; i < 12 && !pick; i++) { await sleep(15000); runs = await list(); pick = runs[0]; }
|
||||
if (!pick) {
|
||||
say(` dispatching: gh workflow run ${WORKFLOW} --ref master`);
|
||||
const d = await gh(['workflow', 'run', WORKFLOW, '--repo', REPO, '--ref', 'master']); if (d.code !== 0) throw new Error(`gh workflow run failed: ${d.out.trim()}`);
|
||||
say(` dispatching: gh workflow run ${WORKFLOW} --ref ${flags.branch || 'master'}`);
|
||||
const d = await gh(['workflow', 'run', WORKFLOW, '--repo', REPO, '--ref', flags.branch || 'master']); if (d.code !== 0) throw new Error(`gh workflow run failed: ${d.out.trim()}`);
|
||||
for (let i = 0; i < 12 && !pick; i++) { await sleep(15000); runs = await list(); pick = runs[0]; }
|
||||
if (!pick) throw new Error(`no run appeared for ${sha.slice(0, 12)}; check https://github.com/${REPO}/actions and retry: ${retryCmd('ci')}`);
|
||||
}
|
||||
|
|
@ -606,7 +615,14 @@ const IMPL = { preflight, bump, inputs, commit, ci, fetch: fetchStep, dmg, copy,
|
|||
async function main() {
|
||||
const start = flags.from ? STEPS.indexOf(flags.from) : 0;
|
||||
// preflight always runs: it is reads only and the later steps need its facts (fork commit, state)
|
||||
const todo = start === 0 ? STEPS : ['preflight', ...STEPS.slice(start)];
|
||||
let todo = start === 0 ? STEPS : ['preflight', ...STEPS.slice(start)];
|
||||
// --until <step>: stage a cut (6 October 2026, 0.3.15: everything built, signed and verified locally, nothing deployed
|
||||
// until the gate's PASS and the project lead's go); the run ends after that step and says what it left for the resume
|
||||
if (flags.until) {
|
||||
if (!STEPS.includes(flags.until)) { console.error(`--until must be one of: ${STEPS.join(', ')}`); process.exit(2); }
|
||||
const cut = todo.indexOf(flags.until);
|
||||
if (cut >= 0) { const left = todo.slice(cut + 1); todo = todo.slice(0, cut + 1); say(`staging: the run stops after ${flags.until}; left for the resume (--from ${left[0] || '-'}): ${left.join(', ') || 'nothing'}`); }
|
||||
}
|
||||
if (start > 0) say(`resuming from ${flags.from} (state ${STATE_FILE})`);
|
||||
const t0 = Date.now();
|
||||
let failed = null;
|
||||
|
|
|
|||
|
|
@ -80,6 +80,30 @@ function growLog() {
|
|||
}
|
||||
setInterval(growLog, 1000);
|
||||
|
||||
// /api/live for the embedded DAG scene (site/live-dag.js): a synthetic chain at one block a second, four miner lanes
|
||||
// ("you" is this machine), parents into the last two seconds, the selected chain through "you" and "miner-2", blue
|
||||
// and red colouring, proofs filling two blocks in three after 20 s, a checkpoint band every 30 s locked after 10 s.
|
||||
const LIVE_T0 = Math.floor(Date.now() / 1000) - 400;
|
||||
function liveNow(windowS) {
|
||||
const t = Math.floor(now()), from = t - Math.min(300, Math.max(30, windowS)) - 25, blocks = [];
|
||||
const lanes = ['you', 'miner-2', 'miner-3', 'miner-4'];
|
||||
let prev = [];
|
||||
for (let s = from; s <= t - 2; s++) {
|
||||
const seed = ((s % 100000) * 7919 + ((s >> 3) % 97) * 131) % 1000;
|
||||
const miner = lanes[seed % 4], hash = 'b' + s.toString(16).padStart(10, '0');
|
||||
const chain = miner === 'you' || miner === 'miner-2' ? (seed % 7 !== 3) : (seed % 5 === 0);
|
||||
const color = seed % 11 === 0 ? 'red' : (s < t - 4 ? 'blue' : 'pending');
|
||||
const proven = color === 'blue' && s < t - 20 && seed % 3 !== 0;
|
||||
const shards = proven ? [{ state: 'paid' }] : color === 'blue' && s < t - 8 ? [{ state: 'proving' }] : [];
|
||||
const parents = prev.slice(-2).map(b => b.hash);
|
||||
const locked = (s - LIVE_T0) % 30 === 0 && s < t - 10;
|
||||
const b = { hash, ts: s * 1000, blue_score: 194000 + (s - LIVE_T0), daa: 198000 + (s - LIVE_T0), parents, chain, color, miner, locked, final: s < t - 40, shards, proven };
|
||||
blocks.push(b); prev.push(b); if (prev.length > 3) prev.shift();
|
||||
}
|
||||
const checkpoints = [];
|
||||
for (let s = from; s <= t - 2; s++) if ((s - LIVE_T0) % 30 === 0) checkpoints.push({ index: Math.floor((s - LIVE_T0) / 30), hash: 'c' + s.toString(16), blue_score: 194000 + (s - LIVE_T0), daa: 198000 + (s - LIVE_T0), state: s < t - 10 ? 'locked' : 'pending', fraction_total: s < t - 10 ? 0.71 : 0.44 });
|
||||
return { ok: true, now: t, state: { stale: false, age_s: 1, height: 198000 + (t - LIVE_T0) }, blocks, finality: { checkpoints } };
|
||||
}
|
||||
function stateNow() {
|
||||
const s = clone(recState);
|
||||
const t = now();
|
||||
|
|
@ -156,6 +180,7 @@ http.createServer((req, res) => {
|
|||
return send(res, 200, types['.html'], fs.readFileSync(path.join(uiDir, 'index.html')));
|
||||
}
|
||||
if (rest === '/api/state' && req.method === 'GET') return json(res, stateNow());
|
||||
if (rest === '/api/live' && req.method === 'GET') return json(res, liveNow(parseInt(u.searchParams.get('window') || '120', 10)));
|
||||
if (rest === '/api/log' && req.method === 'GET') {
|
||||
const after = parseInt(u.searchParams.get('after') || '0', 10), limit = Math.min(20000, parseInt(u.searchParams.get('limit') || '400', 10));
|
||||
let out = log.filter(l => l.seq > after); if (out.length > limit) out = out.slice(out.length - limit);
|
||||
|
|
|
|||