Refactor app services and stabilize refresh/UI flows

- Add refresh scheduler and network refresh service boundaries for typed
  refresh results, ordered RPC collectors, applicators, and price parsing.
- Add daemon lifecycle and wallet security workflow helpers while preserving
  App-owned command RPC, decrypt, cancellation, and UI handoff behavior.
- Split balance, console, mining, amount formatting, and async task logic into
  focused modules with expanded Phase 4 test coverage.
- Fix market price loading by triggering price refresh immediately, avoiding
  queue-pressure drops, tracking loading/error state, and adding translations.
- Polish send, explorer, peers, settings, theme/schema, and related tab UI.
- Replace checked-in generated language headers with build-generated resources.
- Document the cleanup audit, UI static-state guidance, and architecture updates.
This commit is contained in:
dan_s
2026-04-29 12:47:57 -05:00
parent 9e1b1397ad
commit d684db446e
95 changed files with 8776 additions and 37563 deletions

View File

@@ -0,0 +1,20 @@
#include "amount_format.h"
#include <algorithm>
#include <iomanip>
#include <locale>
#include <sstream>
namespace dragonx {
namespace util {
std::string formatAmountFixed(double amount, int decimals)
{
std::ostringstream out;
out.imbue(std::locale::classic());
out << std::fixed << std::setprecision(std::max(0, decimals)) << amount;
return out.str();
}
} // namespace util
} // namespace dragonx

11
src/util/amount_format.h Normal file
View File

@@ -0,0 +1,11 @@
#pragma once
#include <string>
namespace dragonx {
namespace util {
std::string formatAmountFixed(double amount, int decimals = 8);
} // namespace util
} // namespace dragonx

View File

@@ -0,0 +1,118 @@
#include "async_task_manager.h"
#include "logger.h"
#include <exception>
#include <utility>
namespace dragonx {
namespace util {
AsyncTaskManager::~AsyncTaskManager()
{
cancelAll();
joinAll();
}
void AsyncTaskManager::submit(std::string name, Task task)
{
if (!task) return;
reapCompleted();
join(name);
auto cancelled = std::make_shared<std::atomic<bool>>(false);
auto done = std::make_shared<std::atomic<bool>>(false);
Token token(cancelled);
std::string taskName = name;
std::thread worker([taskName, token, done, task = std::move(task)]() mutable {
try {
task(token);
} catch (const std::exception& e) {
DEBUG_LOGF("[AsyncTask:%s] failed: %s\n", taskName.c_str(), e.what());
} catch (...) {
DEBUG_LOGF("[AsyncTask:%s] failed with unknown exception\n", taskName.c_str());
}
done->store(true, std::memory_order_release);
});
std::lock_guard<std::mutex> lock(mutex_);
tasks_.push_back({std::move(name), std::move(cancelled), std::move(done), std::move(worker)});
}
void AsyncTaskManager::cancelAll()
{
std::lock_guard<std::mutex> lock(mutex_);
for (auto& task : tasks_) {
task.cancelled->store(true, std::memory_order_relaxed);
}
}
void AsyncTaskManager::join(const std::string& name)
{
std::vector<TaskEntry> toJoin;
{
std::lock_guard<std::mutex> lock(mutex_);
auto it = tasks_.begin();
while (it != tasks_.end()) {
if (it->name == name) {
toJoin.push_back(std::move(*it));
it = tasks_.erase(it);
} else {
++it;
}
}
}
for (auto& task : toJoin) {
if (task.worker.joinable()) task.worker.join();
}
}
void AsyncTaskManager::joinAll()
{
std::vector<TaskEntry> toJoin;
{
std::lock_guard<std::mutex> lock(mutex_);
toJoin.swap(tasks_);
}
for (auto& task : toJoin) {
if (task.worker.joinable()) task.worker.join();
}
}
void AsyncTaskManager::reapCompleted()
{
std::vector<TaskEntry> toJoin;
{
std::lock_guard<std::mutex> lock(mutex_);
auto it = tasks_.begin();
while (it != tasks_.end()) {
if (it->done->load(std::memory_order_acquire)) {
toJoin.push_back(std::move(*it));
it = tasks_.erase(it);
} else {
++it;
}
}
}
for (auto& task : toJoin) {
if (task.worker.joinable()) task.worker.join();
}
}
bool AsyncTaskManager::isRunning(const std::string& name) const
{
std::lock_guard<std::mutex> lock(mutex_);
for (const auto& task : tasks_) {
if (task.name == name && !task.done->load(std::memory_order_acquire)) {
return true;
}
}
return false;
}
} // namespace util
} // namespace dragonx

View File

@@ -0,0 +1,58 @@
#pragma once
#include <atomic>
#include <functional>
#include <memory>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
namespace dragonx {
namespace util {
class AsyncTaskManager {
public:
class Token {
public:
Token() = default;
explicit Token(std::shared_ptr<std::atomic<bool>> cancelled)
: cancelled_(std::move(cancelled)) {}
bool cancelled() const {
return cancelled_ && cancelled_->load(std::memory_order_relaxed);
}
private:
std::shared_ptr<std::atomic<bool>> cancelled_;
};
using Task = std::function<void(const Token&)>;
AsyncTaskManager() = default;
~AsyncTaskManager();
AsyncTaskManager(const AsyncTaskManager&) = delete;
AsyncTaskManager& operator=(const AsyncTaskManager&) = delete;
void submit(std::string name, Task task);
void cancelAll();
void join(const std::string& name);
void joinAll();
void reapCompleted();
bool isRunning(const std::string& name) const;
private:
struct TaskEntry {
std::string name;
std::shared_ptr<std::atomic<bool>> cancelled;
std::shared_ptr<std::atomic<bool>> done;
std::thread worker;
};
mutable std::mutex mutex_;
std::vector<TaskEntry> tasks_;
};
} // namespace util
} // namespace dragonx

View File

