Commit Graph

603 Commits

Author SHA1 Message Date
5874142186 fix(wallets): don't mislabel a legacy HD wallet as "Seed phrase"
The Wallets modal's "Seed phrase" vs "Legacy" badge came from the offline
wallet.dat probe, which flags hdseed/chdseed/hdchain. But hdchain is present in
BOTH a BIP39-mnemonic wallet AND a legacy HD wallet (DragonX has no separate
mnemonic DB record — the mnemonic is derived from the HD seed), so a legacy HD
wallet was shown as "Seed phrase".

For the ACTIVE wallet the app already knows the truth at runtime via
z_exportmnemonic (wallet_seed_status_). The active row's badge now uses that
authoritative status (activeWalletSeedBadge: seed-phrase / legacy / undecided)
and only falls back to the probe for non-active or not-yet-decided wallets; the
"unknown" seed badge is suppressed once the runtime status is authoritative.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 16:02:50 -05:00
928335dd6b fix(wallet-switch): late-fail revert, shutdown-freeze, per-wallet PIN gate
The two deferred audit MEDs + the per-wallet PIN follow-up:

- Late-init failure revert (MED): the 1.5s start grace only catches a wallet that
  fails IMMEDIATELY. A wallet that fails LATE in dragonxd init (past the grace)
  used to persist as a broken active_wallet_file. Now a switch stays
  "pending confirm" until the daemon actually connects (onConnected clears it);
  if the connect loop instead hits the crash-wedge (crashCount >= 3) while a
  switch is pending, it flags the main-thread revert (which also resets the crash
  count so the restored wallet can start).
- Shutdown freeze (MED): the switch worker's 30s daemon-stop wait now breaks
  promptly when shutdown starts, so beginShutdown's join of the switch task can't
  freeze the UI for the full 30s (shutdown stops the daemon itself). The seed-
  adopt task is intentionally left to finish (fund-safety), as before.
- Per-wallet PIN (follow-up): App::hasPinVault() (and the lock-screen path) now
  gate on the per-wallet vault presence alone, not the GLOBAL getPinEnabled flag —
  so disabling PIN on one wallet no longer suppresses another wallet's PIN
  quick-unlock after a switch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 15:41:03 -05:00
63dcda0ad1 fix(wallet-switch): close gaps found verifying clusters A/B/C
Adversarial verification of the audit fixes found real gaps in them:

- HIGH throw-safety: the switch worker and the encryption-restart worker set
  daemon_restarting_=true but reset it only on their normal/early-return paths —
  a throw from stop/startEmbeddedDaemon left the flag stuck true, wedging
  reconnect and every future switch/rescan/encryption. Both now reset it on all
  paths (try/catch); a throw during a switch is treated as a failed switch and
  triggers the revert.
- HIGH residual chat leak: resetChatSession() cleared the flags but an already-
  posted z_exportmnemonic worker job still held wallet A's secret, and its
  completion callback (guarded only by isLocked(), false for an unencrypted
  wallet) would provision A's identity under B. Add a chat_session_generation_
  epoch bumped on every wallet change; the fetch captures it and its callback
  discards the (previous-wallet) secret if the epoch no longer matches.
- LOW vault-scope collision: the per-wallet vault tag was a lossy char-substitution
  (two distinct files could map to one vault). Append an 8-hex FNV-1a of the raw
  filename so distinct wallets never share a vault. +unit test.
- LOW seed-adopt: removeVault() on adopt so the legacy wallet's PIN passphrase
  isn't left associated with the new seed wallet (same file name).
- LOW: clear lock_unlock_in_progress_ on switch too.

Deferred (documented): the 1.5s start grace can't catch a wallet that fails LATE
in daemon init (the existing crash-wedge detection still applies); beginShutdown's
join of the switch task can briefly freeze the UI during quit (necessary to avoid
orphaning the daemon).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 13:06:56 -05:00
122db9d903 fix(wallet-switch): cluster C — no mis-scoping in the unknown-identity window
From the audit: while a switch/first-load is in flight the wallet identity hash
is empty, so contact/portfolio scope-writes silently fell back to "global"
(leaking a wallet-specific entry into every wallet) and scope-filters showed
every wallet's scoped entries.

- Contacts: a NEW wallet-scoped contact created while the identity is unknown is
  now refused with a clear message (tick global or wait); editing an existing
  scoped contact preserves its scope instead of demoting it to global.
- Portfolio: the "Add group" button is disabled while the identity is unknown
  (with a tooltip), so a new group can't get an empty/global scope.
- Both views' visibility filters now show ONLY global entries when the identity
  is unknown — never another wallet's scoped contacts/groups — instead of
  showing everything.
- +2 i18n strings (8 langs; reworded one zh string to stay within the CJK subset).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 12:45:18 -05:00
fc509a9340 fix(wallet-switch): cluster B — daemon-switch robustness
From the wallet-switching audit:

- Revert on failure (HIGH): switchToWallet persisted active_wallet_file before
  confirming the new daemon started, so a missing/corrupt wallet wedged across
  restarts. The switch worker now confirms dragonxd survives a grace period; on
  failure it flags the main thread (processWalletSwitchRevert), which restores
  the previous wallet file + re-scopes the vault + re-arms reconnect (settings
  writes stay on the main thread).
- Cross-guard concurrent lifecycle ops (HIGH): switchToWallet now refuses during
  a rescan/repair (state_.sync.rescanning) or seed migration; rescan/repair now
  refuse while daemon_restarting_; and restartDaemonAfterEncryption now sets
  daemon_restarting_ (which also fixes a latent reconnect-to-a-stopped-daemon
  race during the encryption restart). So the switch/adopt/restart/rescan/repair/
  encryption ops are mutually exclusive.
- Quit during switch (MED): beginShutdown now joins the "Switch wallet" task (like
  the adopt task) so quitting can't orphan a freshly-started dragonxd.
- Reset the daemon crash count on switch (LOW) so a prior crash-wedge can't block
  reconnecting to the new wallet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 12:37:11 -05:00
f546d3e2b1 fix(wallet-switch): cluster A — identity/secret teardown on wallet switch
From the wallet-switching audit (HIGH-severity cross-wallet leaks):

- Chat identity leak: the full-node switch (switchToWallet) and seed-migration
  adopt reset state_ but NOT the HushChat identity, so wallet A's decrypted
  conversations surfaced under wallet B and outgoing chat was signed with A's
  keypair. Factor the existing teardown into App::resetChatSession() and call it
  on both wallet-change paths (the lite path already reset it via
  rebuildLiteWallet, which now uses the helper too).
- Global PIN vault: the PIN quick-unlock vault was a single vault.dat, so after
  a switch wallet A's stored passphrase was offered/applied to encrypted wallet
  B. SecureVault is now scoped per wallet (vault-<walletfile>.dat); the default
  wallet keeps the legacy vault.dat for back-compat. vault_ is constructed for
  the active wallet and re-scoped on switch, so B has its own (empty) vault.
- Lock-screen state: switching now clears the carried-over failed-attempt
  counter + lockout timer and secure-zeroes the passphrase/PIN entry buffers so
  the previous wallet's unlock state can't apply to the new one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 12:29:45 -05:00
6f0f95f4fb fix(image-picker): avoid shutdown UB in the async decode counter
Adversarial review of the off-thread decode found the one real defect: a
detached decode worker decremented the static s_animInFlight counter, which at
process exit could run after static destruction begins (shutdown UB). Make the
counter a heap std::shared_ptr<atomic> the worker co-owns, so it safely outlives
teardown; the worker now touches only heap objects it holds a share of. Also
noted s_animatingThisFrame is intentionally main-thread-only.

(Review confirmed the rest: worker never touches the thumb map, done
release/acquire publishes the frames, GL upload stays on the UI thread, stb is
thread_local + libwebp per-instance so concurrent decodes are safe, the
in-flight slot is never leaked, and growth is bounded/cleared on navigate.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:55:11 -05:00
a7c50ff19b perf(image-picker): decode hover animations off-thread (no UI hang)
Hovering an animated GIF/WebP previously decoded ALL frames + uploaded every
texture synchronously on the UI thread — a big/long animation froze the UI for
that first hover (stb's GIF decode is monolithic).

Now the decode runs on a detached background worker (bounded to 2 concurrent),
and frames are uploaded to GPU textures a few per UI frame; the still thumbnail
keeps showing until the sequence is ready, then it animates. A shared_ptr job
keeps the worker's result alive if the thumbnail is destroyed mid-decode (e.g.
navigating away), so nothing blocks. Only images the cheap badge-probe already
flagged as animated ever spawn a worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:50:27 -05:00
e7f2d2e3c7 feat(image-picker): animated-image badge on GIF/WebP thumbnails
Animated thumbnails now show a small play-arrow badge (bottom-right) so users can
spot which images move before hovering; it's hidden while the image plays on
hover.

Detection is cheap — a new util::IsAnimatedImageFile probes without a full
decode: animated WebP via WebPGetFeatures.has_animation, and a multi-frame GIF
via a lightweight image-descriptor block walk (stops at the 2nd frame). The
picker only probes .gif/.webp thumbnails and caches the result on the Thumb.

Verified: animated GIF + animated WebP report animated; still GIF/WebP/PNG do not.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:44:57 -05:00
137147921c fix(contacts): don't keep the app awake for off-screen animated avatars
Adversarial review found that the contact list/table loops (no clipper) call
drawContactAvatar for every row, so an animated avatar scrolled out of the
list/table viewport still flagged the render loop as animating — pinning the app
at vsync-rate redraw instead of idling (power drain).

currentAvatarFrame now takes an onScreen flag: off-screen it shows frame 0 and
does NOT set the keep-redrawing flag. The list and table pass ImGui::IsRectVisible
for the row/avatar; the preview passes true; the library grid already yields a
null texture for culled cells. (Two other findings — stb GIF peak host RAM and
the session texture cache — were reviewed and judged bounded/local-only.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:26:31 -05:00
fd0cab41f0 feat(image-picker): play animated thumbnails on hover
Hovering a GIF/WebP thumbnail in the picker now previews its animation:

- The picker's Thumb gains a lazily-loaded frame sequence (via LoadAnimatedRGBA)
  fetched the first time an animatable (.gif/.webp) thumbnail is hovered; still
  images and other formats keep their single static thumbnail (no re-decode).
- On hover the current frame is drawn on the ImGui clock; leaving the thumbnail
  returns it to the still frame-0 preview.
- A clear-on-read flag (ImagePicker::consumeAnimationActive) is OR'd into
  ConsumeContactsAvatarAnimation so the render loop keeps drawing while a hover
  preview plays and idles otherwise. clearThumbs frees all frame textures too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:22:57 -05:00
4b8d80b2fc feat(contacts): animated avatars (GIF/WebP) with a Settings toggle
Animate contact avatars end to end:

- texture_loader gains LoadAnimatedRGBA: decodes an image into a downscaled
  RGBA frame sequence + per-frame durations — animated GIF via stb
  (stbi_load_gif_from_memory) and animated WebP via libwebp's WebPAnimDecoder;
  stills (and APNG, which stb reads as one image) return a single frame. Frames
  are box-downscaled (smaller cap for animations) to bound VRAM, capped at 300.
- The contacts avatar cache now holds a frame sequence; currentAvatarFrame()
  advances animated avatars by the ImGui clock and is used everywhere avatars
  draw (list, cards, table, grid, preview). When a live animated frame is drawn
  it flags the render loop (ConsumeContactsAvatarAnimation, clear-on-read) so
  main.cpp keeps producing frames while animation plays and idles when it stops
  or the contacts view is hidden.
- New animate_avatars setting (default on) + a Settings appearance toggle
  ("Animate avatars"); off shows the first frame only. currentAvatarFrame
  honors it. +i18n (8 langs, CJK subset rebuilt for 帧/播/첫).

Verified: a 3-frame GIF and a 3-frame animated WebP both decode to 3 frames
with correct 120ms delays through the exact libwebp/stb calls used here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:11:06 -05:00
923d086092 feat(images): WebP decode via libwebp + more stb formats
Broaden avatar/image support:

- Add libwebp (FetchContent, static, decode-only) so WebP loads. Built from
  source for Linux / mingw-Windows / macOS-osxcross identically — the cross
  sysroots have no webp, so vendoring from source is the one portable path;
  encode/tool builds are disabled to avoid pulling in libpng/zlib. Linked as
  webp + webpdemux (the latter for animated WebP, wired next).
- texture_loader routes all decode through DecodeImageRGBA: sniffs the RIFF/
  WEBP header and uses libwebp (WebPDecodeRGBAInto into a free()-able buffer),
  else stb — so every existing caller (avatars, QR, thumbnails) gains WebP for
  free with no allocator mismatch.
- Enable stb's TGA / PSD / PNM / PIC decoders and add .webp/.tga/.psd/.pnm/
  .ppm/.pgm/.pic to the image-picker + avatar-library extension lists.

Verified: libwebp builds static and a real .webp decodes to correct RGBA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:57:01 -05:00
6ac8f66644 fix(contacts): review fixes for the avatar library grid
From the adversarial review of the image-library grid:

- MED: the delete badge was an InvisibleButton positioned via SetCursorScreenPos,
  which left CursorPosPrevLine at the badge corner — the next cell's SameLine
  reads that, so a hovered/selected row's trailing cells jittered ~3px and their
  hit-rects overlapped. Hit-test the badge MANUALLY (no layout item, no cursor
  moves); it still takes click priority over selecting the thumbnail.
- MED: avatar textures were uploaded at full native resolution and cached for the
  session with no cap — a library of large photos could cost GBs of VRAM, and the
  contact list decoded every image avatar at once on tab open. Box-downscale to
  <=256px (an avatar renders at most ~112px) and budget decodes to a few per
  frame, so large libraries fill in progressively instead of stalling.

(A third finding — a symlink edge case in the delete guard — was reviewed and
judged below the bar; the weakly_canonical + parent-path guard already holds.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:33:51 -05:00
62fc202557 feat(contacts): image avatars are now a managed library grid
Redesign the Image avatar tab from a single-image chooser into a grid over a
persistent image library (mirrors the Icon tab):

- Images the user adds live in <config>/contact-avatars/ and PERSIST as a
  reusable, portable set — they travel with the wallet data dir, so avatars
  survive moving to another machine without remembering source paths.
- The grid's first cell is always the "+ add image" button (opens the picker,
  decode-verifies, copies into the library, auto-selects the new image).
- Each library image is a selectable thumbnail (Primary ring when selected)
  with a delete badge (top-right, red on hover) to remove it from the library;
  the delete is deferred past the grid loop and clears the selection if it
  pointed at the removed file. Contacts still referencing a deleted image fall
  back to their Z/T badge.
- Removes the previous auto-prune of "unused" images on edit/delete — images
  are only removed by the explicit delete badge now.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:22:35 -05:00
f8034b843a fix(contacts): stricter path guard in pruneOrphanAvatar
Compare the file's parent path to the managed dir instead of a string prefix,
so a sibling dir like contact-avatars-x can't false-match. Our avatar copies
always live directly in the dir, so this is both correct and safer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:09:00 -05:00
731d4e04ff feat(contacts): avatars in Table view + prune orphaned avatar images
- Table view now shows the same avatar (image / icon / Z-T badge) before each
  contact's label, drawn after the row Selectable so its highlight doesn't
  paint over it — visual parity with Cards/List.
- When a contact's custom image avatar is replaced (edit) or the contact is
  deleted, its now-unused file in <config>/contact-avatars/ is removed
  (pruneOrphanAvatar) — but only if no other contact still references it and
  the path is inside our managed dir, and its cached texture is dropped too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:08:04 -05:00
9649654c3c fix(contacts): adversarial-review fixes — delete UB, JPEG decode, footer clip
From an adversarial review of the avatar edit-dialog + image picker:

- HIGH: the per-row Delete icon called doDelete() (which erases from
  book.entries()) INSIDE the loop iterating that same vector — out-of-bounds
  reads / wrong rows on the confirming click. Defer it until after the loop.
- HIGH: the decode stack was compiled PNG-only (STBI_ONLY_PNG) while the image
  picker accepts .jpg/.jpeg/.bmp/.gif, so picking a JPEG (the common photo
  case) silently produced a non-loading avatar + an orphaned copy on disk.
  Enable JPEG/BMP/GIF decoders, and guard the pick: verify the source decodes
  before copying/committing (new contact_avatar_bad_image string, 8 langs).
- MED: the fixed, non-scrolling edit card floored bodyH at 260*dp, which could
  push Save/Cancel below the card on short windows — floor lowered so the
  footer always stays inside.
- LOW: the live-preview panel rounding is now dp-scaled (10*dp) to match the
  real list card it mirrors.
- LOW: re-picking the same source path after its contents changed showed a
  stale cached texture — evict the avatar texture cache entry on re-pick.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:49:34 -05:00
e1df5ea798 feat(image-picker): thumbnails fill width at 6 per row
Fix the thumbnail grid to a constant 6 columns whose square cells scale to the
available width, instead of fixed 96px cells that left dead space on the right.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:43:23 -05:00
a53b13b6b6 feat(image-picker): smooth scroll, inset scrollbar, 2-column folder grid
- Smooth (lerped) wheel scrolling via ApplySmoothScroll on the list, matching
  the app's other modal lists.
- The list is now a bordered/rounded outer frame whose 6px padding insets the
  scrollbar so it clears the card's rounded corners; the inner scroll child is
  transparent (the frame draws the single background — no more box-in-a-box
  from the ChildBg being left on the stack across both BeginChild calls).
- Folders render as a 2-column grid of thin rounded rectangles (folder icon +
  name) instead of full-width rows, so more folders are visible at a glance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:31:48 -05:00
bc3c6037fc fix(image-picker): inset content from rounded card + center footer
- Wrap the picker body in a padded inner child so the filled directory list and
  thumbnail grid keep a clear margin from the card's rounded corners instead of
  running edge-to-edge past them.
- Center the Use image / Cancel buttons and drop the stray separator line above
  them (the faint artifact at the footer's left edge).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:09:18 -05:00
a338fac208 fix(contacts): edit-dialog layout polish + per-row edit + hover address
Address five reported issues:

- Address field is now full width with a centered Paste button beneath it
  (was a narrow field with Paste crammed alongside).
- The pinned "Show in every wallet" checkbox no longer clips at the column
  bottom — the Notes fill reserves a clear margin for it.
- Image mode: more spacing between the preview circle and the Choose/Remove
  row so the button isn't crowding the avatar.
- Contact list: hovering a row now un-collapses the address to its full form
  inline (clipped to the text column) instead of popping a tooltip.
- Per-row copy/edit/delete icons fire on the first click on an unselected row:
  the action lambdas validate s_selected_index freshly (via selValid())
  instead of the frame-top has_selection bool, which was stale in the same
  frame the icon set the selection.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:53:05 -05:00
9bc2a0b62a feat(contacts): tall edit dialog — fill vertical space, accent Save
The dialog was a squat auto-height box with a big dead zone below and an icon
grid that clipped at ~3 rows. Make it a fixed, tall card (up to 84% of the
viewport) whose body flexes to fill the height:

- The icon grid grows into the space — ~8–9 rows visible instead of 3.
- Notes expands to fill the left column above the now bottom-pinned Global
  checkbox, so the left side uses the height too.
- The image-mode preview circle is bigger (r44 → r56) and vertically centered,
  with glyphs scaled to it.
- Footer pins to the bottom; Save/Add is now accent-colored so the primary
  action reads above Cancel.

Notes/actionButton schema lookups that only fed the old fixed heights are gone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:29:04 -05:00
1b3446e43c feat(contacts): richer edit-dialog avatar picker (preview, chips, image, icons)
Four refinements now that the two-column layout has room:

1. Image mode shows a large circular preview of the chosen picture (or a
   placeholder circle with an add-photo glyph, or a broken-image glyph if the
   file went missing), the filename, and centered Choose/Remove buttons —
   instead of a bare button.
2. The live-preview avatar is larger (r20 → r26) and the address is now
   middle-truncated (head + tail) so both ends read, like the real list row.
3. Badge mode shows the two actual chips — Z (shielded) and T (transparent) —
   with labels, making clear the badge is auto-picked from the address type,
   rather than a line of text.
4. The Badge/Icon/Image segmented control gains glyphs (badge / palette /
   image) beside its labels via a small two-font inline control (the shared
   SegmentedControl helper is single-font).

Adds contact_avatar_shielded / _transparent keys and rewords the badge hint
(the chips now carry the Z/T meaning); +8-language translations, all within
the existing CJK subset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 08:07:07 -05:00
19dea53ef8 refactor(contacts): two-column edit dialog — wider, shorter, roomier grid
The edit dialog was a tall, narrow single column that wasted ~half the
horizontal space and cramped the icon grid into ~2.5 clipped rows crowding
the footer. Rework it into the portfolio-editor two-column shape:

- Card widened 560 → 880 logical.
- Full-width live preview stays on top (now shows more of the address).
- Body is two fixed-height columns: form (label / address+paste / notes /
  global) on the left, avatar picker (segmented + icon grid / image / badge
  hint) on the right, each filling its column so the grid gets ~6 columns and
  ~4 rows instead of clipping mid-row.
- Overall modal is much shorter, so Save/Cancel no longer crowd the grid.
- Badge-mode hint re-centered relative to the current cursor (it now sits
  below the segmented control inside the shared column, not a fresh child).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 07:23:19 -05:00
3d2b734541 refactor(contacts): drop now-unused addrInput schema lookup
The revamped edit dialog derives its input widths from the card content
width, so the address-input schema config is no longer read.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 07:10:57 -05:00
2d4ba89c00 feat(contacts): revamp edit dialog with live preview + avatar picker
Rebuild the add/edit contact dialog on the portfolio-editor design language:

- A live preview card at the top shows the contact exactly as it renders in the
  list (avatar + name + address), updating as you type and pick an avatar.
- An avatar picker (Badge / Icon / Image segmented control) lets you keep the
  default Z/T type badge, choose a Material wallet icon from a searchable grid,
  or set a custom image. The picker area is fixed-height so the modal doesn't
  jump when switching modes.
- Custom images go through a new in-app ImagePicker (image_picker.h): a
  Material overlay that browses the filesystem starting at the user's Pictures
  folder, shows a thumbnail grid (decoded to raw pixels, box-downscaled to a
  small texture, cached per directory and freed on navigate/close, budgeted a
  few decodes per frame so large folders don't hitch), and returns the chosen
  path. The chosen image is copied into <config>/contact-avatars/ (named by an
  FNV hash of the source path, so re-picking is idempotent) and stored as
  "img:<path>". Like FolderPicker, it takes over the modal surface while open.
- Paste is now available on both add and edit; buttons are content-sized.

Adds three sweep surfaces (contacts-edit-{icon,badge,image}) that open the
dialog on a seeded contact in each avatar mode, plus i18n keys (+ 8-language
translations; reworded two zh/ja strings to stay within the existing CJK
subset, so no font rebuild).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 07:10:00 -05:00
2249bc31d5 feat(contacts): contact avatars — custom image / Material icon / Z-T badge
Add an `avatar` field to AddressBookEntry ("" = default Z/T type badge,
"icon:<name>" = a Material wallet-icon, "img:<path>" = a custom image),
serialized additively in addressbook.json (only written when non-empty, so
existing books are untouched).

Render it in the Cards/List views via a new drawContactAvatar helper: custom
images are loaded once through a path-keyed texture cache and drawn
circular-cropped (centre-cropped UVs + a thin border ring); icons reuse the
project-icon set (incl. the special pickaxe font path) in a tinted circle;
everything else falls back to the existing Z/T badge (also the fallback when
an image fails to load or an icon name is unknown).

Seed one sweep contact with an icon avatar to exercise the icon-badge path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 06:49:52 -05:00
1ce37aa0fa feat(contacts): deselect on empty click, actions left of globe, right-click menu
Three interaction refinements to the address list:
- The globe badge now stays pinned far-right; the per-row copy/edit/delete actions
  appear to its LEFT on hover/selection instead of replacing it.
- A left-click on empty space in the Cards/List area clears the current selection
  (no row/action hovered -> deselect).
- Right-clicking a row (any view) selects it and opens a shared context menu
  (Copy address / Edit / Delete), rendered once after the list.

Build + hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 06:36:50 -05:00
33317c4e78 feat(contacts): per-row hover actions + fix add/edit modal HiDPI widths
Two follow-ups from the contacts audit:

- Per-row actions: in the Cards/List views, copy/edit/delete icon buttons now appear
  on the right of a row on hover or selection (the globe badge shows otherwise). The
  row Selectable uses SetNextItemAllowOverlap so the action InvisibleButtons take
  click priority; whole-row hover (IsMouseHoveringRect) drives the highlight so it
  survives hovering an icon; the delete icon turns red while armed (two-click confirm);
  each has a tooltip. Trailing space is reserved so the text never reflows on hover,
  and the cursor is restored after the manual action layout.

- Add/edit modal HiDPI: the Layout::kDialog* helpers fold dpiScale() (physical px)
  while raw schema widths are logical, so the schema-path formW/actionW/actionGap/
  notesH were unscaled vs their scaled fallbacks. Scale the schema-path values; and
  since BeginOverlayDialog re-applies dpiScale to cardWidth, divide the already-scaled
  dialogW back out (it was double-scaled). No visible change at 100%; correct at 150%.

Build + ctest + hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 06:23:17 -05:00
03dde25339 test(sweep): seeded contacts-cards/list/table surfaces for the view toggle
Add three sweep surfaces (contacts-cards / -list / -table) that force each Contacts
address-list view mode and seed a few demo contacts (Z + T types, some global) so
the modes render with data instead of the empty state. Teardown restores the real
book and resets the mode.

To avoid touching the user's persisted address book (the Windows sweep can run on
the real HOME, and addEntry/removeEntry call save()), add AddressBook::sweepSetEntries
— a no-save in-memory setter used only here: the surface snapshots the real entries,
swaps in the demo set, and restores the snapshot on teardown, so nothing is written
to disk even if the sweep is interrupted.

Build + hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 22:10:24 -05:00
3fbb64d14c feat(contacts): switchable Cards / List / Table view for the address list
Add a persisted view toggle so the contacts address list can be rendered three
ways, moving it away from a bare data-grid toward a Material look:

- Cards: tactile rounded cards with a circular Z/T type-avatar, label over a muted
  truncated address, trailing globe badge, primary-tint + outline selection, hover
  fill, and a centred Material empty state.
- List: borderless two-line rows (same avatar + label/address) with hover tint and a
  thin per-row divider — denser than cards.
- Table: the previous 3-column table, material-ized (grid borders dropped, row
  backgrounds + interactive sort kept).

The toggle is an icon SegmentedControl (view-agenda / view-list / table-rows)
right-aligned on the toolbar; the choice persists via a new contacts_view_mode_
setting (0 cards / 1 list / 2 table, mirroring portfolio_style_). Cards/List sort by
label; Table keeps its sortable headers. All geometry is dpiScale()-aware; the globe
badge is drawn in the icon font in every mode. Default is Cards.

Build + ctest + hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 21:07:33 -05:00
1b0006a4af fix(contacts): audit fixes — clip, tofu badge, plural, DPI, columns, toolbar
Address the confirmed findings from a workflow audit of the Contacts tab body:

- Label clip: the Label column was WidthFixed 150px and clipped long labels
  mid-word ("drgx pool payout a…"). Make all three columns WidthStretch (label 1.5,
  address 2.6, notes 1.0) so the label grows with the tab and the notes column no
  longer reserves a fixed empty block on the right (notes = low weight, per request).
- Globe badge tofu: the global-contact ICON_MD_PUBLIC badge was drawn with the text
  font (no Material glyphs) → rendered as "?". Push Type().iconSmall() around it.
- Plural: "1 addresses saved" -> add address_book_count_one ("%zu address saved",
  8 langs, %zu kept so the format signature matches) and branch on count == 1.
- DPI: the SameLine badge gaps (6/8px) and the table-height floor (120px) are now
  * Layout::dpiScale(); the WidthStretch columns are relative so need no scaling.
- Toolbar: give the four actions leading Material icons (person-add / edit / delete /
  content-copy) and accent the primary "Add New" — via a small local icon+label
  tactile-button helper (ImGui has no two-font button), icons inherit the
  disabled-aware text alpha so they gray out with BeginDisabled().

Build + ctest + hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:49:15 -05:00
ba13875eaf fix(receive): size the QR popup to its content, not 85% of the window
The QR popup used the same window-relative card width the Export-Key dialog just
moved away from (its comment even said "Match the key-export modal: 85% of the
window width"). On a large monitor that left the centered AddressCopyField floating
far from its left-aligned "Address:" label and inflated the QR's empty margins.

Derive the card width from the address field's own natural (chunked) box width
(mirroring the key-export fix, incl. the overlay card's 28px content inset so the
address stays on one line), bounded to 85% of the window as an upper limit. Also
scale the responsive-QR size literals (280 fallback / 420 cap) by dpiScale() so the
QR renders at the intended size on HiDPI/font-scale. No change to the QR texture /
clipboard logic.

Found via a workflow audit of all 27 redesigned modals for the same anti-pattern —
the QR popup was the only other occurrence (0 false positives).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:10:49 -05:00
a5d44c8aca fix(overview): size the Export-Key modal to its content, not 85% of the window
The Export-Key dialog sized its card to a fixed 85% of the window width. On a large
monitor that's ~1570px, but the content is ~900px, and AddressCopyField centers its
text-clamped box within the available width — so the address/key fields floated far
from their left-aligned "Address:"/"Viewing Key:" labels and read as off-center
(more visible after the warning box became a left-aligned DialogWarningHeader).

Derive the card width from the address field's own natural (chunked) box width —
the widest deterministic element before the key is revealed — mirroring
AddressCopyField's boxW math, plus the overlay card's 28px-per-side content inset so
the address sits on one line under its label. Adapts to address type (z vs t) and
font scale with no magic width; bounded to 85% of the window as an upper safety
limit; the revealed key+QR layout adapts to whatever width results. Width-only
change — the key fetch/wipe/mask/close logic is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:10:49 -05:00
1a41dec8d8 test(sweep): add capture surfaces for the Wave-2 fund/secret modals
Register sweep surfaces for the redesigned Wave-2 modals so they get captured for
visual review: modal-shield + modal-merge (ShieldDialog's two modes),
modal-transfer (a z->t transfer so the converted deshielding DialogWarningHeader
renders), and modal-key-export (KeyExportDialog — named distinctly from the
settings modal-export-key). Each setup opens the dialog through its public show()
without clicking any button, so no async RPC fires; teardown closes it.

Adds a static hide() to ShieldDialog and KeyExportDialog for clean sweep teardown
(AddressTransferDialog already had close()). KeyExportDialog::hide() also clears the
revealed key + frees its QR, mirroring the dialog's own close/dismiss secret-wipe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:37:22 -05:00
88e10f3e06 feat(overview): migrate the Export-Key modal to the reference design
Presentation-only migration of KeyExportDialog::render (reveals a private or
viewing key) — a secret-handling modal:
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth cardW,
  idSuffix keyexport}) replacing the legacy positional overload
- replace the bespoke red-bordered "WARNING" box with material::DialogWarningHeader
  (user-approved) so the key-export warning matches the import-key reference; passes
  the correct per-key-type body key (private vs viewing)
- remove the two dividers (after the warning, above the Close footer)
- 4 StyledButton -> TactileButton (retry / reveal / show-hide / close)

The secret path is byte-for-byte unchanged: the z_exportkey/dumpprivkey/
z_exportviewingkey (and lite exportPrivateKeys) fetch + their secureWipeLiteSecret
calls, the "*"-masking when hidden, the Show/Hide toggle, copySecretToClipboard
(auto-clearing), the QR generate/cache/releaseQr, and the secret-clear on BOTH exit
paths (Close button + the post-EndOverlayDialog dismiss teardown).

Verified: build + ctest + hygiene clean; a 2-lens adversarial review
(secret-hygiene / presentation-fund) returned zero findings, proving every
secret-handling path diffs byte-for-byte identical to HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:23:14 -05:00
d29283a9b7 feat(overview): migrate the Transfer-funds modal to the reference design
Presentation-only migration of AddressTransferDialog::render (drag-one-address-
onto-another transfer) — a fund-moving modal:
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 620,
  idSuffix addrtransfer}) replacing the legacy positional overload
- the top-of-dialog deshielding warning -> material::DialogWarningHeader; the green
  shielding notice stays colored text (it's a positive notice, not a warning)
- remove the footer divider and drop its "+1.0f" footerH reserve term (keep the
  form->result-preview divider)
- reuse the existing shield_operation_id key for the "Operation ID: %s" label (no
  new i18n key)

The fund path is byte-for-byte unchanged: the amountValid guard, totalDeduct/
newFromBal/newToBal math, maxSendableAmount, the Max button, s_app->sendTransaction
+ its ok/fail callback, and the BeginDisabled(!amountValid || s_sending) gate.

Verified: build + ctest + hygiene clean; a 2-lens adversarial review returned zero
findings, proving the fund path diffs byte-for-byte identical to HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:17:13 -05:00
ca65aa8bf7 feat(send): migrate the Shield/Merge modal to the reference design
Presentation-only migration of ShieldDialog::render (Shield coinbase / Merge funds)
— a fund-moving modal:
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth win.width,
  idSuffix shielddialog}) replacing the legacy positional overload
- drop the description divider and the form->footer divider (keep the post-submit
  operation-id section divider)
- 3 StyledButton -> TactileButton (submit / cancel / check-status)
- TR() the hardcoded "No shielded (z) address yet …" note -> new shield_no_zaddr_hint
  key, 8 langs (em-dash preserved); the "DRGX" ticker stays hardcoded per convention

The fund path is byte-for-byte unchanged: the can_submit guard, fee/utxo clamps, the
"ANY_TADDR" setup, the z_shieldcoinbase / z_mergetoaddress worker->post calls + their
opid/status/Notifications/trackOperation callbacks, and the BeginDisabled/tooltip logic.

Verified: build + ctest + hygiene clean; a 2-lens adversarial review
(fund-path-untouched / presentation-i18n) returned zero findings, proving the fund
path diffs byte-for-byte identical to HEAD via isolation diffs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 18:03:54 -05:00
dae677acd8 feat(send): migrate the Confirm-Send modal to the reference design
Presentation-only migration of RenderSendConfirmPopup (the confirm-before-broadcast
popup) — a fund-moving modal:
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth popupW,
  idSuffix confirmsend}) replacing the legacy positional overload; p_open stays
  nullptr (the popup keeps its own Escape + Cancel handling)
- TR() the one hardcoded status string in the synchronous live-revalidation guard
  ("Cannot send now …") -> new send_cannot_send_now key, 8 langs (em-dash preserved)

The fund path is byte-for-byte unchanged: the live re-validation guard, the
app->sendTransaction call + its success/failure callback, the lite-locked
requestLiteUnlock branch, the s_sending/s_show_confirm state, the Escape handler,
and the FROM/TO cards + fee-tier/summary rendering.

Verified: build + ctest + hygiene clean; a 2-lens adversarial review
(fund-path-untouched / ui-i18n) returned zero findings, with the fund-path lens
proving the send path diffs byte-for-byte identical to HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:58:11 -05:00
4ef09d5546 fix(ui): drop Validate-address results divider; remove ungapturable daemon-prompt surface
Two sweep-driven fixes:
- Validate Address: remove the input->results Separator. When no result is shown
  yet it sat directly above the Close button and read as a footer divider,
  inconsistent with the reference design (the description divider was already gone).
- Drop the modal-daemon-prompt sweep surface: renderDaemonUpdatePrompt is gated
  behind !capture_mode_, so it never renders during a sweep (the capture showed the
  Settings page behind it). Its migration stays verified via build + the shared
  BlurFloat/TactileButton pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:40:59 -05:00
21be39c725 test(sweep): add capture surfaces for the Wave-1 standalone modals
Register sweep surfaces so the redesigned Wave-1 modals get captured for visual
review: modal-qr-popup, modal-request-payment, modal-validate-address,
modal-address-label, modal-daemon-prompt, and modal-antivirus (the last renders
only on Windows, where its #ifdef body compiles). Each is driven through the
dialog's public show()/flag; teardown closes it. Adds a static hide() to
RequestPaymentDialog / ValidateAddressDialog / AddressLabelDialog for clean sweep
teardown (they had no public close). The contact add/edit and explorer block-detail
modals are tab-embedded and not yet surfaced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 17:15:24 -05:00
abbdf3f4f7 feat(mining): migrate the Windows Defender antivirus-help dialog to reference design
Bring renderAntivirusHelpDialog onto the reference design and remove its hardcoded
English (the last Wave-1 modal):
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 560,
  idSuffix antivirus}) replacing the legacy positional overload
