feat(mining): persistent start/stop error caption + timeout-aware pool status

- A mining start/stop/pool failure now sets a persistent App::mining_last_error_
  (cleared on the next attempt), rendered as a red caption with "Press Mine to
  try again" below the Mine button (plus a Dismiss, and a Windows-Defender
  "How to fix" on an AV block), so a crashed/blocked/failed miner is no longer
  just a button that "did nothing" once its toast faded. Two more hardcoded
  English strings (pool-payout-invalid, AV-blocked) moved to i18n.
- Pool connection status is now a persistent "Status" row in the pool stats
  card (PoolMiningState.running_since / last_error): it flips from "Connecting…"
  to "Can't reach pool — check the URL" after a 30s timeout instead of sitting
  on red forever, and no longer vanishes once logs arrive. The old transient
  banner in mining_tab.cpp was removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 12:09:49 -05:00
co-authored by Claude Opus 4.8
parent 15c1181aef
commit 8fe20f6385
8 changed files with 104 additions and 29 deletions
+19 -8
View File
@@ -3529,6 +3529,7 @@ void App::startMining(int threads)
const int maxThreads = std::max(1, (int)std::thread::hardware_concurrency());
threads = std::clamp(threads, 1, maxThreads);
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
clearMiningError(); // a fresh attempt wipes any stale error
worker_->post([this, threads]() -> rpc::RPCWorker::MainCb {
bool ok = false;
@@ -3548,7 +3549,9 @@ void App::startMining(int threads)
DEBUG_LOGF("Mining started with %d threads\n", threads);
} else {
DEBUG_LOGF("Failed to start mining: %s\n", errMsg.c_str());
ui::Notifications::instance().error(std::string(TR("toast_mining_failed")) + util::friendlyRpcError(errMsg));
const std::string merr = std::string(TR("toast_mining_failed")) + util::friendlyRpcError(errMsg);
setMiningError(merr);
ui::Notifications::instance().error(merr);
}
};
});
@@ -3559,6 +3562,7 @@ void App::stopMining()
if (!supportsSoloMining()) return;
if (!state_.connected || !rpc_ || !worker_) return;
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
clearMiningError();
worker_->post([this]() -> rpc::RPCWorker::MainCb {
bool ok = false;
@@ -3580,7 +3584,9 @@ void App::stopMining()
} else {
// Don't silently leave generate=true as if it worked: tell the user and let the next
// getmininginfo refresh reconcile the true daemon state. (M-07)
ui::Notifications::instance().error(std::string(TR("toast_stop_mining_failed")) + util::friendlyRpcError(errMsg));
const std::string merr = std::string(TR("toast_stop_mining_failed")) + util::friendlyRpcError(errMsg);
setMiningError(merr);
ui::Notifications::instance().error(merr);
}
};
});
@@ -3634,9 +3640,11 @@ void App::postPoolMinerJob(int threads, bool retargetOnly)
// A pool pays with ordinary transactions, so only a shielded address can be paid (private chain).
if (!cfg.worker_name.empty() && cfg.worker_name != "x" &&
!util::isValidRecipientAddress(cfg.worker_name)) {
ui::Notifications::instance().error(util::isTransparentAddress(cfg.worker_name)
? TR("mining_payout_needs_shielded")
: "Pool payout address is not a valid DragonX address — mining not started.");
const std::string merr = util::isTransparentAddress(cfg.worker_name)
? std::string(TR("mining_payout_needs_shielded"))
: std::string(TR("toast_pool_payout_invalid"));
setMiningError(merr);
ui::Notifications::instance().error(merr);
if (retargetOnly) stopPoolMining(); // never keep mining on a config that no longer validates
return;
}
@@ -3667,6 +3675,7 @@ void App::postPoolMinerJob(int threads, bool retargetOnly)
if (cfg.wallet_address.empty()) {
DEBUG_LOGF("[ERROR] Pool mining: No wallet address available\n");
setMiningError(TR("toast_no_address_generate_z"));
ui::Notifications::instance().error(TR("toast_no_address_generate_z"));
if (retargetOnly) stopPoolMining();
return;
@@ -3715,13 +3724,15 @@ void App::postPoolMinerJob(int threads, bool retargetOnly)
DEBUG_LOGF("[ERROR] Pool mining: %s\n", err.c_str());
// Windows Defender blocking (error 225 = ERROR_VIRUS_INFECTED)
if (err.find("error 225") != std::string::npos || err.find("virus") != std::string::npos) {
ui::Notifications::instance().error(
"Windows Defender blocked xmrig. Add exclusion for %APPDATA%\\ObsidianDragon");
setMiningError(TR("toast_av_blocked_xmrig"), /*av=*/true);
ui::Notifications::instance().error(TR("toast_av_blocked_xmrig"));
#ifdef _WIN32
pending_antivirus_dialog_ = true;
#endif
} else {
ui::Notifications::instance().error(std::string(TR("toast_pool_start_failed")) + util::friendlyRpcError(err));
const std::string merr = std::string(TR("toast_pool_start_failed")) + util::friendlyRpcError(err);
setMiningError(merr);
ui::Notifications::instance().error(merr);
}
}
};