Lite-parity audit, sync-progress semantics (state/presentation only — no send/sync backend call changed):
- A scan failure set scan->failed + lite_error, but no UI read lite_error, so the user saw "Syncing 99.9%"
forever. The status bar now shows the error text on a lite scan/server failure.
- After the initial catch-up, a server loss on the cheap probe path (ProbeFailed) left the last "Synced"
model on screen. The worker now re-publishes that model demoted (not complete, reason "server unreachable").
- refreshModelImpl's height-read "synced" override no longer fires while syncstatus says the backend is
still scanning or is indeterminate, and a server-bound read failure in a full cycle demotes to not-synced.
- The syncstatus parser no longer treats a missing/unrecognised "syncing" field as fully synced: it reports
an explicit "indeterminate" (not complete). Explicit "false" stays caught-up; the fake backend's
syncing:true 1000/1000 behaviour is unchanged.
Tests: parser cases for missing/odd/idle "syncing" (testLiteSyncStatus*).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resilience audit, highest-value finding: a z_sendmany (full node) or lite broadcast whose REPLY is
lost — a transport timeout / connection cut AFTER the request already left us — was reported as an
ordinary "Transaction failed". The user then retries and can broadcast the same payment twice, because
the daemon may already hold the operation (z_sendmany returns an opid and runs async).
Classify the ambiguous case and surface it distinctly, without touching the send itself:
- New pure helpers in rpc/rpc_failure.h: sendReplyMayBeLost() (an RpcTransportError that was requestSent
with no HTTP status and a non-OK curl code — reply-timeout / got-nothing / our-own-abort; definitive
rejections and any HTTP status are excluded), liteSendErrorMayBeInFlight() (a transport-style gRPC text
heuristic, vetoing connect-phase and server-rejection text), and a readable-English marker tag.
- app_network.cpp: the z_sendmany catch records the flag; the failure branch (non-background only) tags
the callback text and forces a wallet re-read so a committed send shows in History. onDisconnected()
tags already-pending send callbacks the same way (they exist only after an opid was returned).
- Lite UserSend (app.cpp): a transport-style broadcast error is tagged identically.
- Send tab: a tagged outcome shows a non-success warning pointing at History and clears the compose form
so any resend must be retyped deliberately; it is NOT presented as a retryable failure.
The actual rpc_->call("z_sendmany", {from, recipients, 1, fee}), the lite send/broadcast call, and all
amounts/fees/recipients/memo bytes are byte-identical; only result-handling and presentation change.
Pure classifier/marker regression tests added (testSendMaybeInFlight).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Defense-in-depth against a local attacker / core dump reading un-zeroed heap
copies of secrets (threat model is local, not remote). Best-effort by nature —
std::string SSO/reallocation and ImGui buffer growth can still strand copies —
so these shrink the exposure window rather than guarantee absence:
- rpc_client performCall: a scope guard now sodium_memzeroes the request body +
payload tree (walletpassphrase / *_importkey args) after the call and detaches
CURLOPT_POSTFIELDS. Request content sent to curl is unchanged.
- New ui::scrubImGuiInputTextState() zeroes ImGui's retained InputText buffers
(live/revert/callback/deactivated) — the app memzeroed its own char[]s but
ImGui kept copies. Called from wipeSecrets()/wipeDisplayedSecrets(). PINNED to
ImGui 1.92.6 via a #error guard so a vendored bump forces a re-audit (the stb
undo buffer is an incomplete type in the header, so it's left as a noted gap).
- Lite exportSeed/exportPrivateKeys wipe result.value + the parsed JSON tree.
- Seed-word grid (SplitSeedWords/RenderSeedWordGrid) zeroes its per-frame word
and cell copies; the wizard word/chip vectors are zeroed before clearing.
- ChatStore::clear() + ResetChatTab() zero decrypted message bodies on
lock/switch/teardown.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The console redacted secret command ARGS but showed the OUTPUT of key-revealing
commands (dumpprivkey / z_exportkey / z_exportviewingkey / z_exportmnemonic /
dumpwallet / z_exportwallet, and the lite seed/export verbs) in full — visible
in the scrollback, Copy All, Ctrl+C and the filter. Now, when the submitted
command is key-revealing, its result is held ONLY in a member buffer (never in
the scrollback model, so Copy-All etc. can't leak it) and a masked placeholder
is shown. An explicit "Copy secret" button routes through the auto-clearing
clipboard; the buffer auto-wipes after 60s, on Clear, and on wallet lock
(via wipeDisplayedSecrets). The command string passed to the executor is
byte-identical. New pure helper ConsoleCommandRevealsSecret is unit-testable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- writeFileAtomically honored restrictPermissions only on POSIX (0600); on
Windows it was a no-op, so exported keys / seed backups / the vault / chat
exports inherited the parent dir's ACL. It now builds a PROTECTED owner-only
DACL (current-user SID, SE_DACL_PROTECTED) on the temp file before the atomic
rename; best-effort, falling back to default ACLs on failure. (Verified it
compiles under mingw-w64; the POSIX path is unchanged.)
- The key-export dialog's Copy wrote the private key to the plain, permanent
clipboard; it now goes through copySecretToClipboard() (45s armed auto-wipe +
its own "copied, auto-clears" notice), like the seed backup copy.
- Bootstrap archive verification dropped the collision-prone MD5 fallback: it now
requires a published SHA-256 and fails closed otherwise. (Authenticity vs a
malicious server still needs a detached ed25519 signature like the updaters —
a separate release-coordinated change.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Locking the wallet (manual / auto / lite) now calls wipeDisplayedSecrets(),
which scrubs + closes any revealed seed phrase, migration seed, or exported
key — previously a locked wallet could leave the phrase/key on screen and
resident. Runs synchronously, before the async lock round-trip.
- The console secret-arg redaction list only had full-node RPC names, so lite
verbs (import / timport / encrypt / decrypt / unlock) echoed keys and
passphrases into the scrollback and up-arrow history; added them.
- The offline wallet-rebuild helper was invoked via a shell string
(popen "sh -c") built from paths including the wallet filename, so a crafted
name could inject a command; it now refuses any path with a shell
metacharacter (rc stays -1 and the existing verify-before-swap fails safe).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feedback-convention + HiDPI cleanup (from the audit's living-hygiene threads):
- Copy actions that were silent now toast a confirmation: transaction
address/txid (context menu, click-to-copy, detail buttons), peer and banned-
peer addresses, the Settings data-dir copy, the chat message "Copy", and the
key-export result copy.
- The first-run wizard's Skip/Back buttons used raw ImVec2(120/80,0) widths that
rendered tiny at HiDPI; they now multiply by ui::Layout::dpiScale().
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Chat: a dismissible first-run intro card (how chat works — encrypted, sent as
shielded txs with a small fee, tied to the seed) with a "Copy my chat address"
button, shown in the empty thread pane; dismissal persisted. The copy logic is
shared with the existing sidebar button (moved to a helper, unchanged).
- Toasts: clicking a toast now opens the existing notification-history panel, and
a "+N more" badge appears when several live toasts are queued (render only ever
showed the newest). Badge opacity is a ui.toml token.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The code changes intended with the settings_window removal in a90aa29 (a bad
git-add pathspec aborted staging them there, leaving that commit with only the
file deletions and CMakeLists still referencing them):
- First-run handoff info toast at the completion point.
- Daemon-updater "restart pending" reminder (Settings → Daemon Binary) + the
Done screen's warning-level "restart required" headline.
- Debug restart-note raised to a readable color with the restart prompt beneath
it; DEBUG OPTIONS header shows a lock + explanatory caption (auth gate
unchanged); a few hardcoded literals → material tokens / dpiScale.
- Drop the legacy settings_window from the build (CMakeLists + the dev-only
references) to match the file deletions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrate-to-seed is irreversible and fund-moving, so its gates are strengthened
(UI only — the sweep/adopt/wipe logic, z_mergetoaddress args, amounts and
destination are byte-identical):
- Continue now requires a second, consequence-stating acknowledgement checkbox
(funds move and are recoverable only via the seed phrase), not just "I wrote
it down".
- Discard, Sweep-all and Sweep-remaining each take a second confirming click
(stating the amount being moved) instead of firing on one click; the confirm
body runs verbatim only after confirmation, and arming never survives a
close/reopen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Sync ETA: a new pure util::SyncRateEstimator (App-owned) replaces the
function-local statics that went stale after a rescan/restart (producing the
"412h" ETA). It self-resets on a height drop and on connect/disconnect/rescan/
restart (via an atomic flag, since onConnected can run off-thread), formats
with days, and uses the Warning() token.
- Settings: the Explorer-tab URL fields + custom-fees/fetch-prices checkboxes
now persist on edit — they were the only controls still relying on the
About-only Save button that a user on this tab never sees.
- Export-all-keys: hidden in the lite build (which has its own key export),
instead of dead-ending on "Not connected to the daemon".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- A mining start/stop/pool failure now sets a persistent App::mining_last_error_
(cleared on the next attempt), rendered as a red caption with "Press Mine to
try again" below the Mine button (plus a Dismiss, and a Windows-Defender
"How to fix" on an AV block), so a crashed/blocked/failed miner is no longer
just a button that "did nothing" once its toast faded. Two more hardcoded
English strings (pool-payout-invalid, AV-blocked) moved to i18n.
- Pool connection status is now a persistent "Status" row in the pool stats
card (PoolMiningState.running_since / last_error): it flips from "Connecting…"
to "Can't reach pool — check the URL" after a 30s timeout instead of sitting
on red forever, and no longer vanishes once logs arrive. The old transient
banner in mining_tab.cpp was removed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the node rejects the wallet's RPC login (bad rpcuser/rpcpassword), the
status bar showed developer jargon ("Auth failed — check rpcuser/rpcpassword")
and the only action was Reconnect, which just repeats the failure. Now a
dedicated AuthFailed banner reason surfaces "Wallet can't sign in to the node"
with a plain-language detail and a real fix: Restart node for the embedded
daemon (regenerates its cookie creds) or Open config folder for an external one
(to fix DRAGONX.conf). Driven by a new rpc_auth_failed_ flag (set on the auth
path, cleared on each connect attempt), with evaluator unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Seed-backup dialog: a failed "Save to file" was drawn in the success (green)
colour; it's now red on failure, green on success (flag reset on close).
- Encrypt-wallet dialog: the disabled Encrypt button now says why once the user
starts typing ("Use at least 8 characters." / "The passphrases don't match."),
and a "Show passphrase" checkbox reveals both fields.
- Key-export dialog: the raw RPC exception (e.g. "...walletpassphrase first")
is routed through util::friendlyRpcError(), so a locked wallet reads
"Unlock your wallet first, then try again." instead of daemon jargon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
renderNodeStatusBanner suppresses itself during a UI sweep (the forced demo
state would otherwise paint a banner over every tab). Add a narrow opt-in
(sweep_show_node_banner_) so ONLY the overview-no-peers target shows it; every
other capture is unchanged. The target also clears connection_in_progress_ so
the evaluator classifies the state as no-peers rather than "connecting".
Verified in a full-node + DRAGONX_ENABLE_UI_SWEEP build: the banner renders as
"Connected, but no network peers — the node can't sync · Check your internet
connection and firewall, then restart the node to find peers." (Restart button
only appears with an embedded daemon, which the sweep has none of.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Seed backup: the one-time 12s toast that marked itself "reminded" the instant
it fired (so a funded, unbacked wallet was never nudged again) is replaced by
an actionable alert with a "Back up now" link that opens the backup dialog and
persists in the bell panel. "Reminded" now means the user actually viewed the
phrase (set from the backup dialog), so the nudge re-fires each session until
they do, and escalates to a warning once a balance exists.
- Console: fund-moving / key-revealing commands (dumpprivkey, z_exportkey,
z_sendmany, z_mergetoaddress, …) now require a confirming re-entry when typed
or PASTED, honoring the command reference's `destructive` flag that was
previously enforced only by the reference dialog's Run button. (Output
masking of key dumps in the scrollback is still a follow-up.)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The no-peers banner headline ("Connected, but no network peers") was followed
by a detail line that just repeated connection_status ("Connected"/"Peers: 0"),
reading as a contradiction, and it offered no action at the exact moment the
wallet looks synced but is silently stalled at a stale height.
- NoPeers now clears the passthrough detail; App renders a translated,
actionable hint (node_banner_no_peers_detail) for this reason instead.
- When the embedded daemon is the node, offer a Restart button (re-triggers
peer discovery via the -addnode seeds); an external node stays action-less
since it is the user's own to restart.
- Update + extend the evaluator unit tests (external = no action/empty detail;
embedded = RestartNode).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On quit the lite controller's destructor never runs, because main()
ends the process with _Exit(). Any state the background refresh had
scanned since the last save was lost: the destructor is where that
final save would normally happen, and it was being skipped on the
one path that always runs.
- shutdown() now calls a new flushOnExit() that persists once on the
way out, bounded to 4 s so the existing 15 s shutdown watchdog
budget still holds. On timeout it abandons the save thread and lets
_Exit follow.
- It is a plain save through the same backend path, which writes a
temp file and renames it into place. That cannot corrupt the wallet
if it is cut off or abandoned, and it does not change the wallet
file format - it only persists sooner on the quit path.
- It reuses the destructor's guards and also skips when a wallet op
is already in progress: shutdown() has already waited for ops to go
idle, and a still-pending op holds the sync lock and issues its own
save, so flushOnExit would only block the budget on a save that
cannot take the lock.
Tests cover a save on the open, synced, idle path (exactly once), a
blocked save returning within the budget, and the retired and
never-opened guards skipping the save.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sidebar unconfirmed-tx badge was recomputed only when the tx list
changed, keyed on last_tx_update plus the list size. That missed the
one case that matters most: a transaction confirming without the list
count changing. last_tx_update did not move for a same-second content
change, and lite never stamped it at all, so the badge stuck on a
stale 0-conf count until some unrelated refresh happened to shift the
list size.
- The badge now keys on tx_revision, which the state bumps on every
content change - including a confirmation flip at an unchanged tx
count - and which lite stamps too, so the count tracks confirmations
on both the full node and the lite wallet.
- The sentinel starts at 0, which tx_revision never takes (it starts
at 1), so the first frame always computes once.
- The recompute still runs only when the revision actually moved, so
the per-frame work the previous guard avoided is still avoided.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Starting the daemon polls for an already-running node holding the
datadir, sleeping up to three 100 ms intervals to let a shutting-down
node clear out. When a start runs on the UI thread, that is up to 300 ms
of blocking sleep on the render thread.
- The lock-wait poll count is now a parameter. A start invoked on the UI
thread passes 0, which does a single check and never sleeps; every
other caller keeps the three-poll wait. The default is unchanged, so
background starts behave exactly as before.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
At quit the UI thread drained and joined the mining-control thread and
then called the miner's blocking stop with a 3 s budget. Both ran on the
UI thread, so the window could stall for up to that long before the
shutdown screen rendered.
- Quit now only signals the mining-control thread (no join) on the UI
thread. The blocking teardown — join the control thread, then stop the
miner — moves to a background thread, so xmrig still has a single
driver but the UI thread never blocks on it.
- On the daemon-stop path that teardown runs first on the shutdown
thread, so the miner is fully stopped before the daemon stop begins.
On the disconnect-only quit paths it runs on its own thread, which
the final shutdown joins before tearing xmrig down, so there is never
a second concurrent join of the control thread.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the daemon was restarted, the output-parse offset was reset to 0.
The daemon's output buffer is retained across runs, so the next poll
re-read the whole retained tail of the previous run and fed it to the
busy tracker as if it were fresh. A witness rebuild or rescan line left
over from the old run could then be mistaken for activity in the new
one.
- On restart the offset is now reset to the absolute end of the output
at that moment, captured on the lifecycle thread after the stop and
before the start, so the UI resumes parsing only the new run's output
rather than replaying the old tail.
- The capture is handed to the UI thread through a pending flag and
applied there before the next read, both on the normal poll and when
skipping the backlog, so the offset is only ever moved on the thread
that owns it.
Tests cover reading from the captured watermark (only the new run is
seen) versus the old reset-to-0 behaviour (the previous run replays).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ui.toml and the default banlist were embedded by a CMake function that
read each file and emitted a C array header one byte at a time in a
configure-time loop. That is slow to configure, and because the result
is a header, editing either resource recompiled every translation unit
that included it.
- Both files are now embedded with INCBIN from a small generated
translation unit, the same pattern already used for the fonts. There
is no generated header, so editing a resource re-assembles one tiny
object file instead of triggering a wide recompile, and configure does
no per-byte work.
- Consumers include a hand-written header declaring the symbols and
always use the embedded data (the __has_include fallback is gone). The
data is not NUL-terminated, so every consumer passes an explicit size.
- The old EmbedResources.cmake script is removed, and the generated unit
is added to the app and test builds.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
While the seed migration sits on the Confirming gate it polled the
sweep's confirmations and a full wallet balance together on every pass.
That balance call is the expensive one, and it only matters once the
sweep is mined and we do not yet have a post-mine figure, yet it ran
every poll even mid-sync when it cannot move the gate.
- The confirmation count is still read on every poll.
- The balance scan now runs only when it can actually advance the adopt
gate: the sweep is mined, no post-mine balance has been recorded yet,
and no sync is in progress. The manual Refresh button forces it
regardless.
- The decision lives in a small pure helper, and the poller snapshots
the main-thread state it needs rather than reading members off the
worker thread.
- The Confirming screen now claims a leftover remainder (and offers the
re-sweep button) only once the balance has actually been measured; while
it is still unknown it keeps waiting instead of prompting on a sentinel.
Tests cover the gate helper across the forced, syncing, and mined cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A secret copied to the clipboard auto-clears after a timeout, but the
deadline was stored as an ImGui::GetTime() value. That clock only
advances while frames are being drawn, so while the app idled (no
redraws) it effectively stood still and the pending clear could be put
off well past the intended window.
- The clear deadline is now a steady_clock time_point, which keeps
counting whether or not the UI is rendering.
- The pump uses the armed hash as its "something is pending" flag rather
than a sentinel deadline value, and compares the real wall time.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The hashrate sparkline guessed the age of its oldest sample from the
sample count, labelling the left edge "5m ago", "1m ago" or "start"
regardless of how fast samples actually arrived. Solo and pool samples
come in at different intervals, and a slow or stalled poll made those
labels wrong.
- Each hashrate sample now carries a wall-clock timestamp, kept in
lockstep with the value as the history fills and rolls over, for both
the solo and pool histories.
- The chart's left-edge label is derived from the oldest sample's actual
timestamp, falling back to "start" only when no stamp is available.
Tests: a mining refresh records a timestamp per sample, and the
timestamp history stays the same length as the value history once the
history is capped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When following the debug.log of a node this session did not launch, every
poll re-read the last 2 MB of the file and re-scanned it for the process
banner. If that node logs to its console instead, the banner is never
there, so the full re-scan repeated on every poll for the life of the
process.
- A miss is now remembered by pid and file size: if nothing has changed
since the last miss, or the file is growing but the backoff has not
elapsed, the scan is skipped.
- The retry schedule re-scans quickly during a fresh-node grace window
(the banner flushes a beat after the pid appears) and then backs off
from 4 s, doubling to a 60 s cap. The memo is cleared on a successful
tie and when the followed process goes away.
Tests: the retry delay is fast within the grace window and then steps
4, 8, 16 s up to the 60 s cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hovering the Paste button in the import-key dialog read the OS clipboard
every frame to draw a ghost preview of the key, and the trimmed
plaintext was rebuilt each frame and never wiped. SDL's clipboard read
is a synchronous round-trip, and the pasted value is a secret private or
viewing key.
- The clipboard is now read only on hover entry and then at most twice a
second, and the classified preview is cached rather than recomputed
every frame.
- The cached preview is a dedicated secret buffer: it is wiped the moment
it stops being shown, wiped on secret wipe, and the transient trimmed
copy is zeroed right after it is classified.
- Trimming moves to a shared clipboard helper shared with the paste path.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
While no lite wallet is open the startup auto-open check called
walletExists() every frame. That call crosses into the backend and
touches the filesystem (and prints), so it ran a disk stat on every
frame the app idled at the first-run prompt.
- walletExists() now caches its result for about two seconds on the main
thread, and the cache is invalidated whenever an open or lifecycle
result is consumed, since the wallet file may have changed.
- The auto-open condition is reordered so the cheap timer checks gate the
call, and the click-time overwrite decision uses a new
walletExistsFresh() that bypasses the cache.
Tests: the cache returns the first queried value within the TTL and the
fresh read re-queries the backend and refreshes the cache.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pool auto-balance step returned early whenever the pool selector was
in Manual mode, so a pending manual Refresh of the pool display stats was
dropped and the button did nothing unless Auto-balance was active.
- A pending refresh is now serviced in Manual mode too, re-querying the
pool display stats without auto-switching the pool or restarting xmrig.
- Auto-balance behaviour is unchanged; the manual path is display-only,
so it runs with no benchmark gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The global keyboard shortcuts tested keys without disabling key-repeat,
so holding one down fired it every frame: the theme cycle, balance and
market layout cycles, the effects and background toggles, the settings
and sidebar-collapse keys all re-triggered on a held key. Cycling themes
also stepped onto skins that had failed to load and wrote the broken
pick into settings.
- Every one-shot shortcut now checks for a fresh key press only, so a
held key fires once.
- Theme cycling skips over invalid skins and only applies, persists, and
shows the toast when a skin actually loads; if none can load, settings
are left untouched and no toast appears.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Once a shutdown was under way the app could still launch or restart the
full node and keep running the RPC connection state machine. That could
spawn a daemon process on the way out and surface a spurious connection
failure toast as the window tore down.
- Starting the embedded daemon now bails out immediately while a
shutdown is in progress, so the node is never (re)launched on exit.
- The connect attempt returns early during shutdown, so there is no
start/connect try and no stray failure toast on the exit screen.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The thread benchmark state machine was advanced from the Mining tab's
render path, so it only progressed while that tab was visible and the
window was on screen. Hiding the tab or minimizing the window froze the
benchmark mid-sweep.
- The benchmark now advances from the main update loop, ungated by
connectivity, so it keeps running when the tab is hidden or the window
is minimized.
- Its clock is driven from a steady wall-clock reading rather than the
render delta, and each step is clamped like the other logic timers so
a suspend/resume cannot leap a benchmark phase.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Market-tab portfolio groups were tied to a wallet by a hash of its
address set. That hash changed whenever an address was added, so a
group built for a wallet could silently drop out of view once the
wallet gained a new address.
- Groups are now scoped by the wallet's stable id, which is persisted
and never derived from the address set, so adding an address can no
longer orphan a group.
- An older build's address-hash scope fails open (the group stays
visible) and is migrated once per wallet per session to the stable id.
A legacy group is claimed when its old hash matches, when one of its
own addresses belongs to this wallet, or when only one wallet is
known; anything ambiguous is left legacy and stays visible.
- Scope filtering waits until the wallet's addresses are loaded, so a
group never flashes an empty 0.0000 balance and Add stays disabled
until the wallet is resolved. This works on Lite, which never stamps
an address-update time.
Behaviour: an older build opened after this change hides groups tagged
with a stable "w:" scope (they are not deleted and return on the next
upgrade) - worth a release note.
Tests: group visibility across global, stable and legacy scopes with the
wallet resolved and not; the migration rules (hash match, address
overlap, single-wallet claim, ambiguous left alone, idempotent); and a
save/load round trip where an address-set change cannot orphan the scope.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A Settings bootstrap was only wrapped up inside the dialog's own draw
path. If the dialog finished downloading, verifying or extracting while
the user had clicked away or minimized the window, completion was never
processed: the connect and reconcile gate stayed held and the
post-bootstrap rescan was never armed.
- Completion is now a pure decision driven from a pump() that both the
per-frame update and the dialog's render call, so a finished bootstrap
is handled every tick whether or not the dialog is drawn.
- With the dialog open the behaviour is unchanged: it advances to the
Done or Failed screen and those screens still own the gate. With the
dialog closed, pump() releases the gate itself and shows a success or
failure toast; a user-cancelled run is released silently.
User-visible: a bootstrap started from Settings now completes, arms its
rescan and reports its result even if the dialog is closed or the window
is minimized when it finishes.
Tests: the completion decision for an unfinished worker, a completed run
with the dialog open and closed, a failed run, and a user-cancelled run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Setting a PIN always unlocked the wallet with a 5 s probe and then
re-locked it, even when the wallet was already unlocked, so a PIN check
silently re-locked an unlocked wallet. And once the daemon's absolute
unlock window ran out the GUI still showed "unlocked", so the next send
failed with nothing on screen explaining it.
- The Set-PIN check now decides on the main thread: an unlocked wallet
is verified with the time it has left (capped at seven days) and is
not re-locked, so the GUI and the daemon keep agreeing; a locked
wallet still uses the 5 s probe and re-locks. Either way the daemon's
lock state is re-read afterwards, which self-heals any drift.
- When the daemon's unlock window has expired but the GUI still says
"unlocked", a bounded reconcile (once per expiry, 30 s back-off)
re-reads getwalletinfo so the lock screen and the transaction-cache
lock follow the daemon. Lite wallets, which have no daemon timer, are
left alone.
Behaviour change: with auto-lock disabled the daemon still re-locks at
its window, and the GUI now lands on the lock screen instead of
appearing unlocked until a send fails.
Tests: the Set-PIN verify plan (unlocked keeps its remaining time and
skips the re-lock, locked or near-expiry falls back to the 5 s probe and
re-lock, long windows are capped) and the unlock-expiry predicate across
encrypted, locked, no-deadline and not-yet-expired cases.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The auto-lock idle clock was reset whenever any ImGui item was active,
so a text field left focused (a half-filled send form, say) counted as
continuous activity and kept the wallet unlocked indefinitely, well past
the configured idle timeout.
- The idle clock is now fed by real input events (key, text, mouse,
touch, pen, gamepad button) as they are ingested on the main thread,
plus a still-held mouse button so a drag or scrollbar grab that emits
no new event still counts. A focused but untouched widget no longer
does.
- A new header-only IdleTracker owns the clock and never moves backwards,
so an event stamped before the previous one cannot shorten the idle
time.
Behaviour change: a form left focused and idle past the timeout now
auto-locks. The form's buffers are app state and survive the lock and
unlock, so nothing typed is lost.
Tests: a new IdleTracker case covers idle time growing with no events
(enough to trip a 900s lock), activity resetting it, the clock never
moving backwards, and a query before the last activity reading zero
rather than a negative value.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bootstrap preflight's protected names could be bypassed by names
that resolve onto them, and its cancel and quit paths had two timing
gaps.
- Entry names are judged as the file system resolves them: '\' is a
separator, "." components are dropped and each component loses its
trailing dots and spaces, as Win32 does ("./wallet.dat" and
"DRAGONX.conf." name protected files). The whole archive is refused
for an absolute or drive-rooted name, a ".." component, any ':'
(drive letter or NTFS alternate data stream, "DRAGONX.conf::$DATA"),
an 8.3 short name ("DRAGON~1.CON"), a component of only dots or
spaces (other than "."), and any byte >= 0x80 or ASCII control
character: macOS folds or ignores some Unicode (U+017F, U+212A,
U+200C) onto a protected name, and the published bootstrap archive
uses plain ASCII names. A refused name is echoed
with \xNN escapes. A member is written at the path it was checked
under, and the protected-name check runs on the resolved name.
- The user's Cancel (Settings dialog and first-run wizard) goes through
requestCancel(). It is refused once the old chain data starts being
replaced, and the start of that window re-checks a cancel under the
same lock, so a click lands before the old chain is touched or not at
all.
- Quit cancels the wizard's and the Settings bootstrap when the
shutdown starts; the HEAD and checksum requests now end at once on
cancel, and a cancelled HEAD keeps the partial download. A run
cancelled before the window leaves the old chain as it was. A run
inside it stops extracting and removes its partial chain while the
shutdown screen shows "Cleaning up...", so it ends with no chain data
(wallet.dat untouched; Retry reuses the zip, or the node resyncs).
That wait is bounded: at most 2 minutes from the start of the
shutdown, then 3 s in shutdown(), after which a worker still running
is left to the process exit. Only a cleanup that overruns that cap
can leave a partial chain, which dragonxd then answers with its
reindex offer.
Tests: refusal of the Windows-resolved, non-ASCII and control-character
names with the escaped echo; the protected-name list incl. the resolved
forms and a planted -wallet file, conf and database/ that stay
byte-identical; the user-cancel window; the bounded quit wait and
safeToAbandon(); a cancel ends a HEAD request that never answers.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ensureLogoTexture() still freed the chat emoji, header logo and coin logo
textures it replaced at once, while every other theme-driven replacement
already goes through retireTexture() and is freed at the next preFrame().
It runs at the top of render(), so nothing referenced them yet; this is
hygiene, so that the same rule covers every replaced texture.
- ensureLogoTexture(): the three direct DestroyTexture() calls become
retireTexture(). Cost: the old textures live one extra frame on a theme
or accent change.
- setGradientTexture() (unused today) retires the texture it replaces
instead of dropping it.
- No drain at shutdown: the process exits and the live logo and gradient
textures are not freed at exit either.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The "Use Tor" checkbox was saved but never read: no build routed any
traffic (node, RPC, price/update/bootstrap/pool HTTP or Lite gRPC)
through Tor, so users who ticked it believed they were anonymous.
- The checkbox is disabled and always shown unticked, on the Settings
page and in the legacy Settings window. Its tooltip says it is not
available and gives the alternatives: for the node, proxy=127.0.0.1:9050
in DRAGONX.conf with Tor running; otherwise a system-wide VPN/Tor (Lite
builds get the short version). The "Use Tor" label is kept.
- Behaviour change: a stored use_tor=true is cleared once at startup
(Settings::consumeLegacyUseTor + a synchronous save before the async
writer and any Settings page state), with a 20 s warning in the user's
language. The cleared key is the "already told" flag, so the notice
never repeats. No settings format change.
- 3 new strings (tt_tor_unavailable, tt_tor_unavailable_lite,
tor_never_applied_notice) in all 8 languages; the tooltip wraps at
tor-tooltip-wrap-width from ui.toml.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A rescan or key import holds the node's cs_main for the whole wallet
scan. On the shared clients it hit the 30 s curl timeout, reported a
failure that wasn't one, abandoned a daemon RPC slot and flipped the app
into "node busy".
- One long wallet RPC at a time (rescan, importprivkey, importaddress,
importwallet, z_importkey, z_importviewingkey, z_importwallet) runs on
its own ungated client with no client timeout (rpc/long_call_lane.h).
It can be aborted from any thread, and an abort is never lost, even
one that lands before the client opens.
- The GUI rescan, key import and sweep use it. The sweep's fund-moving
body is unchanged; only the client it calls differs. runtimeRescan()
returns bool, and a refused post-bootstrap reconcile stays armed.
- The console routes rescan and the import commands through the lane;
its input stays usable meanwhile, and a refusal is printed like any
other result. RPCClient gains callRaw(.., timeoutSec).
- Restart, wallet switch, delete-chain, bootstrap (now also from
Settings) and the other node-stopping flows are refused while it runs,
with a message saying a key import or rescan is still running; quit
asks for confirmation. The lane is aborted at quit.
- A second import or rescan is refused before the key is copied anywhere.
An import cut off after the request reached the node (a lost reply,
or a proxy's 502/504 while the node still imports) says the key may
already be in the wallet and to run Settings -> Rescan. Only the
node's own HTTP-layer refusals (401, 503, work queue full, unhandled
request) say that the key was not imported. A pure
classifyLongCallCutOff() makes that choice.
- New strings in all 8 languages; console results stay English.
Tests: the long-method list and the console's routing; one lane at a
time; an abort lands whether it comes before the client opens or during
the call; a call with no client timeout outlasts a 3 s scan, where a 1 s
gated client times out and marks the node busy; classifyLongCallCutOff()
for every failure kind with and without a sent request, and from raw
HTTP replies (502, 504, a plain 500 -> cut off; 401, 503, queue full,
unhandled -> refused).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The bootstrap removed blocks/, chainstate/ and notarizations/ first and
only then opened the archive. An unsafe entry, a full disk or a corrupt
member therefore left no chain, or a half-written one for dragonxd to
start on.
- Preflight before anything is deleted: every archive entry must be
safe (no absolute path, no "..") and readable, and the disk must hold
the extracted size plus max(256 MiB, 5%), counting what removing the
old chain frees (a symlinked chain dir counts 0). Free space counts as
unknown only when the probe errors, the capacity is 0 or the
available size is -1; a known disk with 0 bytes free is a refusal.
The old chain is kept on every preflight failure.
- Protected members are skipped, compared case-insensitively: any path
component starting with "wallet", any "database" component, a .cookie
or *.conf file, and top-level *.dat files other than fee_estimates,
peers and banlist. The number of non-critical member failures is
logged.
- The removal of the old chain is verified (a locked file fails the run
instead of mixing old and new chain files). If the extraction fails or
is aborted, the partial chain is removed again before the failure is
published; isDone() turns true only after the worker's last
filesystem change.
- Cancel is disabled while the old chain data is being replaced, and
quit aborts a running bootstrap.
- A complete zip from an earlier attempt is reused on Retry; its
checksum is re-verified. A download that cannot fit fails early, and a
disk-full write error says so.
Tests: the space rule (boundaries, unknown vs. full disk, saturation);
planArchive sizes, zip-slip, garbage and truncated zips; the
protected-name list; reclaimableBytes with symlinks; each preflight
refusal keeps the old chain and wallet.dat byte-identical; a corrupt
critical member removes the partial chain before the failure is
published.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The full UI sweep (dev builds only) ran its demo surfaces against the
real profile. Its teardown deleted wallet-{cold,trading,mining,donations,
2023,payroll}.dat from the real datadir, it created zero-filled wallet
files and picker-demo/extra-wallets folders, its demo wallet-index upsert
re-minted the active wallet's scopeId (hiding "w:"-scoped contacts), the
Contacts tab could re-attach legacy-scoped contacts to such a throwaway
id in addressbook.json, and it persisted the demo active wallet file and
Mining pool worker.
- Preflight: a full sweep is refused (toast + log line) while a real
workflow is in flight: a pending seed_migration_* setting or open
migrate dialog, a wallet switch, an encryption/import workflow, an
encrypt/PIN/lock operation, the seed-backup dialog, or a bootstrap.
The DRAGONX_FULL_SWEEP hook then exits as for an empty catalog.
- Persistence latches: AddressBook (snapshot on the first sweepBegin,
save/load latched while a demo-contacts surface is shown), WalletIndex
and Settings (active wallet file, contacts view mode, pool URL/worker)
for the whole sweep. Settings::sweepEnd() is the last statement of
clearDemoWalletData and flushes once only if a save was suppressed; a
quit mid-sweep writes nothing to the profile (the 0700 sandbox, with
zero-filled demo files only, may stay in the temp dir).
- Scope ids: during a full sweep activeWalletScopeId() returns the
persisted id or "" and never mints one, so the Contacts tab cannot
move legacy-scoped contacts to a throwaway id. The only address-book
write a sweep can still cause is the one-time legacy re-attach to the
wallet's persisted id, which opening Contacts does anyway.
- Sandbox: demo wallet files and folders go to a fresh random 0700 dir
under the temp dir (exclusive create, a pre-existing path or symlink
is refused), removed at sweep end. The teardown that deleted those six
wallet files is gone.
- Cleanup: the migrate-to-seed state, wizard phase, demo chat
conversations and a fabricated demo chat identity are reset at sweep
end; the seed-backup surface always shows the demo phrase and a real
z_exportmnemonic result is dropped while the sweep runs.
- i18n: one new key (screenshot_sweep_refused) in i18n.cpp and the 8
language files.
Tests: the address book snapshots only on the first sweepBegin, keeps
addressbook.json byte-identical while latched and restores the real
book; the wallet index keeps wallets.json and the real scope id, also
with the background writer attached; Settings restores the latched
values and flushes once only when a save was suppressed; the sandbox
helper refuses a pre-existing path or symlink and removes only a
prefixed dir, never a symlink's target.
Release note: v2.0.x UI sweeps could delete wallet-{cold,trading,mining,
donations,2023,payroll}.dat from the data directory and re-scope
contacts; this cannot be undone automatically.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reloadThemeImages() replaced gradient_tex_ in each of its three branches
(image loaded, load failed, no background) without freeing the old
texture, so every skin activation, including the one at startup, leaked
one texture.
The logo and coin textures were freed immediately instead, although the
current frame's draw lists can still reference them: the window backdrop
records its images at the start of the frame and skin switches happen
mid-frame (Settings, Ctrl+arrow keys), and the screenshot sweep switches
skins after RenderDrawData() but before the secondary viewports are
rendered. That hands the renderer a freed texture.
Textures replaced mid-frame are now retired to a queue (retireTexture()
also zeroes the member) that the next preFrame() frees first, after the
previous frame's draw data has been rendered. The old gradient is
retired at the top of reloadThemeImages(), and the logo and coin where
they used to be freed. ensureLogoTexture() still frees directly, since
it runs at the top of render(), before this frame draws a logo.
Shutdown does not drain the queue.
Measured with a temporary live-texture counter over a screenshot sweep
of all 10 skins: before, 6, 7, ... 15 live textures (one more per skin);
now a constant 5. The first screenshot after each skin switch is
pixel-identical to before on all 10 skins, and the full sweep matches
within noise.
Not verifiable here: DX11 at runtime.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main() built the font atlas before App::init() loaded ui.toml, so the
first build used the hard-coded fallback sizes (H1 24 px, Body2 12 px).
App::init() then rebuilt for a saved font scale above 1.0, still before
the schema load and so still at the fallback sizes. Only the color-emoji
rebuild requested for the first frame reached the ui.toml sizes, so
color-emoji users built the atlas twice at font scale 1.0 and three
times at 1.5.
Users with chat_emoji_color=false never got that last rebuild. They
built the atlas once or twice and ran the whole session at the fallback
sizes, e.g. Body2 at 12 px instead of 15 and H1 at 24 px instead of 20.
Now:
- App::loadBaseUiSchema(), the ui.toml load moved out of App::init()
unchanged, runs in main() before both InitImGui paths (DX11 and GL),
together with the saved font scale and emoji style from the early
settings read. It is idempotent, so the call left in App::init() is a
no-op;
- the two rebuild triggers in App::init() are gone;
- Typography records what the atlas was built from (the 15 schema sizes,
the schema's font scale, the effective emoji style and the schema
generation), and preFrame() rebuilds once when they differ. While the
schema generation and the emoji style are unchanged this costs two
compares per frame.
Measured on the Lite build (color and mono emoji, font scale 1.0 and
1.5, English and Japanese): every run builds the atlas exactly once, at
the ui.toml sizes times the scale (H1 20 px and Body2 15 px at 1.0,
30 px and 22 px at 1.5), and ui.toml is loaded before the window and GL
context are created. The packaged Lite zip and AppImage log one atlas
build instead of two. Color-emoji screenshot sweeps match the previous
build within noise.
Visible changes:
- users with chat_emoji_color=false had been rendering the whole UI at
the fallback font sizes; they now get the designed ui.toml sizes, so
every mono-emoji screen differs from before and matches the
color-emoji layout;
- a user skin whose [fonts] differ from ui.toml now rebuilds the atlas
once when it is activated or left; on a runtime switch the atlas used
to keep the old sizes until a restart. With a test skin that sets
Body2 to 18 px, startup builds twice and ends at 18 px; switching to a
bundled skin rebuilds once, switching among the 9 bundled skins
rebuilds nothing (they carry an identical copy of [fonts]), and
switching back rebuilds once;
- editing ui.toml while running still gives exactly one rebuild.
Not verifiable here: the Windows DX11 path with the schema loaded before
its first FreeType build, macOS, and the interactive font-scale slider,
Alt+scroll and emoji toggle, which were checked by reading the code.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the node-status banner shows, its detail line was fitted every
frame by popping one character at a time and re-measuring the whole
"· " + detail + "…" string: O(L^2) per frame in the detail length L,
which adds up on the ~1.5 KB Windows crash report. The pop also left a
dangling UTF-8 lead byte before the "…", so a cut in non-ASCII text drew
replacement characters in place of the ellipsis.
- util::textfit::ellipsizeTail builds the same string in O(L + m log m),
m being the length of the line the cut lands in: ImGui measures
multi-line text as its widest line, so the old loop could only stop
inside the first line that overflows on its own. Byte-identical for
ASCII; the cut now lands on a code-point boundary.
- EllipsisFitCache keeps the result until the detail, budget, font size,
font (by FontId) or rasterizer density changes, so an unchanged frame
costs one string compare.
- When ImGui would draw a line wider than the per-line split predicts
(a malformed byte joined with the following newline, as in CP-1252
output from the Windows daemon), the old whole-string loop is used for
that text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a first run or after an upgrade, Windows release builds wrote the
bundled Sapling params, asmap, node and miner (about 88 MB) in
App::init, before the first frame: 0.5-3 s of frozen blank window, 5-30 s
on a slow disk or under antivirus. Zip, AppImage and .app builds copied
the bundled files into the daemon dir and hashed the 51.5 MB of params on
the UI thread at the first node start, a 0.3-1.5 s freeze in the wizard
or the connect loop.
A stat-only check at startup decides whether anything is missing, stale
or unverified; a normal launch starts no thread and shows nothing.
Otherwise one background job extracts (Windows) or copies (Linux, macOS)
the files, then verifies the params once:
- The first frame paints at once. If the job takes longer than 0.4 s, a
chip at the bottom shows "Preparing wallet files... N%", then
"Verifying wallet files...".
- A node start on the UI thread while the job runs is deferred and
replayed when it ends (never into a bootstrap download, a restart or a
live connection); worker threads wait, and give up on quit. After an
upgrade that ships a new miner, the node starts after the miner is
written instead of before. Pool-miner starts and the miner version
check wait too, so the old miner is never run or reported while it is
being replaced.
- Each file goes to "<name>.partial", is flushed and renamed into place
under one lock shared with the daemon start, the bundled-node
reinstall and the miner extraction, and the job stops between 4 MB
chunks at shutdown. An interrupted write used to leave a truncated
binary that was trusted forever; a leftover .partial is now removed.
Other behaviour changes:
- Linux and macOS full-node builds copy the bundled node files (about
77 MB) into the daemon dir at first launch, even if the wallet ends up
on an external node; before, this happened at the first embedded start.
- Lite no longer writes the 53 MB of params and asmap it never reads.
- A disk-full or permission failure that leaves the node files unusable
shows a warning; a miner blocked by antivirus alone does not.
- A corrupt or truncated param in a zip, AppImage or .app install is
replaced from the bundle and verified again instead of trusted forever.
- Reinstalling the bundled node holds off the connect loop and a deferred
start for its whole run (either could launch the old binary while the
new one was written), and is refused while another restart is running.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows every full-node launch read the whole installed dragonxd.exe
(about 16.7 MB) on the UI thread and byte-scanned it for a version
string, to decide whether to offer the bundled node, which only needs
whether the file exists and its size. getInstalledDaemonInfo() takes a
scanVersion flag, and the update offer now only stats the file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New address, Import key, Encrypt and Remove encryption called the
backend on the UI thread. Each ends in the backend's save, which waits on
the lock a scan holds for its whole duration and every refresh read holds
briefly, so during a scan the window froze until the scan ended. A create
or restore with a passphrase encrypted and saved just after its first
scan had started: for a restore, a freeze for the whole initial scan. A
server switch destroyed the old controller on the UI thread and joined
its refresh worker, which can sit for minutes in a read against a dead
server (each balance, notes or list read retries for ~62 s).
- The four ops run one at a time on a background lane and their results
are applied on the main thread; Settings shows "Working…" meanwhile.
Key and passphrase copies are wiped after the call, or when a queued
op is dropped.
- A create or restore encrypts and saves (an open unlocks) before the
first scan starts, so the save takes milliseconds and the scan runs on
the encrypted file.
- Import, Encrypt, Remove encryption and Receive's New address are
disabled while a scan runs ("Wait for blockchain to sync"); Redownload
blocks is disabled while an op is queued.
- A switch retires the old controller without blocking (queued ops
dropped, pending ones reported failed, its client shut down) and
destroys it on a background thread. It no longer saves on the way out:
the new client re-scans the blocks since the last save, as after a
restart.
- A switch waits for an op in flight, at most 15 s, so the op can't
finish and save on the old client while the new one owns the wallet
file. The Network tab already shows the new selection meanwhile.
- The refresh worker stops between backend commands, skips the remaining
server reads once one fails, and then retries after 4, 8, then 15 s
instead of every 2 s.
- Quit waits up to 2.5 s for an op in flight, e.g. Encrypt's save.
Encrypt and Remove encryption now report a failed save: the backend
returns it as a JSON error that the old check never saw, so the failure
was reported as success. The trimmed copy of a key typed into Import is
now wiped too, and stopping the refresh worker can no longer miss its
wakeup.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The daemon's RSS was read on the UI thread: a scan of every /proc entry
on Linux (1-7 ms), a forked ps on macOS (up to ~30 ms). The mining poll
probed every 1-5 s on every tab, and the Mining tab's memory bar probed
again every 1.5 s.
util::BackgroundSampler now runs the probe on one lazily started thread
and caches the result. getDaemonMemoryUsageMB() returns the last sample
and asks for a new one at most every 3 s, and only while the Mining tab
memory bar or the Peers tab Memory cell is drawn: nothing on other tabs
or while minimized. EmbeddedDaemon::memoryProbe() hands the sampler a
pid snapshot (on Windows the value read from the owned process handle),
so nothing off-thread touches daemon state. The mining poll does no I/O
any more; it carries the last sample. Both shutdown paths stop the
sampler.
Fixed along the way: an unreadable /proc made the directory iterator
throw on the UI thread; the scan now just stops.
User-visible:
- the Peers tab Memory cell keeps updating during sync and shows an
external daemon's memory (it showed the mining poll's value, which
pauses during sync and covers only the embedded daemon);
- the Mining tab memory bar refreshes every ~3 s (was 1.5 s).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>