Commit Graph
260 Commits
Author SHA1 Message Date
DanSandClaude Opus 5.5 5993488176 feat(daemon): deliver stop reliably and never kill the node unasked
Closing the wallet sent "stop" at most twice, gated on a getinfo probe
that needs the daemon's cs_main, then killed the node after 20 s (Windows)
or 30 s (POSIX) whether or not stop had arrived. A node busy with an
hour-long witness rebuild therefore never got stop, was killed mid-
rebuild, and replayed everything since its last flush on every start.

Delivery (src/daemon/stop_delivery.h, RPCClient::open/trySendStop):
- stop goes over a dedicated client with no probe, so it never waits on
  rpc_'s lock or a cs_main-bound getinfo
- every attempt is classified, and StopDeliveryPolicy decides the next:
  Accepted / 503 = delivered; Queued (reply timeout) is resent at most
  every 30 s; QueueFull / not listening are retried every 1.5 s; a
  refusal after we were heard means the node is shutting down; a 401 on
  our own node tries the next credential source
- POSIX: a group SIGTERM goes out as soon as an attempt fails, which
  bypasses the HTTP queue entirely

Waiting (DaemonController::executeDaemonStop, pure, virtual-clock
tested):
- never kills on its own. Quitting waits as long as the node needs; the
  shutdown screen offers "Close wallet — leave node running" after 3 s,
  and "Kill node (unsafe)" (confirmed) only once stop was delivered and
  the node showed no CPU/I-O progress for ~10 min outside a rebuild
- other callers (restart, rescan, reinstall, switch, ...) wait up to
  120 s, let go once the app starts quitting, and keep watching a node
  that is still running; wallet switch refuses to go ahead then
- EmbeddedDaemon gains stop-session primitives (beginStop,
  pollOwnedProcess, signalGracefulStop, forceKillOwnedProcess,
  ownedResourceUsage over the Linux process group / Windows handle,
  endStop) and only one stop session runs at a time
- the Force Quit button and its stall heuristic are gone; "Leave
  running" aborts a pending stop attempt and closes at once

Verified against the fake daemon with the real GUI: closing mid-rebuild
now delivers stop (or SIGTERM 2 ms after a queue-full refusal) and the
node exits cleanly after its rebuild, where it was SIGKILLed before. The
shutdown screen, its "Leave running" button and the idle Kill offer
(after ~11 min) were checked by screenshot. New strings are English-
only for now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 22:50:11 -05:00
DanSandClaude Opus 5.5 a091bbc52f fix(daemon): never kill a dragonxd the wallet didn't launch
On Windows, stop() with no live process of its own looked up the first
"dragonxd.exe" by name and TerminateProcess'd it after the grace period:
an old node still shutting down (how an in-progress shutdown was killed),
a node on another datadir, or the user's main node. It was reached on a
lock-gated close, for an external node, after the tracked process had
exited, and from ~EmbeddedDaemon's stop(3000).

- Windows stop(): drop the by-name branch; with no tracked process it
  logs and returns. findProcessByName is detection only.
- ~EmbeddedDaemon no longer calls stop(); it stops watching and releases
  its handles. stop() returns whether the process exited on its own.
- SeedWalletCreator never sent its isolated node an RPC stop, so on
  Windows that node was always force-killed holding the wallet.dat it
  had just created, and the stack object's destructor could then kill
  the main node by name. It now stops the node over the node's own RPC
  on every path. A node that still had to be force-killed fails the
  creation (seed wiped; nothing has been swept into it yet), so an
  uncleanly closed wallet is never adopted.
- Reinstall waited only for the RPC port, which closes long before the
  process exits (locked exe on Windows, held datadir lock everywhere).
  It now waits for the process to exit and otherwise aborts without
  touching the binaries or starting a second node.
- Linux isDaemonProcessRunning also matches /proc/<pid>/exe, so a node
  whose main thread is already renamed "hush-shutoff" (still holding the
  datadir lock) counts as running.

Tests: a Windows real-process test (a fake dragonxd.exe on an isolated
datadir survives stop() and the destructor), a POSIX test that the
destructor leaves the process running, and the pure identity rule.
scripts/test-windows.sh passes the stub path to the Windows suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 19:44:35 -05:00
DanSandClaude Opus 5.5 2c04b88e19 fix(daemon): let POSIX stop() see the daemon exit instead of always SIGKILLing
POSIX stop() set should_stop_, which made the monitor thread return
without reaping the child, and then polled the state-based isRunning(),
which stays true while the state is Stopping. So every Linux/macOS stop
waited out its whole grace period, sent SIGTERM, waited 10 s more and
SIGKILLed, even when the node had exited seconds earlier. Closing the GUI
with an idle node took ~30 s; a node still flushing at 30 s was killed.

- stop() joins the monitor first, then polls waitpid(pid, WNOHANG) on
  a cached pid, so it is the only reaper and sees the exit at once.
- markStopRequested() (set by App::stopEmbeddedDaemon before the RPC
  stop, and by stop() itself; cleared by start()) makes an exit that
  lands before stop() runs a clean stop instead of a crash, on POSIX
  and in the Windows monitor. crash_count_ drives auto-restart, and a
  crash recorded before the request keeps its count.
- The SIGTERM -> SIGKILL escalation stays for now, but only fires for
  a daemon that has really not exited. Replacing it with a wait policy
  and an explicit user choice is follow-up work.

Verified against the fake daemon: an idle node that exits 3.4 s after
stop used to be SIGTERMed at +20 s and SIGKILLed at +30 s (GUI closed
in 30.6 s); it is now reaped as "exited cleanly" ~50 ms after it exits
(GUI closes in 4.0 s). New POSIX tests cover the clean exit, the early
requested exit and a graceful SIGTERM, each time-bounded so the old
code fails fast.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 09:02:45 -05:00
DanSandClaude Opus 4.8 fae8b20fb7 fix: FAQ CJK glyphs, outgoing-tx history, lite encryption, migrate & sync safety
Bug-report fixes plus adversarial-audit follow-ups. Bumps full 2.0.1->2.0.2 and
lite 1.1.0->1.1.1.

i18n / fonts:
- Rebuild the NotoSansCJK subset with the glyphs the FAQ back-fill introduced
  (什/么/门/做); Chinese FAQ titles rendered as "??" on zh (and ja/ko).

Transaction history (enumeration gaps):
- Surface outgoing shielded z->z sends via z_listsentbyaddress (listtransactions
  and z_listreceivedbyaddress never report them).
- Surface z->t deshields by parsing z_listsentbyaddress transparentSends (which
  z_viewtransaction does not expose); dedupe t->t against listtransactions.
- Key send-row dedup on address so equal-value multi-output sends aren't dropped.

Lite wallet (key safety):
- The async create/restore/open path the UI uses now applies the passphrase:
  encrypt a new/restored wallet, unlock an existing one. It previously discarded
  the passphrase, storing the seed/keys in PLAINTEXT (create/restore) or leaving
  an encrypted wallet locked (open).

Migrate-to-seed (fund safety):
- Persist a sweep-submitted marker at broadcast so a resumed migration can't treat
  a ~0 balance (an unconfirmed in-flight sweep) as an empty wallet and adopt (swap
  wallet.dat) before the sweep confirms; best-effort locate routes to the confirm gate.

