feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings

Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):

- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
  status-bar "?" (and the Windows title bar), styled like the Wallets
  modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
  Layout::dpiScale() across ~30 files so nothing renders native-size at
  HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
  match the input-box height; move the stratum-host toggle into
  Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
  the preceding i18n commit), so the security/PIN/lock flow, seed-backup
  wizard, and witness-rebuild dialog localize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 19:37:16 -05:00
co-authored by Claude Opus 4.8
parent ed675f90d8
commit d27f387d6d
35 changed files with 867 additions and 366 deletions
+44 -45
View File
@@ -300,14 +300,14 @@ void App::restartDaemonAfterEncryption(const char* taskName, bool announceRestar
});
} else {
ui::Notifications::instance().warning(
"Please restart your daemon for encryption to take effect.");
TR("sec_restart_daemon_for_encryption"));
}
}
void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (!rpc_ || !rpc_->isConnected()) return;
encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet...";
encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) {
worker_->post([this, passphrase]() mutable -> rpc::RPCWorker::MainCb {
@@ -317,7 +317,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
if (result.encrypted) {
return [this]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Wallet encrypted. Restarting daemon...";
encrypt_status_ = TR("sec_wallet_encrypted_restarting_daemon");
DEBUG_LOGF("[App] Wallet encrypted — restarting daemon\n");
// Immediately update local encryption state so the
@@ -336,7 +336,7 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
}
ui::Notifications::instance().info(
"Wallet encrypted successfully", 5.0f);
TR("sec_wallet_encrypted_successfully"), 5.0f);
// The daemon shuts itself down after encryptwallet.
// Update connection_status_ so the loading overlay
@@ -348,11 +348,11 @@ void App::encryptWalletWithPassphrase(const std::string& passphrase) {
std::string err = result.error;
return [this, err]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Encryption failed: " + err;
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
DEBUG_LOGF("[App] encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error(
"Encryption failed: " + err);
std::string(TR("sec_encryption_failed_prefix")) + err);
// Return to passphrase entry on failure
if (show_encrypt_dialog_ &&
@@ -393,7 +393,7 @@ void App::processDeferredEncryption() {
std::string pin = std::move(deferredEncryption.pin);
encrypt_in_progress_ = true;
encrypt_status_ = "Encrypting wallet...";
encrypt_status_ = TR("sec_encrypting_wallet");
if (worker_) {
worker_->post([this, request = services::WalletSecurityController::DeferredEncryptionSnapshot{std::move(passphrase), std::move(pin)}]() mutable -> rpc::RPCWorker::MainCb {
@@ -413,13 +413,13 @@ void App::processDeferredEncryption() {
if (result.pinStored) {
settings_->setPinEnabled(true);
settings_->save();
ui::Notifications::instance().info("Wallet encrypted & PIN set", 5.0f);
ui::Notifications::instance().info(TR("sec_wallet_encrypted_and_pin_set"), 5.0f);
} else {
ui::Notifications::instance().warning(
"Wallet encrypted but PIN vault failed");
TR("sec_wallet_encrypted_but_pin_vault_failed"));
}
} else {
ui::Notifications::instance().info("Wallet encrypted successfully", 5.0f);
ui::Notifications::instance().info(TR("sec_wallet_encrypted_successfully"), 5.0f);
}
wallet_security_.clearDeferredEncryption();
@@ -432,9 +432,9 @@ void App::processDeferredEncryption() {
std::string err = result.error;
return [this, err]() {
encrypt_in_progress_ = false;
encrypt_status_ = "Encryption failed: " + err;
encrypt_status_ = std::string(TR("sec_encryption_failed_prefix")) + err;
DEBUG_LOGF("[App] Deferred encryptwallet failed: %s\n", err.c_str());
ui::Notifications::instance().error("Encryption failed: " + err);
ui::Notifications::instance().error(std::string(TR("sec_encryption_failed_prefix")) + err);
wallet_security_.clearDeferredEncryption();
};
}
@@ -531,7 +531,7 @@ void App::lockWallet() {
if (!lock_failure_warned_) {
lock_failure_warned_ = true;
ui::Notifications::instance().warning(
"Couldn't lock the wallet — it is still unlocked. Check the daemon connection.", 12.0f);
TR("sec_couldnt_lock_wallet"), 12.0f);
}
}
};
@@ -541,7 +541,7 @@ void App::lockWallet() {
void App::changePassphrase(const std::string& oldPass, const std::string& newPass) {
if (!rpc_ || !rpc_->isConnected() || !worker_) return;
encrypt_in_progress_ = true;
encrypt_status_ = "Changing passphrase...";
encrypt_status_ = TR("sec_changing_passphrase");
auto* w = (fast_worker_ && fast_worker_->isRunning()) ? fast_worker_.get() : worker_.get();
auto* r = (fast_rpc_ && fast_rpc_->isConnected()) ? fast_rpc_.get() : rpc_.get();
@@ -574,9 +574,9 @@ void App::changePassphrase(const std::string& oldPass, const std::string& newPas
memset(change_confirm_buf_, 0, sizeof(change_confirm_buf_));
unlockTransactionHistoryCacheWithPassphrase(newPass);
storeTransactionHistoryCacheIfAvailable();
ui::Notifications::instance().info("Passphrase changed successfully");
ui::Notifications::instance().info(TR("sec_passphrase_changed_successfully"));
} else {
encrypt_status_ = "Failed: " + err_msg;
encrypt_status_ = std::string(TR("sec_failed_prefix")) + err_msg;
}
util::SecureVault::secureZero(newPass.data(), newPass.size());
};
@@ -637,8 +637,7 @@ void App::refreshWalletEncryptionState() {
!encryption_incomplete_warned_) {
encryption_incomplete_warned_ = true;
ui::Notifications::instance().warning(
"Wallet encryption did not complete — your wallet is NOT encrypted. "
"Open Settings to finish encrypting it.", 30.0f);
TR("sec_encryption_did_not_complete"), 30.0f);
}
if (state_.transactions.empty()) {
loadTransactionHistoryCacheIfAvailable();
@@ -951,7 +950,7 @@ void App::renderLockScreen() {
ImU32 textCol = ui::material::OnSurface();
{
const char* title = "Wallet Locked";
const char* title = TR("sec_wallet_locked_title");
ImVec2 ts = titleFont->CalcTextSizeA(titleFont->LegacySize, FLT_MAX, 0, title);
dl->AddText(titleFont, titleFont->LegacySize,
ImVec2(cardX + (cardW - ts.x) * 0.5f, cy), textCol, title);
@@ -964,7 +963,7 @@ void App::renderLockScreen() {
if (lock_lockout_timer_ < 0) lock_lockout_timer_ = 0;
char msg[128];
snprintf(msg, sizeof(msg), "Too many attempts. Wait %.0f seconds...", lock_lockout_timer_);
snprintf(msg, sizeof(msg), TR("sec_too_many_attempts_wait"), lock_lockout_timer_);
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy), ui::material::Warning(), msg);
@@ -977,10 +976,10 @@ void App::renderLockScreen() {
// Mode toggle (PIN / Passphrase) — only show if PIN vault exists
if (hasPinVault) {
const char* modeIcon = lock_use_pin_ ? ICON_MD_DIALPAD : ICON_MD_PASSWORD;
const char* modeText = lock_use_pin_ ? " PIN" : " Passphrase";
const char* modeText = lock_use_pin_ ? " PIN" : TR("sec_mode_passphrase");
const char* switchLabel = lock_use_pin_
? "Use passphrase instead"
: "Use PIN instead";
? TR("sec_use_passphrase_instead")
: TR("sec_use_pin_instead");
// Current mode indicator — icon with icon font, text with caption font
ImFont* iconFont = ui::material::Type().iconSmall();
@@ -1082,7 +1081,7 @@ void App::renderLockScreen() {
if (lock_unlock_in_progress_) {
// Animated spinner dots
char msg[64];
snprintf(msg, sizeof(msg), "Unlocking%s", ui::material::LoadingDots());
snprintf(msg, sizeof(msg), TR("sec_unlocking_fmt"), ui::material::LoadingDots());
ImVec2 ms = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, 0, msg);
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cardX + (cardW - ms.x) * 0.5f, cy),
@@ -1106,7 +1105,7 @@ void App::renderLockScreen() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnPrimary()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!canSubmit);
bool btnClicked = ui::material::TactileButton("Unlock", ImVec2(unlockW, unlockH));
bool btnClicked = ui::material::TactileButton(TR("sec_unlock_button"), ImVec2(unlockW, unlockH));
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
@@ -1160,7 +1159,7 @@ void App::renderLockScreen() {
r->call("walletpassphrase", {passphrase, timeout});
rpcOk = true;
} else {
rpcErr = "Not connected to daemon";
rpcErr = TR("sec_not_connected_to_daemon");
}
} catch (const std::exception& e) {
rpcErr = e.what();
@@ -1176,7 +1175,7 @@ void App::renderLockScreen() {
// so route through applyUnlockFailure so the lockout curve applies
// (this path previously bumped the counter but skipped the lockout math).
return [this, rpcErr, passphrase = std::move(passphrase)]() mutable {
applyUnlockFailure("Unlock failed: " + rpcErr);
applyUnlockFailure(std::string(TR("sec_unlock_failed_prefix")) + rpcErr);
util::SecureVault::secureZero(passphrase.data(), passphrase.size());
};
}
@@ -1579,7 +1578,7 @@ void App::renderDecryptWalletDialog() {
if (!passphrase.empty()) sodium_memzero(&passphrase[0], passphrase.size());
if (!unlock.ok) {
return [this]() {
wallet_security_workflow_.failEntry("Incorrect passphrase");
wallet_security_workflow_.failEntry(TR("sec_incorrect_passphrase_decrypt"));
};
}
@@ -1695,7 +1694,7 @@ void App::renderDecryptWalletDialog() {
});
ui::Notifications::instance().info(
"Importing keys & rescanning blockchain — wallet is usable while this runs",
TR("sec_importing_keys_rescanning"),
8.0f);
};
});
@@ -1714,7 +1713,7 @@ void App::renderDecryptWalletDialog() {
wallet_security_workflow_.finishImport();
ui::Notifications::instance().error(
err +
"\nEncrypted backup: wallet.dat.encrypted.bak",
TR("sec_encrypted_backup_suffix"),
12.0f);
};
});
@@ -1740,7 +1739,7 @@ void App::renderDecryptWalletDialog() {
refreshPeerInfo();
ui::Notifications::instance().success(
"Wallet decrypted successfully! All keys imported.",
TR("sec_wallet_decrypted_all_keys_imported"),
8.0f);
DEBUG_LOGF("[App] Wallet decrypted successfully\n");
};
@@ -1862,7 +1861,7 @@ void App::renderDecryptWalletDialog() {
int tMins = (int)(totalElapsed / 60);
int tSecs = (int)(totalElapsed % 60);
ImGui::Spacing();
ImGui::TextDisabled("Total elapsed: %dm %02ds", tMins, tSecs);
ImGui::TextDisabled(TR("sec_total_elapsed_fmt"), tMins, tSecs);
}
// ---- Phase 2: Success ----
@@ -1965,7 +1964,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_set_pin"), ImVec2(btnW, 40))) {
pin_in_progress_ = true;
pin_status_ = "Verifying passphrase...";
pin_status_ = TR("sec_verifying_passphrase");
// Verify passphrase + store vault on worker thread to avoid
// blocking the UI with Argon2id key derivation.
@@ -1985,7 +1984,7 @@ void App::renderPinDialogs() {
if (!passphrase.empty()) util::SecureVault::secureZero(&passphrase[0], passphrase.size());
if (!pin.empty()) util::SecureVault::secureZero(&pin[0], pin.size());
return [this]() {
pin_status_ = "Incorrect passphrase";
pin_status_ = TR("sec_incorrect_passphrase_pin_setup");
pin_in_progress_ = false;
};
}
@@ -2009,15 +2008,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_setup_ = false;
ui::Notifications::instance().info("PIN set successfully");
ui::Notifications::instance().info(TR("sec_pin_set_successfully"));
} else {
pin_status_ = "Failed to create vault";
pin_status_ = TR("sec_failed_to_create_vault");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Not connected to daemon";
pin_status_ = TR("sec_not_connected_to_daemon_pin");
pin_in_progress_ = false;
}
}
@@ -2082,7 +2081,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_change_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Changing PIN...";
pin_status_ = TR("sec_changing_pin");
std::string oldPin(pin_old_buf_);
std::string newPinCopy = newPin;
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2098,15 +2097,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_change_ = false;
ui::Notifications::instance().info("PIN changed successfully");
ui::Notifications::instance().info(TR("sec_pin_changed_successfully"));
} else {
pin_status_ = "Incorrect current PIN";
pin_status_ = TR("sec_incorrect_current_pin");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Internal error";
pin_status_ = TR("sec_internal_error_change_pin");
pin_in_progress_ = false;
}
}
@@ -2147,7 +2146,7 @@ void App::renderPinDialogs() {
ImGui::BeginDisabled(!valid || pin_in_progress_);
if (ui::material::TactileButton(TR("settings_remove_pin"), ImVec2(-1, 40))) {
pin_in_progress_ = true;
pin_status_ = "Verifying PIN...";
pin_status_ = TR("sec_verifying_pin");
std::string oldPin(pin_old_buf_);
memset(pin_old_buf_, 0, sizeof(pin_old_buf_));
@@ -2167,15 +2166,15 @@ void App::renderPinDialogs() {
pin_status_.clear();
pin_in_progress_ = false;
show_pin_remove_ = false;
ui::Notifications::instance().info("PIN removed");
ui::Notifications::instance().info(TR("sec_pin_removed"));
} else {
pin_status_ = "Incorrect PIN";
pin_status_ = TR("sec_incorrect_pin_remove");
pin_in_progress_ = false;
}
};
});
} else {
pin_status_ = "Internal error";
pin_status_ = TR("sec_internal_error_remove_pin");
pin_in_progress_ = false;
}
}