diff --git a/src/app.cpp b/src/app.cpp index 254121a..b8d1a86 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -822,6 +822,11 @@ void App::update() ps.algo = xs.algo; ps.version = xs.version; ps.connected = xs.connected; + // Pool mining has a connect delay — announce once it's actually connected/hashing. + if (pool_starting_.load(std::memory_order_relaxed) && (ps.connected || ps.hashrate_10s > 0.0)) { + pool_starting_.store(false, std::memory_order_relaxed); + ui::Notifications::instance().success("Pool miner connected and hashing."); + } // Get memory directly from OS (more reliable than API) double memMB = xmrig_manager_->getMemoryUsageMB(); ps.memory_used = static_cast(memMB * 1024.0 * 1024.0); @@ -836,6 +841,7 @@ void App::update() } } else if (xmrig_manager_ && !xmrig_manager_->isRunning()) { state_.pool_mining.xmrig_running = false; + pool_starting_.store(false, std::memory_order_relaxed); // stopped / never came up } // Auto-balance: periodically re-pick the pool by hashrate (self-throttled). @@ -1243,6 +1249,15 @@ void App::render() // Process deferred encryption from wizard (runs in background) processDeferredEncryption(); + // Surface the blockchain-delete completion on the main thread (the worker can't touch Notifications). + { + int deletedN = pending_delete_result_.exchange(-1, std::memory_order_relaxed); + if (deletedN >= 0) { + ui::Notifications::instance().success("Blockchain data deleted (" + std::to_string(deletedN) + + " items). The daemon is restarting to re-sync from the network."); + } + } + // Debug screenshot sweep — pins the current (skin,page) and arms capture once settled. Must run // before the sidebar reads current_page_ (below) so the forced page is reflected. updateScreenshotSweep(); @@ -3344,6 +3359,8 @@ void App::deleteBlockchainData() daemon::AsyncLifecycleTaskContext context(token, shutting_down_); auto result = daemon_controller_->executeLifecycleOperation(decision, runtime, context); DEBUG_LOGF("[App] Blockchain data deleted (%d items removed), restarting daemon...\n", result.deletedItems); + // Hand the count to the main thread for a completion toast (Notifications isn't thread-safe). + pending_delete_result_.store(result.deletedItems, std::memory_order_relaxed); }); } @@ -3480,6 +3497,17 @@ void App::renderShutdownScreen() }; shutdown_timer_ += ImGui::GetIO().DeltaTime; + // Track how long the status text has been unchanged. A normal shutdown keeps updating the status + // ("Stopping pool miner…" -> "Flushing…" -> "Cleaning up…"); a genuine hang stalls it. Offer Force + // Quit only once the status has STALLED (likely hung), not on a bare timer, with a hard ceiling so + // it's never impossible to escape. + static std::string s_lastShutStatus; + static float s_shutStallTimer = 0.0f; + if (shutdown_status_ != s_lastShutStatus) { s_lastShutStatus = shutdown_status_; s_shutStallTimer = 0.0f; } + else s_shutStallTimer += ImGui::GetIO().DeltaTime; + const bool shutdownStalled = s_shutStallTimer >= 8.0f; + const bool allowForceQuit = shutdownStalled || shutdown_timer_ >= 20.0f; + // Use the main viewport so the overlay covers the primary window ImGuiViewport* vp = ImGui::GetMainViewport(); ImVec2 vp_pos = vp->Pos; @@ -3499,8 +3527,9 @@ void App::renderShutdownScreen() ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings; - // Allow input after 10s so Force Quit button is clickable - if (shutdown_timer_ < 10.0f) + // Allow input only once Force Quit is offered (status stalled / hard ceiling), so the screen stays + // click-through while a normal shutdown is progressing. + if (!allowForceQuit) shutdownFlags |= ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoNav; ImGui::Begin("##ShutdownOverlay", nullptr, shutdownFlags); @@ -3601,11 +3630,22 @@ void App::renderShutdownScreen() } // ------------------------------------------------------------------- - // 4b. Force Quit button — appears after 10 seconds + // 4b. Force Quit button — appears once the shutdown appears stalled (or a hard-ceiling backstop) // ------------------------------------------------------------------- - if (shutdown_timer_ >= 10.0f) { + if (allowForceQuit) { ImGui::Spacing(); ImGui::Spacing(); + // State-aware caution: while the status is a daemon flush/exit step, force-quitting risks the + // chainstate; say so instead of a bare button. + if (shutdownStalled && !shutdown_status_.empty()) { + std::string stalledMsg = "Still \"" + shutdown_status_ + "\" — force quitting now may corrupt chain data."; + ImVec2 ms = ImGui::CalcTextSize(stalledMsg.c_str()); + ImGui::SetCursorPosX(cx - ms.x * 0.5f); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); + ImGui::TextUnformatted(stalledMsg.c_str()); + ImGui::PopStyleColor(); + ImGui::Spacing(); + } const char* forceLabel = TR("force_quit"); ImVec2 btnSize(ImGui::CalcTextSize(forceLabel).x + 32.0f, 0); ImGui::SetCursorPosX(cx - btnSize.x * 0.5f); diff --git a/src/app.h b/src/app.h index b739239..3e135c7 100644 --- a/src/app.h +++ b/src/app.h @@ -601,6 +601,10 @@ private: // Daemon restart (e.g. after changing debug log categories) std::atomic daemon_restarting_{false}; + // Set by the deleteBlockchainData worker (item count); the main loop surfaces a completion toast + // and resets it to -1. Atomic because the worker thread writes it and the UI thread reads/clears it. + std::atomic pending_delete_result_{-1}; + // Encryption state check timeout float encryption_check_timer_ = 0.0f; @@ -702,6 +706,10 @@ private: // Mining toggle guard (prevents concurrent setgenerate calls) std::atomic mining_toggle_in_progress_{false}; + // True from a successful startPoolMining() until the miner is confirmed connected/hashing in the + // poll — drives the "connecting…" → "connected" feedback for pool mining (which has a connect delay). + std::atomic pool_starting_{false}; + // Auto-shield guard (prevents concurrent auto-shield operations) std::atomic auto_shield_pending_{false}; diff --git a/src/app_network.cpp b/src/app_network.cpp index d41f635..6e13c38 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -1949,6 +1949,10 @@ void App::startPoolMining(int threads) } else { ui::Notifications::instance().error("Failed to start pool miner: " + err); } + } else { + // Miner spawned — it still needs a few seconds to connect to the pool and start hashing. + pool_starting_.store(true, std::memory_order_relaxed); + ui::Notifications::instance().info("Starting pool miner — connecting to the pool…"); } } diff --git a/src/ui/windows/mining_controls.cpp b/src/ui/windows/mining_controls.cpp index 877e1eb..9526d03 100644 --- a/src/ui/windows/mining_controls.cpp +++ b/src/ui/windows/mining_controls.cpp @@ -453,14 +453,27 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& // Require a wallet address for pool mining std::string worker(s_pool_worker); if (!worker.empty()) { - s_benchmark.reset(); - s_benchmark.was_pool_running = state.pool_mining.xmrig_running; - s_benchmark.prev_threads = s_selected_threads; - s_benchmark.buildCandidates(max_threads); - s_benchmark.phase = ThreadBenchmark::Phase::Starting; - // Stop any active solo mining first - if (mining.generate) - app->stopMining(); + static bool s_benchConfirm = false; + if (!s_benchConfirm) { + // First click: build candidates so we can estimate the total duration, then + // require a confirming click (the benchmark interrupts mining and runs a while). + s_benchmark.reset(); + s_benchmark.buildCandidates(max_threads); + s_benchConfirm = true; + char msg[128]; + snprintf(msg, sizeof(msg), + "Benchmark takes ~%ds and interrupts mining. Click again to start.", + (int)(s_benchmark.totalEstimatedSecs() + 0.5f)); + Notifications::instance().warning(msg); + } else { + // Second click: start (candidates already built above). + s_benchConfirm = false; + s_benchmark.was_pool_running = state.pool_mining.xmrig_running; + s_benchmark.prev_threads = s_selected_threads; + s_benchmark.phase = ThreadBenchmark::Phase::Starting; + if (mining.generate) + app->stopMining(); + } } }