The network step (0.3.23, main's design with two corrections, 7 October 2026): a fourth first-run screen before the address step and a Settings card, two cards, Devnet 3 (igneum-devnet-3, chain id 4463; staging: the chain may reset, coins have no value) and igneum-testnet-1 (the public test chain: coins have no value, resets are announced); 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's testnet_open names it open, a manifest naming the testnet default before the open is refused by the parser; an existing install keeps the network it runs and sees the step as the Settings card; nothing switches a running miner but the user's click and the confirm that names what resets (the node's data and the mining state; the key and the address stay); the choice lives in settings.network, read before the runtime so igneum-testnet-1 starts the node with --testnet --netsuffix=1 and the three seeds as --addpeer (no DNS seeders compiled; no override file on the testnet), taking effect at the next start; api/network, Cmd::Network, config::network_switch with its tests, manifest default_network_rules, the Rust round trips, two view tests known-failed first
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
ecc8fb5443
commit
2920265b1d
11 changed files with 292 additions and 16 deletions
|
|
@ -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 ""
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1055,3 +1055,46 @@ test('a locale with no currency row falls back to USD with the override prompt v
|
|||
assert.equal(V.currencyAsk(V.currencyChoice({}, 'en-GB')), '');
|
||||
assert.ok(pageHtml.includes('id="s-currency-ask"') && pageHtml.includes('id="region-currency-ask"'), 'the prompt element on Settings and on the first-run step');
|
||||
});
|
||||
|
||||
// ---------- 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');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue