fix: large-wallet sync starvation + shutdown/console-flash UX (Windows full node) #2

Open
DanS wants to merge 12 commits from fix/balance-poll-sync-contention into dev
6 changed files with 83 additions and 6 deletions
Showing only changes of commit 5daf2d83b6 - Show all commits

View File

@@ -878,6 +878,9 @@ void App::update()
// (a prior/unwitnessed salvage likely moved the coins into a wallet.<ts>.bak). // (a prior/unwitnessed salvage likely moved the coins into a wallet.<ts>.bak).
maybeWarnEmptyWalletWithFundedSiblings(); maybeWarnEmptyWalletWithFundedSiblings();
// One-time nudge if wallet.dat has bloated past the threshold (toast + clickable alert → consolidate).
maybeWarnLargeWallet();
// Classify the wallet's mnemonic status (once per connect) so the Migrate-to-seed button can // Classify the wallet's mnemonic status (once per connect) so the Migrate-to-seed button can
// glow for a legacy, pre-seed-phrase wallet. // glow for a legacy, pre-seed-phrase wallet.
probeWalletSeedStatus(); probeWalletSeedStatus();
@@ -2421,10 +2424,19 @@ void App::renderAlertHistoryPanel()
return; return;
} }
// Scrollable list, newest first. Height adapts to the entry count but caps so a busy session // Scrollable list, newest first. Measure the TRUE content height so wrapped (multi-line) messages
// scrolls inside the panel instead of blowing past the popup's max height. // and optional action links aren't clipped by an under-estimate; cap so a busy session scrolls
const float perEntry = txtF->LegacySize * 2.0f + 14.0f * dp; // message line + time line + spacing // inside the panel instead of blowing past the popup's max height.
const float listH = std::min(300.0f * dp, static_cast<float>(hist.size()) * perEntry); const float msgWrapW = std::max(40.0f * dp, innerW - 2.0f * padX - icoF->LegacySize - 6.0f * dp);
float contentH = 0.0f;
for (const auto& a : hist) {
const float msgH = txtF->CalcTextSizeA(txtF->LegacySize, FLT_MAX, msgWrapW, a.message.c_str()).y;
contentH += std::max(msgH, static_cast<float>(icoF->LegacySize)); // icon + wrapped message
contentH += txtF->LegacySize; // relative-age line
if (a.onClick && !a.actionHint.empty()) contentH += txtF->LegacySize; // action-link line
contentH += 8.0f * dp; // inter-entry spacing
}
const float listH = std::min(300.0f * dp, contentH);
ImGui::BeginChild("##AlertRows", ImVec2(0, listH), false); ImGui::BeginChild("##AlertRows", ImVec2(0, listH), false);
int idx = 0; int idx = 0;
for (auto it = hist.rbegin(); it != hist.rend(); ++it, ++idx) { for (auto it = hist.rbegin(); it != hist.rend(); ++it, ++idx) {
@@ -2452,6 +2464,19 @@ void App::renderAlertHistoryPanel()
ImGui::TextWrapped("%s", a.message.c_str()); ImGui::TextWrapped("%s", a.message.c_str());
ImGui::PopTextWrapPos(); ImGui::PopTextWrapPos();
ImGui::PopStyleColor(); ImGui::PopStyleColor();
// Optional clickable action (accent link), directly under the message so it stays prominent.
if (a.onClick && !a.actionHint.empty()) {
ImGui::SetCursorPosX(padX + icoF->LegacySize + 6.0f * dp);
ImGui::PushStyleColor(ImGuiCol_Text, m::Primary());
ImGui::TextUnformatted(a.actionHint.c_str());
ImGui::PopStyleColor();
if (ImGui::IsItemHovered()) {
const ImVec2 lmn = ImGui::GetItemRectMin(), lmx = ImGui::GetItemRectMax();
ImGui::GetWindowDrawList()->AddLine(ImVec2(lmn.x, lmx.y), ImVec2(lmx.x, lmx.y), m::Primary());
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
if (ImGui::IsItemClicked()) { a.onClick(); ImGui::CloseCurrentPopup(); }
}
// Relative age, dim, indented under the message. // Relative age, dim, indented under the message.
ImGui::SetCursorPosX(padX + icoF->LegacySize + 6.0f * dp); ImGui::SetCursorPosX(padX + icoF->LegacySize + 6.0f * dp);
ImGui::PushStyleColor(ImGuiCol_Text, m::OnSurfaceDisabled()); ImGui::PushStyleColor(ImGuiCol_Text, m::OnSurfaceDisabled());

View File

@@ -817,6 +817,7 @@ private:
// install) to back up their seed phrase. Cheap early-outs keep it idle until it can act. // install) to back up their seed phrase. Cheap early-outs keep it idle until it can act.
void maybeRemindSeedBackup(); void maybeRemindSeedBackup();
void maybeWarnEmptyWalletWithFundedSiblings(); // full-node: empty active wallet + a funded sibling → warn once void maybeWarnEmptyWalletWithFundedSiblings(); // full-node: empty active wallet + a funded sibling → warn once
void maybeWarnLargeWallet(); // full-node: wallet.dat past bloat threshold → one-time toast + clickable alert
void scanFundedSiblingsAsync(); // off-UI-thread probe of sibling wallet files void scanFundedSiblingsAsync(); // off-UI-thread probe of sibling wallet files
static void scrubAndRemoveExport(const std::string& path); // zero + delete a plaintext key export (H-02) static void scrubAndRemoveExport(const std::string& path); // zero + delete a plaintext key export (H-02)
void sweepStaleDecryptExports(); // startup net: purge stale obsidiandecryptexport* files (H-02) void sweepStaleDecryptExports(); // startup net: purge stale obsidiandecryptexport* files (H-02)
@@ -1007,6 +1008,7 @@ private:
bool seed_backup_loading_ = false; bool seed_backup_loading_ = false;
bool seed_backup_no_mnemonic_ = false; bool seed_backup_no_mnemonic_ = false;
bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe bool seed_backup_reminder_in_flight_ = false; // guards the one-time backup nudge probe
bool large_wallet_checked_ = false; // gate: stat wallet.dat for the bloat nudge once per launch
// Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed // Cached mnemonic status of the current wallet, driving the Migrate-to-seed button glow. Probed
// once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a // once per connect (probeWalletSeedStatus, via exportSeedPhrase); NoMnemonic = a legacy wallet a

View File

@@ -35,6 +35,7 @@
#include "rpc/connection.h" #include "rpc/connection.h"
#include "chat/chat_identity.h" // deriveChatIdentityFromSecret for HushChat identity provisioning #include "chat/chat_identity.h" // deriveChatIdentityFromSecret for HushChat identity provisioning
#include "ui/windows/chat_tab.h" // ui::ResetChatTab — wipe chat UI plaintext on a wallet switch #include "ui/windows/chat_tab.h" // ui::ResetChatTab — wipe chat UI plaintext on a wallet switch
#include "ui/windows/shield_dialog.h" // ui::ShieldDialog — Merge to Address shortcut from the bloat nudge
#include "ui/windows/mining_pool_panel.h" // ui::resolveMiningUserAddress #include "ui/windows/mining_pool_panel.h" // ui::resolveMiningUserAddress
#include <sodium.h> // sodium_memzero for wiping the fetched mnemonic #include <sodium.h> // sodium_memzero for wiping the fetched mnemonic
#include <cctype> #include <cctype>
@@ -4232,6 +4233,36 @@ void App::maybeRemindSeedBackup()
}); });
} }
// One-time nudge (full-node) when the BDB wallet.dat has bloated past the threshold. Berkeley DB never
// shrinks in place and shielded-note witness data accumulates, so a mining/shielded wallet can grow
// unbounded. Fires ONCE (persisted flag) a warning toast + a clickable "Consolidate notes…" entry in the
// bell/alert panel that opens Merge to Address; re-arms if the file later drops back under the threshold.
void App::maybeWarnLargeWallet()
{
if (capture_mode_ || lite_wallet_) return; // no live nags during a UI sweep; lite has no wallet.dat
if (!supportsFullNodeLifecycleActions() || !settings_) return;
if (!state_.connected || !state_.encryption_state_known) return;
if (state_.warming_up || state_.daemon_initializing || !state_.sync.isSynced()) return;
if (large_wallet_checked_) return; // stat wallet.dat at most once per launch
large_wallet_checked_ = true;
static constexpr uint64_t kWalletBloatWarnBytes = 500ull * 1024 * 1024; // 500 MB (matches the Settings banner)
const std::string walletPath = util::Platform::getDragonXDataDir() + "/wallet.dat";
const uint64_t sz = util::Platform::getFileSize(walletPath);
if (sz <= kWalletBloatWarnBytes) {
// Re-arm the one-time warning if the file shrank back under the threshold (e.g. after a fresh seed wallet).
if (settings_->getLargeWalletWarned()) { settings_->setLargeWalletWarned(false); settings_->save(); }
return;
}
if (settings_->getLargeWalletWarned()) return; // already warned once for this bloat episode
settings_->setLargeWalletWarned(true);
settings_->save();
ui::Notifications::instance().action(
TR("wallet_size_warn"), ui::NotificationType::Warning,
[]() { ui::ShieldDialog::show(ui::ShieldDialog::Mode::MergeToAddress); },
TR("wallet_size_consolidate"), 12.0f);
}
// Complementary on-disk safety net (full-node) for a wallet salvage we did NOT witness this launch — it // Complementary on-disk safety net (full-node) for a wallet salvage we did NOT witness this launch — it
// happened on a prior run, or under an external daemon whose startup output we never captured, so // happened on a prior run, or under an external daemon whose startup output we never captured, so
// detectWalletAutoRecovery() never fired. If the active wallet loads EMPTY while a sibling wallet file in // detectWalletAutoRecovery() never fired. If the active wallet loads EMPTY while a sibling wallet file in

View File

@@ -232,6 +232,7 @@ bool Settings::load(const std::string& path)
} }
loadScalar(j, "wizard_completed", wizard_completed_); loadScalar(j, "wizard_completed", wizard_completed_);
loadScalar(j, "seed_backup_reminded", seed_backup_reminded_); loadScalar(j, "seed_backup_reminded", seed_backup_reminded_);
loadScalar(j, "large_wallet_warned", large_wallet_warned_);
if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) { if (j.contains("empty_wallet_warning_acked") && j["empty_wallet_warning_acked"].is_array()) {
empty_wallet_warning_acked_.clear(); empty_wallet_warning_acked_.clear();
for (const auto& w : j["empty_wallet_warning_acked"]) for (const auto& w : j["empty_wallet_warning_acked"])
@@ -506,6 +507,7 @@ bool Settings::save(const std::string& path)
} }
j["wizard_completed"] = wizard_completed_; j["wizard_completed"] = wizard_completed_;
j["seed_backup_reminded"] = seed_backup_reminded_; j["seed_backup_reminded"] = seed_backup_reminded_;
j["large_wallet_warned"] = large_wallet_warned_;
j["empty_wallet_warning_acked"] = json::array(); j["empty_wallet_warning_acked"] = json::array();
for (const auto& w : empty_wallet_warning_acked_) for (const auto& w : empty_wallet_warning_acked_)
j["empty_wallet_warning_acked"].push_back(w); j["empty_wallet_warning_acked"].push_back(w);

