feat(lite): add ObsidianDragonLite build mode and gate full-node features

Add --lite build flow and ObsidianDragonLite target naming, hide full-node pages/features in lite mode, enforce pool-only mining in lite, and include chat port feasibility audit documentation.
This commit is contained in:
2026-05-06 03:42:05 -05:00
parent 975743f754
commit e95ad50e41
10 changed files with 255 additions and 92 deletions

View File

@@ -130,6 +130,8 @@ public:
* @brief Whether we are in the shutdown phase
*/
bool isShuttingDown() const { return shutting_down_; }
bool isLiteBuild() const { return DRAGONX_LITE_BUILD != 0; }
bool supportsEmbeddedDaemon() const { return DRAGONX_ENABLE_EMBEDDED_DAEMON != 0; }
/**
* @brief Render the shutdown overlay (called instead of normal UI during shutdown)
@@ -439,7 +441,7 @@ private:
bool show_address_book_ = false;
// Embedded daemon state
bool use_embedded_daemon_ = true;
bool use_embedded_daemon_ = (DRAGONX_ENABLE_EMBEDDED_DAEMON != 0);
std::string daemon_status_;
mutable std::string daemon_mem_diag_; // diagnostic info for daemon memory detection
size_t daemon_output_offset_ = 0; // for incremental output parsing (rescan detection)

View File

@@ -60,6 +60,23 @@ using NetworkRefreshService = services::NetworkRefreshService;
namespace {
bool isPageEnabledForBuild(ui::NavPage page)
{
#if DRAGONX_LITE_BUILD
switch (page) {
case ui::NavPage::Console:
case ui::NavPage::Peers:
case ui::NavPage::Explorer:
return false;
default:
return true;
}
#else
(void)page;
return true;
#endif
}
std::string unencryptedTransactionHistoryCacheKey(const std::string& walletIdentity)
{
return std::string("obsidian-dragon-unencrypted-tx-cache-v1:") +
@@ -545,6 +562,9 @@ bool App::shouldRunWalletTransactionRefresh() const
void App::setCurrentPage(ui::NavPage page)
{
if (!isPageEnabledForBuild(page)) {
page = ui::NavPage::Overview;
}
if (page == current_page_) return;
current_page_ = page;
applyRefreshPolicy(page);
@@ -1372,6 +1392,11 @@ void App::refreshMarketData()
void App::startMining(int threads)
{
#if DRAGONX_LITE_BUILD
(void)threads;
ui::Notifications::instance().warning("Solo mining is unavailable in lite build");
return;
#endif
if (!state_.connected || !rpc_ || !worker_) return;
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress
@@ -1401,6 +1426,9 @@ void App::startMining(int threads)
void App::stopMining()
{
#if DRAGONX_LITE_BUILD
return;
#endif
if (!state_.connected || !rpc_ || !worker_) return;
if (mining_toggle_in_progress_.exchange(true)) return; // already in progress

View File

@@ -12,7 +12,7 @@
#define DRAGONX_VERSION_MINOR 2
#define DRAGONX_VERSION_PATCH 0
#define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_APP_NAME "ObsidianDragonLite"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings

View File

@@ -12,7 +12,7 @@
#define DRAGONX_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define DRAGONX_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define DRAGONX_APP_NAME "ObsidianDragon"
#define DRAGONX_APP_NAME "@DRAGONX_APP_NAME@"
#define DRAGONX_ORG_NAME "Hush"
// Default RPC settings

View File

@@ -1073,6 +1073,7 @@ void RenderSettingsPage(App* app) {
// Privacy, Network & Daemon checkboxes — all on one line, shrink text to fit
{
const bool showDaemonOptions = !app->isLiteBuild();
float cbSpacing = Layout::spacingMd();
float fh = ImGui::GetFrameHeight();
float inner = ImGui::GetStyle().ItemInnerSpacing.x;
@@ -1080,9 +1081,12 @@ void RenderSettingsPage(App* app) {
float totalW = cbW(TR("save_z_transactions")) + cbSpacing
+ cbW(TR("auto_shield")) + cbSpacing
+ cbW(TR("use_tor")) + cbSpacing
+ cbW(TR("keep_daemon")) + cbSpacing
+ cbW(TR("stop_external"));
+ cbW(TR("use_tor")) + cbSpacing;
if (showDaemonOptions) {
totalW += cbW(TR("keep_daemon")) + cbSpacing
+ cbW(TR("stop_external")) + cbSpacing;
}
totalW += cbW(TR("verbose_logging"));
float scale = (totalW > contentW) ? contentW / totalW : 1.0f;
if (scale < 1.0f) ImGui::SetWindowFontScale(scale);
float sp = cbSpacing * scale;
@@ -1095,18 +1099,20 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(0, sp);
ImGui::Checkbox(TrId("use_tor", "tor").c_str(), &s_settingsState.use_tor);
if (ImGui::IsItemHovered()) ImGui::SetTooltip("%s", TR("tt_tor"));
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) {
saveSettingsPageState(app->settings());
if (showDaemonOptions) {
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("keep_daemon", "keep_dmn").c_str(), &s_settingsState.keep_daemon_running)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_keep_daemon"));
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &s_settingsState.stop_external_daemon)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_stop_external"));
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_keep_daemon"));
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("stop_external", "stop_ext").c_str(), &s_settingsState.stop_external_daemon)) {
saveSettingsPageState(app->settings());
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_stop_external"));
ImGui::SameLine(0, sp);
if (ImGui::Checkbox(TrId("verbose_logging", "verbose").c_str(), &s_settingsState.verbose_logging)) {
dragonx::util::Logger::instance().setVerbose(s_settingsState.verbose_logging);
@@ -1469,8 +1475,8 @@ void RenderSettingsPage(App* app) {
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Node maintenance buttons
{
// Node maintenance buttons (full-node build only)
if (!app->isLiteBuild()) {
ImFont* btnFont = S.resolveFont("button");
float nodeBtnW;
{

View File

@@ -66,6 +66,16 @@ inline const char* NavSectionLabel(const NavItem& item) {
return item.section_tr_key ? TR(item.section_tr_key) : item.section_label;
}
inline bool IsNavPageVisible(NavPage page)
{
#if DRAGONX_LITE_BUILD
return page != NavPage::Console && page != NavPage::Peers && page != NavPage::Explorer;
#else
(void)page;
return true;
#endif
}
// Get the Material Design icon string for a navigation page.
inline const char* GetNavIconMD(NavPage page)
{
@@ -446,12 +456,13 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
// Separate fixed parts (don't scale) from flex parts (scale when tight).
float fixedH = stripH; // collapse strip
for (int i = 0; i < (int)NavPage::Count_; ++i)
if (kNavItems[i].section_label && showLabels)
if (IsNavPageVisible(kNavItems[i].page) && kNavItems[i].section_label && showLabels)
fixedH += olFsz + 2.0f + sectionLabelPadBot; // section label + pad below
fixedH += bottomPadding + stripH; // exit area
float baseFlexH = baseNavGap;
for (int i = 0; i < (int)NavPage::Count_; ++i) {
if (!IsNavPageVisible(kNavItems[i].page)) continue;
if (kNavItems[i].section_label) {
if (showLabels) baseFlexH += baseSectionGap;
else baseFlexH += baseSectionGap * 0.8f;
@@ -479,6 +490,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float curY = stripH + navGap; // after collapse strip + gap
for (int i = 0; i < (int)NavPage::Count_; ++i) {
if (!IsNavPageVisible(kNavItems[i].page)) continue;
if (kNavItems[i].section_label) {
if (showLabels) {
curY += sectionGap;
@@ -560,6 +572,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float maxSectionW = panelRight - wp.x - sbSectionLabelPadLeft;
int si = 0;
for (int i = 0; i < (int)NavPage::Count_; ++i) {
if (!IsNavPageVisible(kNavItems[i].page)) continue;
if (kNavItems[i].section_label && si < nSectionLabels) {
float ly = panelTopY + sectionLabelY[si++];
const char* sLabel = NavSectionLabel(kNavItems[i]);
@@ -587,6 +600,7 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
float btnPadX = collapsed ? btnPadCollapsed : btnPadExpanded;
for (int i = 0; i < (int)NavPage::Count_; ++i) {
const NavItem& item = kNavItems[i];
if (!IsNavPageVisible(item.page)) continue;
bool selected = (current == item.page);
float btnY = panelTopY + itemY[i];
float btnRnd = itemH * 0.22f;
@@ -695,10 +709,14 @@ inline bool RenderSidebar(NavPage& current, float sidebarWidth, float contentHei
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) {
int idx = static_cast<int>(current);
bool nav = false;
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K))
if (idx > 0) { idx--; nav = true; }
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J))
if (idx < (int)NavPage::Count_ - 1) { idx++; nav = true; }
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) {
do { idx--; } while (idx >= 0 && !IsNavPageVisible(static_cast<NavPage>(idx)));
if (idx >= 0) nav = true;
}
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) {
do { idx++; } while (idx < (int)NavPage::Count_ && !IsNavPageVisible(static_cast<NavPage>(idx)));
if (idx < (int)NavPage::Count_) nav = true;
}
if (ImGui::IsKeyPressed(ImGuiKey_LeftBracket))
collapsed = !collapsed;
if (nav) { current = static_cast<NavPage>(idx); changed = true; }

View File

@@ -151,6 +151,13 @@ static void RenderMiningTabContent(App* app)
strncpy(s_pool_worker, app->settings()->getPoolWorker().c_str(), sizeof(s_pool_worker) - 1);
s_pool_state_loaded = true;
}
#if DRAGONX_LITE_BUILD
if (!s_pool_mode) {
s_pool_mode = true;
app->settings()->setPoolMode(true);
app->settings()->save();
}
#endif
// Default pool worker to user's first shielded (z) address once available.
// For new wallets without a z-address, leave the field blank so the user
@@ -235,10 +242,11 @@ static void RenderMiningTabContent(App* app)
// MODE TOGGLE — SOLO | POOL segmented control
// ================================================================
{
const bool liteBuild = app->isLiteBuild();
float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs;
float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size;
float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size;
float totalW = toggleW * 2;
float totalW = liteBuild ? toggleW : (toggleW * 2);
ImVec2 tMin = ImGui::GetCursorScreenPos();
ImVec2 tMax(tMin.x + totalW, tMin.y + toggleH);
@@ -247,16 +255,16 @@ static void RenderMiningTabContent(App* app)
dl->AddRectFilled(tMin, tMax, WithAlpha(OnSurface(), 15), toggleRnd);
dl->AddRect(tMin, tMax, WithAlpha(OnSurface(), 40), toggleRnd);
// SOLO button (left half)
ImVec2 soloMin = tMin;
ImVec2 soloMax(tMin.x + toggleW, tMax.y);
bool soloHov = material::IsRectHovered(soloMin, soloMax);
if (!s_pool_mode) {
dl->AddRectFilled(soloMin, soloMax, WithAlpha(Primary(), 180), toggleRnd);
} else if (soloHov) {
dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd);
}
{
if (!liteBuild) {
// SOLO button (left half)
if (!s_pool_mode) {
dl->AddRectFilled(soloMin, soloMax, WithAlpha(Primary(), 180), toggleRnd);
} else if (soloHov) {
dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd);
}
const char* label = TR("mining_solo");
ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
float lx = soloMin.x + (toggleW - sz.x) * 0.5f;
@@ -267,7 +275,7 @@ static void RenderMiningTabContent(App* app)
// POOL button (right half) — disabled when solo mining is active
bool soloMiningActive = mining.generate;
ImVec2 poolMin(tMin.x + toggleW, tMin.y);
ImVec2 poolMin(tMin.x + (liteBuild ? 0.0f : toggleW), tMin.y);
ImVec2 poolMax = tMax;
bool poolHov = material::IsRectHovered(poolMin, poolMax);
if (s_pool_mode) {
@@ -288,19 +296,21 @@ static void RenderMiningTabContent(App* app)
}
// Invisible buttons for click targets
ImGui::SetCursorScreenPos(soloMin);
ImGui::InvisibleButton("##SoloMode", ImVec2(toggleW, toggleH));
if (ImGui::IsItemClicked() && s_pool_mode) {
s_pool_mode = false;
app->settings()->setPoolMode(false);
app->settings()->save();
app->stopPoolMining();
if (!liteBuild) {
ImGui::SetCursorScreenPos(soloMin);
ImGui::InvisibleButton("##SoloMode", ImVec2(toggleW, toggleH));
if (ImGui::IsItemClicked() && s_pool_mode) {
s_pool_mode = false;
app->settings()->setPoolMode(false);
app->settings()->save();
app->stopPoolMining();
}
if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetCursorScreenPos(poolMin);
ImGui::InvisibleButton("##PoolMode", ImVec2(toggleW, toggleH));
if (ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) {
if (!liteBuild && ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) {
s_pool_mode = true;
app->settings()->setPoolMode(true);
app->settings()->save();
@@ -308,7 +318,7 @@ static void RenderMiningTabContent(App* app)
// so no need to call stopMining() — it would just set the
// toggle-in-progress flag and make the button show "STARTING".
}
if (poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (!liteBuild && poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (poolHov && soloMiningActive && !s_pool_mode) {
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
}
@@ -1925,8 +1935,12 @@ static void RenderMiningTabContent(App* app)
&& tx.memo.find("Mining Pool payout") != std::string::npos);
if (isSoloMined || isPoolPayout) {
// Apply earnings filter
if (s_earnings_filter == 1 && !isSoloMined) continue;
if (s_earnings_filter == 2 && !isPoolPayout) continue;
if (app->isLiteBuild()) {
if (s_earnings_filter == 1 && !isPoolPayout) continue;
} else {
if (s_earnings_filter == 1 && !isSoloMined) continue;
if (s_earnings_filter == 2 && !isPoolPayout) continue;
}
double amt = std::abs(tx.amount);
minedAllTime += amt;
@@ -1970,8 +1984,10 @@ static void RenderMiningTabContent(App* app)
// === Earnings filter toggle button (top-right of card) ===
{
const char* filterLabels[] = { TR("mining_filter_all"), TR("mining_solo"), TR("mining_pool") };
const char* filterIcons[] = { ICON_MD_FUNCTIONS, ICON_MD_MEMORY, ICON_MD_CLOUD };
const bool liteBuild = app->isLiteBuild();
const char* filterLabels[] = { TR("mining_filter_all"), liteBuild ? TR("mining_pool") : TR("mining_solo"), TR("mining_pool") };
const char* filterIcons[] = { ICON_MD_FUNCTIONS, liteBuild ? ICON_MD_CLOUD : ICON_MD_MEMORY, ICON_MD_CLOUD };
const int filterCount = liteBuild ? 2 : 3;
const char* curIcon = filterIcons[s_earnings_filter];
const char* curLabel = filterLabels[s_earnings_filter];
@@ -2011,13 +2027,13 @@ static void RenderMiningTabContent(App* app)
ImGui::SetCursorScreenPos(bMin);
ImGui::InvisibleButton("##EarningsFilter", ImVec2(btnW, btnH));
if (ImGui::IsItemClicked()) {
s_earnings_filter = (s_earnings_filter + 1) % 3;
s_earnings_filter = (s_earnings_filter + 1) % filterCount;
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
const char* tips[] = {
TR("mining_filter_tip_all"),
TR("mining_filter_tip_solo"),
liteBuild ? TR("mining_filter_tip_pool") : TR("mining_filter_tip_solo"),
TR("mining_filter_tip_pool")
};
ImGui::SetTooltip("%s", tips[s_earnings_filter]);