diff --git a/app/igneum-app/src/config.rs b/app/igneum-app/src/config.rs index 5bdcfaf1..050b79bb 100644 --- a/app/igneum-app/src/config.rs +++ b/app/igneum-app/src/config.rs @@ -70,9 +70,16 @@ pub struct Settings { #[serde(default)] pub prove: bool, /// The efficiency sweep (src/sweep.rs): once after install, then weekly, each NVIDIA card's cap is stepped from - /// 100% to 50% on the live program and held at the best MH per watt. Default on. A pinned card is skipped. - #[serde(default = "yes")] + /// 100% to 50% on the live program and held at the best MH per watt. Default off; implied by `power_control` + /// (on when that is switched on, never effective while it is off). A pinned card is skipped. + #[serde(default)] pub sweep: bool, + /// Power control (the project lead, 5 October 2026: "if we don't have to ask then don't ask"): the NVIDIA power cap and the + /// efficiency sweep need administrator rights (one UAC prompt on Windows). Default OFF on every machine; the app + /// never raises the prompt on its own. Switching it on asks once, at that moment; a refused, cancelled or + /// unanswered prompt switches it back off with a notice, no retries. + #[serde(default)] + pub power_control: bool, /// When this install first ran (unix s), for the "first hour after install" sweep. #[serde(default)] pub installed_at: u64, @@ -98,16 +105,26 @@ fn yes() -> bool { 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, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: 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: false, power_control: false, installed_at: 0, dev_fee: true, fee_total: 0, proof_verify_trust: false } } } impl Settings { pub fn load(path: &Path) -> Settings { let mut s: Settings = std::fs::read_to_string(path).ok().and_then(|t| serde_json::from_str(&t).ok()).unwrap_or_default(); + let mut dirty = false; if s.installed_at == 0 { // an install from before the sweep existed counts as installed now: it gets its first-hour sweep s.installed_at = crate::platform::unix_now(); + dirty = true; + } + if s.sweep && !s.power_control { + // the sweep is implied by power control (5 October 2026): an install from before that setting carried + // sweep = true by default; it no longer prompts on its own + s.sweep = false; + dirty = true; + } + if dirty { s.save(path); } s diff --git a/app/igneum-app/src/engine.rs b/app/igneum-app/src/engine.rs index 85075432..90d4483b 100644 --- a/app/igneum-app/src/engine.rs +++ b/app/igneum-app/src/engine.rs @@ -63,6 +63,8 @@ pub enum Cmd { SweepStop, SweepPin(String, bool), SweepEnable(bool), + /// Settings > Power control (config.rs power_control): on asks for administrator rights once, at that moment + PowerControl(bool), /// the probe that decides how caps are set during a sweep: Ok(true) = nvidia-smi -pl works from this process /// (the engine runs elevated), Ok(false) = the elevated helper is needed; Err = nvidia-smi did not answer SweepCapMode(Result), @@ -113,7 +115,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, dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust }; + 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(), dev_fee: settings.dev_fee, proof_verify_trust: settings.proof_verify_trust }; 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(); st.finality.message = "waiting for the miner".into(); @@ -880,6 +882,11 @@ impl Engine { if applied.is_empty() && missing.is_empty() { self.shared.log(&format!("power cap: nothing to read back for {what}")); } + match power_control_after_prompt(&r) { + Some(note) => self.power_control_off(note), + None if !applied.is_empty() => self.st().settings.power_note = "administrator rights given; the cap and the sweep run".into(), + None => {} + } } Cmd::ElevatedDone(r) => { if let Some((line, what, want, _)) = self.power_via_host.take() { @@ -903,6 +910,7 @@ impl Engine { match st.mining.cards.iter().find(|c| c.key == key) { Some(c) if !c.sweep_supported => (c.name.clone(), false, c.sweep_note.clone()), Some(c) if !c.enabled => (c.name.clone(), false, "the card is switched off".to_string()), + Some(c) if !self.elevation_allowed() => (c.name.clone(), false, "switch Power control on in Settings first (Windows asks for administrator rights once)".to_string()), Some(c) => (c.name.clone(), true, String::new()), None => (key.clone(), false, "no such card".to_string()), } @@ -938,10 +946,38 @@ impl Engine { self.shared.event("info", &if pinned { format!("{name}: cap pinned; the sweep records but does not change it") } else { format!("{name}: the sweep chooses the cap again from its next run") }); } Cmd::SweepEnable(on) => { + let allowed = self.elevation_allowed(); + let on = on && allowed; self.shared.settings.lock().unwrap().sweep = on; self.shared.save_settings(); self.st().settings.sweep = on; - self.shared.event("info", if on { "efficiency sweep on: once after install, then weekly" } else { "efficiency sweep off; Sweep now on a card still runs one" }); + self.shared.event("info", if on { "efficiency sweep on: once after install, then weekly" } else if allowed { "efficiency sweep off; Sweep now on a card still runs one" } else { "efficiency sweep stays off: switch Power control on in Settings first" }); + } + Cmd::PowerControl(on) => { + { + let mut s = self.shared.settings.lock().unwrap(); + s.power_control = on; + s.sweep = on; // implied + } + self.shared.save_settings(); + { + let mut st = self.st(); + st.settings.power_control = on; + st.settings.sweep = on; + st.settings.power_note = if on { "asking Windows for administrator rights once".into() } else { String::new() }; + } + if on { + self.shared.event("info", "power control on: Windows asks for administrator rights once; the cap and the sweep need them"); + for c in self.st().mining.cards.iter_mut().filter(|c| c.vendor == "nvidia") { + c.power_applied = false; // the one prompt sets every cap now + } + self.apply_power_limits("power control on"); + if !self.power_busy { + self.st().settings.power_note = "on; no NVIDIA card needs a cap right now".into(); + } + } else { + self.power_control_off("power control off; the cap and the sweep do not run and nothing asks for administrator rights"); + } } Cmd::SweepCapMode(r) => self.sweep_mode_known(r), Cmd::SweepHelperDone(r) => { @@ -1418,21 +1454,10 @@ impl Engine { self.shared.log(&format!("power cap ({why}): deferred, a sweep is running")); return; } - let mut cmds = Vec::new(); - let mut what = Vec::new(); - { - let mut st = self.st(); - for c in st.mining.cards.iter_mut().filter(|c| c.vendor == "nvidia" && c.enabled && c.power_default_w > 0.0) { - let pct = if c.power_pct == 0 { 80 } else { c.power_pct.clamp(crate::sweep::MIN_PCT, 100) }; - c.power_pct = pct; - let watts = requested_watts(c); - if (c.power_limit_w - watts).abs() < 1.0 && c.power_applied { - continue; - } - cmds.push(format!("\"{}\" -i {} -pl {}", crate::platform::tool("nvidia-smi").display(), c.device, watts as u64)); - what.push(format!("{} {} W ({}% of {} W)", c.name, watts as u64, pct, c.power_default_w as u64)); - c.power_note = "setting the power cap (administrator prompt)".into(); - } + let allowed = self.elevation_allowed(); + let (cmds, what, held) = power_cap_plan(&mut self.st().mining.cards, allowed, &crate::platform::tool("nvidia-smi").display().to_string()); + if held > 0 { + self.shared.log(&format!("power cap ({why}): not asked, Power control is off in Settings ({held} card(s) would need it)")); } if cmds.is_empty() { return; @@ -1486,11 +1511,36 @@ impl Engine { if cmds.is_empty() { return; } - self.shared.log(&format!("restoring the GPU power limits: {}", cmds.join(" & "))); - match crate::platform::run_elevated(&cmds.join(" & ")) { - Ok(()) => self.shared.log("GPU power limits restored"), - Err(e) => self.shared.log(&format!("GPU power limits not restored ({e}); they reset at the next reboot")), + // no administrator prompt on quit (5 October 2026: the app never asks on its own); the limits reset at the + // next reboot, and the next start with Power control on sets them again + self.shared.log(&format!("GPU power limits left as set (they reset at the next reboot; no prompt on quit): {}", cmds.join(" & "))); + } + + /// Power control (config.rs power_control): may the engine ask for administrator rights for the cap or the sweep? + /// The elevated PC sweep job (--sweep) sets caps directly and counts as allowed. + fn elevation_allowed(&self) -> bool { + elevation_allowed(self.shared.settings.lock().unwrap().power_control, self.shared.runtime.sweep_only) + } + + /// Power control off, with the reason beside the switch and in the feed; a running or queued sweep ends. + fn power_control_off(&mut self, note: &str) { + { + let mut s = self.shared.settings.lock().unwrap(); + s.power_control = false; + s.sweep = false; } + self.shared.save_settings(); + { + let mut st = self.st(); + st.settings.power_control = false; + st.settings.sweep = false; + st.settings.power_note = note.to_string(); + } + self.sweep_queue.clear(); + if self.sweep.is_some() || self.sweep_pending.is_some() { + self.sweep_abort("power control is off"); + } + self.shared.event(if note.starts_with("power control off:") { "error" } else { "info" }, note); } /// nvidia-smi -l 5: power draw, GPU and memory temperature, the limit in force, every 5 s, as a child whose @@ -1519,16 +1569,10 @@ impl Engine { } if let Some((line, what, want, since)) = self.power_via_host.clone() { if now.duration_since(since) > Duration::from_secs(150) { + // no second prompt through PowerShell (5 October 2026): an unanswered prompt is a refusal self.power_via_host = None; - self.shared.log("the window host did not answer the elevated step in 150 s; running it through PowerShell"); - let shared = self.shared.clone(); - std::thread::spawn(move || { - let r = crate::platform::run_elevated(&line); - std::thread::sleep(Duration::from_millis(800)); - let back: std::collections::HashMap = crate::detect::nvidia_power_limits().into_iter().map(|(k, v)| (k, v.1)).collect(); - let _ = want; - shared.send(Cmd::PowerApplied(what, r, back)); - }); + self.shared.log(&format!("the window host did not answer the elevated step in 150 s: {line}")); + self.finish_power(what, Err("the administrator prompt was not answered in 150 s".into()), want); } } } @@ -1631,12 +1675,12 @@ impl Engine { if self.quitting || !self.running || self.power_busy || self.job_hold || self.jobs.holds_miners() { return; } - let (auto_on, paused) = { + let (auto_on, paused, allowed) = { let s = self.shared.settings.lock().unwrap(); let st = self.st(); - (s.sweep, st.mining.paused) + (s.sweep, st.mining.paused, elevation_allowed(s.power_control, self.shared.runtime.sweep_only)) }; - if paused { + if paused || !allowed { return; } let unix = crate::platform::unix_now(); @@ -1767,6 +1811,9 @@ impl Engine { /// The elevated helper (src/sweep.rs helper_script_*): one administrator prompt; it polls /sweep/cmd.txt. fn sweep_helper_start(&mut self, c: &CardState) -> Result<(), String> { + if !self.elevation_allowed() { + return Err("Power control is off in Settings".into()); + } let dir = self.sweep_dir(); std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?; let _ = std::fs::write(dir.join("cmd.txt"), ""); @@ -3002,6 +3049,51 @@ impl Engine { } /// The watts a card's cap asks for: power_pct of the default limit, inside the card's min and max. +/// the project lead, 5 October 2026: "if we don't have to ask then don't ask". The NVIDIA power cap and the efficiency sweep need +/// administrator rights (one UAC prompt on Windows, pkexec on Linux); the engine builds an elevated command only when +/// Power control is on in Settings, or when it is itself the elevated PC sweep job (--sweep). +fn elevation_allowed(power_control: bool, sweep_only: bool) -> bool { + power_control || sweep_only +} + +/// The notice when the one prompt was refused, cancelled or not answered: Power control goes back off, no retries. +pub const POWER_CONTROL_REFUSED: &str = "power control off: administrator rights were not given"; + +/// After the elevated step: Some(notice) when the administrator prompt was refused, cancelled or timed out (the +/// words platform::run_elevated and the window host use), None when rights were given, even if a card then +/// disagreed with the readback. +fn power_control_after_prompt(r: &Result<(), String>) -> Option<&'static str> { + match r { + Err(e) if e.contains("administrator prompt") || e.contains("refused") || e.contains("cancel") => Some(POWER_CONTROL_REFUSED), + _ => None, + } +} + +/// The nvidia-smi -pl lines one elevated step runs, the human list of what they set, and how many cards were held +/// back because Power control is off (their note says so). Nothing is built when not allowed. +fn power_cap_plan(cards: &mut [CardState], allowed: bool, smi: &str) -> (Vec, Vec, usize) { + let mut cmds = Vec::new(); + let mut what = Vec::new(); + let mut held = 0; + for c in cards.iter_mut().filter(|c| c.vendor == "nvidia" && c.enabled && c.power_default_w > 0.0) { + let pct = if c.power_pct == 0 { 80 } else { c.power_pct.clamp(crate::sweep::MIN_PCT, 100) }; + c.power_pct = pct; + let watts = requested_watts(c); + if (c.power_limit_w - watts).abs() < 1.0 && c.power_applied { + continue; + } + if !allowed { + c.power_note = "power cap not set: Power control is off in Settings".into(); + held += 1; + continue; + } + cmds.push(format!("\"{smi}\" -i {} -pl {}", c.device, watts as u64)); + what.push(format!("{} {} W ({}% of {} W)", c.name, watts as u64, pct, c.power_default_w as u64)); + c.power_note = "setting the power cap (administrator prompt)".into(); + } + (cmds, what, held) +} + fn requested_watts(c: &CardState) -> f64 { let pct = if c.power_pct == 0 { 80 } else { c.power_pct.clamp(crate::sweep::MIN_PCT, 100) }; let mut w = c.power_default_w * pct as f64 / 100.0; @@ -3108,6 +3200,42 @@ pub fn parse_race(body: &str) -> Option { #[cfg(test)] mod tests { + #[test] + fn power_control_off_builds_no_elevated_command() { + // the decision (the project lead, 5 October 2026): off = the app never asks; the elevated PC sweep job is the exception + assert!(!super::elevation_allowed(false, false)); + assert!(super::elevation_allowed(true, false)); + assert!(super::elevation_allowed(false, true)); + let mut cards = vec![ + super::CardState { vendor: "nvidia".into(), enabled: true, device: "0".into(), name: "RTX 5090".into(), power_default_w: 575.0, power_limit_w: 575.0, power_pct: 80, ..Default::default() }, + super::CardState { vendor: "amd".into(), enabled: true, device: "1".into(), name: "RX 9070 XT".into(), power_default_w: 300.0, power_limit_w: 300.0, power_pct: 80, ..Default::default() }, + ]; + let (cmds, what, held) = super::power_cap_plan(&mut cards, false, "nvidia-smi"); + assert!(cmds.is_empty() && what.is_empty(), "{cmds:?}"); + assert_eq!(held, 1); + assert_eq!(cards[0].power_note, "power cap not set: Power control is off in Settings"); + let (cmds, what, held) = super::power_cap_plan(&mut cards, true, "nvidia-smi"); + assert_eq!(cmds, vec!["\"nvidia-smi\" -i 0 -pl 460".to_string()]); + assert_eq!(what, vec!["RTX 5090 460 W (80% of 575 W)".to_string()]); + assert_eq!(held, 0); + // a cap already in force asks for nothing either way + cards[0].power_limit_w = 460.0; + cards[0].power_applied = true; + assert!(super::power_cap_plan(&mut cards, true, "nvidia-smi").0.is_empty()); + } + + #[test] + fn a_refused_prompt_switches_power_control_off_with_the_notice() { + let refused = Err("the administrator prompt was refused, cancelled or timed out (exit 251)".to_string()); + assert_eq!(super::power_control_after_prompt(&refused), Some("power control off: administrator rights were not given")); + assert_eq!(super::power_control_after_prompt(&Err("the administrator prompt was cancelled".into())), Some(super::POWER_CONTROL_REFUSED)); + assert_eq!(super::power_control_after_prompt(&Err("elevated fail: the administrator prompt was cancelled".into())), Some(super::POWER_CONTROL_REFUSED)); + assert_eq!(super::power_control_after_prompt(&Err("the administrator prompt was not answered in 150 s".into())), Some(super::POWER_CONTROL_REFUSED)); + // rights given: the step ran, whatever the card then said + assert_eq!(super::power_control_after_prompt(&Ok(())), None); + assert_eq!(super::power_control_after_prompt(&Err("the elevated step exited with code 2".into())), None); + } + use super::{parse_race, sync_decision, Reading}; #[test] diff --git a/app/igneum-app/src/platform.rs b/app/igneum-app/src/platform.rs index 5e506e91..a3e8e694 100644 --- a/app/igneum-app/src/platform.rs +++ b/app/igneum-app/src/platform.rs @@ -429,7 +429,7 @@ pub fn run_elevated(cmdline: &str) -> Result<(), String> { Ok(()) } else { let err = String::from_utf8_lossy(&out.stderr).trim().to_string(); - Err(if err.contains("canceled") || err.contains("cancelled") || err.is_empty() { "the administrator prompt was cancelled".into() } else { err }) + Err(elevated_failure(out.status.code(), &err)) } } #[cfg(target_os = "linux")] @@ -444,6 +444,19 @@ pub fn run_elevated(cmdline: &str) -> Result<(), String> { } } +/// The reason an elevated step failed, from the launcher's exit code and stderr: exit 251 (the prompt refused, +/// cancelled or timed out, `elevated_ps_line`) and the "canceled" wording name the prompt; any other code is the +/// step's own exit (the engine then keeps Power control on: rights were given). +pub fn elevated_failure(code: Option, stderr: &str) -> String { + if code == Some(ELEVATED_LAUNCH_FAILED) || stderr.contains("canceled") || stderr.contains("cancelled") { + "the administrator prompt was refused, cancelled or timed out".into() + } else if stderr.is_empty() { + format!("the elevated step exited with code {}", code.map(|c| c.to_string()).unwrap_or_else(|| "?".into())) + } else { + stderr.to_string() + } +} + /// Doubles the single quotes of `s` for a single-quoted PowerShell literal. pub fn ps_quote(s: &str) -> String { s.replace('\'', "''") @@ -500,6 +513,10 @@ mod tests { assert!(l.contains("exit 251 }; if ($null -eq $p) { Write-Error 'elevated launch failed: no process'; exit 251 }; exit $p.ExitCode"), "{l}"); assert!(l.ends_with("exit $p.ExitCode"), "{l}"); assert_eq!(super::ELEVATED_LAUNCH_FAILED, 251); + assert_eq!(super::elevated_failure(Some(251), "elevated launch failed (UAC refused, cancelled or timed out): ..."), "the administrator prompt was refused, cancelled or timed out"); + assert_eq!(super::elevated_failure(Some(1), "The operation was canceled by the user."), "the administrator prompt was refused, cancelled or timed out"); + assert_eq!(super::elevated_failure(Some(2), ""), "the elevated step exited with code 2"); + assert_eq!(super::elevated_failure(Some(3), "nvidia-smi: bad"), "nvidia-smi: bad"); assert_eq!(super::ps_quote("a'b''c"), "a''b''''c"); assert_eq!(super::ps_quote("plain"), "plain"); } diff --git a/app/igneum-app/src/server.rs b/app/igneum-app/src/server.rs index 0849d683..e9884e36 100644 --- a/app/igneum-app/src/server.rs +++ b/app/igneum-app/src/server.rs @@ -319,6 +319,12 @@ fn api_post(shared: &Arc, path: &str, body: Value) -> Result { + let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?; + shared.send(Cmd::PowerControl(on)); + Ok(json!({ "ok": true })) + } "/api/sweep/enable" => { let on = body.get("on").and_then(|v| v.as_bool()).ok_or("on missing")?; shared.send(Cmd::SweepEnable(on)); diff --git a/app/igneum-app/src/state.rs b/app/igneum-app/src/state.rs index faaa8199..72a865ef 100644 --- a/app/igneum-app/src/state.rs +++ b/app/igneum-app/src/state.rs @@ -205,8 +205,13 @@ pub struct SettingsState { pub remote_jobs: bool, /// the prover service (src/prover.rs) pub prove: bool, - /// the efficiency sweep (src/sweep.rs): once after install, then weekly + /// the efficiency sweep (src/sweep.rs): once after install, then weekly; effective only with `power_control` pub sweep: bool, + /// the NVIDIA power cap and the sweep may ask for administrator rights (config.rs: default off, one prompt when + /// switched on) + pub power_control: bool, + /// the line beside the Power control switch: why it is off, or that the rights were given + pub power_note: String, /// the miner software's dev fee switch (settings; `--dev-fee 0` when off) pub dev_fee: bool, /// devnet only: the node trusts proof records without a verifier (`IGNEUM_PROOF_VERIFY=trust`) diff --git a/app/igneum-app/ui/app.js b/app/igneum-app/ui/app.js index 3d9b9128..b7673a9c 100644 --- a/app/igneum-app/ui/app.js +++ b/app/igneum-app/ui/app.js @@ -392,6 +392,7 @@ if (typeof document !== 'undefined') (function () { $('s-trust').addEventListener('change', function () { var on = this.checked; api('api/settings', { proof_verify_trust: on }).then(function (r) { if (r.ok) toast(on ? 'Trust mode on (devnet only); the node restarts' : 'Trust mode off; the node restarts'); else { toast(r.error || 'could not change'); $('s-trust').checked = !on; } }); }); $('pv-setup').addEventListener('click', function () { api('api/prove/setup', {}).then(function (r) { toast(r.ok ? 'Setup started in its own window' : (r.error || 'could not start')); }); }); $('s-jobs-allow').addEventListener('change', function () { api('api/jobs/allow', { on: this.checked }); setTimeout(fillSettings, 800); }); + $('s-power-control').addEventListener('change', function () { var on = this.checked; api('api/power/control', { on: on }).then(function (r) { if (r.ok) toast(on ? 'Power control on: Windows asks for administrator rights once' : 'Power control off: nothing asks for administrator rights'); else { toast(r.error || 'could not change'); $('s-power-control').checked = !on; } setTimeout(fillSettings, 1500); }); }); $('s-sweep').addEventListener('change', function () { api('api/sweep/enable', { on: this.checked }).then(function (r) { if (r.ok) toast($('s-sweep').checked ? 'Sweep on: once after install, then weekly' : 'Sweep off'); }); }); $('s-jobs-check').addEventListener('click', function () { api('api/jobs/check', {}); $('s-jobs-note').textContent = 'Checking.'; setTimeout(fillSettings, 4000); }); @@ -1052,7 +1053,11 @@ if (typeof document !== 'undefined') (function () { $('s-jobs-allow').checked = !!j.allowed; $('s-prove').checked = !!(state.settings && state.settings.prove); $('s-trust').checked = !!(state.settings && state.settings.proof_verify_trust); + var pc = !!(state.settings && state.settings.power_control); + $('s-power-control').checked = pc; + $('s-power-note').textContent = (state.settings && state.settings.power_note) || ''; $('s-sweep').checked = !!(state.settings && state.settings.sweep); + $('s-sweep').disabled = !pc; $('s-jobs-key').textContent = j.key_fingerprint ? 'signing key sha256:' + j.key_fingerprint : ''; var parts = []; if (j.account) parts.push(j.account); diff --git a/app/igneum-app/ui/index.html b/app/igneum-app/ui/index.html index ab1f1625..ba54c8ce 100644 --- a/app/igneum-app/ui/index.html +++ b/app/igneum-app/ui/index.html @@ -299,6 +299,11 @@

Only when no verifier is found next to the engine: the node then includes proof records it never checked. Never on a testnet. A found verifier always wins. Changing this restarts the node.

+
+
power control
+ +

Windows asks for administrator rights once; the cap and the sweep need them. Off, the app never asks.

+
efficiency sweep
diff --git a/tools/windows/power-prompts-off.ps1 b/tools/windows/power-prompts-off.ps1 new file mode 100644 index 00000000..119e11d0 --- /dev/null +++ b/tools/windows/power-prompts-off.ps1 @@ -0,0 +1,28 @@ +# Stops the administrator prompts a 0.3.9 Igneum Miner app raises on its own on a PC (the project lead, 5 October 2026: "if we +# don't have to ask then don't ask"), through the settings API that app has, until the build with the Power control +# setting ships: the weekly/first-hour efficiency sweep off (api/sweep/enable), a running sweep stopped +# (api/sweep/stop). The power cap has no off switch in 0.3.9 (it asks at every app start and on a slider change, and +# nowhere else); this script reports the cards' cap state so the next prompt's source is known. A signed `run` job, +# not elevated, a few seconds: +# packaging/ota/publish-jobs.sh add --kind run --target ae432dc7 --timeout-minutes 3 \ +# --script tools/windows/power-prompts-off.ps1 --title "PC 1: sweep off (no administrator prompts)" --deploy +$ErrorActionPreference = 'Continue' +$base = (Get-Content (Join-Path $env:LOCALAPPDATA 'igneum\app\app.url') -Raw).Trim().TrimEnd('/') +function Snapshot([string] $tag) { + $s = Invoke-RestMethod -Uri "$base/api/state" -TimeoutSec 20 + $set = $s.settings + "RESULT $tag settings: sweep " + $set.sweep + " | power_control " + $(if ($null -ne $set.power_control) { $set.power_control } else { '(not in this build)' }) + " | prove " + $set.prove + " | remote_jobs " + $set.remote_jobs + " | version " + $s.version + foreach ($c in $s.mining.cards) { + if ($c.vendor -ne 'nvidia') { continue } + "RESULT $tag card: " + $c.name + " | enabled " + $c.enabled + " | power_pct " + $c.power_pct + " | limit " + $c.power_limit_w + " W of " + $c.power_default_w + " W default | applied " + $c.power_applied + " | pinned " + $c.pinned + " | sweep_state " + $c.sweep_state + " | note: " + $c.power_note + " | " + $c.sweep_note + } +} +Snapshot 'before' +$r = Invoke-RestMethod -Uri "$base/api/sweep/stop" -Method Post -ContentType 'application/json' -Body '{}' -TimeoutSec 30 +"RESULT api/sweep/stop: " + ($r | ConvertTo-Json -Compress) +$r = Invoke-RestMethod -Uri "$base/api/sweep/enable" -Method Post -ContentType 'application/json' -Body '{"on":false}' -TimeoutSec 30 +"RESULT api/sweep/enable off: " + ($r | ConvertTo-Json -Compress) +Start-Sleep -Seconds 3 +Snapshot 'after' +"RESULT note: the 0.3.9 power cap asks only at an app start or a slider change; no setting turns it off before the Power control build" +exit 0