View File

@@ -330,6 +330,10 @@ public:
bool getSeedBackupReminded() const { return seed_backup_reminded_; } bool getSeedBackupReminded() const { return seed_backup_reminded_; }
void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; } void setSeedBackupReminded(bool v) { seed_backup_reminded_ = v; }
// One-time nudge when wallet.dat grows past the bloat threshold (re-armed if it shrinks back).
bool getLargeWalletWarned() const { return large_wallet_warned_; }
void setLargeWalletWarned(bool v) { large_wallet_warned_ = v; }
// Wallet filenames for which the one-time "this wallet is empty but a sibling holds funds" // Wallet filenames for which the one-time "this wallet is empty but a sibling holds funds"
// warning has been dismissed. Keyed per active wallet file so switching to a different empty // warning has been dismissed. Keyed per active wallet file so switching to a different empty
// wallet can warn again (see App::maybeWarnEmptyWalletWithFundedSiblings). // wallet can warn again (see App::maybeWarnEmptyWalletWithFundedSiblings).
@@ -597,6 +601,7 @@ private:
std::map<std::string, AddressMeta> address_meta_; std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false; bool wizard_completed_ = false;
bool seed_backup_reminded_ = false; bool seed_backup_reminded_ = false;
bool large_wallet_warned_ = false;
std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed std::set<std::string> empty_wallet_warning_acked_; // wallet files whose empty-wallet warning was dismissed
bool encryption_pending_ = false; bool encryption_pending_ = false;
long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt long long daemon_update_prompted_size_ = 0; // bundled daemon size last offered via the update prompt

View File

@@ -31,6 +31,8 @@ struct AlertRecord {
std::string message; std::string message;
NotificationType type; NotificationType type;
std::int64_t epoch; // std::time(nullptr) at push — wall-clock, for relative-age display std::int64_t epoch; // std::time(nullptr) at push — wall-clock, for relative-age display
std::function<void()> onClick; // optional: makes this bell-panel entry actionable
std::string actionHint; // optional: accent link label rendered for the action
}; };
struct Notification { struct Notification {
@@ -92,15 +94,25 @@ public:
if (duration < 0.0f) duration = schemaDuration("duration-error", 4.0f); if (duration < 0.0f) duration = schemaDuration("duration-error", 4.0f);
push(message, NotificationType::Error, duration); push(message, NotificationType::Error, duration);
} }
// An actionable alert: a normal toast PLUS a clickable entry in the bell/alert-history panel.
// onClick fires when the user clicks the accent `actionHint` link in that panel.
void action(const std::string& message, NotificationType type, std::function<void()> onClick,
const std::string& actionHint, float duration = -1.0f) {
if (duration < 0.0f) duration = schemaDuration("duration-warning", 3.5f);
push(message, type, duration, std::move(onClick), actionHint);
}
void push(const std::string& message, NotificationType type, float duration = 5.0f) { void push(const std::string& message, NotificationType type, float duration = 5.0f,
std::function<void()> onClick = nullptr, const std::string& actionHint = "") {
notifications_.emplace_back(message, type, duration); notifications_.emplace_back(message, type, duration);
// Retain a copy in the persistent history (the toast above will fade in seconds; this // Retain a copy in the persistent history (the toast above will fade in seconds; this
// survives so the user can review what happened). Thread note: every push is on the UI // survives so the user can review what happened). Thread note: every push is on the UI
// thread (RPC results run as main-thread MainCb callbacks), so this container needs no lock, // thread (RPC results run as main-thread MainCb callbacks), so this container needs no lock,
// consistent with the rest of this class. Do NOT push from a raw worker thread. // consistent with the rest of this class. Do NOT push from a raw worker thread.
history_.push_back(AlertRecord{message, type, static_cast<std::int64_t>(std::time(nullptr))}); history_.push_back(AlertRecord{message, type, static_cast<std::int64_t>(std::time(nullptr)),
std::move(onClick), actionHint});
++total_pushed_; ++total_pushed_;
while (history_.size() > kMaxHistory) { while (history_.size() > kMaxHistory) {
history_.pop_front(); history_.pop_front();