diff --git a/src/app.h b/src/app.h index b033a87..e4ec8c9 100644 --- a/src/app.h +++ b/src/app.h @@ -422,6 +422,10 @@ private: // P4: Incremental transaction cache int last_tx_block_height_ = -1; // block height at last full tx fetch + // Dirty flags for demand-driven refresh + bool addresses_dirty_ = true; // true → refreshAddresses() will run + bool encryption_state_prefetched_ = false; // suppress duplicate getwalletinfo on connect + // First-run wizard state WizardPhase wizard_phase_ = WizardPhase::None; std::unique_ptr bootstrap_; diff --git a/src/app_network.cpp b/src/app_network.cpp index b50491d..8f71d86 100644 --- a/src/app_network.cpp +++ b/src/app_network.cpp @@ -187,6 +187,13 @@ void App::onConnected() }); } + // onConnected already fetched getwalletinfo — tell refreshData to skip + // the duplicate call on the very first cycle. + encryption_state_prefetched_ = true; + + // Addresses are unknown on fresh connect — force a fetch + addresses_dirty_ = true; + // Initial data refresh refreshData(); refreshMarketData(); @@ -211,11 +218,28 @@ void App::refreshData() if (refresh_in_progress_.exchange(true)) return; refreshBalance(); - refreshAddresses(); + + // Addresses: only re-fetch when explicitly dirtied (new address, send, etc.) + if (addresses_dirty_) { + refreshAddresses(); + } + refreshTransactions(); - refreshMiningInfo(); - refreshPeerInfo(); - refreshWalletEncryptionState(); + + // Mining: handled by the 1-second fast_refresh_timer_ — skip here to + // avoid queuing a redundant call every 5 seconds. + + // Peers: only fetch when the Peers tab is visible + if (current_page_ == ui::NavPage::Peers) { + refreshPeerInfo(); + } + + // Encryption state: skip if onConnected() already prefetched it + if (encryption_state_prefetched_) { + encryption_state_prefetched_ = false; + } else { + refreshWalletEncryptionState(); + } // Clear the guard after all tasks are posted (they'll execute sequentially // on the worker thread, so the last one to finish signals completion). @@ -270,6 +294,13 @@ void App::refreshBalance() if (blockInfo.contains("verificationprogress")) state_.sync.verification_progress = blockInfo["verificationprogress"].get(); state_.sync.syncing = (state_.sync.blocks < state_.sync.headers - 2); + + // Consolidate chain-tip fields that were previously fetched + // via a separate getinfo call in refreshMiningInfo. + if (blockInfo.contains("longestchain")) + state_.longestchain = blockInfo["longestchain"].get(); + if (blockInfo.contains("notarized")) + state_.notarized = blockInfo["notarized"].get(); } // Auto-shield transparent funds if enabled @@ -391,6 +422,8 @@ void App::refreshAddresses() state_.t_addresses = std::move(tAddrs); // P8: single rebuild via dirty flag (drains in update()) address_list_dirty_ = true; + // Addresses fetched successfully — clear the demand flag + addresses_dirty_ = false; }; }); } @@ -583,13 +616,16 @@ void App::refreshMiningInfo() daemonMemMb = embedded_daemon_->getMemoryUsageMB(); } - // Slow-tick counter: run full getmininginfo + getinfo every ~5 seconds + // Slow-tick counter: run full getmininginfo every ~5 seconds // to reduce RPC overhead. getlocalsolps (returns H/s for RandomX) runs every tick (1s). + // NOTE: getinfo is NOT called here — longestchain/notarized are updated by + // refreshBalance (via getblockchaininfo), and daemon_version/protocol_version/ + // p2p_port are static for the lifetime of a connection (set in onConnected). bool doSlowRefresh = (mining_slow_counter_++ % 5 == 0); worker_->post([this, daemonMemMb, doSlowRefresh]() -> rpc::RPCWorker::MainCb { - json miningInfo, localHashrateJson, nodeInfo; - bool miningOk = false, hashrateOk = false, nodeOk = false; + json miningInfo, localHashrateJson; + bool miningOk = false, hashrateOk = false; // Fast path: only getlocalsolps (single RPC call, ~1ms) — returns H/s (RandomX) try { @@ -599,7 +635,7 @@ void App::refreshMiningInfo() DEBUG_LOGF("getLocalHashrate error: %s\n", e.what()); } - // Slow path: getmininginfo + getinfo every ~5s + // Slow path: getmininginfo every ~5s if (doSlowRefresh) { try { miningInfo = rpc_->call("getmininginfo"); @@ -607,16 +643,9 @@ void App::refreshMiningInfo() } catch (const std::exception& e) { DEBUG_LOGF("getMiningInfo error: %s\n", e.what()); } - - try { - nodeInfo = rpc_->call("getinfo"); - nodeOk = true; - } catch (const std::exception& e) { - DEBUG_LOGF("getInfo error: %s\n", e.what()); - } } - return [this, miningInfo, localHashrateJson, nodeInfo, miningOk, hashrateOk, nodeOk, daemonMemMb]() { + return [this, miningInfo, localHashrateJson, miningOk, hashrateOk, daemonMemMb]() { try { if (hashrateOk) { state_.mining.localHashrate = localHashrateJson.get(); @@ -640,18 +669,6 @@ void App::refreshMiningInfo() state_.mining.chain = miningInfo["chain"].get(); state_.last_mining_update = std::time(nullptr); } - if (nodeOk) { - if (nodeInfo.contains("version")) - state_.daemon_version = nodeInfo["version"].get(); - if (nodeInfo.contains("protocolversion")) - state_.protocol_version = nodeInfo["protocolversion"].get(); - if (nodeInfo.contains("p2pport")) - state_.p2p_port = nodeInfo["p2pport"].get(); - if (nodeInfo.contains("longestchain")) - state_.longestchain = nodeInfo["longestchain"].get(); - if (nodeInfo.contains("notarized")) - state_.notarized = nodeInfo["notarized"].get(); - } } catch (const std::exception& e) { DEBUG_LOGF("[refreshMiningInfo] callback error: %s\n", e.what()); } @@ -965,6 +982,7 @@ void App::createNewZAddress(std::function callback) rpc_->z_getNewAddress([this, callback](const json& result) { std::string addr = result.get(); + addresses_dirty_ = true; refreshAddresses(); if (callback) callback(addr); }); @@ -976,6 +994,7 @@ void App::createNewTAddress(std::function callback) rpc_->getNewAddress([this, callback](const json& result) { std::string addr = result.get(); + addresses_dirty_ = true; refreshAddresses(); if (callback) callback(addr); }); @@ -1206,7 +1225,11 @@ void App::sendTransaction(const std::string& from, const std::string& to, } catch (const std::exception& e) { result_str = e.what(); } - return [callback, ok, result_str]() { + return [this, callback, ok, result_str]() { + if (ok) { + // A send changes address balances — refresh on next cycle + addresses_dirty_ = true; + } if (callback) callback(ok, result_str); }; }); diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index e4f4a23..420be9f 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -46,6 +46,12 @@ static bool s_pool_settings_dirty = false; static bool s_pool_state_loaded = false; static bool s_show_pool_log = false; // Toggle: false=chart, true=log +// Chart smooth-scroll state +static size_t s_chart_last_n = 0; +static double s_chart_last_newest = -1.0; +static double s_chart_update_time = 0.0; +static float s_chart_interval = 1.0f; // measured seconds between data updates + // Get max threads based on hardware static int GetMaxMiningThreads() { @@ -983,17 +989,69 @@ void RenderMiningTab(App* app) float plotW = plotRight - plotLeft; float plotH = std::max(1.0f, plotBottom - plotTop); + // --- Smooth scroll: detect new data and measure interval --- size_t n = chartHistory.size(); - std::vector points(n); + double newestVal = chartHistory.back(); + double nowTime = ImGui::GetTime(); + bool dataChanged = (n != s_chart_last_n) || (newestVal != s_chart_last_newest); + if (dataChanged) { + float dt = (float)(nowTime - s_chart_update_time); + if (dt > 0.3f && dt < 10.0f) + s_chart_interval = s_chart_interval * 0.6f + dt * 0.4f; // smoothed + s_chart_last_n = n; + s_chart_last_newest = newestVal; + s_chart_update_time = nowTime; + } + float elapsed = (float)(nowTime - s_chart_update_time); + float scrollFrac = std::clamp(elapsed / s_chart_interval, 0.0f, 1.0f); + + // Build raw data points with smooth scroll offset. + // Newest point is anchored at plotRight; as scrollFrac grows + // the spacing compresses by one virtual slot so the next + // incoming point will appear seamlessly at plotRight. + float virtualSlots = (float)(n - 1) + scrollFrac; + if (virtualSlots < 1.0f) virtualSlots = 1.0f; + float stepW = plotW / virtualSlots; + + std::vector rawPts(n); for (size_t i = 0; i < n; i++) { - float t2 = (n > 1) ? (float)i / (float)(n - 1) : 0.0f; - float x = plotLeft + t2 * plotW; + float x = plotRight - (float)(n - 1 - i) * stepW; float y = plotBottom - (float)((chartHistory[i] - yMin) / (yMax - yMin)) * plotH; - points[i] = ImVec2(x, y); + rawPts[i] = ImVec2(x, y); + } + + // Catmull-Rom spline interpolation for smooth curve + std::vector points; + if (n <= 2) { + points = rawPts; + } else { + const int subdivs = 8; // segments between each pair of data points + points.reserve((n - 1) * subdivs + 1); + for (size_t i = 0; i + 1 < n; i++) { + // Four control points: p0, p1, p2, p3 + ImVec2 p0 = rawPts[i > 0 ? i - 1 : 0]; + ImVec2 p1 = rawPts[i]; + ImVec2 p2 = rawPts[i + 1]; + ImVec2 p3 = rawPts[i + 2 < n ? i + 2 : n - 1]; + for (int s = 0; s < subdivs; s++) { + float t = (float)s / (float)subdivs; + float t2 = t * t; + float t3 = t2 * t; + // Catmull-Rom basis + float q0 = -t3 + 2.0f * t2 - t; + float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f; + float q2 = -3.0f * t3 + 4.0f * t2 + t; + float q3 = t3 - t2; + float sx = 0.5f * (p0.x * q0 + p1.x * q1 + p2.x * q2 + p3.x * q3); + float sy = 0.5f * (p0.y * q0 + p1.y * q1 + p2.y * q2 + p3.y * q3); + points.push_back(ImVec2(sx, sy)); + } + } + points.push_back(rawPts[n - 1]); // final point } // Fill under curve - for (size_t i = 0; i + 1 < n; i++) { + for (size_t i = 0; i + 1 < points.size(); i++) { ImVec2 quad[4] = { points[i], points[i + 1], ImVec2(points[i + 1].x, plotBottom), @@ -1033,14 +1091,27 @@ void RenderMiningTab(App* app) } // ================================================================ - // EARNINGS — Horizontal row card (Today | All Time | Est. Daily) + // EARNINGS — Horizontal row card (Today | Yesterday | All Time | Est. Daily) // ================================================================ { // Gather mining transactions from state - double minedToday = 0.0, minedAllTime = 0.0; - int minedTodayCount = 0, minedAllTimeCount = 0; + double minedToday = 0.0, minedYesterday = 0.0, minedAllTime = 0.0; + int minedTodayCount = 0, minedYesterdayCount = 0, minedAllTimeCount = 0; int64_t now = (int64_t)std::time(nullptr); - int64_t dayStart = now - 86400; + + // Calendar-day boundaries (local time) + time_t nowT = (time_t)now; + struct tm local; +#ifdef _WIN32 + localtime_s(&local, &nowT); +#else + localtime_r(&nowT, &local); +#endif + local.tm_hour = 0; + local.tm_min = 0; + local.tm_sec = 0; + int64_t todayStart = (int64_t)mktime(&local); + int64_t yesterdayStart = todayStart - 86400; struct MinedTx { int64_t timestamp; @@ -1055,9 +1126,12 @@ void RenderMiningTab(App* app) double amt = std::abs(tx.amount); minedAllTime += amt; minedAllTimeCount++; - if (tx.timestamp >= dayStart) { + if (tx.timestamp >= todayStart) { minedToday += amt; minedTodayCount++; + } else if (tx.timestamp >= yesterdayStart) { + minedYesterday += amt; + minedYesterdayCount++; } if (recentMined.size() < 4) { recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100}); @@ -1089,7 +1163,8 @@ void RenderMiningTab(App* app) // === Earnings section (top of combined card) === { - float colW = (availWidth - pad * 2) / 3.0f; + const int numCols = 4; + float colW = (availWidth - pad * 2) / (float)numCols; float ey = cardMin.y + pad * 0.5f; char valBuf[64], subBuf2[64]; @@ -1106,6 +1181,10 @@ void RenderMiningTab(App* app) strncpy(todayVal, valBuf, sizeof(todayVal)); strncpy(todaySub, subBuf2, sizeof(todaySub)); + char yesterdayVal[64], yesterdaySub[64]; + snprintf(yesterdayVal, sizeof(yesterdayVal), "+%.4f", minedYesterday); + snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d blk)", minedYesterdayCount); + char allVal[64], allSub[64]; snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime); snprintf(allSub, sizeof(allSub), "(%d blk)", minedAllTimeCount); @@ -1117,12 +1196,13 @@ void RenderMiningTab(App* app) snprintf(estVal, sizeof(estVal), "N/A"); EarningsEntry entries[] = { - { "TODAY", todayVal, todaySub, greenCol2 }, - { "ALL TIME", allVal, allSub, OnSurface() }, - { "EST. DAILY", estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() }, + { "TODAY", todayVal, todaySub, greenCol2 }, + { "YESTERDAY", yesterdayVal, yesterdaySub, OnSurface() }, + { "ALL TIME", allVal, allSub, OnSurface() }, + { "EST. DAILY", estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() }, }; - for (int ei = 0; ei < 3; ei++) { + for (int ei = 0; ei < numCols; ei++) { float sx = cardMin.x + pad + ei * colW; float centerX = sx + colW * 0.5f;