Sync / threading:
- Gate isSynced() on a peer-derived tip (tip_known) so a peerless node isn't reported
  synced (Send against a stale balance); add a "no peers" status banner.
- Guard daemon_status_ with a mutex (monitor-thread write vs UI-thread read).

Tests: extends tests/test_phase4.cpp across all of the above; suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwycPDQSfEKtrTS2uC4JeT
2026-09-04 04:02:52 -05:00
DanSandClaude Opus 4.8 d9fa00bb38 perf: memoize per-frame render hot paths (console, transactions, recent lists)
Immediate-mode render functions re-run every frame; these rebuilt O(N)
state each time even when nothing changed. From a 6-lens perf audit, each
finding verified on a hot-path basis:

- Console: ConsoleModel gains revision(); the full-model filter scan and the
  glyph-by-glyph text-layout pass (BuildConsoleLayout) rebuild only when the
  model / filter / wrap-width / zoom change — previously it re-shaped up to
  10,000 lines every frame even when idle/scrolled. clear() force-invalidates
  the memo mid-render (no OOB on the just-emptied visible set).
- Transactions: the summary-card totals memoize behind the tab's existing
  FNV-1a fingerprint (also folds away a now-duplicate O(N) display-key pass).
- Send / Receive recent lists: early-exit the prefix scan (state.transactions
  is kept newest-first) instead of filtering the whole tx history every frame.
- network_refresh_service: O(new x total) txid find-and-replace -> hash map.
- Sidebar unconfirmed-tx badge cached on last_tx_update + tx count; the
  daemon-memory probe (/proc scan on Linux, popen on macOS) throttled to ~1.5s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 22:55:04 -05:00
DanSandClaude Opus 4.8 d27f387d6d feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings
Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):

- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
  status-bar "?" (and the Windows title bar), styled like the Wallets
  modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
  Layout::dpiScale() across ~30 files so nothing renders native-size at
  HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
  match the input-box height; move the stratum-host toggle into
  Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
  the preceding i18n commit), so the security/PIN/lock flow, seed-backup
  wizard, and witness-rebuild dialog localize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:37:16 -05:00
DanSandClaude Opus 4.8 56d93b6128 fix(shutdown): don't warn "node is rebuilding" for routine witness activity / on v1.3.0
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>
2026-09-01 02:21:49 -05:00
DanSandClaude Opus 4.8 398fb274fa fix(ui): only toast "Blockchain rescan complete" for user-initiated rescans
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>
2026-09-01 01:18:57 -05:00
DanSandClaude Opus 4.8 0343d48c13 feat(ui): keep the wallet syncing while minimized
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>
2026-09-01 01:09:50 -05:00
DanSandClaude Opus 4.8 a3892c0fd3 fix(ui): stop spurious "Blockchain rescan complete" toast after restoring from minimize
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>
2026-09-01 00:49:06 -05:00
DanSandClaude Opus 4.8 f7df315695 fix(ui): remove the "Taking longer than expected" startup stall notice
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>
2026-09-01 00:15:27 -05:00
DanSandClaude Opus 4.8 7e8b99a82b feat(shutdown): confirm before stopping the daemon mid witness-cache rebuild
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>
2026-08-30 23:32:03 -05:00
DanSandClaude Opus 4.8 29274c2f48 fix(ui): trim verbose startup notice; show daemon output on shutdown for external daemons
- 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>
2026-08-30 23:09:17 -05:00
DanSandClaude Opus 4.8 870793433b fix(sync): stop large-wallet balance polling from starving block connection
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>
2026-08-30 22:45:51 -05:00
DanSandClaude Opus 4.8 5daf2d83b6 feat(ui): large-wallet nudge as a one-time toast + clickable alert
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>
2026-08-20 18:09:36 -05:00
DanSandClaude Opus 4.8 558cfcbe56 fix(ui): restore ObsidianDragon logo in header and About tab
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>
2026-08-20 17:06:04 -05:00
DanSandClaude Opus 4.8 d0bd55b9c1 feat(ui): settings polish — button retune, daemon card, RPC 2-row, chat preview
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>
2026-08-20 16:59:03 -05:00
DanSandClaude Opus 4.8 8778398d31 feat(ui): layout polish — fill dead space, pair/box/collapse, warning weight, dialog glass
Implements the layout-improvement suggestions from the layout audit (visual arrangement
only; no functionality added or removed):

- Send/Receive: the recent-activity list now grows to fill the space below the fixed
  compose/receive card (more history visible), with a centered empty-state when there is
  none — instead of leaving dead canvas.
- Shield/Merge: pair the Fee and UTXO-Limit fields on one row to tighten vertical rhythm.
- Market: extend + frame the portfolio group-list as one contained panel (with a bottom
  edge) and center its empty-state, closing the previously un-anchored gap.
- Overlay dialogs: raise the card glass fill/border alpha (35/50 -> 60/90 of 255) so the
  dialog card reads as a distinct surface over busy backdrops (global, all overlays).
- Wallets: size the list height to the actual wallet count instead of always reserving 7
  rows, removing the large gap before the scan/create prompts (still scrolls when many).
- Contacts: width-aware address truncation shows more of the address on wide rows.
- Transfer Funds: give the "sends the full balance" warning a warning icon + color so the
  stakes stand out from the neutral result-preview lines.
- First-run wizard: collapse a completed Step 1 (Appearance) to the compact pill like
  Step 2, so a finished step is no longer taller than the active one.
- Explorer: distribute the Chain card's two stats to match the density of the sibling
  metrics grid.
- Validate Address: a "Results will appear here" caption fills the pre-interaction blank.
- Change Passphrase: add the warning banner its sibling security dialogs have.
- Migration ShowSeed: box the 24-word mnemonic grid (a GlassSectionScope behind the
  existing RenderSeedWordGrid) so the critical secret reads as a distinct artifact —
  purely visual, no seed/logic/state change.

Verified at 1280 across full-node + Lite + Windows (ctest green) and an adversarial diff
review (clean). New i18n key backfilled into all 8 languages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-19 13:56:13 -05:00
DanSandClaude Opus 4.8 9205addf55 feat(ui): width-responsiveness — content max-width cap + per-surface form/input clamps
Wide/ultrawide (1440-3440px) responsiveness was unhealthy: no page/content-level
max-width cap existed, and every card/form/table derived width from raw
GetContentRegionAvail().x with floor-only clamps, so surfaces stretched edge-to-edge
(2000-3000px inputs, ballooning cards, giant grid cells, 2000px+ dead row-voids).

Root cause: adopt the (previously dead-code) clamp helpers.
- New Layout::kContentMaxWidth() (~1600dp, tunable via ui.toml [layout]
  content-max-width; <=0 disables). Cap ##ContentArea to it and center the column in
  wider windows — every tab derives from this child, so one change tames the app at
  wide widths. No-op below the cap (fills as before), so 1080p/1440p are unaffected.

Per-surface upper-clamps (std::min(cap*dp, expr), floors preserved) where a single
element is still too wide even within the capped column:
- Settings: Theme/Layout/Language combos, the font-scale slider (~3000px -> 360dp),
  the effect sliders, Explorer URL and RPC credential fields.