@@ -68,6 +68,7 @@ Bootstrap::~Bootstrap() {
void Bootstrap::start(const std::string& dataDir, const std::string& url) {
if (worker_running_) return; // already running
if (worker_.joinable()) worker_.join();
cancel_requested_ = false;
worker_running_ = true;
@@ -175,7 +176,6 @@ void Bootstrap::start(const std::string& dataDir, const std::string& url) {
setProgress(State::Completed, "Bootstrap complete!");
worker_running_ = false;
});
worker_.detach();
}
void Bootstrap::cancel() {

View File

@@ -917,6 +917,7 @@ void I18n::loadBuiltinEnglish()
strings_["market_now"] = "Now";
strings_["market_pct_shielded"] = "%.0f%% Shielded";
strings_["market_portfolio"] = "PORTFOLIO";
strings_["market_price_loading"] = "Loading price data...";
strings_["market_price_unavailable"] = "Price data unavailable";
strings_["market_refresh_price"] = "Refresh price data";
strings_["market_trade_on"] = "Trade on %s";

View File

@@ -6,6 +6,8 @@
#include <cstdlib>
#include <cstdio>
#include <cerrno>
#include <cctype>
#include <cstring>
#include <fstream>
#include <sstream>
@@ -31,6 +33,8 @@
#include <pwd.h>
#include <dirent.h>
#include <dlfcn.h>
#include <spawn.h>
#include <sys/wait.h>
#ifdef __APPLE__
#include <mach-o/dyld.h>
#include <sys/sysctl.h>
@@ -40,25 +44,83 @@
#include "../util/logger.h"
#ifndef _WIN32
extern char **environ;
#endif
namespace dragonx {
namespace util {
namespace {
bool hasAllowedUrlScheme(const std::string& url)
{
auto startsWithNoCase = [&url](const char* prefix) {
for (size_t i = 0; prefix[i] != '\0'; ++i) {
if (i >= url.size()) return false;
unsigned char lhs = static_cast<unsigned char>(url[i]);
unsigned char rhs = static_cast<unsigned char>(prefix[i]);
if (std::tolower(lhs) != std::tolower(rhs)) return false;
}
return true;
};
return startsWithNoCase("http://") || startsWithNoCase("https://");
}
#ifndef _WIN32
bool launchOpener(const char* opener, const std::string& target)
{
char* const argv[] = {
const_cast<char*>(opener),
const_cast<char*>(target.c_str()),
nullptr
};
pid_t pid = 0;
int rc = posix_spawnp(&pid, opener, nullptr, nullptr, argv, environ);
if (rc != 0) {
DEBUG_LOGF("Failed to launch %s: %s\n", opener, std::strerror(rc));
return false;
}
int status = 0;
pid_t waited = 0;
do {
waited = waitpid(pid, &status, 0);
} while (waited < 0 && errno == EINTR);
if (waited < 0) {
DEBUG_LOGF("Failed waiting for %s launcher: %s\n", opener, std::strerror(errno));
return false;
}
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
DEBUG_LOGF("Launcher %s exited with status %d\n", opener,
WIFEXITED(status) ? WEXITSTATUS(status) : -1);
return false;
}
return true;
}
#endif
} // namespace
bool Platform::openUrl(const std::string& url)
{
if (url.empty()) return false;
if (!hasAllowedUrlScheme(url)) {
DEBUG_LOGF("Refusing to open URL with unsupported scheme: %s\n", url.c_str());
return false;
}
#ifdef _WIN32
// Windows: Use ShellExecute
HINSTANCE result = ShellExecuteA(nullptr, "open", url.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
return (reinterpret_cast<intptr_t>(result) > 32);
#elif defined(__APPLE__)
// macOS: Use 'open' command
std::string cmd = "open \"" + url + "\" &";
return (system(cmd.c_str()) == 0);
return launchOpener("open", url);
#else
// Linux: Use xdg-open
std::string cmd = "xdg-open \"" + url + "\" >/dev/null 2>&1 &";
return (system(cmd.c_str()) == 0);
return launchOpener("xdg-open", url);
#endif
}
@@ -68,15 +130,12 @@ bool Platform::openFolder(const std::string& path, bool createIfMissing)
// Create directory if it doesn't exist
if (createIfMissing) {
#ifdef _WIN32
// Windows: Create directory recursively
std::string cmd = "mkdir \"" + path + "\" 2>nul";
(void)system(cmd.c_str()); // Ignore return value - dir may already exist
#else
// Linux/macOS: Create directory with parents
std::string cmd = "mkdir -p \"" + path + "\" 2>/dev/null";
(void)system(cmd.c_str()); // Ignore return value - dir may already exist
#endif
std::error_code ec;
std::filesystem::create_directories(path, ec);
if (ec) {
DEBUG_LOGF("Failed to create folder %s: %s\n", path.c_str(), ec.message().c_str());
return false;
}
}
#ifdef _WIN32
@@ -84,13 +143,9 @@ bool Platform::openFolder(const std::string& path, bool createIfMissing)
HINSTANCE result = ShellExecuteA(nullptr, "explore", path.c_str(), nullptr, nullptr, SW_SHOWNORMAL);
return (reinterpret_cast<intptr_t>(result) > 32);
#elif defined(__APPLE__)
// macOS: Use 'open' command (works for folders too)
std::string cmd = "open \"" + path + "\" &";
return (system(cmd.c_str()) == 0);
return launchOpener("open", path);
#else
// Linux: Use xdg-open (works for folders too)
std::string cmd = "xdg-open \"" + path + "\" >/dev/null 2>&1 &";
return (system(cmd.c_str()) == 0);
return launchOpener("xdg-open", path);
#endif
}