Fixes found while diagnosing a Windows full-node wallet that appeared stuck "downloading blocks" on a large (~5.3k-tx) fully-shielded wallet. Root cause was not the network — the wallet was starving the daemon's single block-connect thread. Five commits:
8707934 sync starvation — the Overview balance poll (z_gettotalbalance, O(mapWallet) under cs_main, ~20s on this wallet) fired every ~2s and held cs_main, so ActivateBestChain/ConnectBlock only ran in the gaps and the node fell further behind than it caught up. Fix: adaptive balance cadence (next poll waits >= scan-cost / 10%) + sync-settle hysteresis so it can't re-starve near tip; wallet mutations still force an immediate balance refresh.
29274c2 startup/shutdown UX — trim the verbose "taking longer than expected" notice; the shutdown screen now tails debug.log (windowless) for external daemons so the node flush/exit is visible.
7e8b99a shutdown safety — confirm before stopping the daemon mid Sapling-witness-rebuild (stopping discards it -> multi-minute redo next launch): Keep-running / Stop-anyway / Cancel.
a2f84be console-flash on launch — spawn dragonxd with CREATE_NO_WINDOW instead of CREATE_NEW_CONSOLE.
0942691 windowless shell-outs — route the remaining GPU/xmrig/wallet-rebuild _popen calls through a CreateProcess+CREATE_NO_WINDOW helper (Platform::runHiddenCapture) so they never flash cmd.exe.
Testing
Linux build + test_phase4 green on every commit; Windows cross-compile (--win-release) verified per commit.
Root cause live-verified on a running mainnet node: z_gettotalbalance/z_listunspent ~20s each holding cs_main; one core pegged on GetFilteredNotes; block-connect thread in Wait; download idle (~17 B/s) with all peers at max in-flight.
Out of scope (separate, unvalidated)
A daemon-side fix to persist witness-rebuild progress across an interrupted rescan lives in the external/dragonx repo on branch fix/witness-rebuild-checkpoint. It is not compiled or tested (fund-adjacent) and must pass a node build + regtest/testnet/mainnet validation + adversarial review before shipping — intentionally excluded from this PR.
## Summary
Fixes found while diagnosing a Windows full-node wallet that appeared stuck "downloading blocks" on a large (~5.3k-tx) fully-shielded wallet. Root cause was **not** the network — the wallet was starving the daemon's single block-connect thread. Five commits:
1. **`8707934` sync starvation** — the Overview balance poll (`z_gettotalbalance`, O(mapWallet) under `cs_main`, ~20s on this wallet) fired every ~2s and held `cs_main`, so `ActivateBestChain`/`ConnectBlock` only ran in the gaps and the node fell further behind than it caught up. Fix: adaptive balance cadence (next poll waits >= scan-cost / 10%) + sync-settle hysteresis so it can't re-starve near tip; wallet mutations still force an immediate balance refresh.
2. **`29274c2` startup/shutdown UX** — trim the verbose "taking longer than expected" notice; the shutdown screen now tails `debug.log` (windowless) for external daemons so the node flush/exit is visible.
3. **`7e8b99a` shutdown safety** — confirm before stopping the daemon mid Sapling-witness-rebuild (stopping discards it -> multi-minute redo next launch): Keep-running / Stop-anyway / Cancel.
4. **`a2f84be` console-flash on launch** — spawn `dragonxd` with `CREATE_NO_WINDOW` instead of `CREATE_NEW_CONSOLE`.
5. **`0942691` windowless shell-outs** — route the remaining GPU/xmrig/wallet-rebuild `_popen` calls through a `CreateProcess`+`CREATE_NO_WINDOW` helper (`Platform::runHiddenCapture`) so they never flash `cmd.exe`.
## Testing
- Linux build + `test_phase4` green on every commit; Windows cross-compile (`--win-release`) verified per commit.
- Root cause live-verified on a running mainnet node: `z_gettotalbalance`/`z_listunspent` ~20s each holding `cs_main`; one core pegged on `GetFilteredNotes`; block-connect thread in Wait; download idle (~17 B/s) with all peers at max in-flight.
## Out of scope (separate, unvalidated)
A daemon-side fix to persist witness-rebuild progress across an interrupted rescan lives in the `external/dragonx` repo on branch `fix/witness-rebuild-checkpoint`. It is **not compiled or tested** (fund-adjacent) and must pass a node build + regtest/testnet/mainnet validation + adversarial review before shipping — intentionally excluded from this PR.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Send and Receive are now consistent in layout, and the Send recipient row's buttons
render correctly.
Card envelope (Send ⇄ Receive consistency):
- Add Layout::mainComposeCardBox(availW) — a single shared source for the compose card's
width + centering (fill the available column up to content-max-width, then center). Both
tabs derive their card from it, so they can't drift again. Previously Send capped at
760dp and Receive at 860dp, so the Send card rendered ~150px narrower on any window wider
than ~860dp; now they fill available width identically.
Receive:
- Justify the footer buttons edge-to-edge (equal shares over the live count) instead of
left-clustering with dead space, matching Send's full-width footer rhythm.
- Build the address-dropdown preview to the combo's real pixel width so the trailing
balance ("— 12.00000000 DRGX") no longer hard-clips at 150% (was char-count truncation).
Send recipient row (input | Paste | contacts-icon):
- Pin the contacts icon button to the frame height so the larger iconMed font doesn't
auto-size it taller than Paste/the input.
- Reserve the real ItemSpacing.x gaps (not the smaller spacingSm token) so the row no
longer overshoots the card and clips the icon's right border.
draw_helpers (root cause, app-wide):
- TactileButton's icon path measured/drew the label INCLUDING the "##id" suffix (which
CalcTextSizeA/AddText don't strip the way ImGui's text render does), shoving the glyph
off-center-left. Strip at "##" before measuring/drawing. Corrects any icon button that
passes an explicit size and a "##id" label; no-op for labels without "##".
Verified via headless sweeps at 1.0x and 1.5x, plus a real 3800px-wide render (both cards
byte-identical at L=1174/R=2773). ctest 1/1.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Settings tabs brought closer to the approved mockup:
- ActionButton/renderCardButton retune (settings-scoped): 7px radius, 9px
padX, Primary → accent-outline chip, Secondary/card buttons more defined.
- Daemon-binary card: compact status right-aligned on the DAEMON BINARY
heading (Up to date / Version differs / Not installed), filled/rounded
status box, neutral danger divider (was alarming red), roomier spacing.
- RPC Connection: two-row column-aligned layout (Host | Port, then
Username | Password) so the password no longer clips off the card edge.
- Chat settings tab: live conversation preview below the Appearance /
Messaging cards; "Focus input on open" checkbox reflowed onto the console
color-toggle row.
- Debug Options: "Current theme only" toggle restricts either screenshot
sweep to the active theme instead of cycling every skin.
- Tabs fill the full content width (content-max-width cap disabled) and the
sidebar nav panel centers within the true visible area.
- i18n: new keys for the above (untranslated keys fall back to English).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ensureLogoTexture() rasterized the embedded DragonX SVG into logo_tex_ and
returned early (added in 1752500 "themed DragonX logo"), so the app/product
branding — the top-left header (app.cpp AddImage) and the About tab
(getLogoTexture) — showed the DragonX coin mark instead of the ObsidianDragon
logo. Drop that step so logo_tex_ resolves via the intended path: active-skin
override → ui.toml header-icon → bundled ObsidianDragon dark/light PNG (disk,
then embedded RESOURCE_LOGO). The DragonX SVG stays for coin_logo_tex_ (balance
card) and drgx_emoji_tex_ (chat emoji), which are the currency mark and correct.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add "The DragonX Developers" to the About-tab credits (after The Hush
Developers), acknowledging the DragonX chain/daemon this wallet drives.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The BDB wallet.dat bloats with shielded-note witness data and never shrinks
in place, so a mining/shielded wallet can grow past 500 MB. Below the Wallet
Size row, show a one-line amber hint once wallet.dat crosses 500 MB with a
"Consolidate notes…" shortcut that opens the Merge to Address (z_mergetoaddress)
dialog. Full-node only (lite has no wallet.dat here); threshold is a single
named constant. i18n keys fall back to English for non-English locales.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the wallet-bloat warning beyond the Settings banner: when wallet.dat
first crosses 500 MB (full-node, synced), fire a one-time warning toast plus a
clickable "Consolidate notes…" entry in the bell/alerts panel that opens Merge
to Address. The persisted large_wallet_warned flag keeps it once-only and
re-arms if the file later shrinks back under the threshold.
- AlertRecord gains an optional onClick + actionHint; Notifications::action()
pushes a toast and a clickable history entry. renderAlertHistoryPanel() now
renders the accent action link (under the message) and measures true content
height so wrapped messages + the link aren't clipped.
- App::maybeWarnLargeWallet() (mirrors maybeRemindSeedBackup) runs once per
launch from update(); reuses the existing wallet_size_warn/consolidate strings.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make Merge to Address actually serve wallet-bloat consolidation and be far less
opaque. New ShieldDialog::showConsolidate() preset (used by the large-wallet
Settings banner + alert action) frames it as "Consolidate funds" and targets
shielded notes — the bloat the nudge warns about.
- Source selector: consolidate shielded notes (ANY_SAPLING), transparent
(ANY_TADDR), or both (*) — previously hardcoded to ANY_TADDR, which never
reduced the shielded-witness bloat. Batch limit now applies to the right side.
- Scope: on open, count spendable UTXOs + notes (listunspent / z_listunspent)
and show "N transparent + M shielded · ~X DRGX"; warn "repeat to finish" when
the set exceeds one batch.
- Destination auto-selects the best spendable z-address (button enabled by
default); empty wallets get an inline "Create shielded address" (z_getnewaddress).
- Advanced disclosure hides Fee + "Max inputs per batch" (renamed from the "UTXO
Limit" jargon) with sane defaults.
- Inline confirm step before the fund-moving call (amount + input count + dest).
- Live progress: self-polls z_getoperationstatus to show Consolidating… →
Done/Failed, replacing the raw opid + manual "Check status" button.
All three merge entry points now use the typed showMerge()/showConsolidate()
(no stale-static leaks from direct show(MergeToAddress)). Shield-coinbase mode
keeps working. New i18n keys fall back to English.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On a fully-shielded (ac_private=1) chain, z_gettotalbalance is O(mapWallet) and
holds the daemon's cs_main for its whole duration — ~20s on a ~5k-tx wallet. The
Overview refresh polled it every ~2s (twice: minconf 0 and 1), so cs_main was
held almost continuously, starving the single block-connection thread: the node
connected blocks only in the gaps between polls and could fall further behind
the tip than it caught up (observed live: gap growing 58→100 blocks while the
GUI was open, one core pegged on GetFilteredNotes, 22 idle, ~17 B/s download).
Two hardening changes on top of the existing "skip balance while syncing" guard:
- Hysteresis: keep the low-impact sync profile (and balance suppression) for a
short settle window after catching up, so a large-wallet scan can't
immediately re-starve connection and bounce the node back into syncing. Armed
only on the syncing→caught-up edge, so a wallet synced from the start is never
throttled at connect (effectivelySyncing()).
- Adaptive balance cadence: time each z_gettotalbalance scan and require the
next poll to wait at least (cost / 10%), so balance scanning never occupies
more than ~10% of wall-clock. Cheap wallets are unaffected (the tab's Core
timer stays the cadence); a ~20s scan backs off to ~200s. Wallet mutations
(send/shield) force the next poll through so the user's own action updates the
balance immediately (balanceRefreshDue()).
getblockchaininfo keeps its normal cadence throughout, so sync progress stays
live. Build + test_phase4 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Loading "taking longer than expected" notice: shorten the body + hint so the
startup screen reads less wordy (same info, ~half the text).
- Shutdown screen: when the wallet attached to an EXTERNAL daemon (no captured
stdout — debug_log_path_ is only set when we spawn it), the "dragonxd output"
panel was always empty, leaving just a spinner. Fall back to tailing the
daemon's debug.log so the user can watch the node flush the block index and
exit. Adds App::tailDaemonDebugLog() (best-effort, reads only the file tail).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stopping dragonxd while it's rebuilding the Sapling witness cache discards the
in-progress work — BuildWitnessCache aborts on shutdown without persisting — so
the next launch redoes a multi-minute rebuild (the "Activating best chain…" hang).
This bites especially with stop_external_daemon enabled, where wallet exit sends
the node a stop.
beginShutdown() now defers when it would StopDaemon while a rebuild is active and
shows a confirm modal: "Keep node running & quit" (DisconnectOnly — leaves it up
to finish), "Stop anyway & quit", or "Cancel". Rebuild detection reads the
debug.log tail markers (Cleared witness data / Setting Initial Sapling Witness /
Reading blocks for witness rebuild, vs. the "rebuilt … in …ms" / abort lines).
The gate lives entirely in beginShutdown()/render() — no SDL event-loop changes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The embedded daemon was launched with CREATE_NEW_CONSOLE + SW_HIDE. CREATE_NEW_CONSOLE
allocates a console window that flashes on screen before SW_HIDE hides it — visible as a
console-window flash every time the wallet starts dragonxd (i.e. on launch). Switch to
CREATE_NO_WINDOW (the console child gets no window at all, matching the xmrig launcher);
dragonxd logs to debug.log, not a console, so nothing is lost.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_popen/_popen-style shell-outs flash a cmd.exe console window on Windows. Add
Platform::runHiddenCapture() — CreateProcess + CREATE_NO_WINDOW capturing stdout on
Windows, popen on POSIX — and route the remaining shell-outs through it:
- GPU-aware idle detection (getGpuUtilization: "where nvidia-smi" / "nvidia-smi --query-gpu")
- xmrig discovery + version (findXmrigBinary "where xmrig.exe"; "<bin> --version", stderr merged)
- wallet-rebuild helper (app_network) — keeps its exit-code check via the new exitCode out-param
None of these are on the launch path (that was the daemon spawn, fixed in a2f84be); each
would flash a console only when it ran (idle-GPU mining, mining tab, wallet recovery).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.3.0 daemons auto-shield coinbase by default (when the HD seed is recoverable — which
every ObsidianDragon-created wallet is, via -usemnemonic=1). The wallet also ran its own
client-side auto-shield every refresh tick, so both raced for the same coinbase UTXOs and
split funds across different z-addresses (the wallet picks the first z_listaddresses entry;
the daemon uses a seed-hardened derivation).
Probe z_autoshieldstatus once per connection (while synced, so the daemon is past warmup)
and skip the wallet's client-side shield when the daemon reports it active. Fail-closed: a
pre-1.3.0 daemon has no such RPC, so the probe returns active=false and the wallet keeps
shielding — no regression on the currently-bundled v1.0.3. Re-probes on reconnect (handles
a live daemon upgrade/swap).
Follow-up (not done): drive the Settings "Auto-shield" toggle + disabled_reason from
z_autoshieldstatus (O1) — needs runtime verification of the RPC fields on a live v1.3.0 node.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the wallet's stale hardcoded -addnode list (node. + node1-4.dragonx.is — a drifted
subset with a bogus bare 'node.') with the daemon's authoritative vSeeds: seed.dragonx.is (a
round-robin A record over the live seed set, so it self-updates without a wallet release) plus
node1/node5 as static fallbacks. Applied in both seeding sites: the launch args
(embedded_daemon.cpp) and the generated DRAGONX.conf (connection.cpp).
Kept (not deleted) — pre-1.3.0 daemons had broken peer discovery and rely on these -addnode
entries to find peers at all; plain hostname resolution works on every daemon version.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rec3 — v1.3.0+ opens a wallet that lost its hdchain in DEGRADED mode (existing funds spendable,
but no new HD-key derivation) instead of aborting. Add a daemon-log classifier
(walletOpenedDegraded) + detectWalletDegraded(), warned once per session. Pre-1.3.0 daemons
never emit that line, so it's a no-op there.
O1 — probe z_autoshieldstatus once per connection (now decoupled from our own toggle/balance) and,
in Settings, show whether the node handles auto-shield itself (+ its destination, or the daemon's
disabled_reason). The checkbox now governs only the wallet's fallback shielder, which defers to the
node. Nothing renders on pre-1.3.0 daemons (no such RPC), so behaviour is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an opt-in "Host a mining pool (stratum)" toggle so a v1.3.0+ node can run its native
RandomX stratum server for other miners to point at. Settings toggle + optional allow-IP/CIDR;
passes -stratum (+ -stratumallowip) to the daemon launch args, mirroring the -maxconnections
plumbing (EmbeddedDaemon::setStratumHosting <- DaemonController::syncSettings <- Settings).
Backwards compatible / safe by default:
- UI gated on daemon_version >= 1030000, so it's never offered where it would do nothing.
- The launch flag is harmless on older daemons (they ignore unknown args), and the toggle can
only be enabled while connected to a v1.3.0+ node anyway.
- Blank allow-IP => the daemon serves loopback only (its safe default); entering a subnet opens
it to that LAN, with an explicit exposure warning in the UI.
- Takes effect on the next daemon start/restart.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
It added clutter to the loading screen (the yellow title + two lines of explanatory text).
The live daemon-output panel below it is the real progress signal. connect_stall_since_ stays
maintained in app_network.cpp for connection bookkeeping; it just no longer drives any on-screen
text (loading_stall_* i18n strings + util::connectHasStalled are now unused).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
While minimized the main loop skips the frame body (SDL_Delay+continue), so App::update() —
which drains the daemon output via outputSince(daemon_output_offset_) — never runs and the
offset isn't advanced. On restore the whole accumulated backlog is parsed in one batch: a
background witness rebuild's progress lines (parsed as a rescan → state_.sync.rescanning=true)
AND its "rebuilt … in Xms" completion (parsed as finished) arrive together and fire
"Blockchain rescan complete" for a scan the user never initiated.
On WINDOW_RESTORED, discard the daemon-output backlog (advance the offset to the current end,
via App::skipDaemonOutputBacklog) before the resumed update parses it — so only new output is
parsed. Genuine user-initiated rescans still surface completion via the getrescaninfo monitor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously the main loop just did SDL_Delay+continue when minimized, skipping app.update() —
so the wallet stopped draining RPC results, ticking the refresh scheduler, and reconnecting
until it was restored (and a large daemon-output backlog piled up, which is what produced the
spurious "rescan complete" toast on restore).
Now app.update() runs while minimized (it only reads GetIO/GetTime/IsAnyItemActive, all valid
outside a NewFrame) with a real-clock DeltaTime, skipping only the ImGui frame + GPU present,
throttled to ~5 Hz so CPU stays near-idle. Also clamp io.DeltaTime at the top of App::update()
so a long minimize (or machine sleep) can't report a huge delta and fire every refresh/animation
timer at once on the next update. No backlog now builds, so skipDaemonOutputBacklog becomes a
harmless no-op.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The daemon-output parser treats autonomous background witness rebuilds as a rescan (they set
state_.sync.rescanning via the foundWitness branch), so one completing fired "Blockchain rescan
complete" even though the user never started a rescan — most visibly after a minimize, where a
whole rebuild's start+finish arrives in one batch.
Add user_initiated_rescan_ (atomic — some triggers run on worker threads), set it at the wallet's
real rescan triggers (the Rescan button, a -rescan/salvage/zap/reindex restart, key import, seed
migration), and gate the three "rescan complete" toasts on it, clearing it when shown. The
rescan/witness progress state machine is untouched — only the toast is gated — so background
rebuilds no longer announce a completed rescan while genuine user rescans still do (and can't
double-toast).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shutdown guard fired its "Node is rebuilding its witness cache" prompt almost always on an
active wallet: the daemon does a per-tx VerifyAndSetInitialWitness as each newly received wallet
tx lands during normal sync, and those markers (sparse — minutes apart — with no matching
"rebuilt N note witness cache(s) … in Xms") kept the heuristic latched "active".
- daemonWitnessRebuildActive() now requires the last progress marker to be part of the CURRENT
log activity (within ~15s of the newest log line, via a same-log timestamp delta), so routine
minutes-old per-tx witness sets no longer count as an ongoing rebuild.
- shouldConfirmDaemonStop() suppresses the prompt entirely on v1.3.0+ daemons (version >= 1030000):
they checkpoint witness-rescan progress, so stopping mid-rebuild resumes on the next start rather
than redoing it — the warning's "restarts it (several minutes)" premise no longer holds.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Auto-shield status: the node's z_autoshieldstatus disabled_reason was echoed
verbatim ("HD seed origin is not known-recoverable; back the seed up and pass
-autoshield=1") and drawn INSIDE the Wallet OPTIONS checkbox grid, wedging a
full-width line between the checkboxes. Move it to a full-width note BELOW the
grid, and replace the raw daemon text with friendly, actionable wording keyed
on the seed_recoverable flag (back up your seed to enable it); the raw daemon
reason is kept on hover. Adds App::daemonAutoShieldSeedRecoverable().
- FAQ: add a Chat & Contacts entry ("How do I hide, delete, or block a
conversation?") covering the hide / delete-revive / delete-&-block actions and
the local-only caveat — data + i18n only, no UI code change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The macOS release path hardcoded aarch64-apple-darwin for the wallet-rebuild
helper's static libdb and — unlike the Linux/Windows paths — never passed
-DBDB_INCLUDE_DIR/-DBDB_LIBRARY to CMake. So on an Intel Mac the helper either
couldn't find libdb, or picked up macOS's SDK stub /usr/include/db.h (a DB 1.85
shim with no db_create) and failed to compile.
- mac_bdb_dir(): map the target arch to the depends triple (x86_64 ->
x86_64-apple-darwin, arm64 -> aarch64-apple-darwin; universal / unknown falls
back to the host arch).
- Pass -DBDB_INCLUDE_DIR / -DBDB_LIBRARY (when the vendored libdb is present) in
BOTH the native and osxcross configures, so CMake uses our BDB 6.2 header
instead of the SDK stub. Full-node only (lite has no BDB wallet.dat).
- Use the derived triple in require_wallet_rebuild_helper's fix hint.
Verified end-to-end by building both ObsidianDragon 2.0.1 and ObsidianDragonLite
1.1.0 .dmg/.app on an Intel Mac (x86_64; built single-arch via DRAGONX_MAC_ARCHS
since the vendored deps are x86_64-only).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Fixes found while diagnosing a Windows full-node wallet that appeared stuck "downloading blocks" on a large (~5.3k-tx) fully-shielded wallet. Root cause was not the network — the wallet was starving the daemon's single block-connect thread. Five commits:
8707934sync starvation — the Overview balance poll (z_gettotalbalance, O(mapWallet) undercs_main, ~20s on this wallet) fired every ~2s and heldcs_main, soActivateBestChain/ConnectBlockonly ran in the gaps and the node fell further behind than it caught up. Fix: adaptive balance cadence (next poll waits >= scan-cost / 10%) + sync-settle hysteresis so it can't re-starve near tip; wallet mutations still force an immediate balance refresh.29274c2startup/shutdown UX — trim the verbose "taking longer than expected" notice; the shutdown screen now tailsdebug.log(windowless) for external daemons so the node flush/exit is visible.7e8b99ashutdown safety — confirm before stopping the daemon mid Sapling-witness-rebuild (stopping discards it -> multi-minute redo next launch): Keep-running / Stop-anyway / Cancel.a2f84beconsole-flash on launch — spawndragonxdwithCREATE_NO_WINDOWinstead ofCREATE_NEW_CONSOLE.0942691windowless shell-outs — route the remaining GPU/xmrig/wallet-rebuild_popencalls through aCreateProcess+CREATE_NO_WINDOWhelper (Platform::runHiddenCapture) so they never flashcmd.exe.Testing
test_phase4green on every commit; Windows cross-compile (--win-release) verified per commit.z_gettotalbalance/z_listunspent~20s each holdingcs_main; one core pegged onGetFilteredNotes; block-connect thread in Wait; download idle (~17 B/s) with all peers at max in-flight.Out of scope (separate, unvalidated)
A daemon-side fix to persist witness-rebuild progress across an interrupted rescan lives in the
external/dragonxrepo on branchfix/witness-rebuild-checkpoint. It is not compiled or tested (fund-adjacent) and must pass a node build + regtest/testnet/mainnet validation + adversarial review before shipping — intentionally excluded from this PR.🤖 Generated with Claude Code
Send and Receive are now consistent in layout, and the Send recipient row's buttons render correctly. Card envelope (Send ⇄ Receive consistency): - Add Layout::mainComposeCardBox(availW) — a single shared source for the compose card's width + centering (fill the available column up to content-max-width, then center). Both tabs derive their card from it, so they can't drift again. Previously Send capped at 760dp and Receive at 860dp, so the Send card rendered ~150px narrower on any window wider than ~860dp; now they fill available width identically. Receive: - Justify the footer buttons edge-to-edge (equal shares over the live count) instead of left-clustering with dead space, matching Send's full-width footer rhythm. - Build the address-dropdown preview to the combo's real pixel width so the trailing balance ("— 12.00000000 DRGX") no longer hard-clips at 150% (was char-count truncation). Send recipient row (input | Paste | contacts-icon): - Pin the contacts icon button to the frame height so the larger iconMed font doesn't auto-size it taller than Paste/the input. - Reserve the real ItemSpacing.x gaps (not the smaller spacingSm token) so the row no longer overshoots the card and clips the icon's right border. draw_helpers (root cause, app-wide): - TactileButton's icon path measured/drew the label INCLUDING the "##id" suffix (which CalcTextSizeA/AddText don't strip the way ImGui's text render does), shoving the glyph off-center-left. Strip at "##" before measuring/drawing. Corrects any icon button that passes an explicit size and a "##id" label; no-op for labels without "##". Verified via headless sweeps at 1.0x and 1.5x, plus a real 3800px-wide render (both cards byte-identical at L=1174/R=2773). ctest 1/1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>- Auto-shield status: the node's z_autoshieldstatus disabled_reason was echoed verbatim ("HD seed origin is not known-recoverable; back the seed up and pass -autoshield=1") and drawn INSIDE the Wallet OPTIONS checkbox grid, wedging a full-width line between the checkboxes. Move it to a full-width note BELOW the grid, and replace the raw daemon text with friendly, actionable wording keyed on the seed_recoverable flag (back up your seed to enable it); the raw daemon reason is kept on hover. Adds App::daemonAutoShieldSeedRecoverable(). - FAQ: add a Chat & Contacts entry ("How do I hide, delete, or block a conversation?") covering the hide / delete-revive / delete-&-block actions and the local-only caveat — data + i18n only, no UI code change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Pull request closed