- Send / Receive: cap the compose / receive cards to a readable form width and center
  them (Indent(pad+offset) so the auto-layout fields align with the hand-drawn card);
  the recent-tx lists below keep the full column width.
- Chat message bubbles + composer, mining pool URL/payout inputs + stats left/right
  split, contacts search, and the lite-network add-server row / server cards / status
  panel (capped + centered).
- Wizard: vertically center the cards when they fit (was top-anchored, leaving a void
  on tall monitors), compensating the content-height measurement so it can't oscillate.

The 1600 cap also subsumes the fixed-4-column balance grids (~400px cards) and the
right-anchored row dead-gaps (voids shrink from ~2700px to ~800px), so those are left
to the cap rather than blind column/row redesigns.

Verified at 1024/1280 (and via a temporary 900dp cap to exercise the cap+center path,
since the test display clamps to 1280) across full-node + Lite + Windows (ctest green)
and an adversarial diff review (clean). The true wide/ultrawide look and the 1600dp cap
value still want eyes on a real wide monitor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 23:15:39 -05:00
DanSandClaude Opus 4.8 e24ca015d1 feat(ui): implement UI/UX audit — i18n, HiDPI, theme, destructive-action & overflow fixes
Implements all 26 confirmed UI/UX audit findings plus the 4 dashboard/timeline
tile labels. Verified across full-node + Lite + Windows builds (ctest green) and
an adversarial diff review (one market-tab delete regression caught + fixed).

i18n coverage:
- Balance hero/quick-actions/toasts + dashboard & timeline tiles (Total Balance,
  Shielded, Transparent, Quick Send, Quick Receive, Click to open, Market)
- Send: Review Send / Cancel / Paste / view-only tooltip / memo byte counter
- Settings: Lite lifecycle errors, plaintext-RPC security warning, 8 toasts
- Mining pool-payout tooltip
- Resolve daemon_update_title double-assignment collision (new daemon_update_prompt_title)
- 23 new keys translated into all 8 locales; CJK subset font rebuilt

HiDPI: DPI-scale the Send amount bar, mining thread-tile clamp bounds, receive
loading skeleton, and sidebar notification-badge insets.

Light theme: theme-aware material::SurfaceOverlay() for balance bar tracks and
row-hover highlights; contacts active-pill foreground uses OnPrimary().

Destructive actions: arm/confirm for portfolio-group delete, saved pool/worker
remove, and avatar-image delete.

Interaction/overflow: route balance star/eye buttons and the console fold-toggle
through popup-safe guards; clip peer addr/subver; truncate the balance custom
label; measure Settings tool-button widths; mining stepper disabled-state
feedback; +/- stepper buttons restyled to match the thread tiles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 17:54:15 -05:00
DanSandClaude Opus 4.8 06afbee4f8 fix(mining): remediate mining-tab audit (22 findings) — crash-safety, async control, validation, math
Fixes all 22 confirmed findings from the mining-tab audit (10 Medium, 12 Low; 0 Critical/High),
adversarially reviewed (6 follow-ups found + fixed, incl. the review-caught idle-auto-start bypass
and a wrong benchmark-restore condition).

Crash-safety & lifecycle:
- M-04: join a stale/finished monitor thread in XmrigManager::start() and ~XmrigManager so an xmrig
  crash-then-restart (or quit) no longer std::terminate()s the wallet.
- L-03/L-10: surface an unexpected miner exit once and clear the stale running flag.

UI never blocks (M-03/L-06/L-08/L-09/L-13): pool start/stop now run on a dedicated serialized FIFO
mining-control thread (joined before teardown), so the ~13 call sites don't block the render thread on
stop()'s SIGTERM->SIGKILL->join; the spawn result marshals back to the UI.

Miner-process / pool trust boundary:
- M-01: validate the payout address (util::isValidRecipientAddress) at EVERY start path — the UI gate
  AND App::startPoolMining() (idle auto-start / thread scaling) — so a stale/wrong-chain address can't
  silently lose rewards.
- M-09: SSRF guard skips the background pool-stats GET for loopback/private/link-local/single-label hosts.
- M-02/L-02: cap the pool-stats + xmrig-API HTTP response bodies.
- L-01: write the xmrig config 0600 at creation (POSIX open with mode) — no world/group-readable window.
- M-10: reject shell-metacharacter binary paths before the version popen (excluding '()' so Program Files
  (x86) still works).

Solo mining: M-06/M-08 clamp thread count to [1, cores] at the setgenerate/xmrig boundary; M-07 notify +
don't lie on stop failure.

Correctness: L-05 block-time constant 75->150s (chainparams); M-05 discloses pool-mode "Est. Daily" as a
rough solo-equivalent; L-04/L-11/L-12 benchmark lifecycle (cancel on nav-away / mode-switch with restore,
skip rebalance mid-benchmark); L-07 honor cancel mid-extract in both the xmrig and daemon updaters.

Two new i18n keys back-filled across all 8 languages; CJK subset font rebuilt for the new glyphs.
Verified across full-node, lite, and Windows builds; tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 14:53:46 -05:00
DanSandClaude Opus 4.8 6ee81a5abe fix: security-audit remediation (15 findings), empty-wallet warning, and send/chat/console/shutdown UX
Security audit remediation (15 confirmed findings from the codebase audit):
- H-02: scrub+delete the decrypt-flow plaintext key export on ALL exit paths
  (RAII guard) and purge stale obsidiandecryptexport* files at startup.