- the hardcoded "Windows Defender Blocked Miner" title and all 10 step/intro/bullet
  strings -> TR() (11 new av_* keys + reuse `close`; 8-lang translations, Windows
  env vars / xmrig.exe kept literal, menu names localized)
- 2 StyledButton -> TactileButton, drop the footer divider, dp-scale the button widths
- CJK subset rebuilt (glyphs 1741->1751; kept — new glyphs are used)

NOTE: this dialog is inside #ifdef _WIN32, so the Linux build compiles it out and
cannot verify it — needs a Windows build to compile-check. The edits mirror the
already-verified daemon-update-prompt / migrate-to-seed struct-overlay pattern and
leave the ShellExecuteA call untouched. i18n.cpp + the daemon prompt (cross-platform)
build + ctest + hygiene clean on Linux.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:38:07 -05:00
28288d7197 feat(ui): migrate Explorer detail/loading + daemon-update prompt to reference design
Presentation-only reference-design migration:
- Explorer loading modal -> struct-form overlay (idSuffix explorerloading); block-
  detail modal loses its header divider; the search button + the prev/next nav
  become TactileButton; TR() the hardcoded "Previous block"/"Next block" tooltips,
  "TxID:", and the "shielded" output fallback (3 new explorer_* keys + reuse the
  existing `shielded` key), all 8 langs. Block-fetch RPC path untouched.
