feat(ui): say "Node busy" instead of "Launching dragonxd"; honest quit copy

A node rebuilding note witnesses for an hour was shown as "Starting
DragonX node... Launching dragonxd", which invited closing it.

- Overlay, a new NodeBusy banner and the status bar now say what the node
  is doing (rebuilding witnesses / catching up on blocks / up but not
  answering), with the elapsed time and Phase-1 block progress. A rebuild
  during startup warm-up replaces the stale "-28 Activating best chain"
  text, and the overlay re-applies its text once a second instead of only
  on a connect attempt (30s apart while the node is busy).
- Quit prompt: elapsed time, "Keep node running & quit" is the default,
  "Stop anyway & quit" becomes "Stop node & wait" (it no longer kills).
- A reinstall skipped because the node is still running now notifies.
- Status bar: don't repeat the warm-up status on the right, and don't
  print "Connected" there in non-English locales.
- i18n: 31 new keys in all 8 languages; 10 dead force-quit/old-modal keys
  removed; CJK subset rebuilt for the new glyphs.
- Tests: chooseDaemonStatus, NodeBusy banner priority, and every new key
  present in all 8 JSONs with matching printf conversions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 10:26:32 -05:00
co-authored by Claude Opus 5.5
parent 0f25c67600
commit 1215ded1a0
17 changed files with 822 additions and 133 deletions
+87
View File
@@ -54,6 +54,7 @@
#include "util/amount_format.h"
#include "util/payment_uri.h"
#include "util/platform.h"
#include "util/i18n.h"
#include "util/xmrig_updater.h"
#include "util/pool_registry.h"
#include "util/daemon_updater.h"
@@ -83,6 +84,7 @@
#include <fstream>
#include <iostream>
#include <algorithm>
#include <cstring>
#include <functional>
#include <memory>
#include <new>
@@ -2987,6 +2989,31 @@ void testNodeStatusBanner()
{
using namespace dragonx::ui;
// S9: a busy node gets its own banner, ahead of every other full-node state. It must show even while
// daemon_initializing is set (the busy overlay sets it), and offers no action (a reconnect would only
// queue more requests behind the busy node).
{
NodeBannerInputs in;
in.daemon_initializing = true;
in.node_busy = true;
in.node_busy_detail = "Running for 12:03.";
NodeBannerState s = evaluateNodeStatusBanner(in);
EXPECT_TRUE(s.show);
EXPECT_TRUE(s.reason == NodeBannerReason::NodeBusy);
EXPECT_TRUE(s.action == NodeBannerAction::None);
EXPECT_TRUE(s.severity == NodeBannerSeverity::Warning);
EXPECT_EQ(s.detail, std::string("Running for 12:03."));
}
{ // ...over "no peers" while connected, and over "offline" when not.
NodeBannerInputs connected; connected.connected = true; connected.tip_known = false; connected.blocks = 5;
connected.node_busy = true;
EXPECT_TRUE(evaluateNodeStatusBanner(connected).reason == NodeBannerReason::NodeBusy);
NodeBannerInputs offline; offline.node_busy = true;
EXPECT_TRUE(evaluateNodeStatusBanner(offline).reason == NodeBannerReason::NodeBusy);
NodeBannerInputs lite; lite.lite = true; lite.node_busy = true; // lite has no node to be busy
EXPECT_FALSE(evaluateNodeStatusBanner(lite).show);
}
// Connected full node → no banner.
{
NodeBannerInputs in; in.connected = true;
@@ -4321,6 +4348,64 @@ void testConnectRetryBackoff()
EXPECT_TRUE(attempts <= 6);
}
// S9: the startup overlay says what the node is really doing, most specific first.
void testChooseDaemonStatus()
{
using dragonx::daemon::DaemonBusyTracker;
using dragonx::daemon::DaemonStatusKind;
using dragonx::daemon::chooseDaemonStatus;
DaemonBusyTracker idle, rebuilding, replaying;
rebuilding.feedLine("Reading blocks for witness rebuild: 60000 / 62583");
replaying.feedLine("init message: Activating best chain...");
// The incident: a full queue during a rebuild showed "Launching dragonxd…".
EXPECT_TRUE(chooseDaemonStatus(false, true, rebuilding) == DaemonStatusKind::Rebuilding);
EXPECT_TRUE(chooseDaemonStatus(true, true, rebuilding) == DaemonStatusKind::Rebuilding);
EXPECT_TRUE(chooseDaemonStatus(true, false, replaying) == DaemonStatusKind::Replaying);
EXPECT_TRUE(chooseDaemonStatus(true, true, idle) == DaemonStatusKind::InitPhase); // "Loading block index"
EXPECT_TRUE(chooseDaemonStatus(false, true, idle) == DaemonStatusKind::Busy); // reachable, no answer
EXPECT_TRUE(chooseDaemonStatus(false, false, idle) == DaemonStatusKind::Launching); // not bound yet
}
// S10: every string added by the daemon-stop work exists in English and in all 8 languages, with the same
// printf arguments (a mismatch is rejected at runtime and would fall back to English, or worse).
void testDaemonStopStringsTranslated()
{
const char* keys[] = {
"shutdown_leave_running", "shutdown_leave_running_tip", "shutdown_kill_node", "shutdown_kill_node_tip",
"shutdown_kill_confirm_title", "shutdown_kill_confirm_msg", "shutdown_kill_confirm_yes",
"shutdown_stop_asking", "shutdown_stop_queued", "shutdown_stop_delivered", "shutdown_stop_cpu_suffix",
"shutdown_stop_idle", "node_busy_rebuild_title", "node_busy_rebuild_desc", "node_busy_rebuild_reading",
"node_busy_rebuild_extending", "node_busy_replay_title", "node_busy_replay_desc", "node_busy_rpc_title",
"node_busy_rpc_desc", "node_starting_title", "node_starting_busy_desc", "node_starting_launch_desc",
"sb_node_busy", "stopconfirm_rebuild_title", "stopconfirm_rebuild_body", "stopconfirm_busy_title",
"stopconfirm_busy_body", "stopconfirm_stop_wait", "appx_daemon_reinstall_node_busy", "node_busy_running",
};
// printf conversions in order, "%%" excluded: what must survive translation.
auto conversions = [](const std::string& s) {
std::string sig;
for (size_t i = 0; i < s.size(); ++i) {
if (s[i] != '%' || ++i >= s.size()) continue;
if (s[i] == '%') continue;
while (i < s.size() && std::strchr("-+ #0123456789.*lhLzjtq", s[i])) ++i;
if (i < s.size()) sig.push_back(s[i]);
}
return sig;
};
const fs::path langDir = fs::path(fixtureDir()) / ".." / ".." / "res" / "lang";
for (const char* key : keys) {
const std::string english = dragonx::util::I18n::instance().translate(key);
EXPECT_TRUE(english != key); // present in the built-in English table
for (const char* lang : {"de", "es", "fr", "pt", "ru", "zh", "ja", "ko"}) {
std::ifstream in(langDir / (std::string(lang) + ".json"));
const auto j = nlohmann::json::parse(in, nullptr, false);
const bool has = !j.is_discarded() && j.contains(key) && j[key].is_string();
if (!has) std::cerr << "missing translation: " << lang << "." << key << "\n";
EXPECT_TRUE(has);
if (has) EXPECT_EQ(conversions(j[key].get<std::string>()), conversions(english));
}
}
}
// S3: on Linux a node in its final shutdown phase reads comm "hush-shutoff"; its executable still says
// dragonxd, so it must still count as running (it holds the datadir lock until it exits).
void testDaemonProcessIdentity()
@@ -8917,6 +9002,8 @@ int main()
testDaemonBusyTracker();
testShouldConfirmStop();
testOutputSinceAcrossTrim();
testChooseDaemonStatus();
testDaemonStopStringsTranslated();
#ifdef _WIN32
testWindowsStopNeverKillsForeignDaemon();
#endif