feat(lite): Console tab with connection + open/create diagnostics

The lite variant had no visibility into why a wallet failed to open — just a
"disconnected" spinner. Add a lite-only Console tab (full-node keeps its RPC
console) that shows a live diagnostic log.

- LiteDiagnostics: a small thread-safe, bounded ring buffer (header-only). The
  controller writes to it from its background threads: each failover server
  attempt and result, wallet open/create/restore outcomes, sync start, and
  blocked-open reasons. The App logs controller (re)builds with the preferred
  server.
- lite_console_tab: a terminal-styled, read-only view of the log (newest at the
  bottom, error/success lines coloured) with Clear / Copy / Auto-scroll. Reachable
  even when the wallet is locked (it's diagnostics, no secrets). Registered as
  NavPage::LiteConsole, gated lite-only via WalletUiSurface::LiteConsole.

A unit test drives an open-with-failover and asserts the log records the
connection attempt and the successful open. Built clean for full-node, lite, and
Windows cross-compile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 18:46:25 -05:00
parent dbeae3ac98
commit 85a1080b52
10 changed files with 258 additions and 7 deletions

View File

@@ -30,6 +30,7 @@ enum class NavPage {
Market,
// --- separator ---
Console,
LiteConsole, // lite-only diagnostics console (full-node uses the RPC Console); one per variant
Peers,
LiteNetwork, // lite-only "Network" tab (full-node uses Peers); exactly one shows per variant
Explorer,
@@ -52,9 +53,10 @@ inline const NavItem kNavItems[] = {
{ "History", NavPage::History, nullptr, "history", nullptr },
{ "Mining", NavPage::Mining, "TOOLS", "mining", "tools" },
{ "Market", NavPage::Market, nullptr, "market", nullptr },
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
{ "Network", NavPage::LiteNetwork, nullptr, "network", nullptr },
{ "Console", NavPage::Console, "ADVANCED","console", "advanced" },
{ "Console", NavPage::LiteConsole, "ADVANCED","console", "advanced" },
{ "Network", NavPage::Peers, nullptr, "network", nullptr },
{ "Network", NavPage::LiteNetwork, nullptr, "network", nullptr },
{ "Explorer", NavPage::Explorer, nullptr, "explorer", nullptr },
{ "Settings", NavPage::Settings, nullptr, "settings", nullptr },
};
@@ -79,6 +81,7 @@ inline wallet::WalletUiSurface NavPageSurface(NavPage page)
case NavPage::Mining: return wallet::WalletUiSurface::Mining;
case NavPage::Market: return wallet::WalletUiSurface::Market;
case NavPage::Console: return wallet::WalletUiSurface::Console;
case NavPage::LiteConsole: return wallet::WalletUiSurface::LiteConsole;
case NavPage::Peers: return wallet::WalletUiSurface::Peers;
case NavPage::LiteNetwork: return wallet::WalletUiSurface::LiteNetwork;
case NavPage::Explorer: return wallet::WalletUiSurface::Explorer;
@@ -103,6 +106,7 @@ inline const char* GetNavIconMD(NavPage page)
case NavPage::Mining: return ICON_MD_CONSTRUCTION;
case NavPage::Market: return ICON_MD_TRENDING_UP;
case NavPage::Console: return ICON_MD_TERMINAL;
case NavPage::LiteConsole: return ICON_MD_TERMINAL;
case NavPage::Peers: return ICON_MD_HUB;
case NavPage::LiteNetwork: return ICON_MD_HUB;
case NavPage::Explorer: return ICON_MD_EXPLORE;
@@ -648,6 +652,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
// Click detection
bool pageNeedsUnlock = locked &&
item.page != NavPage::Console &&
item.page != NavPage::LiteConsole &&
item.page != NavPage::Peers &&
item.page != NavPage::Settings;
if (hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left) && !pageNeedsUnlock) {

View File

@@ -0,0 +1,98 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "lite_console_tab.h"
#include "../../app.h"
#include "../../util/i18n.h"
#include "../../wallet/lite_diagnostics.h"
#include "../layout.h"
#include "../material/type.h"
#include "../material/colors.h"
#include "imgui.h"
#include <cstdint>
#include <string>
#include <vector>
namespace dragonx {
namespace ui {
using namespace material;
namespace {
// Re-snapshot the (mutex-guarded) log only when it actually changes, not every frame.
std::vector<std::string> s_lines;
std::uint64_t s_cachedGeneration = static_cast<std::uint64_t>(-1);
bool s_autoScroll = true;
// Colour error/success lines for at-a-glance scanning (substring match on the messages the
// controller emits). Anything else renders in the muted default colour.
ImU32 lineColor(const std::string& line)
{
const auto has = [&line](const char* s) { return line.find(s) != std::string::npos; };
if (has("failed") || has(" unreachable") || has("blocked") || has("could not") ||
has("Error") || has("error"))
return Error();
if (has(": connected") || has("opened") || has("wallet ready") || has("Ready"))
return Success();
return OnSurfaceMedium();
}
} // namespace
void RenderLiteConsoleTab(App* app)
{
(void)app;
auto& diag = wallet::LiteDiagnostics::instance();
const std::uint64_t gen = diag.generation();
if (gen != s_cachedGeneration) {
s_lines = diag.snapshot();
s_cachedGeneration = gen;
}
// ── Header ──────────────────────────────────────────────────────────────────
Type().text(TypeStyle::H6, TR("lite_console_title"));
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("lite_console_intro"));
ImGui::Spacing();
// ── Toolbar ─────────────────────────────────────────────────────────────────
if (ImGui::Button(TR("lite_console_clear"))) diag.clear();
ImGui::SameLine();
if (ImGui::Button(TR("lite_console_copy"))) {
std::string all;
for (const auto& l : s_lines) { all += l; all.push_back('\n'); }
ImGui::SetClipboardText(all.c_str());
}
ImGui::SameLine();
ImGui::Checkbox(TR("lite_console_autoscroll"), &s_autoScroll);
ImGui::Spacing();
// ── Log (terminal-styled scroll region) ─────────────────────────────────────
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 90));
ImGui::BeginChild("##LiteConsoleLog", ImVec2(0, 0), true,
ImGuiWindowFlags_HorizontalScrollbar);
ImGui::PushFont(Type().caption());
if (s_lines.empty()) {
ImGui::PushStyleColor(ImGuiCol_Text, OnSurfaceDisabled());
ImGui::TextUnformatted(TR("lite_console_empty"));
ImGui::PopStyleColor();
} else {
for (const auto& line : s_lines) {
ImGui::PushStyleColor(ImGuiCol_Text, lineColor(line));
ImGui::TextUnformatted(line.c_str()); // not format-interpreted — safe for any content
ImGui::PopStyleColor();
}
}
// Keep pinned to the newest line only while the user is already at the bottom.
if (s_autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY())
ImGui::SetScrollHereY(1.0f);
ImGui::PopFont();
ImGui::EndChild();
ImGui::PopStyleColor();
}
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,18 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
//
// Lite-wallet "Console" tab: a read-only, terminal-styled view of the lite diagnostics log
// (server connection attempts, wallet open/create/restore, sync milestones, errors). Lite
// builds only. See src/ui/windows/lite_console_tab.cpp.
#pragma once
namespace dragonx {
class App;
namespace ui {
void RenderLiteConsoleTab(App* app);
} // namespace ui
} // namespace dragonx