- M-01/L-03/L-04/L-05/L-07: sodium_memzero the Set-PIN and encrypt-PIN worker
  passphrase/PIN copies, the RPC Basic-auth string (auth_), the exported/imported
  key buffers (App::wipeSecrets, called from ~App and before main's _Exit), and
  the first-run wizard "Skip" buffers.
- M-03/M-04/M-05/L-06: return locked COPIES from XmrigManager/EmbeddedDaemon
  getters (dedicated error_mutex_; DaemonController::lastError now by value),
  route xmrig last_error_ writes through a locked setter, and wrap
  shutdown_status_/wizard_stop_status_ in a locking GuardedStatus
  (wizard_stopping_external_ -> std::atomic).
- M-02: persist after a console send/shield/import in the lite backend.
- L-01: require the confirm click for z_shieldcoinbase/z_mergetoaddress.
- L-02: quote/escape each Windows daemon argv per the MSDN CommandLineToArgvW rules.
- L-08: pin json/tomlplusplus/libwebp FetchContent to immutable commit SHAs.
- I-01: extract updater archives from the already-verified in-memory buffer
  (no disk re-read TOCTOU).

Feature: warn once (full-node) when the active wallet loads empty while a sibling
wallet file in the datadir holds keys. A funded salvage wallet.<ts>.bak routes to
the recovery/Restore flow; a funded sibling .dat routes to the wallet manager.
Per-wallet-file dismissal; gated on synced + address-list-loaded to avoid false
positives on warm reconnect / spent-down wallets.

UX fixes:
- send: show the TOTAL balance (with a spendable "available" note) in the source
  dropdown and keep pending-change addresses visible.
- chat: insert emoji at the cursor position; restrict new-chat recipients to
  shielded (z) addresses.
- console: optional auto-focus of the command input on tab open (off by default).
- shutdown: when "stop external daemon" is on, keep the shutdown screen up until
  the external node actually exits, showing live status.

Adversarially reviewed; verified across full-node, lite, and Windows builds; tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 13:18:49 -05:00
DanSandClaude Opus 4.8 08aed34bbe fix(daemon): actually stop an external daemon on quit when the setting is on
"Stop external daemon" silently left an external dragonxd running. beginShutdown()
calls rpc_->requestAbort() (a sticky abort flag, cleared only by connect()) to
unblock in-flight requests; the shutdown thread's stopEmbeddedDaemon() then sent
the graceful "stop" over that same connection, so curl self-aborted it
(CURLE_ABORTED_BY_CALLBACK). doRPC swallowed the error but stop_sent was set true
anyway, skipping the temp-connection fallback that would have worked — so the
daemon never received "stop" and only died via the 20s by-name force-kill (which
collides with the 8s "Force Quit / may corrupt chain data" prompt, so it read as
"doesn't work").

Clear the abort before the shutdown stop and send it synchronously via
sendStopCommandSafely so real delivery success is surfaced (and the fallback can
still run on failure). The graceful stop now reaches the daemon, it exits in a
second or two, and the 20s stall / Force-Quit prompt no longer appears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-12 00:08:08 -05:00
DanSandClaude Opus 4.8 5edbe8a276 feat(recovery): redesign the wallet auto-recovery flow
Turns the "Daemon Error + raw log dump" moment into one calm, honest recovery
dialog plus a recovery-aware rescan screen. Presentation + orchestration only —
the file-safety logic in rebuildWalletDatabase()/restoreOriginalWallet() (source
selection, verify-before-swap, copy/rename-never-delete, .bak) is unchanged.

- One authoritative dialog with a phase machine Offer -> Working -> Done/Failed.
  The duplicate in-overlay recovery card, the untranslated red "Daemon Error"
  heading, and the raw daemon-log dump are gone for the recovery case (they stay
  for genuine, unrelated crashes).
- Offer is a choice-cards layout: "Repair automatically" (recommended, accent-
  tinted) vs "Restore original", side by side; the rare actions ("Show me the
  files", "Decide later") and a plain-language "What happens to my files?" sit in
  a quiet footer. When the rebuild helper is missing, it collapses to a single
  Restore card — never a dead end.
- Post-repair rescan shows a calm "Finishing your wallet repair" screen with
  elapsed time + the growing wallet size, instead of "RPC timeout / taking longer
  than expected / restart daemon"; the daemon-crash toast is suppressed and the
  detection toast is downgraded from red to info.
- Fixes a confirmed dead-end: if a repair succeeds but the restarted daemon then
  crashes for a *different* reason (block index, disk, OOM), the recovery flags
  now clear (in tryConnect + onConnected) so it surfaces as a normal daemon
  failure instead of freezing forever on a reassuring "don't restart" screen.
- Clickable "Wallet repair available" status-bar chip for re-entry.

The same app.cpp changes HiDPI-harden the surfaces the recovery flow lives on:
the status-bar and loading-overlay hand-drawn geometry are multiplied by dpiScale
(they rendered native-size and clipped at HiDPI / font_scale>1), the loading-
overlay status text wraps instead of running off both edges, and the node-status
banner floors its height to its DPI-baked font so the title can't clip off the top.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-10 22:31:55 -05:00
DanSandClaude Opus 4.8 ac49f44f84 fix(node): show recovery actions in the daemon-error overlay (not a separate dialog)
Reported with a screenshot: on a salvage-then-abort, the status correctly read
"Wallet needs recovery — see the prompt" but no prompt appeared — the separate
BeginOverlayDialog is occluded by the full-frame loading/daemon-error overlay
that's drawn every frame while the node is down.

Render the recovery actions directly IN the daemon-error overlay when a salvage is
detected: a concise message + prominent one-click "Rebuild wallet database" /
"Restore original" / "Open data folder" buttons (same handlers as the dialog),
placed right after the title and skipping the verbose daemon-output dump so they
stay on-screen. The verbose diagnostics + crash-count hint still show for
non-recovery errors. Build clean, suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-10 12:28:48 -05:00
DanSandClaude Opus 4.8 bc183257b7 feat(node): in-app "Rebuild wallet database" recovery for a BDB-inconsistent wallet
Automates the manual recovery that fixed a wallet.dat with stale Berkeley DB
extent metadata (the "main" subdb metapage records a low last_pgno while its live
data spans thousands of pages beyond it). A tolerant page-walk reads every record,
but the daemon's BDB verify rejects the file and auto-salvages it — finding nothing
and shrinking the wallet to empty on each restart (the salvage cascade that looks
like fund loss). Plain "Restore original" can't fix it (hands the same broken file
back → re-salvage); a rebuild must produce a fresh, consistent DB.

Pieces (Approach A from the design workflow — out-of-process helper keeps AGPL
Berkeley DB out of the GPLv3 GUI):
- util/wallet_file_probe.h: extractWalletBtreeRecords() — sibling to parseWalletBtree
  that collects raw (key,value) bytes (same bounds-checked, subdb-aware walk).
  Records copied verbatim → encrypted key material passes through as opaque
  ciphertext (no passphrase). Overflow-page values (only large tx history) are
  skipped + counted; a rescan rebuilds history — funds unaffected.
- tools/wallet_rebuild/main.cpp: dragonx-wallet-rebuild CLI — reads via the tolerant
  reader, writes the records into a fresh BDB "main" btree via libdb (DB_EXCL, never
  overwrites), prints a JSON summary. New BDB-guarded CMake target.
- App::rebuildWalletDatabase(): picks the largest readable wallet/.bak as source,
  stops the daemon, runs the helper, VERIFIES the output (readable BDB with keys)
  before swapping, moves the current wallet aside (kept, timestamped), installs the
  rebuilt one, clears the stale BDB env, sets -rescan, restarts. Copy/rename only —
  never deletes. Result surfaced via the existing pumpWalletRestore channel.
- Wired as the preferred action on the existing wallet-auto-recovery dialog
  (shown only when the helper is present). Full-node only; lite-safe.

Verified end-to-end against the real broken wallet: helper reads 3,808 t-keys + 1
z-key + HD seed and the daemon LOADS the rebuilt output with no salvage. Adds
extractWalletBtreeRecords coverage. Build clean, suite green (1/1).

Remaining (follow-up): release packaging — build.sh bundling the helper built
against the vendored per-platform static libdb (DRAGONX_BDB_ROOT), and a macOS
Berkeley DB port (no in-tree artifact).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-10 11:26:55 -05:00
DanSandClaude Opus 4.8 384d64ea5d feat(node): one-click "Restore original wallet" after a daemon auto-recovery
Adds the restore action to the wallet-auto-recovery warning: undo the daemon's
salvage by swapping the untouched original (wallet.<ts>.bak) back over the
salvaged copy and clearing the stale BDB env that triggered the false recovery,
then restarting. Modeled on beginAdoptSeedWallet (stop daemon → file ops →
restart on a worker; result pumped to the main thread for notifications).

Safety (fund-adjacent file ops on a real wallet — copy/rename only, never delete
user data):
- picks the newest wallet.<unixtime>.bak via the pure, unit-tested
  newestWalletSalvageBak(); aborts if none.
- verifies the .bak is a real Berkeley DB (probeWalletFile) before touching
  anything — won't overwrite a working wallet with a bad backup.
- stops the daemon first (stopDaemonForWalletSwitch) so wallet.dat is released.
- moves the salvaged copy aside to wallet.dat.salvaged-<ts>.dat (kept), COPIES
  the .bak into place (the .bak stays), moves database/ aside to
  database.pre-restore-<ts>.bak (kept), and drops only the transient __db.*
  BDB region files. Rolls back the move if the copy fails.
- relaunches the node even on failure so it's never left down.

The warning dialog now offers Restore original wallet / Open data folder /
Keep salvaged copy. Full-node only; lite-safe. Build clean, suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-10 00:45:47 -05:00
DanSandClaude Opus 4.8 3b7423f3a1 feat(node): warn when the daemon auto-recovers (salvages) wallet.dat
dragonxd auto-recovers a wallet.dat that fails BDB verification on startup — no
flag needed (CWallet::Verify → CDBEnv::Verify(walletFile, CWalletDB::Recover)):
it moves the original to wallet.<timestamp>.bak, salvages readable keys into a
fresh wallet.dat, and keeps running. The salvage can be incomplete (or the whole
thing a FALSE POSITIVE from stale/cross-platform BDB env state — __db.* / the
database/ dir carried between machines), so the node silently comes up on a
possibly-empty wallet. To the user that reads as fund loss, with no warning.

Detect it and warn loudly instead:
- daemon/daemon_startup_diagnosis.h: pure walletAutoRecovered() (the salvage /
  "Original wallet.dat saved as wallet.<ts>.bak" markers) + newestWalletSalvageBak()
  (picks the wallet.<unixtime>.bak the recovery just made).
- onConnected() scans the node's captured output once per session; on a match it
  shows a warning dialog + notification: the ORIGINAL is safe in wallet.<ts>.bak,
  the shown balance may be incomplete, and here are the exact steps to restore it
  (rename the .bak back + delete the stale database/ + __db.* env). One-click
  "Open data folder" jumps straight there. Full-node only; lite-safe.

Deliberately does NOT auto-swap the wallet files (untested per-platform file
manipulation on a real wallet is not worth the risk) — it informs + guides.

Adds walletAutoRecovered / newestWalletSalvageBak coverage to
testBlockDbOutputDiagnosis. Suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-10 00:30:11 -05:00
DanSandClaude Opus 4.8 d136916e80 feat(node): detect an unreadable block DB on startup and offer a one-click reindex
When a daemon update changes the block-index on-disk format (or the index is
corrupt), dragonxd aborts at startup — "non-canonical optional discriminant" →
"Error loading block database. Aborted." — and the wallet silently shows a zero
balance. Previously the connect loop just crash-restarted into the same abort up
to 3x and then reported a bare "Daemon crashed N times", with no path forward.

Now:
- daemon/daemon_startup_diagnosis.h: pure blockDbOutputLooksBroken() classifies
  the crashed node's captured console output (the fatal block-DB markers).
- The connect loop detects it on the FIRST abort, STOPS crash-restarting into the
  same failure (each retry reloads the whole index — wasteful), and offers a fix.
- A one-shot -reindex flag (EmbeddedDaemon::setReindexOnNextStart → DaemonController
  forwarder → args) rebuilds the block index + chainstate from the intact raw
  blocks; App::reindexBlockDatabase() arms it and un-gates the loop to restart.
- An auto-shown dialog (renderBlockDbReindexDialog) + a notification explain the
  situation ("your coins are safe; the node just can't load the chain") and offer
  a one-click "Rebuild block database". Full-node only (gated), lite-safe.

This is the exact trap behind a real "big wallet shows no funds" report: a
post-format-change daemon over pre-change chaindata. Reindex also fixes a plain
corrupt index.

Adds testBlockDbOutputDiagnosis (the abort sequence + individual markers trip it;
normal startup / wallet-corruption / asmap errors do not). Suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-09 23:49:46 -05:00
DanSandClaude Opus 4.8 5296dd7ae5 fix(parsers): harden RPC/price/updater parsing against valid-but-unhandled input
Audit of the full-node RPC response parsers and updater release-body parsers
(find -> adversarially-verify workflow) surfaced three worth fixing; three
others guard formats the project doesn't emit and are backstopped by signature
verification, so they're documented rather than churned.

- Price (Medium): parseCoinGeckoPriceResponse used .value(key, 0.0), which
  throws type_error on a PRESENT null. CoinGecko emits null for usd_24h_change/
  usd_24h_vol on illiquid tokens (DRGX is one) while still returning a valid
  spot price; the outer catch turned that into no price update at all. Read
  null-tolerantly so the valid usd/btc survives.

- Daemon updater (Medium): parseDaemonChecksums blanked '|'/backtick but not
  markdown emphasis, so a bolded **archive.zip** checksum row was dropped and a
  valid, correctly-signed release would be refused. Also blank '*'/'_' (cannot
  cause a wrong-asset match; the 64-hex + .zip-suffix tests are unchanged).

- Opid poll (Low, severe failure mode): parseOperationStatusPoll read id/status
  via .value() (throws on a present non-string) and the call site parsed OUTSIDE
  its try/catch, so a throw left opid_poll_in_progress_ stuck true and wedged all
  z-operation polling for the session. Type-check the reads and parse inside the
  guard. (dragonxd can't emit non-string id/status; this is defense-in-depth.)

Regression tests: CoinGecko null field; opid non-string id/status; **bold**
checksum row. Suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-09 13:38:50 -05:00
DanSandClaude Opus 4.8 a1d3964e34 fix(lite): require exactly 24 words on first-run restore (crash on valid seed)
The lite first-run restore wizard enabled Restore for {12,15,18,21,24}-word
phrases, but the SDXL backend only accepts 24-word / 32-byte-entropy seeds:
LightWallet::new does copy_from_slice(&phrase.entropy()) into a [u8;32]
(lightwallet.rs:231), which panics on 16/20/24/28-byte entropy. Mnemonic::
from_phrase accepts the shorter valid phrase, and the restore FFI
litelib_initialize_new_from_phrase (lib.rs:127) has no catch_unwind (unlike
litelib_execute), so the panic unwinds across extern "C" -> process abort
(UB on the pinned rustc 1.63). A user restoring a legitimate 12-word seed
from another wallet crashed the app.

The Settings restore gate was already tightened to == 24 (6ff1fda) but the
first-run wizard gate (df14533) was never updated — same restore path, two
verdicts, crash only via the more-common first-run path.

Add shared util/seed_phrase.{h,cpp} as the single source of truth:
- normalizeSeedPhrase: fold NBSP/en/em/ideographic/narrow spaces to ASCII,
  strip zero-width marks, collapse+trim (word bytes untouched)
- seedPhraseWordCount
- isCompleteRecoveryPhrase(int) == 24  (the sole SDXL contract)

Both restore gates now count via the normalizer and gate via
isCompleteRecoveryPhrase, and both submit the normalized phrase. This closes
the crash, reconciles the two gates so they can't drift again, and — because
tiny-bip39 splits on literal ASCII space with no NFKD — makes an NBSP-pasted
24-word seed (common from PDFs/note apps) restore correctly instead of being
undercounted and rejected.

Adds testSeedPhraseHelpers. Suite green (1/1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-09 01:32:40 -05:00
DanSandClaude Opus 4.8 c61c211dfe fix(ui): add vertical padding above and below the recent-alerts popup content
The alert-history popup content sat flush against the popup's top and bottom edges. Add a
padY spacer above the header and below the content (on both the empty and populated paths).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 23:12:20 -05:00
DanSandClaude Opus 4.8 8bb3198562 fix(diagnostics): address adversarial review of the QoL UI (popup, staleness, DPI, i18n)
Follow-up to the node-banner / staleness-badge / alert-history features — a 5-dimension
finder->verify review surfaced 4 real issues (the ImGui-stack-balance finder found none):

- Alert popup grew off the right edge: pivot (0,1) pinned the panel's LEFT edge at the
  bell, which sits near the window's right edge, so a 320px panel overflowed rightward
  (an explicit SetNextWindowPos pivot skips ImGui's on-screen clamp). Anchor the
  bottom-RIGHT corner at the bell instead (pivot (1,1) at bellMax.x) so it grows left.

- Staleness badge could flash red on reconnect: WalletState::clear() reset everything
  except the four last_*_update stamps, so the pre-outage timestamp survived and the
  badge briefly showed "Updated Nm ago" the same frame the node banner cleared. Zero the
  stamps in clear() (all readers treat 0 as "never"; app_network.cpp:1473 guards != 0).

- Banner min-height floor wasn't DPI-scaled: std::max(minH, baseH*vScale()) now uses
  minH * dpiScale() so both operands are in scaled px.

- New i18n keys weren't in res/lang/: back-filled all 16 diagnostics/QoL keys into the 8
  language files, additively (128 insertions, 0 deletions). zh/ja/ko reworded around 2
  glyphs missing from the CJK subset and hard-asserted tofu-free against the subset font.

Build-clean both variants; ctest 1/1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 21:14:38 -05:00
DanSandClaude Opus 4.8 4b3f0fa92b feat(diagnostics): persistent alert history with a status-bar bell (Foundation QoL)
Toasts fade in 1-4s, so anything that scrolled past was gone. Notifications now retains
every pushed alert in a capped (100) ring buffer with a wall-clock epoch (AlertRecord) —
separate from the 5-item live-toast deque — plus a monotonic total_pushed_ counter.

A bell in the status-bar right cluster opens an upward popup listing recent alerts
newest-first: severity icon + colour (reusing the toast palette), the message, and a
relative age (formatTimeAgoShort), with a Clear-all action. An unread dot on the bell,
coloured by the most-severe unseen alert, marks alerts that arrived since the panel was
last opened — driven by totalPushed() deltas so it survives capping/clearing.

Thread note: every push is on the UI thread (RPC results run as main-thread MainCb
callbacks), matching this class's existing lock-free model; documented as a
no-raw-worker-thread invariant.

New i18n keys (alerts_*). Build-clean; ctest 1/1 (adds testNotificationHistory: retention,
order, cap, monotonic counter, clear). Closes the QoL bundle and the Foundation tier.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 20:57:02 -05:00
DanSandClaude Opus 4.8 e779ded2e8 feat(diagnostics): persistent node/RPC error banner at top of content (Foundation QoL)
A persistent horizontal strip now appears at the top of the content column whenever the
wallet can't reach its node — unlike the transient toasts it stays up for as long as the
fault persists, so an offline wallet is never silently mistaken for a working one.

The show/severity/action decision is a pure, unit-tested function
(ui/node_status_banner.h::evaluateNodeStatusBanner) fed a state snapshot by the new
App::renderNodeStatusBanner(). Three cases:
  - full-node offline        -> amber, "Reconnect"    (App::tryConnect)
  - embedded daemon crashed
    & auto-restart gave up    -> red,   "Restart node" (App::restartDaemon)
  - lite wallet open failed   -> red,   message-only

Suppressed during the wizard / wallet-switch / daemon-restart / screenshot-sweep / shutdown,
and while an expected startup phase (warmup / init / connect-in-progress) already owns the
screen. Banner height lives in res/themes/ui.toml (banners.node-status); colours come from the
material semantic palette; the detail text is ellipsis-clipped so it can't push the action
button off-screen. Drawn before the content edge-fade vertex capture so it stays fully opaque.

New i18n keys (node_banner_*). Build-clean both variants; ctest 1/1 (adds testNodeStatusBanner).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 20:21:05 -05:00
DanSandClaude Opus 4.8 940dd21464 feat(diagnostics): add "Copy diagnostics" + "Open log folder" actions (Foundation QoL)
Settings (logging section) gains two support-friendly actions, now that the logging
foundation actually produces logs (W7-2):

- Open log folder: opens the config dir (Platform::openFolder) so users can find
  dragonx-debug.log / dragonx-crash.log.

- Copy diagnostics: copies a plaintext support snapshot to the clipboard via the new
  App::buildDiagnosticsReport() — version, build variant, platform, connection status,
  active wallet path + existence + size, encryption/lock state, sync heights, and (full-
  node) daemon status/running/crash-count/lastError, plus the log paths. No secrets.

Build-clean; ctest 1/1. Remaining QoL: persistent alert history, a daemon/RPC error
banner, and the W6-2 refresh-staleness badge. See docs/wallet-hardening.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 15:37:52 -05:00
DanSandClaude Opus 4.8 f9ddab059e fix(wallet): stamp syncedHere only after identity verified + guard the startup wallet file (W1-3)
- W1-3 (Med): updateWalletIndexForActiveWallet stamped syncedHere in the markOpened block
  at bare connect (idHash still empty), letting a freshly-restored wallet skip its needed
  rescan. syncedHere is now stamped only once the wallet's identity is verified (idHash
  non-empty), so it takes effect at the post-address-refresh index update; lastOpenedEpoch
  still records at open.

- Startup guard (the W1-1 launch counterpart): App::init now exists()-checks the recorded
  active wallet before the daemon is configured. A non-default active wallet moved/deleted
  between sessions falls back to the default wallet.dat with a warning, instead of the
  daemon silently auto-creating an empty wallet under the missing name. Runs before the PIN
  vault init so the vault is scoped to the wallet actually opened.

Completes P1-B. Remaining P1: W3-3 (sweep opid persistence) deferred for careful
adversarially-reviewed work — re-tracking a stale opid could hang the migration if the op
poller doesn't time out; the existing balance/mined gates already prevent fund loss. See
docs/wallet-hardening.md.

Build-clean; ctest 1/1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 15:18:09 -05:00
DanSandClaude Opus 4.8 eb69e491b9 fix(startup): surface a "taking too long" notice when the daemon won't come up
F3: the daemon connect loop retried forever with only an animated spinner when the
daemon was reachable-but-never-ready (stuck in RPC warmup / -28, or an external daemon
that never finishes init) -- no error, no guidance, no escape. It now stamps
connect_stall_since_ the moment the daemon first goes "reachable but not ready" (the
warmup branch + applyDaemonInitStatus) and clears it on connect / disconnect /
warmup-complete. A pure, unit-testable util::connectHasStalled() helper (new
util/connect_stall.h, 45s default from ui.toml [screens.loading].stall-timeout-sec)
drives a "Taking longer than expected" notice in renderLoadingOverlay(): a title, a
reassuring body with elapsed seconds, and a full-node hint to Settings > Restart Daemon
or the Console. The background retry keeps running underneath, so the notice self-clears
the instant it connects. Guarded off while the daemon is in State::Error (that case is
owned by the existing crash-count hint).

The overlay is a pure draw-list layer with no interactive widgets, so this follows the
existing crash-hint idiom (guidance text, not injected buttons); the stalled state is
computed locally in the overlay, so the only new App member is connect_stall_since_.

Adds testConnectHasStalled to test_phase4.cpp and three i18n keys to i18n.cpp (English
source of truth; the res/lang/*.json back-fill is deferred to a single
add_missing_translations.py run at the end of the batch).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 11:33:26 -05:00
DanSandClaude Opus 4.8 2675b8ab93 fix(startup): surface filesystem failures and verify Sapling param integrity
Three verified daemon-startup edge-case fixes centered on the config/params
filesystem path:

- F7: new non-throwing Platform::ensureDirectory(dir, outError) with one
  consistent "Cannot create <dir>: <reason>. Check permissions / free space."
  message. Replaces the unchecked/throwing create_directories sites at main.cpp
  (pre-init: log + Windows MessageBox + return 1), connection.cpp's
  autoDetectConfig (was the *throwing* overload -- could raise an uncaught
  filesystem_error through its callers; now sets the new
  ConnectionConfig::dir_error), and both app.cpp daemon-dir sites (surface via
  daemon_status_ + return false). The primary connect path (app_network.cpp)
  checks dir_error and shows it instead of mislabelling it "waiting for config".
  embedded_resources.cpp already checked its error_code, so it is left as-is.

- F6: verifySaplingParams() now hash-verifies each param against its pinned
  canonical SHA-256 (source of truth: scripts/build-lite-backend-artifact.sh)
  instead of only checking existence, so a truncated / corrupt-but-present param
  is rejected up front rather than failing later on a shielded operation. A
  <params_dir>/.sapling_verified marker keyed on size:mtime avoids re-hashing
  ~48MB on every startup. Logic extracted to the injectable, unit-testable
  verifySaplingParamsIn(dir, digests); reuses util::sha256Hex (no new hash impl).

- F5: startEmbeddedDaemon() now checks extractEmbeddedResources()'s return and
  the previously-dropped copy_file error_code in the daemon-binary fallback loop,
  aborting with a clear status (sb_daemon_extract_failed / sb_daemon_files_failed)
  instead of failing opaquely at spawn. An absent source file stays non-fatal.

Adds testPlatformEnsureDirectory and testVerifySaplingParams to test_phase4.cpp.
i18n keys added to i18n.cpp (English source of truth); the res/lang/*.json
back-fill via add_missing_translations.py is deferred to a single run at the end
of the batch. Progress tracked in docs/daemon-startup-hardening.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-02 11:07:32 -05:00
DanSandClaude Fable 5 bcee4bfe72 fix(security): apply audit remediations to dev (ported + dev-only)
Dev-branch audit (docs/_archive/security-audit-dev-2026-07-22.md) re-found the
master issues (absent on dev) plus new ones in dev-only code. Applied here;
full-node build + test suite green; the subtle fixes were adversarially re-verified.

Ported from the master remediation (adapted to dev's code):
- bootstrap: reject zip-slip / path-traversal archive members (isSafeArchivePath)
  before writing (S2-1). (dev already fail-closes on a missing checksum.)
- xmrig updater: fail closed when a signature is required but no key is pinned (F1-1).
- http_download.httpGetString: 16 MiB hard cap + MAXFILESIZE on the shared
  metadata/price fetch (F1-2 / caps the updater + exchange paths at one site).
- rpc_client: explicit SSL_VERIFYPEER/VERIFYHOST (F4-1) and a 256 MiB response
  cap in WriteCallback (F4-3).
- lite_connection_service: reject remote http:// lite servers, loopback only (L1-1).
  Loopback is matched by a strict dotted-decimal 127.0.0.0/8 check (not a
  startsWith("127.") prefix, which would wrongly accept 127.0.0.1.evil.com), with
  userinfo/fragment stripping.
- lite controller: propagate encrypt/decrypt save() failure instead of reporting
  success (F7-1).
- xmrig_manager: chmod(0600) the pool config before writing secrets (F5-2).
- app: clear the copied secret from the OS clipboard on shutdown (F3b-1).
- export_transactions: neutralize CSV/spreadsheet formula injection (F13-1).
- build pipeline: build-from-source lite backend + remove the self-attested
  CMake signature gate (F15-1); pinned+verified appimagetool (F15-3/4);
  verified Sapling params in setup.sh (F15-6); build.sh exits 0 on success.
  (F14-1 empty-quoted-arg and F8-2 NUL-termination were already fixed on dev.)

Dev-only findings:
- rpc_client.callRaw: scrub the raw buffer + parsed tree (templated scrubJsonSecrets
  for ordered_json) so console dumpprivkey/z_exportkey keys don't linger in freed
  heap (N1-1).
- seed_wallet_creator: wipe the exported mnemonic on the failure path so a discarded
  failed result never carries a live seed (W1-2).
- export_all_keys: write the plaintext key dump 0600 + atomically via
  writeFileAtomically (U1-2).
- chat_database: restrict chat_messages.sqlite and its WAL/SHM sidecars to owner-only (C3-1).

Not done (need a decision, documented in the report):
- Chat header metadata (cid/z/p) rides outside the AEAD (C1/C2) — binding it is a
  wire-protocol change requiring SilentDragonXLite interop review.
- Bootstrap lacks an offline-rooted signature (S2-2 residual) — needs signing infra.
- Console scrollback retains console-typed key-export output in plaintext (N1-1
  residual) — inherent to an echoing console; would need output redaction.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 12:08:24 -05:00
DanSandClaude Opus 4.8 203967411a feat(chat): theme the :drgx: emoji to the accent (was fixed brand colors)
The custom DragonX chat emoji now recolors to the theme like the logo — body =
accent, detail = white on dark skins / on-surface (dark) on light skins — and
re-rasterizes on a theme/dark-light change (moved out of the one-time fixed-color
load into ensureLogoTexture's re-render block). The detail highlights keep it
legible even on accent-tinted outgoing bubbles.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:12:44 -05:00
DanSandClaude Opus 4.8 3d0305a9ca fix(ui): darken the logo SVG detail on light skins
The SVG's white highlight (detail) washed out against a light card/background on
light skins — the dragon's wings/detail nearly vanished, leaving only the accent
body. Make the detail colour theme-aware: white on dark skins (unchanged), the
theme's on-surface colour (dark) on light skins, so the full mark reads on both.
Applies to the header logo and the balance coin icon; re-rasterizes on the
dark↔light flip (already tracked by the logo guard). The fixed-brand ":drgx:"
emoji is unaffected (its crimson body carries white detail fine on any bubble).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:09:03 -05:00
DanSandClaude Opus 4.8 e7f38c2a45 fix(ui): theme the balance-card coin icon per skin too (not just the header)
The prominent DragonX mark on the Overview (next to the balance) is the coin /
currency icon (coin_logo_tex_), a separate texture from the header logo — and it
was still loaded from the fixed logo_dragonx_128.png, so it never recolored when
switching themes. Route it through the same themed SVG: ensureLogoTexture() now
rasterizes BOTH the header logo and the coin icon from logo_dragonx.svg recolored
to the theme accent, re-rasterizing both together on a skin / dark-light change
(coin done first, since the header branch early-returns on success). The old
one-shot PNG coin-logo load in render() is removed (superseded), the PNG remains
a fallback, and the coin texture is now freed on theme switch (was leaked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 14:02:13 -05:00
DanSandClaude Opus 4.8 3a2be661ea feat(chat): custom DragonX emoji via the :drgx: shortcode
Add a DragonX custom emoji to chat. The emoji picker gains a DragonX tile (first
cell) that inserts the text ":drgx:"; message bodies containing it render the mark
inline via layoutChatBodyRich, which flows text words + the emoji image with word
wrap (plain bodies keep the tighter layoutChatBody path, so normal messages are
unaffected). Other clients simply show the literal ":drgx:" text — a portable
encoding with graceful degradation.

The emoji is the DragonX SVG rasterized once at fixed brand colors (crimson body,
white detail) via LoadTextureFromSvg — theme-independent so it looks identical for
sender and receiver — exposed as App::getDrgxEmojiTexture(). Emoji insertion is
factored into one insertToken() helper (space-prepend + on-chain byte cap), shared
by the DragonX tile and the Unicode emojis. Drag-to-select text is skipped on
":drgx:" messages (their inline layout doesn't match the plain-text hit-test
geometry); right-click "copy" still copies the whole message.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:42:30 -05:00
DanSandClaude Opus 4.8 17525003c5 feat(ui): themed DragonX logo — runtime-rasterized SVG recolored per skin
Replace the shared app logo (Overview header, first-run wizard, lock screen) with
the DragonX mark rendered from res/img/logos/logo_dragonx.svg, recolored to each
theme: the dragon body takes the theme accent (Primary()), the inner detail stays
light. Vendor nanosvg (memononen, zlib/public-domain) in libs/nanosvg/ and add
util::LoadTextureFromSvg (parse a copy, recolor shapes by luminance, rasterize to
RGBA, upload via the existing CreateRawTexture — GL + DX11). The SVG is embedded as
a string (src/embedded/logo_dragonx_svg.h) so it's available in every build.

The load moves into App::ensureLogoTexture(), called at the top of render() BEFORE
the wizard/lock early-returns (so those screens get the logo too, which they never
did before), and re-rasterizes when the accent or the dark/light variant changes.
The per-skin PNG remains a fallback if rasterization ever fails; logo-texture
lifetime is now freed on replace + on theme switch (was leaked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:28:51 -05:00
DanSandClaude Opus 4.8 b0a4333cdf feat(chat): pre-split note buffer for rapid sends + status + console logging
Each chat message is a shielded tx that spends a verified note, so a burst of
messages hit "insufficient verified funds" once the single note is spent. Add a
per-frame note-buffer coordinator (both variants) that keeps ~10 verified
spendable notes: it serializes sends through the one broadcast channel, counts
verified notes by block depth (lite) or a rate-limited z_listunspent scan
(full node), self-splits in the background to refill toward the target, and
queues overflow to drain as change matures — with honest Sent/Failed status
instead of the prior optimistic "Sent". Guardrails: single split in flight with
a watchdog, cooldown, and session-generation guards so a wallet switch can't
drain another wallet's queue. Surface a "Chat buffer: N/10 ready" indicator in
the status bar while the Chat tab is active, and route chat diagnostics through
the console on both variants (the full-node console now drains the shared ring).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 22:00:00 -05:00
DanSandClaude Opus 4.8 5c570613c8 feat(console): lite backend command-reference modal
Give the lite console the parity analog of the full-node RPC command
reference: the same searchable two-pane modal (browse/search, detail
pane, examples, Insert / Insert & run, destructive confirm), driven by
the lite backend's own command set instead of daemon RPC.

- ConsoleCommandExecutor::commandReference() returns the category table
  (full node = consoleCommandCategories(); lite = new
  liteConsoleCommandCategories() -- 25 backend verbs in 5 categories).
  The shared renderCommandsPopup reads the table from the executor.
- The Commands button now shows for both variants (gated on
  commandReference()!=nullptr); title/tooltip/arg-quoting branch on
  hasRpcReference() (clarified to mean "speaks JSON-RPC / full node").
- Lite args are bare tokens (the backend takes one unsplit arg string
  and does not strip quotes), so the param-builder's JSON string
  auto-quoting is disabled for lite -- Insert & run emits runnable bare
  commands. send uses the JSON-array form (its positional form is
  unreachable via the single-arg transport); new uses zs/R; import
  takes just the key (the backend hardcodes birthday=0).

Adds 7 i18n keys across all 8 languages (additive). Full-node behavior
is unchanged. Adversarially reviewed (data vs the backend registry,
plumbing/regression, i18n) with all findings fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-19 12:27:51 -05:00
DanSandClaude Opus 4.8 3b5db02e09 feat(app): font-atlas rebuild hook + clock/emoji sync
- requestFontRebuild() + preFrame handling so toggling color emoji swaps the atlas live;
  request a rebuild at startup when the saved setting wants color.
- preFrame syncs Typography's color-emoji flag and the util clock flag from settings.
- Chat 0-conf fast-scan cadence now reads the user's poll-rate setting (was a hardcoded 2.5s).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 18:44:55 -05:00
DanSandClaude Opus 4.8 125ffa8863 feat(chat): dedicated ~2.5s poll for the 0-conf fast-scan
The fast-scan was hung on the page's Transactions timer, which is 10–15s on most
pages (15s on Chat), so incoming messages still took up to ~15s to appear. Give it
its own ~2.5s accumulator (delta-time based, independent of the page cadence) so
messages land in a few seconds — network propagation then dominates, which is as
fast as 0-conf gets. Still gated behind the warmup/rescan block and self-gated in
fastScanChatMemos; the in-flight guard prevents overlapping RPCs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:58:42 -05:00
DanSandClaude Opus 4.8 30bd0d99ff feat(chat): 0-conf fast-scan so incoming messages show before a block
The receive harvest gates each z-address behind scannedAtTip — it only re-scans
when a new block advances the tip — so incoming chat waited ~1 confirmation even
though the daemon already exposes mempool notes (FindMySaplingNotes runs on
mempool txs; z_listreceivedbyaddress(addr,0) returns them).

Add App::fastScanChatMemos(): every transaction-refresh cycle, re-scan JUST the
chat reply address (where peers send) at minconf=0, extract chat metadata, and
ingest — so messages surface at mempool speed (a few seconds) instead of waiting
for a block. Full-node only (lite has its own harvest). An in-flight guard avoids
stacking RPCs; the store dedups on txid+position, so the confirmed harvest never
double-inserts.

Hidden conversations are deliberately skipped by the fast path — they don't get
the mempool speed-up and still come back through the normal confirmed harvest
(which un-hides on a new message). New non-muted messages toast off-tab as usual.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 00:38:41 -05:00