- Daemon-update startup prompt -> struct-form overlay (idSuffix daemonupdprompt),
  2 StyledButton -> TactileButton, drop the footer divider. The reinstall/
  prompted-size persistence logic is unchanged.

Build + ctest + hygiene clean; "DRGX" ticker left hardcoded per convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:33:52 -05:00
304f65c3b4 feat(ui): migrate Validate-address, Address-label & Contacts modals to reference design
Presentation-only reference-design migration of three more modals:
- Validate Address: struct-form overlay (idSuffix validateaddr), 3 StyledButton ->
  TactileButton, drop the description divider, and TR() the hardcoded "Error: %s"
  (reuse the existing error_format key). The validateaddress RPC/worker path is untouched.
- Address label: struct-form overlay (idSuffix addrlabel); drop the footer divider and
  zero its layout-reserve term (separatorH) so the fixed-height icon-picker card math
  stays consistent. Buttons were already TactileButton.
- Contacts add/edit: struct-form overlay (keeps its idSuffix + BeginOverlayDialogFooter);
  StyledButton -> TactileButton across the file (3 modal + 4 contact-list buttons) for a
  consistent Contacts tab. The address-book add/update/notify logic is untouched.

Build + hygiene clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:24:28 -05:00
04a5078133 feat(receive): migrate QR + Request-payment modals to the reference design
Bring the two Receive-tab modals onto the settings-modal reference language
(presentation-only):
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth, idSuffix})
  replacing the legacy positional overload (idSuffix qrpopup / requestpay)
