diff --git a/app/igneum-app/Cargo.lock b/app/igneum-app/Cargo.lock index 6340779ff..f1cf1a2b4 100644 --- a/app/igneum-app/Cargo.lock +++ b/app/igneum-app/Cargo.lock @@ -219,7 +219,7 @@ dependencies = [ [[package]] name = "igneum-app" -version = "0.3.22" +version = "0.3.23" dependencies = [ "ed25519-dalek", "getrandom", diff --git a/app/igneum-app/Cargo.toml b/app/igneum-app/Cargo.toml index 8e1140652..578d89ec5 100644 --- a/app/igneum-app/Cargo.toml +++ b/app/igneum-app/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "igneum-app" -version = "0.3.22" +version = "0.3.23" 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" diff --git a/app/igneum-app/resources/igneum-app.rc b/app/igneum-app/resources/igneum-app.rc index 6e29c5dc8..ce75cfab6 100644 --- a/app/igneum-app/resources/igneum-app.rc +++ b/app/igneum-app/resources/igneum-app.rc @@ -6,8 +6,8 @@ 1 ICON "igneum.ico" 1 VERSIONINFO -FILEVERSION 0,3,22,0 -PRODUCTVERSION 0,3,22,0 +FILEVERSION 0,3,23,0 +PRODUCTVERSION 0,3,23,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.22" + VALUE "FileVersion", "0.3.23" VALUE "InternalName", "igneum-app" VALUE "LegalCopyright", "Igneum contributors" VALUE "OriginalFilename", "igneum-app.exe" VALUE "ProductName", "Igneum Miner" - VALUE "ProductVersion", "0.3.22" + VALUE "ProductVersion", "0.3.23" END END BLOCK "VarFileInfo" diff --git a/app/igneum-app/src/config.rs b/app/igneum-app/src/config.rs index 761979d84..5d5b6ba3a 100644 --- a/app/igneum-app/src/config.rs +++ b/app/igneum-app/src/config.rs @@ -121,6 +121,10 @@ pub struct Settings { /// idle offset of the card sensor above the room (0 = the default). #[serde(default)] pub region: String, + /// the network step (0.3.23): the chain this machine runs, chosen at first run or in Settings ("devnet-3" | + /// "testnet-1"; "" = not chosen, the package's own network). Read at start; a change takes effect at the next start. + #[serde(default)] + pub network: String, /// currency-21 (7 October 2026): the ISO 4217 code the miner chose in Settings ("" = follow the region, which /// follows the OS locale at first run); the price above is in hundredths of this unit per kWh. Survives restart here. #[serde(default)] @@ -180,13 +184,38 @@ fn balanced() -> String { fn nineteen() -> f64 { 19.0 } +/// The network step's rule (0.3.23): a switch is refused when the network is unknown, when it names the testnet while the +/// manifest has not opened it, and when the engine runs another network and the user has not confirmed what resets. +pub fn network_switch(want: &str, running: &str, testnet_open: bool, confirmed: bool) -> Result<(), String> { + if !["devnet-3", "testnet-1"].contains(&want) { + return Err(format!("'{want}' is not a network this app knows")); + } + if want == "testnet-1" && !testnet_open { + return Err("igneum-testnet-1 is not yet open: the choice waits for the manifest to open it".into()); + } + if network_name(want) != running && !confirmed { + return Err("switching the network needs the confirm: the node's data and the mining state reset at the next start".into()); + } + Ok(()) +} +/// The chain's name for a choice ("" = the package's own, read as Devnet 3 from 0.3.22). +pub fn network_name(choice: &str) -> &'static str { + match choice { + "testnet-1" => "igneum-testnet-1", + _ => "igneum-devnet-3", + } +} +/// The seeds of igneum-testnet-1 (docs/plans/testnet-go.md: p2p 26811, chain id 4462). The node compiles no DNS seeders for +/// the testnet (the node lane, 7 October 2026), so the engine passes these as --addpeer. +pub const TESTNET_SEEDS: [&str; 3] = ["seed1.testnet.igneum.network:26811", "seed2.testnet.igneum.network:26811", "seed3.testnet.igneum.network:26811"]; + fn yes() -> bool { true } impl Default for Settings { fn default() -> Settings { - Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, power_control: false, tune_goal: "balanced".into(), power_price_pence: 0.0, tune_climb: false, region: String::new(), currency: String::new(), heat_on: false, heat_set_c: 19.0, heat_schedule: Vec::new(), heat_tz_min: 0, heat_room_c: 0.0, heat_room_at: 0.0, heat_offset_c: 0.0, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false, profile_public: false, ui_builtin: false, prove_instead: false } + Settings { setup_done: false, address: String::new(), address_source: String::new(), key_saved: false, identities: 1, cards: HashMap::new(), display_name: String::new(), vote: true, paused: false, accepted_total: 0, auto_update: true, remote_jobs: true, prove: false, sweep: true, power_control: false, tune_goal: "balanced".into(), power_price_pence: 0.0, tune_climb: false, region: String::new(), currency: String::new(), network: String::new(), heat_on: false, heat_set_c: 19.0, heat_schedule: Vec::new(), heat_tz_min: 0, heat_room_c: 0.0, heat_room_at: 0.0, heat_offset_c: 0.0, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false, prove_default_applied: false, profile_public: false, ui_builtin: false, prove_instead: false } } } @@ -416,8 +445,16 @@ impl Runtime { /// `packaged_suffix` and `packaged_peers` are the package's network (config.rs Packaged); the environment wins. pub fn from_env_with(packaged_suffix: Option, packaged_peers: Option<&[String]>) -> Runtime { + Self::from_env_with_choice(packaged_suffix, packaged_peers, "") + } + /// The network step: `choice` is settings.network ("testnet-1" turns the runtime to the testnet object, `--testnet + /// --netsuffix=1` with the three seeds as peers; "devnet-3" or "" keeps the package's network); the environment wins inside. + pub fn from_env_with_choice(packaged_suffix: Option, packaged_peers: Option<&[String]>, choice: &str) -> Runtime { let env = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty()); - let network = env("IGNEUM_APP_NETWORK").unwrap_or_else(|| "devnet".into()); + let testnet = choice == "testnet-1"; + let network = env("IGNEUM_APP_NETWORK").unwrap_or_else(|| if testnet { "testnet".into() } else { "devnet".into() }); + let packaged_peers: Option<&[String]> = if testnet { None } else { packaged_peers }; + let packaged_suffix = if testnet { None } else { packaged_suffix }; let rpc_port = env("IGNEUM_APP_RPC_PORT").and_then(|v| v.parse().ok()).unwrap_or(26610); 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") { @@ -426,6 +463,7 @@ impl Runtime { // 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 packaged_peers.map(|p| !p.is_empty()).unwrap_or(false) => packaged_peers.unwrap().to_vec(), 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(_) if network == "testnet" => TESTNET_SEEDS.iter().map(|s| s.to_string()).collect(), Err(_) => vec![], }; let unsynced_mining = env("IGNEUM_APP_UNSYNCED").map(|v| v == "1").unwrap_or(false); @@ -459,6 +497,28 @@ impl Runtime { #[cfg(test)] mod tests { + #[test] + fn network_switch_rules() { + // the network step: unknown refused; the testnet refused until open; another network needs the confirm; the same network is fine + assert!(super::network_switch("mainnet", "igneum-devnet-3", true, true).is_err()); + assert!(super::network_switch("testnet-1", "igneum-devnet-3", false, true).unwrap_err().contains("not yet open")); + assert!(super::network_switch("testnet-1", "igneum-devnet-3", true, false).unwrap_err().contains("needs the confirm")); + assert!(super::network_switch("testnet-1", "igneum-devnet-3", true, true).is_ok()); + assert!(super::network_switch("devnet-3", "igneum-devnet-3", false, false).is_ok(), "the network already running needs no confirm"); + assert_eq!(super::network_name(""), "igneum-devnet-3"); + assert_eq!(super::network_name("testnet-1"), "igneum-testnet-1"); + // a fresh settings file carries no choice; an old file without the field reads none too, so it keeps the package's network + assert_eq!(super::Settings::default().network, ""); + let old: super::Settings = serde_json::from_str(r#"{"region":"gb"}"#).unwrap(); + assert_eq!(old.network, ""); + let chosen: super::Settings = serde_json::from_str(r#"{"network":"testnet-1"}"#).unwrap(); + assert_eq!(chosen.network, "testnet-1"); + let r = super::Runtime::from_env_with_choice(Some(3), None, "testnet-1"); + assert_eq!(r.network, "testnet"); + assert_eq!(r.devnet_suffix, None); + assert!(r.peers.iter().any(|p| p.starts_with("seed1.testnet.igneum.network"))); + } + #[test] fn currency_round_trip() { // currency-21: the override is a field of the settings file, so it survives a restart; an old file without it reads as "" diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index a4e852afb..b3eb9c021 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -104,6 +104,8 @@ pub enum Cmd { TuneCardGoal(String, String), /// Ember Heat (src/heat.rs): the region code and the typed price (Settings > Electricity, the first-run step) Region(Option, Option, Option), + /// the network step: the chosen network ("devnet-3" | "testnet-1") and whether the user confirmed what resets + Network(String, bool), /// Ember Heat: the switch, the set point, the schedule text with the window's clock offset, a typed room reading Heat(HeatPatch), /// restart the node with the verifier decided again (src/verifier.rs): the trust setting changed, or the @@ -175,6 +177,8 @@ impl Shared { } let mut st = State { version: VERSION.into(), ..Default::default() }; st.network = runtime.network.clone(); + st.network_name = if runtime.network == "testnet" { "igneum-testnet-1".into() } else if runtime.devnet_suffix == Some(3) { "igneum-devnet-3".into() } else if runtime.network == "devnet" && runtime.devnet_suffix.is_none() { "igneum-devnet-v4".into() } else { format!("igneum-{}-{}", runtime.network, runtime.devnet_suffix.unwrap_or(0)) }; + st.network_pending = if !settings.network.is_empty() && crate::config::network_name(&settings.network) != st.network_name { settings.network.clone() } else { String::new() }; st.chain = if runtime.network == "devnet" { "devnet v4".into() } else { format!("{} (private)", runtime.network) }; st.host = runtime.host.clone(); st.display_name = if settings.display_name.is_empty() { runtime.host.clone() } else { settings.display_name.clone() }; @@ -190,7 +194,7 @@ impl Shared { st.mining.accepted_total = settings.accepted_total; st.mining.fee_total = settings.fee_total; st.address = address_state(&settings, &wallet_path); - st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, sweep: settings.sweep, power_control: settings.power_control, power_note: String::new(), tuning_off: false, tuning_note: String::new(), tune_goal: settings.tune_goal.clone(), power_price_pence: settings.power_price_pence, tune_climb: settings.tune_climb, region: settings.region.clone(), currency: settings.currency.clone(), heat_on: settings.heat_on, heat_set_c: settings.heat_set_c, heat_schedule: crate::heat::schedule_text(&settings.heat_schedule), heat_tz_min: settings.heat_tz_min, heat_room_c: settings.heat_room_c, heat_room_at: settings.heat_room_at, heat_offset_c: settings.heat_offset_c, tune_period_s: crate::ember::PERIOD_S, dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust, profile_public: settings.profile_public, ui_builtin: settings.ui_builtin, prove_instead: settings.prove_instead }; + st.settings = crate::state::SettingsState { identities: settings.identities, vote: settings.vote, start_at_login: crate::platform::start_at_login_is_on(), auto_update: settings.auto_update, remote_jobs: settings.remote_jobs, prove: settings.prove, sweep: settings.sweep, power_control: settings.power_control, power_note: String::new(), tuning_off: false, tuning_note: String::new(), tune_goal: settings.tune_goal.clone(), power_price_pence: settings.power_price_pence, tune_climb: settings.tune_climb, region: settings.region.clone(), currency: settings.currency.clone(), network: settings.network.clone(), heat_on: settings.heat_on, heat_set_c: settings.heat_set_c, heat_schedule: crate::heat::schedule_text(&settings.heat_schedule), heat_tz_min: settings.heat_tz_min, heat_room_c: settings.heat_room_c, heat_room_at: settings.heat_room_at, heat_offset_c: settings.heat_offset_c, tune_period_s: crate::ember::PERIOD_S, dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust, profile_public: settings.profile_public, ui_builtin: settings.ui_builtin, prove_instead: settings.prove_instead }; st.heat = heat_state_off(settings.heat_on, settings.heat_set_c); st.dev_fee = crate::state::DevFeeState { on: settings.dev_fee, percent: if settings.dev_fee { 1 } else { 0 }, address: String::new(), line: String::new() }; st.live_page = packaged.live_page.clone(); @@ -1559,6 +1563,30 @@ impl Engine { drop(st); self.shared.event("info", &format!("tune goal: {}{}{}", s.tune_goal, if s.power_price_pence > 0.0 { format!(", electricity {:.1} p/kWh", s.power_price_pence) } else { String::new() }, if s.tune_climb { ", hill-climb on" } else { "" })); } + Cmd::Network(want, confirmed) => { + let (running, open) = { let st = self.st(); (st.network_name.clone(), st.update.testnet_open) }; + match crate::config::network_switch(&want, &running, open, confirmed) { + Err(why) => { + self.shared.event("warn", &format!("network: {why}")); + self.st().network_error = why; + } + Ok(()) => { + { + let mut s = self.shared.settings.lock().unwrap(); + s.network = want.clone(); + } + self.shared.save_settings(); + let pending = if crate::config::network_name(&want) != running { want.clone() } else { String::new() }; + { + let mut st = self.st(); + st.network_error.clear(); + st.settings.network = want.clone(); + st.network_pending = pending.clone(); + } + self.shared.event("info", &format!("network: {}{}", crate::config::network_name(&want), if pending.is_empty() { " (already running)" } else { ": takes effect at the next start" })); + } + } + } Cmd::Region(region, price, currency) => { let (r, p, c) = { let mut s = self.shared.settings.lock().unwrap(); @@ -2019,11 +2047,18 @@ impl Engine { if let Some(n) = r.devnet_suffix { a.push(format!("--devnet-suffix={n}")); } + if r.network == "testnet" { + // the node lane, 7 October 2026: `--testnet` alone takes suffix 10 and exits; igneum-testnet-1 is --netsuffix=1 + a.push("--netsuffix=1".into()); + } for p in &r.peers { a.push(format!("--addpeer={p}")); } - if let Some(path) = self.node_override_file() { - a.push(format!("--override-params-file={}", path.display())); + if r.network != "testnet" { + // the testnet object refuses an override file (the node lane, 7 October 2026) + if let Some(path) = self.node_override_file() { + a.push(format!("--override-params-file={}", path.display())); + } } a.extend(["--nodnsseed", "--disable-upnp", "--nologfiles", "--yes"].iter().map(|s| s.to_string())); if r.unsynced_mining { diff --git a/app/igneum-app/src/main.rs b/app/igneum-app/src/main.rs index 7e55a0607..f15447b1e 100644 --- a/app/igneum-app/src/main.rs +++ b/app/igneum-app/src/main.rs @@ -157,8 +157,11 @@ fn main() { let packaged = config::Packaged::load(&candidates).with_env_overrides(); // Devnet 3 (7 October 2026): the package names the network its node joins; the runtime read above knew only the // environment, so it is read again with the package's suffix and peers (the environment still wins inside) - let runtime = if packaged.node_devnet_suffix.is_some() || packaged.node_peers.is_some() { - config::Runtime::from_env_with(packaged.node_devnet_suffix, packaged.node_peers.as_deref()) + // the network step (0.3.23): the saved choice is read first (the settings file lives in /app whatever the + // network), so settings.network can turn the runtime to the testnet object before the node starts + let chosen_network = config::Settings::load(&runtime.app_dir.join("settings.json")).network; + let runtime = if packaged.node_devnet_suffix.is_some() || packaged.node_peers.is_some() || !chosen_network.is_empty() { + config::Runtime::from_env_with_choice(packaged.node_devnet_suffix, packaged.node_peers.as_deref(), &chosen_network) } else { runtime }; diff --git a/app/igneum-app/src/manifest.rs b/app/igneum-app/src/manifest.rs index 460c91201..08bb87465 100644 --- a/app/igneum-app/src/manifest.rs +++ b/app/igneum-app/src/manifest.rs @@ -69,6 +69,10 @@ pub struct Manifest { pub tuning: Option, /// ui: the interface channel (src/uiota.rs); None = no over-the-air interface is published pub ui: Option, + /// the network step (0.3.23): the network a FRESH install selects by default ("devnet-3" | "testnet-1"; "" = the + /// package's own), and whether igneum-testnet-1 is open; a manifest naming the testnet default before the open is refused + pub default_network: String, + pub testnet_open: bool, /// drivers: the per-vendor driver table (src/drivers.rs; branch driver-check, 7 October 2026), validated here and /// written as is to /drivers.json; None = no table published, the rows say nothing about drivers pub drivers: Option, @@ -187,7 +191,17 @@ pub fn parse(text: &str) -> Result { Ok(Some(e)) }; let consensus = v.get("consensus").cloned().unwrap_or(serde_json::Value::Null); + let default_network = s(&v, "default_network"); + let testnet_open = v.get("testnet_open").and_then(|b| b.as_bool()).unwrap_or(false); + if !default_network.is_empty() && !["devnet-3", "testnet-1"].contains(&default_network.as_str()) { + return Err(format!("default_network '{default_network}' is not a network this app knows")); + } + if default_network == "testnet-1" && !testnet_open { + return Err("default_network names the testnet before it is open (testnet_open is not true)".into()); + } Ok(Manifest { + default_network, + testnet_open, version, published_at: s(&v, "published_at"), channel: s(&v, "channel"), @@ -535,6 +549,22 @@ mod tests { (sk, pk) } + #[test] + fn default_network_rules() { + // the network step: absent = the package's own; devnet-3 fine; the testnet default only once the manifest opens it + let m = parse(SAMPLE).unwrap(); + assert_eq!(m.default_network, ""); + assert!(!m.testnet_open); + let d = parse(r#"{"version":"0.3.23","default_network":"devnet-3"}"#).unwrap(); + assert_eq!(d.default_network, "devnet-3"); + let e = parse(r#"{"version":"0.3.23","default_network":"testnet-1"}"#).unwrap_err(); + assert!(e.contains("before it is open"), "{e}"); + let t = parse(r#"{"version":"0.3.23","default_network":"testnet-1","testnet_open":true}"#).unwrap(); + assert_eq!(t.default_network, "testnet-1"); + assert!(t.testnet_open); + assert!(parse(r#"{"version":"0.3.23","default_network":"mainnet"}"#).unwrap_err().contains("not a network")); + } + #[test] fn parses_manifest() { let m = parse(SAMPLE).unwrap(); diff --git a/app/igneum-app/src/ota.rs b/app/igneum-app/src/ota.rs index d639c2d4c..f52ced1bc 100644 --- a/app/igneum-app/src/ota.rs +++ b/app/igneum-app/src/ota.rs @@ -754,6 +754,12 @@ impl Updater { self.write_tuning(shared, &m); self.write_drivers(shared, &m); self.ui_change = Some(m.ui.clone()); + { + // the network step: the manifest's default for a fresh install and the testnet's open flag + let mut st = shared.state.lock().unwrap(); + st.update.default_network = m.default_network.clone(); + st.update.testnet_open = m.testnet_open; + } if let Some(e) = entry { if changed { self.file = None; diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index 28baeee3f..b124fd80b 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -313,6 +313,13 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result { + // the network step: {network: "devnet-3" | "testnet-1", confirm: bool}; the engine applies config::network_switch + let want = body.get("network").and_then(|v| v.as_str()).unwrap_or("").to_string(); + let confirm = body.get("confirm").and_then(|v| v.as_bool()).unwrap_or(false); + shared.send(Cmd::Network(want, confirm)); + Ok(json!({ "ok": true })) + } "/api/region" => { // Ember Heat: the region code and the typed price per kWh in that region's minor unit (never fetched) let region = body.get("region").and_then(|v| v.as_str()).map(|r| r.to_string()); diff --git a/app/igneum-app/src/state.rs b/app/igneum-app/src/state.rs index 0a368dabd..d60fd2706 100644 --- a/app/igneum-app/src/state.rs +++ b/app/igneum-app/src/state.rs @@ -365,6 +365,8 @@ pub struct SettingsState { pub region: String, /// currency-21: the ISO 4217 override from the settings file ("" = follow the region) pub currency: String, + /// the network step: the chosen network ("" = the package's own) + pub network: String, pub heat_on: bool, pub heat_set_c: f64, pub heat_schedule: String, @@ -506,6 +508,9 @@ pub struct UpdateState { pub min_supported: String, pub channel: String, pub published_at: String, + /// the network step: the manifest's default for a fresh install and whether the testnet is open + pub default_network: String, + pub testnet_open: bool, pub file: String, // the downloaded installer or disk image (the manual path opens it) pub updated_from: String, // set on the first run after an update pub rolled_back: String, // set when the helper restored the previous version @@ -533,6 +538,11 @@ pub struct State { pub phase: String, // welcome | cards | address | dashboard pub setup_done: bool, pub network: String, + /// the network step: the chain this engine runs by name (igneum-devnet-3, igneum-testnet-1, igneum-devnet-v4) and + /// the one chosen for the next start when it differs ("" = none) + pub network_name: String, + pub network_pending: String, + pub network_error: String, pub chain: String, pub host: String, // the hostname, a friendly label only pub display_name: String, // the user's name for this machine (settings), defaults to the hostname diff --git a/app/igneum-app/ui/app.css b/app/igneum-app/ui/app.css index 76244e793..27805a21b 100644 --- a/app/igneum-app/ui/app.css +++ b/app/igneum-app/ui/app.css @@ -856,6 +856,18 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} #bc-card>span{min-width:0;overflow:hidden;text-overflow:ellipsis} .help.ask-cur{color:var(--ember)} +/* the network step (0.3.23): two cards, the chosen one in ember, the one running marked, a closed one dimmed */ +.net-cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s-3);margin:var(--s-3) 0} +.net-card{display:flex;flex-direction:column;gap:4px;text-align:left;padding:14px 16px;border-radius:12px;border:1px solid var(--line-2);background:var(--row);color:var(--bone);cursor:pointer;min-width:0} +.net-card:hover{border-color:var(--ash)} +.net-card[aria-checked="true"]{border-color:var(--ember);box-shadow:inset 0 0 0 1px var(--ember)} +.net-card[disabled]{opacity:.55;cursor:default} +.net-card .nc-name{font-family:var(--head);font-weight:700;font-size:var(--t-md)} +.net-card .nc-line{font-family:var(--mono);font-size:var(--t-xs);letter-spacing:.06em;color:var(--ash)} +.net-card .nc-sub{font-size:var(--t-sm);color:var(--ash);line-height:1.4} +.net-card .nc-cur{font-family:var(--mono);font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--molten)} +@media (max-width:720px){.net-cards{grid-template-columns:1fr}} + /* scaling-21 (the project lead, 7 October 2026: "the miner needs some scaling so more is visible in the initial window"): the Overview's vertical rhythm tightened so the rate band, the big button, the ladder strip, the chain card and the node card's first row sit above the fold at 1280 by 800 (fold.test.mjs measures it on build-2 at three viewports). Same @@ -873,4 +885,4 @@ body.drawer-open .toast{bottom:calc(var(--drawer-h) + 16px)} #page-overview #dag-live{height:184px} #page-overview .legend{margin-top:8px} #page-overview .wait-card{padding:12px 20px} -#page-overview .node-card{padding-top:18px} +#page-overview .node-card{padding-top:18px} \ No newline at end of file diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 1204cdfba..9fc61652d 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -305,6 +305,25 @@ var View = (function () { // the tooltip on a card's name: what the tool calls it (gfx1201), its device index, the OpenCL platform, the PCI address function cardTitle(cd) { var t = []; if (cd.code && cd.code !== cd.name) t.push(cd.code); if (cd.device !== undefined && cd.device !== '') t.push(cd.worker + ' device ' + cd.device); if (cd.platform) t.push(cd.platform); if (cd.bus) t.push('bus ' + cd.bus); return t.join(' · '); } + // ---------- the network step (0.3.23) ---------- + // Two cards. A fresh install selects the manifest's default_network (Devnet 3 until the go); the testnet card reads + // "not yet open" and is refused until the manifest names it open; an existing install keeps the network it runs. + var NETWORKS = [ + { id: 'devnet-3', label: 'Devnet 3', name: 'igneum-devnet-3', chain: 4463, sub: 'staging: the chain may reset, coins have no value' }, + { id: 'testnet-1', label: 'igneum-testnet-1', name: 'igneum-testnet-1', chain: 4462, sub: 'the public test chain: coins have no value, resets are announced' } + ]; + function networkIdOf(name) { for (var i = 0; i < NETWORKS.length; i++) if (NETWORKS[i].name === name) return NETWORKS[i].id; return 'devnet-3'; } + function networkWords(s) { + s = s || {}; var st = s.settings || {}, u = s.update || {}, open = !!u.testnet_open, current = networkIdOf(s.network_name || ''), pending = s.network_pending || ''; + var chosen = st.network || '', def = (u.default_network === 'testnet-1' && open) ? 'testnet-1' : 'devnet-3'; + var selected = s.setup_done ? (pending || current) : (chosen || def); + var cards = NETWORKS.map(function (n) { var isOpen = n.id !== 'testnet-1' || open; return { id: n.id, label: n.label, name: n.name, line: n.id === 'devnet-3' ? 'igneum-devnet-3, chain id 4463' : (open ? 'chain id 4462' : 'chain id 4462 · not yet open'), sub: n.sub, open: isOpen, selected: n.id === selected, current: !!s.setup_done && n.id === current }; }); + var pend = NETWORKS.filter(function (n) { return n.id === pending; })[0]; + var line = pend ? 'Switching to ' + pend.name + ' at the next start: quit Igneum Miner and open it again.' : s.setup_done ? 'This machine runs ' + (s.network_name || 'igneum-devnet-3') + '.' : ''; + return { cards: cards, selected: selected, current: current, pending: pending, line: line, error: s.network_error || '' }; + } + function networkAsk(want, runningName) { var n = NETWORKS.filter(function (x) { return x.id === want; })[0]; return 'Switch this machine to ' + (n ? n.name : want) + '? The node’s data and the mining state on ' + runningName + ' reset at the next start. Your key and your address stay.'; } + // ---------- vendor marks (gpu-logos, 7 October 2026) ---------- // One simplified monochrome glyph per GPU vendor, drawn by hand from the public mark's geometry (never a copied logo // file, never a raster): a nominative use that names the hardware. Each glyph is under 2 KB and takes currentColor, @@ -1277,7 +1296,7 @@ var View = (function () { } function addressAsk(a) { return 'Pay ' + shortHex(a, 6, 4) + ' from the next block? Mining restarts.'; } function trustAsk(on) { return on ? 'Include proofs the node never checked? Devnet only. The node restarts.' : 'Check proofs again? The node restarts.'; } - return { ISO_CURRENCY: ISO_CURRENCY, EU_MEMBERS: EU_MEMBERS, CURRENCY_CODES: CURRENCY_CODES, minorOf: minorOf, priceFactor: priceFactor, currencyInfo: currencyInfo, regionFromLocale: regionFromLocale, currencyChoice: currencyChoice, currencyAsk: currencyAsk, applyMoney: applyMoney, fmtMoney: fmtMoney, setLocale: setLocale, setCurrency: setCurrency, currencyNow: currencyNow, unitWords: unitWords, VENDORS: VENDORS, MARKS: MARKS, WELL_ALPHA_LIGHT: WELL_ALPHA_LIGHT, WELL_ALPHA_DARK: WELL_ALPHA_DARK, vendorOf: vendorOf, seriesLine: seriesLine, vendorMark: vendorMark, markHtml: markHtml, nameHtml: nameHtml, rowHead: rowHead, vendorContrast: vendorContrast, contrast: contrast, hexRgb: hexRgb, PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, driverWords: driverWords, vendorWord: vendorWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveInsteadWords: proveInsteadWords, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, firstWait: firstWait, staleCard: staleCard, cardSeenBody: cardSeenBody, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords }; + return { NETWORKS: NETWORKS, networkWords: networkWords, networkAsk: networkAsk, networkIdOf: networkIdOf, ISO_CURRENCY: ISO_CURRENCY, EU_MEMBERS: EU_MEMBERS, CURRENCY_CODES: CURRENCY_CODES, minorOf: minorOf, priceFactor: priceFactor, currencyInfo: currencyInfo, regionFromLocale: regionFromLocale, currencyChoice: currencyChoice, currencyAsk: currencyAsk, applyMoney: applyMoney, fmtMoney: fmtMoney, setLocale: setLocale, setCurrency: setCurrency, currencyNow: currencyNow, unitWords: unitWords, VENDORS: VENDORS, MARKS: MARKS, WELL_ALPHA_LIGHT: WELL_ALPHA_LIGHT, WELL_ALPHA_DARK: WELL_ALPHA_DARK, vendorOf: vendorOf, seriesLine: seriesLine, vendorMark: vendorMark, markHtml: markHtml, nameHtml: nameHtml, rowHead: rowHead, vendorContrast: vendorContrast, contrast: contrast, hexRgb: hexRgb, PAGES: PAGES, page: page, withCommas: withCommas, compact: compact, rel: rel, ago: ago, shortHex: shortHex, kindWord: kindWord, shownCards: shownCards, present: present, cardTitle: cardTitle, cardRow: cardRow, rowWord: rowWord, driverWords: driverWords, vendorWord: vendorWord, tuneWord: tuneWord, tuneEta: tuneEta, tuningCard: tuningCard, poundsPerDay: poundsPerDay, money: money, effOf: effOf, effText: effText, wattsTotal: wattsTotal, noReading: noReading, cells: cells, nextCheck: nextCheck, tunePoint: tunePoint, tunable: tunable, tuneWords: tuneWords, schedule: schedule, GOALS: GOALS, GOAL_NAMES: GOAL_NAMES, goalWords: goalWords, ign: ign, balanceWords: balanceWords, fleetWatts: fleetWatts, fleetPounds: fleetPounds, isMine: isMine, liveNote: liveNote, toggle: toggle, nodeWords: nodeWords, nodeLine: nodeLine, skewWord: skewWord, peersLine: peersLine, heightLine: heightLine, tipAge: tipAge, nodeSource: nodeSource, finalityWords: finalityWords, nextSwitch: nextSwitch, switchLine: switchLine, proveWords: proveWords, verifierWords: verifierWords, proveTier: proveTier, proveInsteadWords: proveInsteadWords, proveLine: proveLine, devFeeText: devFeeText, devFeeLine: devFeeLine, jobsNote: jobsNote, eventLine: eventLine, plainEvent: plainEvent, pill: pill, beforeOf: beforeOf, ember: ember, fleetSaving: fleetSaving, addressAsk: addressAsk, trustAsk: trustAsk, interfaceWords: interfaceWords, shouldReload: shouldReload, FIELDS: FIELDS, fmtIgn: fmtIgn, pct: pct, gapWords: gapWords, spanWords: spanWords, chainNums: chainNums, poisson: poisson, ignPerDay: ignPerDay, windowWords: windowWords, daysIn: daysIn, ladderRungs: ladderRungs, currentRung: currentRung, firstWait: firstWait, staleCard: staleCard, cardSeenBody: cardSeenBody, blockCard: blockCard, earningsLines: earningsLines, cardIgnDay: cardIgnDay, timeline: timeline, profileWords: profileWords, REGIONS: REGIONS, CURRENCIES: CURRENCIES, RESTRICTED_SENTENCE: RESTRICTED_SENTENCE, regionOf: regionOf, currencyOf: currencyOf, setRegion: setRegion, restrictedLine: restrictedLine, priceNote: priceNote, RENT: RENT, rentLine: rentLine, heatLine: heatLine, heatRowWords: heatRowWords }; })(); /* ---------- Ember Tune: the card row's tuning line (pure; tune-line.test.mjs loads this block) ---------- One line per card from the card state (src/state.rs, src/ember.rs): running (the phase and the step), tuned @@ -1398,6 +1417,7 @@ if (typeof document !== 'undefined') (function () { if (!dash && drawerOpen()) setDrawer(false); if (p === 'cards' && !cardsSeen) { cardsSeen = true; api('api/detect', {}); } // the region step; ?region= preselects one for screenshots + if (p === 'network' && state) renderNetworkStep(state); if (p === 'region') { fillRegions($('region-select')); fillCurrencies($('region-currency'), regionNow() || View.regionFromLocale(lang)); var rc = params.get('region') || regionNow() || View.regionFromLocale(lang); $('region-select').value = rc; $('region-currency').value = currencyNowCode(); var ask0 = View.currencyAsk(View.currencyChoice({ region: rc, currency: currencyNowCode() }, lang)); setText('region-currency-ask', ask0); setHidden('region-currency-ask', !ask0); regionWords(rc, $('region-price'), $('region-unit'), $('region-note'), $('region-restricted'), !(priceNow() > 0)); if (priceNow() > 0) $('region-price').value = String(priceNow()); } if (dash) { showPage(page); mountDag(); } api('api/phase', { phase: p }); @@ -1459,6 +1479,27 @@ if (typeof document !== 'undefined') (function () { if (restrictedEl) { restrictedEl.hidden = !line; setText(restrictedEl.id, line); } } function regionNow() { return (state && state.settings && state.settings.region) || stored('igneum.region') || ''; } + // ---------- the network step (0.3.23): the cards on the first-run step and in Settings ---------- + var netPick = ''; + function renderNetCards(el, w, pick) { + el.innerHTML = w.cards.map(function (c) { var on = (pick || w.selected) === c.id; return ''; }).join(''); + } + function renderNetworkStep(s) { + var w = View.networkWords(s); renderNetCards($('network-cards'), w, netPick); + var note = w.error || (w.cards[1].open ? '' : 'igneum-testnet-1 opens when the network does; Devnet 3 is the chain to mine today.'); + setText('network-note', note); setHidden('network-note', !note); + } + $('network-cards').addEventListener('click', function (e) { var b = e.target.closest('.net-card'); if (!b || b.disabled) return; netPick = b.getAttribute('data-net'); if (state) renderNetworkStep(state); }); + $('btn-network-back').addEventListener('click', function () { show('region'); }); + $('btn-network-next').addEventListener('click', function () { var w = View.networkWords(state || {}), pick = netPick || w.selected; api('api/network', { network: pick, confirm: true }).then(function () { show('address'); }).catch(function () { show('address'); }); }); + function renderNetworkCard(s) { + var w = View.networkWords(s); renderNetCards($('s-network-cards'), w, ''); + setText('s-network-line', w.line); setText('s-network-eyebrow', w.pending ? 'next start' : 'running'); + setText('s-network-note', w.error); setHidden('s-network-note', !w.error); + } + $('s-network-cards').addEventListener('click', function (e) { var b = e.target.closest('.net-card'); if (!b || b.disabled || !state) return; var want = b.getAttribute('data-net'), w = View.networkWords(state); if (want === w.selected) return; $('ask-network-text').textContent = View.networkAsk(want, state.network_name || 'igneum-devnet-3'); $('ask-network').dataset.want = want; $('ask-network').hidden = false; }); + $('ask-network-no').addEventListener('click', function () { $('ask-network').hidden = true; }); + $('ask-network-yes').addEventListener('click', function () { var want = $('ask-network').dataset.want; $('ask-network').hidden = true; api('api/network', { network: want, confirm: true }).then(function (r) { if (r.ok) toast('Network saved: it takes effect at the next start'); }); }); function saveRegion(code, price, cur) { store('igneum.region', code || null); store('igneum.price', price > 0 ? price : null); if (cur !== undefined) store('igneum.currency', cur || null); var body = { region: code, price_pence: price }; if (cur !== undefined) body.currency = cur || ''; @@ -1474,14 +1515,14 @@ if (typeof document !== 'undefined') (function () { $('region-select').addEventListener('change', function () { regionWords(this.value, $('region-price'), $('region-unit'), $('region-note'), $('region-restricted'), true); }); $('region-currency').addEventListener('change', function () { saveCurrency(this, $('region-price'), $('region-unit'), $('region-note'), $('region-select')); var a = View.currencyAsk(View.currencyChoice({ region: $('region-select').value, currency: this.value }, lang)); setText('region-currency-ask', a); setHidden('region-currency-ask', !a); }); $('btn-region-back').addEventListener('click', function () { show('cards'); }); - $('btn-region-skip').addEventListener('click', function () { show('address'); }); + $('btn-region-skip').addEventListener('click', function () { show('network'); }); $('btn-region-next').addEventListener('click', function () { var code = $('region-select').value, p = parseFloat($('region-price').value) * View.priceFactor(View.currencyNow().code); if (!(p >= 0)) p = 0; - saveRegion(code, p, $('region-currency').value).then(function () { show('address'); }).catch(function () { show('address'); }); + saveRegion(code, p, $('region-currency').value).then(function () { show('network'); }).catch(function () { show('network'); }); }); $('btn-cards-retry').addEventListener('click', function () { cardsSeen = false; $('cards-detecting').hidden = false; api('api/detect', {}); }); - $('btn-address-back').addEventListener('click', function () { show('cards'); }); + $('btn-address-back').addEventListener('click', function () { show('network'); }); // address options var opts = document.querySelectorAll('.option'); @@ -2498,6 +2539,7 @@ if (typeof document !== 'undefined') (function () { setText('s-profile-text', View.profileWords(!!st.profile_public)); setHidden('s-climb-row', st.tune_climb === undefined); setText('s-power-note', st.tuning_off ? ' ' + (st.tuning_note || '') : (st.power_note ? ' ' + st.power_note : '')); + renderNetworkCard(s); fillRegions($('s-region')); var rc = regionNow(); if (document.activeElement !== $('s-region') && $('s-region').value !== rc) { $('s-region').value = rc; } diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index 3e9b53aaa..58382e519 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -104,7 +104,7 @@
-
step 1 of 3
+
step 1 of 4

Your graphics card

Asking the graphics cards to report in.

@@ -129,7 +129,7 @@
-
step 2 of 3
+
step 2 of 4

Your electricity

Every money figure in the app comes from the price you type here, in your currency. Nothing is fetched.

@@ -157,10 +157,26 @@
- + +
+
+
step 3 of 4
+

Which network

+

The chain this machine mines. You can change it later in Settings; a change takes effect at the next start.

+
+ +
+ + +
+
+
+ +
-
step 3 of 3
+
step 4 of 4

Where should rewards go?

Every block this machine finds pays one address. Pick one way.

@@ -527,6 +543,18 @@
+
+

Network

+

+
+ + +
+

This machine

diff --git a/app/igneum-app/ui/view.test.mjs b/app/igneum-app/ui/view.test.mjs index d367cecfe..b1e0a9bb0 100644 --- a/app/igneum-app/ui/view.test.mjs +++ b/app/igneum-app/ui/view.test.mjs @@ -1067,5 +1067,47 @@ test('Settings: "Check for an update" posts api/update/check and "Check for jobs const upd = /\$\('s-update'\)\.addEventListener\('click', function \(\) \{ api\('api\/update\/check', \{\}\);/.exec(src); const jobs = /\$\('s-jobs-check'\)\.addEventListener\('click', function \(\) \{ api\('api\/jobs\/check', \{\}\);/.exec(src); assert.ok(upd, 'the update button posts api/update/check'); assert.ok(jobs, 'the jobs button posts api/jobs/check'); - assert.equal(/\$\('s-update'\)[^\n]*api\/jobs\/check/.test(src), false); assert.equal(/\$\('s-jobs-check'\)[^\n]*api\/update\/check/.test(src), false); + assert.equal(/\$\('s-update'\)[^\n]*api\/jobs\/check/.test(src), false); assert.equal(/\$\('s-jobs-check'\)[^\n]*api\/update\/check/.test(src), false);}); + +// ---------- the network step (0.3.23, main's design with two corrections, 7 October 2026) ---------- +// A fourth first-run screen and a Settings card: two cards, Devnet 3 and igneum-testnet-1. The fresh-install default comes +// from the manifest's default_network (Devnet 3 until the go); the testnet card reads "not yet open" and is refused until the +// manifest names it open; an existing install keeps its network; nothing switches a running miner without the user's click +// and the confirm that names what resets. +test('network words: the manifest default selects the card on a fresh install, the testnet card is refused until open, an existing install keeps its network (known-failed on 0.3.23)', () => { + const fresh = { setup_done: false, network_name: 'igneum-devnet-3', network_pending: '', settings: { network: '' }, update: { default_network: 'devnet-3', testnet_open: false } }; + const w = V.networkWords(fresh); + assert.deepEqual(w.cards.map((c) => c.id), ['devnet-3', 'testnet-1']); + assert.equal(w.selected, 'devnet-3', 'the manifest default'); + const dn3 = w.cards[0], tn = w.cards[1]; + assert.equal(dn3.label, 'Devnet 3'); assert.equal(dn3.line, 'igneum-devnet-3, chain id 4463'); assert.equal(dn3.sub, 'staging: the chain may reset, coins have no value'); assert.equal(dn3.open, true); + assert.equal(tn.label, 'igneum-testnet-1'); assert.equal(tn.line, 'chain id 4462 · not yet open'); assert.equal(tn.sub, 'the public test chain: coins have no value, resets are announced'); assert.equal(tn.open, false); + assert.equal(V.networkWords({ ...fresh, update: { default_network: '', testnet_open: false } }).selected, 'devnet-3', 'no manifest read yet: Devnet 3'); + const open = V.networkWords({ ...fresh, update: { default_network: 'testnet-1', testnet_open: true } }); + assert.equal(open.selected, 'testnet-1'); assert.equal(open.cards[1].line, 'chain id 4462'); assert.equal(open.cards[1].open, true); + // an existing install keeps what it runs; the card it runs is marked current + const old = V.networkWords({ setup_done: true, network_name: 'igneum-devnet-3', network_pending: '', settings: { network: '' }, update: { default_network: 'testnet-1', testnet_open: true } }); + assert.equal(old.selected, 'devnet-3'); assert.equal(old.current, 'devnet-3'); assert.equal(old.cards[0].current, true); + assert.equal(old.line, 'This machine runs igneum-devnet-3.'); + // the pending state after a confirmed switch + const pend = V.networkWords({ setup_done: true, network_name: 'igneum-devnet-3', network_pending: 'testnet-1', settings: { network: 'testnet-1' }, update: { default_network: 'testnet-1', testnet_open: true } }); + assert.equal(pend.line, 'Switching to igneum-testnet-1 at the next start: quit Igneum Miner and open it again.'); + for (const s of [w, old, pend]) for (const c of s.cards) assert.equal(/—|–/.test(c.label + c.line + c.sub), false); +}); + +test('the switch is refused without the confirm, and the confirm names what resets; the testnet choice is refused while not open', () => { + assert.equal(V.networkAsk('testnet-1', 'igneum-devnet-3'), 'Switch this machine to igneum-testnet-1? The node’s data and the mining state on igneum-devnet-3 reset at the next start. Your key and your address stay.'); + assert.equal(V.networkAsk('devnet-3', 'igneum-testnet-1'), 'Switch this machine to igneum-devnet-3? The node’s data and the mining state on igneum-testnet-1 reset at the next start. Your key and your address stay.'); + // the Rust rule the server applies (config::network_switch): mirrored here by its source text + const cfg = rustSrc('config.rs'); + assert.match(cfg, /pub fn network_switch\(want: &str, running: &str, testnet_open: bool, confirmed: bool\) -> Result<\(\), String>/); + assert.match(cfg, /fn network_switch_rules\(\)/, 'the Rust test exists'); + assert.match(rustSrc('manifest.rs'), /default_network names the testnet before it is open/, 'a manifest with the testnet default before the open is refused'); + assert.match(rustSrc('manifest.rs'), /fn default_network_rules\(\)/, 'the manifest test exists'); + assert.match(rustSrc('server.rs'), /"\/api\/network" =>/); + // the markup: the step sits before the address step, four steps now; the Settings card exists + const idx = pageHtml; + assert.ok(idx.indexOf('id="screen-network"') > idx.indexOf('id="screen-region"') && idx.indexOf('id="screen-network"') < idx.indexOf('id="screen-address"'), 'the network step comes after the region step and before the address step'); + assert.equal((idx.match(/step [1-4] of 4/g) || []).length, 4, 'four steps'); + assert.ok(idx.includes('id="s-network-cards"') && idx.includes('id="ask-network"'), 'the Settings card and its confirm'); }); diff --git a/app/windows/version.h b/app/windows/version.h index e4ef07234..4b47504f0 100644 --- a/app/windows/version.h +++ b/app/windows/version.h @@ -3,8 +3,8 @@ // 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.22" -#define IGNEUM_HOST_VERSION_RC 0,3,22,0 +#define IGNEUM_HOST_VERSION_STR "0.3.23" +#define IGNEUM_HOST_VERSION_RC 0,3,23,0 // The WebView2 runtime the host's loader needs at least (the SDK 1.0.2903.40 loader's minimum, from its release note at the // cut); the installer's rights step (src/rights.rs) installs the evergreen runtime when the PC's is absent or below it, and a // raised minimum is a new right the next update asks once for. diff --git a/packaging/mac/app/Info.plist b/packaging/mac/app/Info.plist index 167730407..389729749 100644 --- a/packaging/mac/app/Info.plist +++ b/packaging/mac/app/Info.plist @@ -11,7 +11,7 @@ CFBundleVersion VERSION_STAMP CFBundleShortVersionString - 0.3.22 + 0.3.23 CFBundlePackageType APPL CFBundleExecutable diff --git a/packaging/windows/Igneum-Miner.iss b/packaging/windows/Igneum-Miner.iss index 7cbd2c939..98a3c892b 100644 --- a/packaging/windows/Igneum-Miner.iss +++ b/packaging/windows/Igneum-Miner.iss @@ -9,7 +9,7 @@ #define ArtDir "..\..\brand\icons" #endif #ifndef AppVersion - #define AppVersion "0.3.22" + #define AppVersion "0.3.23" #endif #define AppName "Igneum Miner" #define Publisher "Igneum" diff --git a/packaging/windows/host.sha256 b/packaging/windows/host.sha256 index 76abfd08b..b195d30cf 100644 --- a/packaging/windows/host.sha256 +++ b/packaging/windows/host.sha256 @@ -2,3 +2,5 @@ # whose "Igneum Miner.exe" is not one of these. The shipper replaces the line at every cut from the CI artifact igneum-windows-host # (or PC 1's BUILD-APP.bat run). 7 October 2026: PC 1's MSVC 0.3.21 host, the one running on PC 2 since 19:14 BST. e223db18bcfa74a1f60335de62390729a19975e6a6bddd7547bf9d6f0676df17 +# 7 October 2026: PC 1's MSVC 0.3.23 host from the installer kit at release-0.3.23 7c7489ac (dpi-23 and install-close-23 host.cpp; run-20261007-204937, 8 s) +365d32104ede41db22837ece4eb4fa8860505360955c974d3055f1114339d4c5