feat(onboarding): friendlier wizard bootstrap + Done screen starts the daemon

First-run bootstrap polish:
- Failed card wraps the (often long) error instead of clipping it, and offers a
  "Try mirror" button; Retry now keeps whichever source (main/mirror) was used.
- Bootstrap offer shows an honest size/time hint and relabels "Mirror" →
  "Use mirror" with a clearer tooltip.
- Daemon status dot/label is keyed off the EmbeddedDaemon::State enum instead of
  matching the English substring "Stopping" (which broke in every other
  language), and the faint ~55%-alpha label is raised to ~86%.
- Bootstrap Done screen: for the embedded daemon it now offers "Start Daemon"
  (and says the node will start) instead of telling the user to start it by hand.

Also carries the new string-table keys for these (and the shielded-card tooltip
used by the next commit).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-10-18 18:36:55 -05:00
co-authored by Claude Opus 4.8
parent 2fdf33f99b
commit b3e09a3314
4 changed files with 78 additions and 32 deletions
+63 -28
View File
@@ -800,21 +800,22 @@ void App::renderFirstRunWizard() {
// Daemon status indicator
{
bool daemonUp = isEmbeddedDaemonRunning();
const std::string& dStatus = getDaemonStatus();
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200) // green
: IM_COL32(120, 120, 120, 160); // gray
if (dStatus.find("Stopping") != std::string::npos)
dotCol = IM_COL32(255, 167, 38, 200); // orange
// Key the dot + label off the daemon state ENUM, not an English substring of the status
// text — the substring match silently failed under every non-English language.
const bool daemonStopping = daemon_controller_ &&
daemon_controller_->state() == daemon::EmbeddedDaemon::State::Stopping;
ImU32 dotCol = daemonStopping ? IM_COL32(255, 167, 38, 230) // orange
: daemonUp ? IM_COL32(76, 175, 80, 230) // green
: IM_COL32(120, 120, 120, 180); // gray
float dotR = 3.5f * dp;
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
dotR, dotCol);
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
? TR("bootstrap_daemon_stopping")
: TR("bootstrap_daemon_running"))
: TR("bootstrap_daemon_stopped");
const char* label = daemonStopping ? TR("bootstrap_daemon_stopping")
: daemonUp ? TR("bootstrap_daemon_running")
: TR("bootstrap_daemon_stopped");
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,220), label);
cy += captionFont->LegacySize + 6.0f * dp;
}
@@ -862,14 +863,19 @@ void App::renderFirstRunWizard() {
ui::material::Error(), TR("wiz_download_failed"));
cy += bodyFont->LegacySize + 8.0f * dp;
// Wrap the (often long) error within the card instead of letting it clip off the edge.
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy), textCol,
errMsg.c_str());
cy += captionFont->LegacySize + 16.0f * dp;
errMsg.c_str(), nullptr, contentW);
ImVec2 es = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, errMsg.c_str());
cy += es.y + 16.0f * dp;
// Retry / Skip
// Retry / (Try mirror) / Skip. Offer the mirror unless the last attempt already used it.
const std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
const bool showMirror = (wizard_bootstrap_url_ != mirrorUrl);
float btnW2 = 120.0f * dp;
float btnH2 = 40.0f * dp;
float totalBW = btnW2 * 2 + 12.0f * dp;
const int nBtn = showMirror ? 3 : 2;
float totalBW = btnW2 * nBtn + 12.0f * dp * (nBtn - 1);
float bx = rightX + (colW - totalBW) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(bx, cy));
@@ -883,14 +889,33 @@ void App::renderFirstRunWizard() {
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
if (wizard_bootstrap_url_.empty()) bootstrap_->start(dataDir);
else bootstrap_->start(dataDir, wizard_bootstrap_url_); // keep the user's chosen source
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);
ImGui::SetCursorScreenPos(ImVec2(bx + btnW2 + 12.0f * dp, cy));
float nextBx = bx + btnW2 + 12.0f * dp;
if (showMirror) {
ImGui::SetCursorScreenPos(ImVec2(nextBx, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton(TR("bootstrap_try_mirror"), ImVec2(btnW2, btnH2)) && !refuseIfNodeBusy()) {
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
bootstrap_->start(util::Platform::getDragonXDataDir(), mirrorUrl);
wizard_bootstrap_url_ = mirrorUrl;
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) ui::material::Tooltip(TR("bootstrap_use_mirror_tooltip"));
ImGui::PopStyleVar();
nextBx += btnW2 + 12.0f * dp;
}
ImGui::SetCursorScreenPos(ImVec2(nextBx, cy));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
if (ui::material::TactileButton(TR("wiz_skip"), ImVec2(btnW2, btnH2))) {
wizard_phase_ = WizardPhase::EncryptOffer;
@@ -1014,6 +1039,15 @@ void App::renderFirstRunWizard() {
cy += bsSize.y + 8.0f * dp;
}
// Honest size/time hint (no number is known until the download HEADs the remote file).
{
const char* hintText = TR("wiz_bootstrap_size_hint");
ImVec2 hs = captionFont->CalcTextSizeA(captionFont->LegacySize, FLT_MAX, contentW, hintText);
dl->AddText(captionFont, captionFont->LegacySize, ImVec2(cx, cy),
(dimCol & 0x00FFFFFF) | IM_COL32(0, 0, 0, 200), hintText, nullptr, contentW);
cy += hs.y + 8.0f * dp;
}
// Trust warning
{
float warnOpacity = S.drawElement("screens.first-run", "trust-warning").opacity;
@@ -1031,21 +1065,20 @@ void App::renderFirstRunWizard() {
// Daemon status indicator (subtle, before buttons)
{
bool daemonUp = isEmbeddedDaemonRunning();
const std::string& dStatus = getDaemonStatus();
ImU32 dotCol = daemonUp ? IM_COL32(76, 175, 80, 200)
: IM_COL32(120, 120, 120, 160);
if (dStatus.find("Stopping") != std::string::npos)
dotCol = IM_COL32(255, 167, 38, 200);
const bool daemonStopping = daemon_controller_ &&
daemon_controller_->state() == daemon::EmbeddedDaemon::State::Stopping;
ImU32 dotCol = daemonStopping ? IM_COL32(255, 167, 38, 230)
: daemonUp ? IM_COL32(76, 175, 80, 230)
: IM_COL32(120, 120, 120, 180);
float dotR = 3.5f * dp;
dl->AddCircleFilled(ImVec2(cx + dotR, cy + captionFont->LegacySize * 0.5f),
dotR, dotCol);
const char* label = daemonUp ? (dStatus.find("Stopping") != std::string::npos
? TR("bootstrap_daemon_stopping")
: TR("bootstrap_daemon_running"))
: TR("bootstrap_daemon_stopped");
const char* label = daemonStopping ? TR("bootstrap_daemon_stopping")
: daemonUp ? TR("bootstrap_daemon_running")
: TR("bootstrap_daemon_stopped");
dl->AddText(captionFont, captionFont->LegacySize,
ImVec2(cx + dotR * 2.0f + 6.0f * dp, cy),
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,140), label);
(dimCol & 0x00FFFFFF) | IM_COL32(0,0,0,220), label);
cy += captionFont->LegacySize + 10.0f * dp;
}
@@ -1071,6 +1104,7 @@ void App::renderFirstRunWizard() {
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
bootstrap_->start(dataDir);
wizard_bootstrap_url_.clear(); // default source; Retry won't force the mirror
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
@@ -1084,17 +1118,18 @@ void App::renderFirstRunWizard() {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(ui::material::OnSurface()));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.0f * dp);
ImGui::BeginDisabled(!supportsFullNodeLifecycleActions());
if (ui::material::TactileButton(TR("bootstrap_mirror"), ImVec2(mirrorW, btnH2)) && !refuseIfNodeBusy()) {
if (ui::material::TactileButton(TR("bootstrap_use_mirror"), ImVec2(mirrorW, btnH2)) && !refuseIfNodeBusy()) {
stopDaemonForBootstrap();
bootstrap_ = std::make_unique<util::Bootstrap>();
std::string dataDir = util::Platform::getDragonXDataDir();
std::string mirrorUrl = std::string(util::Bootstrap::kMirrorUrl) + "/" + util::Bootstrap::kZipName;
bootstrap_->start(dataDir, mirrorUrl);
wizard_bootstrap_url_ = mirrorUrl; // Retry on the failed card keeps this source
wizard_phase_ = WizardPhase::BootstrapInProgress;
}
ImGui::EndDisabled();
if (ImGui::IsItemHovered()) {
ui::material::Tooltip(TR("bootstrap_mirror_tooltip"));
ui::material::Tooltip(TR("bootstrap_use_mirror_tooltip"));
}
ImGui::PopStyleVar();
ImGui::PopStyleColor(3);