fix(ui): prevent stuck spinners and double-submits in dialogs

Address a cluster of low-severity robustness nits where an async
callback could be skipped (leaving a spinner spinning forever) or a
synchronous action could be re-fired mid-flight:

- app_network: add catch(...) fallbacks around the importPrivateKey and
  submitZSendMany worker lambdas so a non-std throw can't escape the
  worker and skip the main-thread callback (stuck "Importing…"/"Sending…").
- export_transactions: re-entrancy guard disabling Export while a write
  is in flight.
- bootstrap_download: disable Cancel once clicked so the request is
  visibly acknowledged and can't be re-fired before the worker stops.
- network_tab: give clear "already in list" feedback on a duplicate
  server (keep the inputs, clear only the stale invalid-URL error) and
  disable/relabel Refresh while a probe is in flight.
- key_export: offer Retry from the error branch (falls back to Reveal)
  and guard against an empty/malformed address before dispatching.
- receive_tab: only enter the "generating…" state when a dispatch is
  actually possible (guards a stuck spinner when disconnected).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 15:08:43 -05:00
parent 1dbbd44759
commit 41f5548593
6 changed files with 86 additions and 30 deletions

View File

@@ -25,6 +25,9 @@ namespace ui {
static bool s_open = false;
static char s_filename[256] = "";
static std::string s_status;
// Re-entrancy guard: true while a CSV write is in flight (disables the
// Export button so a second synchronous write can't be kicked off).
static bool s_exporting = false;
// Helper to escape CSV field
static std::string escapeCSV(const std::string& field)
@@ -91,14 +94,16 @@ void ExportTransactionsDialog::render(App* app)
ImGui::Separator();
ImGui::Spacing();
// Export button
// Export button (disabled while a write is already in flight)
ImGui::BeginDisabled(s_exporting);
if (material::StyledButton(TR("export"), ImVec2(exportBtn.width, 0), S.resolveFont(exportBtn.font))) {
if (state.transactions.empty()) {
Notifications::instance().warning(TR("export_tx_none"));
} else {
s_exporting = true;
std::string configDir = util::Platform::getConfigDir();
std::string filepath = configDir + "/" + s_filename;
std::ofstream file(filepath);
if (!file.is_open()) {
s_status = "Failed to create file";
@@ -138,13 +143,15 @@ void ExportTransactionsDialog::render(App* app)
file.close();
s_status = "Exported " + std::to_string(state.transactions.size()) +
s_status = "Exported " + std::to_string(state.transactions.size()) +
" transactions to: " + filepath;
Notifications::instance().success(TR("export_tx_success"), 5.0f);
}
s_exporting = false;
}
}
ImGui::EndDisabled();
ImGui::SameLine();
if (material::StyledButton("Close", ImVec2(closeBtn.width, 0), S.resolveFont(closeBtn.font))) {
s_open = false;