Merge remote-tracking branch 'build/release-0.3.23' into install-close-23

This commit is contained in:
igneum-labs 2026-10-07 21:31:34 +00:00
commit db4cc51afb
18 changed files with 304 additions and 27 deletions

View file

@ -219,7 +219,7 @@ dependencies = [
[[package]]
name = "igneum-app"
version = "0.3.22"
version = "0.3.23"
dependencies = [
"ed25519-dalek",
"getrandom",

View file

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

View file

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

View file

@ -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<u32>, 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<u32>, 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 ""

View file

@ -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<String>, Option<f64>, Option<String>),
/// 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 {

View file

@ -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 <data root>/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
};

View file

@ -69,6 +69,10 @@ pub struct Manifest {
pub tuning: Option<serde_json::Value>,
/// ui: the interface channel (src/uiota.rs); None = no over-the-air interface is published
pub ui: Option<UiEntry>,
/// 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 <app data>/drivers.json; None = no table published, the rows say nothing about drivers
pub drivers: Option<serde_json::Value>,
@ -187,7 +191,17 @@ pub fn parse(text: &str) -> Result<Manifest, String> {
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();

View file

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

View file

@ -313,6 +313,13 @@ fn api_post(shared: &Arc<Shared>, path: &str, body: Value) -> Result<Value, Stri
shared.send(Cmd::TuneGoal(goal, price, climb));
Ok(json!({ "ok": true }))
}
"/api/network" => {
// 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());

View file

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

View file

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

View file

@ -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=<code> 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 '<button type="button" class="net-card" role="radio" data-net="' + esc(c.id) + '" aria-checked="' + (on ? 'true' : 'false') + '"' + (c.open ? '' : ' disabled') + '>' + (c.current ? '<span class="nc-cur">running now</span>' : '') + '<span class="nc-name">' + esc(c.label) + '</span><span class="nc-line">' + esc(c.line) + '</span><span class="nc-sub">' + esc(c.sub) + '</span></button>'; }).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; }

View file

@ -104,7 +104,7 @@
<!-- 2. cards -->
<section class="screen" id="screen-cards">
<div class="step">
<div class="eyebrow">step 1 of 3</div>
<div class="eyebrow">step 1 of 4</div>
<h2>Your graphics card</h2>
<p class="sub" id="cards-sub">Asking the graphics cards to report in.</p>
<div class="cards" id="cards-list">
@ -129,7 +129,7 @@
<!-- 3. region and price (Ember Heat, mission item 7): the region list, a typed price never fetched, the restricted line -->
<section class="screen" id="screen-region">
<div class="step">
<div class="eyebrow">step 2 of 3</div>
<div class="eyebrow">step 2 of 4</div>
<h2>Your electricity</h2>
<p class="sub">Every money figure in the app comes from the price you type here, in your currency. Nothing is fetched.</p>
<div class="field">
@ -157,10 +157,26 @@
</div>
</section>
<!-- 4. address -->
<!-- 4. the network (0.3.23): two cards; the fresh-install default comes from the manifest's default_network, the testnet
card reads "not yet open" and is refused until the manifest names it open -->
<section class="screen" id="screen-network">
<div class="step">
<div class="eyebrow">step 3 of 4</div>
<h2>Which network</h2>
<p class="sub">The chain this machine mines. You can change it later in Settings; a change takes effect at the next start.</p>
<div class="net-cards" id="network-cards" role="radiogroup" aria-label="Network"></div>
<p class="help ask-cur" id="network-note" hidden></p>
<div class="cta">
<button class="btn primary" id="btn-network-next">Continue</button>
<button class="btn ghost" id="btn-network-back">Back</button>
</div>
</div>
</section>
<!-- 5. address -->
<section class="screen" id="screen-address">
<div class="step">
<div class="eyebrow">step 3 of 3</div>
<div class="eyebrow">step 4 of 4</div>
<h2>Where should rewards go?</h2>
<p class="sub">Every block this machine finds pays one address. Pick one way.</p>
<div class="options">
@ -527,6 +543,18 @@
</div>
</div>
<div class="card">
<div class="card-head"><h3>Network</h3><span class="eyebrow" id="s-network-eyebrow"></span></div>
<p class="help" id="s-network-line"></p>
<div class="net-cards" id="s-network-cards" role="radiogroup" aria-label="Network"></div>
<p class="help ask-cur" id="s-network-note" hidden></p>
<div class="ask inline" id="ask-network" hidden>
<span class="ask-text" id="ask-network-text"></span>
<button class="btn small primary" id="ask-network-yes">Switch</button>
<button class="btn small ghost" id="ask-network-no">Keep</button>
</div>
</div>
<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>

View file

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

View file

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

View file

@ -11,7 +11,7 @@
<key>CFBundleVersion</key>
<string>VERSION_STAMP</string>
<key>CFBundleShortVersionString</key>
<string>0.3.22</string>
<string>0.3.23</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleExecutable</key>

View file

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

View file

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