- StyledButton -> TactileButton (2 + 3 sites)
- drop Request-payment's description divider

No logic touched: the QR texture lifecycle, payment-URI builder, address combo,
and clipboard/notification calls are unchanged. The "DRGX" ticker stays hardcoded
per the existing codebase convention (7 sites, no currency TR key).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:20:02 -05:00
b6b4fdacb5 fix(material): light-theme modal backdrop reads light, not dark
BlurFloat overlays draw a full-window veil over the live blur so the card-less
floating modal content reads as foreground. The light-theme branch used a strong
neutral DARK veil (IM_COL32(16,18,24,200)) on the theory that "light content pops
on a dark veil" — but on a light theme the modal chrome is dark-on-light, so it
sank INTO the veil: the heading, description, Sort row, and the create/scan/
reveal/close buttons all washed out to low-contrast dark-on-dark, and a "light"
theme read dark whenever a modal opened.

UI controls already carry a dropshadow on light themes for figure/ground, so the
backdrop no longer needs to supply contrast by going dark. Switch the light-theme
veil to a theme-tinted LIGHT frost (WithAlpha(Background(), 205)); the dark-theme
branch is untouched. Fixes every BlurFloat modal at once.

Verified via full sweep: marble / light read as clean light frost with all chrome
legible, dune gets a theme-appropriate warm frost, and obsidian (dark) is
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:09:16 -05:00
f6024557d5 refactor(updater): drop dead RenderReleaseList picker; keep shared ReleaseRow
The miner and node updater dialogs were rebuilt around their own two-pane
tactile-Material version lists, which superseded release_list_view.h's
RenderReleaseList() picker. That function had zero call sites — both dialogs
include the header only for the shared ReleaseRow struct. Remove the dead
function (and the material/i18n/imgui includes it alone needed), leaving just
the ReleaseRow model, and correct CLAUDE.md's stale "shared picker" description
to reflect that the two-pane list is the browse-all UI and only the row model
is shared.

No behavior change: build + ctest + hygiene clean; grep confirms no remaining
RenderReleaseList references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 14:51:02 -05:00
d82821ba06 feat(settings): redesign the migrate-to-seed modal onto reference design
Migrate App::renderSeedMigrationDialog onto the settings-modal reference design.
This is the most fund-critical dialog in the app (create isolated seed wallet ->
z_mergetoaddress sweep -> confirm -> adopt/rescan), already fully i18n'd and
dp-scaled, so the change is purely the presentation shell:

- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 580,
  idSuffix "migrate"}) replacing the legacy positional overload
- all 26 StyledButton -> TactileButton (identical signature; height-0 auto-sizing)
- remove all 13 footer Separator dividers
- ShowSeed seed-phrase warning -> material::DialogWarningHeader (red text -> amber
  ⚠ header, matching the export-key reference dialog)

The fund pyramid is byte-for-byte unchanged: the busy/dismiss veto, the wipeSeed
(sodium_memzero) + close lambdas and the post-EndOverlayDialog seed scrub, all
seed_migration_step_ transitions, begin CreateSeedWallet/SweepToSeedWallet/
AdoptSeedWallet, refreshSeedMigrationBalance, pollSweepStatus, the snprintf
balance/confs/remaining formatting, both confirmation checkboxes and their gates,
RenderSeedWordGrid, copySecretToClipboard, writeFileAtomically, and the Discard
remove_all + settings cleanup.

Verified: build + ctest + hygiene clean; Spacing() count unchanged (28->28,
proving no collateral deletion); StyledButtons elsewhere in app.cpp untouched
(the swap was range-bounded to this function); a 3-lens adversarial review
(fund-path-untouched / secret-hygiene / widget-visual) returned zero findings,
with the fund-path lens proving the logic diffs byte-for-byte identical to HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:55:01 -05:00
8290e96ec4 test(sweep): add modal-pin-setup/-change/-remove capture surfaces
Register the three redesigned PIN dialogs as sweep surfaces for visual review.
Each setup only sets the show flag (never fires the async vault store/verify); the
teardown clears the status and zeroes the PIN/passphrase buffers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:29:02 -05:00