Commit Graph
100 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 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
DanSandClaude Opus 4.8 46f7da4fac feat(settings): redesign the PIN setup/change/remove modals onto reference design
Migrate the three PIN dialogs in App::renderPinDialogs (Set PIN / Change PIN /
Remove PIN) onto the settings-modal reference design — presentation-only, plus a
deliberate secret-hygiene addition:

- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 420/420/400,
  idSuffix pinsetup/pinchange/pinremove}) replacing the legacy positional overload
- drop each dialog's redundant post-description Separator
- every rendered-chrome string -> TR(): titles/buttons reuse settings_set_pin /
  settings_change_pin / settings_remove_pin, the Set-PIN confirm reuses
  wiz_pin_confirm; 7 new pin_* keys added to the English source + all 8
  res/lang/*.json additively; ImGui::Text -> TextUnformatted for labels
- HARDENING: wipe the passphrase/PIN input buffers on BlurFloat dismiss
  (X/Esc/outside-click) for all three dialogs. Set PIN's wallet-passphrase buffer
  (pin_passphrase_buf_) was previously left resident on dismiss; it is now zeroed.

The vault/RPC path is byte-for-byte unchanged: the submit-time memsets, the
walletpassphrase/walletlock RPCs, vault_->store/changePin/retrieve/removeVault,
SecureVault::secureZero, and setPinEnabled/save are untouched. The pin_status_ and
toast strings inside the worker callbacks are left in English (a separate
service-layer i18n pass), to avoid editing the secret path.

Verified: build + ctest + hygiene clean; i18n complete in 8 langs with no format
specifiers; a 3-lens adversarial review (secret-hygiene / presentation-only /
ui-i18n) returned zero findings, with the presentation lens proving the vault path
diffs byte-for-byte identical against HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 13:29:02 -05:00
DanSandClaude Opus 4.8 5bd07b0505 test(sweep): add modal-decrypt capture surface
Register the redesigned "Remove Wallet Encryption" dialog as a sweep surface so
its passphrase-entry phase is captured for visual review. The setup resets the
WalletSecurityWorkflow (keeping it in PassphraseEntry) and never fires the async
unlock/export/restart pyramid; the teardown resets and zeroes decrypt_pass_buf_.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:02:11 -05:00
DanSandClaude Opus 4.8 90df19b8b6 feat(settings): redesign the remove-encryption (decrypt) modal onto reference design
Migrate App::renderDecryptWalletDialog ("Remove Wallet Encryption") onto the
settings-modal reference design — presentation-only for a fund-critical dialog,
plus one deliberate secret-hygiene addition:

- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 480,
  idSuffix "decrypt"}) replacing the legacy positional overload
- Phase 0 amber TextWrapped warning -> material::DialogWarningHeader(decrypt_warning)
- drop the redundant Phase 0 Separator
- every rendered-chrome string across all 4 phases (PassphraseEntry / Working /
  Success / Error) -> TR() (14 new decrypt_*/try_again keys added to the English
  source + all 8 res/lang/*.json additively; the button reuses
  settings_remove_encryption, the label reuses change_pass_current, cancel/close reused)
- HARDENING: wipe decrypt_pass_buf_ on BlurFloat dismiss (X/Esc/outside-click),
  mirroring the change-passphrase fix; canClose() gates pOpen so this cannot fire
  during the Working phase

The fund-moving async worker pyramid (unlock -> export -> stop -> backup ->
restart -> re-import, vault/PIN cleanup) is byte-for-byte unchanged; the three
in-worker toast notifications and the "Incorrect passphrase" status are left in
English to avoid touching the fund path (a separate service-layer i18n pass).

Verified: build + ctest + hygiene clean; i18n complete in 8 langs with no format
specifiers and the wallet.dat.encrypted.bak literal preserved; a 3-lens adversarial
review (secret-hygiene / fund-path-untouched / ui-i18n) returned zero findings, with
the fund-path lens proving the async pyramid diffs IDENTICAL against HEAD.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 12:02:11 -05:00
DanSandClaude Opus 4.8 5985f05fb2 test(sweep): add modal-encrypt & modal-change-passphrase capture surfaces
The redesigned settings-tab Encrypt Wallet and Change Passphrase dialogs had no
sweep surfaces, so the full UI sweep only captured the first-run wizard-encrypt /
wizard-pin variants — the settings modals were never rendered for visual review.
Register modal-encrypt (redesigned passphrase-entry phase) and
modal-change-passphrase, each with a defensive teardown that resets the phase and
zeroes the passphrase buffers. Neither setup path fires the async encrypt/change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:31:31 -05:00
DanSandClaude Opus 4.8 60ee73bf1d fix(settings): wipe change-passphrase buffers on dialog dismiss
The Change Passphrase dialog only zeroed change_old/new/confirm_pass_buf_ on
the success path (inside changePassphrase); dismissing via X / Esc / outside-
click left the typed passphrases resident in the input buffers. Add a same-
frame teardown mirroring the encrypt dialog's X-button cleanup: when the
BlurFloat overlay clears show_change_passphrase_ mid-frame, memset the three
buffers. The failure path still keeps them intact for retry, and the async
change already copies the passphrases into worker-owned strings, so wiping the
UI buffers cannot disturb an in-flight change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:17:34 -05:00
DanSandClaude Opus 4.8 625df8abe6 feat(settings): redesign encrypt & change-passphrase modals onto reference design
Migrate the Encrypt Wallet (3-phase) and Change Passphrase dialogs in
renderEncryptWalletDialog onto the settings-modal reference design —
presentation-only, no change to the crypto/state-machine/secret-wipe logic:

- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth, idSuffix})
  replacing the legacy positional overload (same visual style, dp-scaled width)
- amber TextWrapped warning -> material::DialogWarningHeader(wiz_encrypt_warning)
- drop redundant Separator dividers
- every hardcoded English string -> TR() (13 new enc_*/change_pass_* keys added to
  the English source + all 8 res/lang/*.json additively; CJK subset rebuilt)

The strength-meter math, 3-phase transitions, "cannot close while encrypting"
guard, memset/SecureVault buffer wipes, and the encrypt/changePassphrase/vault
async paths are byte-for-byte unchanged. Verified: build + ctest + hygiene clean,
wiz_strength %s format-string safe across all languages, and a 3-lens adversarial
review (secret-hygiene / presentation-only / ui-i18n) returned zero findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:13:44 -05:00
DanSandClaude Opus 4.8 5dd68dc9bd feat(settings): migrate Seed phrase backup modal to the reference design
Phase 2 (second security modal), presentation-only + the review's one nit.

Struct-form BlurFloat overlay (was the legacy positional overload); the phrase
state's plain red warning -> material::DialogWarningHeader; StyledButtons ->
TactileButtons with the Copy/Save/Close row and shared Close centered via
PlaceOverlayDialogActions (the existing helper) and their separators dropped;
success status now wraps. Already fully i18n'd.

UNCHANGED (verified by the adversarial review): the closeAndWipe sodium_memzero of
the 24-word phrase on every close path (Begin-false early return, outside-click/Esc
teardown, phrase Close, shared Close), the async exportSeedPhrase fetch + its
show_seed_backup_ callback guard, the atomic restricted-perms file-write +
sodium_memzero of its content buffer, and copySecretToClipboard / RenderSeedWordGrid.
No seed leak; Begin/EndOverlayDialog balanced on every return path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 00:28:25 -05:00
DanSandClaude Opus 4.8 8a30578872 feat(settings): migrate Export Private Key modal + fix outside-click secret leak
Phase 2 (first security modal), presentation-only + the review's one finding.

Migration: struct-form BlurFloat overlay (was legacy positional); full i18n
(reusing export_private_key / key_export_private_warning / key_export_private_key /
select_address / key_export_fetching / export / copy / close, plus one new
key_export_failed); DialogWarningHeader; centered DialogActionFooter (Export +
Close) + a Copy TactileButton; async UX (export_in_progress_ spinner, export_error_
line). The exported key stays in the char[256] buffer shown via a read-only
InputTextMultiline (deliberately NOT AddressCopyField, which would copy the secret
into a lingering std::string). exportPrivateKey is untouched.

Security fix (found by the adversarial review): the 0a "outside-click/Esc wipe" via
the Begin-returns-false early-return was UNREACHABLE — BeginOverlayDialog clears
show_export_key_ mid-frame but still returns true, so an outside-click/Esc dismiss
left the exported spending key un-wiped in export_result_. Add the in-body
`if (!show_export_key_)` teardown before EndOverlayDialog (the proven import-dialog
pattern), which is the real wipe point for every dismiss path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 23:47:11 -05:00
DanSandClaude Opus 4.8 63d5c817aa chore(sweep): add modal-bootstrap surface for the standalone bootstrap dialog
The settings-triggered BootstrapDownloadDialog wasn't captured (only the wizard's
bootstrap page was). Add BootstrapDownloadDialog::hide() + a modal-bootstrap sweep
surface (opens the confirm screen) so the migrated dialog is verifiable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:37:26 -05:00
DanSandClaude Opus 4.8 3662df550c feat(settings): Bootstrap modal — DialogWarningHeader + centered confirm buttons
Phase 1 (last low-risk modal). Replace the hand-rolled amber warning child-card
with the standard material::DialogWarningHeader (⚠ + Warning color, wrapping),
and center the Download / Mirror / Cancel button row. Already struct-form,
TactileButton, and i18n'd. Completes Phase 1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:17:48 -05:00
DanSandClaude Opus 4.8 067c96c425 feat(settings): Wallets modal — TactileButton + drop the footer divider
Phase 1. Swap the 5 StyledButton actions (Open / Create / Scan folder / Reveal /
Close) to TactileButton, and remove the ImGui::Separator above the footer (the
reference has no divider). Already struct-form + fully i18n'd. Deferred: R6
glass-panel grouping of the scanned-folders section (it interacts with the
dialog's pre-computed cardHeight — a separate, more careful change).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:16:25 -05:00
DanSandClaude Opus 4.8 a2c4a8df73 fix(material): wrap DialogWarningHeader so long warnings don't clip
Export All Keys' long DANGER warning was clipped at the card edge because the
header rendered a single unwrapped line. Add PushTextWrapPos so a long warning
flows to a second line; short one-line warnings (import/backup/settings) fit and
are unaffected. Surfaced by the Windows sweep on modal-export-all-keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 22:13:54 -05:00
DanSandClaude Opus 4.8 822891d4ef feat(settings): migrate Export All Keys modal to the reference design
Phase 1. DialogWarningHeader care header (was a hand-rolled red icon+text);
centered DialogActionFooter (Export + Close) replacing the StyledButtons + two
separators; on full success the saved path shows in a copy field with a green
check, partial/error render inline; export progress uses LoadingDots. Full i18n:
the previously-hardcoded notification + status strings now use TR (8 new
export_keys_* keys in en + 8 langs). Added hide() + a modal-export-all-keys sweep
surface. Also fixes a latent Begin/EndOverlayDialog imbalance on the 0-address
early return. No change to the export RPC logic.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:43:53 -05:00
DanSandClaude Opus 4.8 22638b094c feat(settings): migrate Backup Wallet modal to the reference design
Phase 1. Struct-form BlurFloat overlay (was the legacy positional overload);
DialogWarningHeader care header; centered DialogActionFooter (Create Backup +
Close) replacing the StyledButtons + separator; full i18n — title/destination/
button reuse existing backup_* keys, plus two new keys (backup_warn one-line care
line, backup_overwrite_confirm) added in en + 8 langs. No change to backupWallet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:37:58 -05:00
DanSandClaude Opus 4.8 bf81ec1702 feat(settings): migrate Export-Transactions-CSV modal to the reference design
Phase 1 (first modal). Bring the CSV export dialog onto the key-import reference:
- Struct-form OverlayDialogSpec / BlurFloat (was the legacy positional overload).
- material::DialogActionFooter (centered TactileButton pair) replacing the
  StyledButtons + the two ImGui::Separators.
- Full i18n: TR("close") and the success/error status now via TR (were hardcoded
  English); on success the saved path shows in a widgets::AddressCopyField with a
  green check.
- Add ExportTransactionsDialog::hide() + a modal-export-transactions sweep surface
  so the dialog is captured (it wasn't before).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:22:41 -05:00
DanSandClaude Opus 4.8 56cd9b6273 fix(settings): make the rescan slider visible on light themes
The scan-height slider drew its track, % label, and thumb in pure white, which
vanished on light skins because the glass sub-section goes opaque white there
(only the numeric input showed). Switch those to OnSurface-based colors so they
adapt: near-white on dark (unchanged look) and dark on light (now visible).
Surfaced by the full Windows UI sweep on the marble/light skins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 21:04:58 -05:00
DanSandClaude Opus 4.8 8b9cd5bf80 feat(material): extract DialogActionFooter + GlassSectionScope helpers
Phase 0b of the settings-modal redesign — pull the reference dialog's two
repeated patterns into shared helpers so the upcoming migrations are mechanical
and identical:

- material::DialogActionFooter(primary, enabled, close, &outPrimary, &outClose)
  — the centered fixed-width primary+Close TactileButton pair with no divider.
- material::GlassSectionScope — an RAII auto-sized glass sub-section using a LOCAL
  ImDrawListSplitter (combo-safe, unlike GlassCardScope's shared ChannelsSplit) with
  the reference's 12/10 padding and 8dp rounding; interiorWidth() for full-width kids.

Dogfooded by refactoring renderImportKeyDialog's footer and scan-height panel onto
them — same padding/spec/centering, so the render is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 20:49:04 -05:00
DanSandClaude Opus 4.8 2b191cea34 fix(settings): securely wipe the exported private key (no heap-lingering secret)
The Export Private Key dialog held the exported spending key in a plain
std::string cleared only with .clear() — never sodium_memzero'd — and on
outside-click/Esc the overlay's early return skipped even that, so the secret
lingered in freed heap. Hold it in a fixed char[256] and sodium_memzero on every
close path (Close button, outside-click/Esc early return, address change). Guard
the async export callback so it can't populate the buffer after the dialog closed.

Standalone security fix ahead of the settings-modal redesign (Phase 0a); the
dialog's full restyle is Phase 2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 20:44:25 -05:00
DanSandClaude Opus 4.8 6ecbf835ec refactor(settings): unify the viewing-key care header with the private-key one
Settle the key-import reference before propagating its design to the other
settings modals: the viewing-key import now uses the same DialogWarningHeader
(⚠ warning icon + Warning color) as the private-key import, instead of the milder
grey eye + grey note. The viewing-key note is longer, so that mode gets a wider
card (760 vs 640) to keep it on one line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 20:37:51 -05:00
DanSandClaude Opus 4.8 a13190a6df refactor(settings): import dialog layout polish
Several layout refinements to the Import Private/Viewing Key dialog:
- Widen the dialog to 640px so the one-line warning under the heading no longer wraps.
- Vertically center DialogWarningHeader's label with the (taller) warning icon
  instead of top-aligning it — a consistent fix for every warning header.
- Add a small gap between the key input's text and the flush eye toggle.
- Center + slightly enlarge the Paste / Clear buttons.
- Center the Import/Sweep + Close action buttons.
- Scan-height control: put the numeric input first with the slider below it, and
  drop the redundant "0 = rescan from the start" hint (0 is the field default; the
  transparent-key note is kept).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:59:53 -05:00
DanSandClaude Opus 4.8 ffc24ee99b refactor(settings): drop the divider above the import dialog's action buttons
Remove the separator between the content and the Import/Sweep + Close buttons;
the spacing alone separates them cleanly enough.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:50:05 -05:00
DanSandClaude Opus 4.8 3274c72a58 refactor(settings): drop the redundant "Key" label above the import field
The "Import Private Key" / "Import Viewing Key" dialog heading already gives the
context, so the "Key" / "Viewing key" label above the input was redundant. Remove
it; the field is the dialog's primary input and reads fine without it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 19:47:59 -05:00
DanSandClaude Opus 4.8 b7b32bfde8 fix(settings): stop paste-preview hover flicker from layout shift
Hovering Paste reflected the clipboard's classification in the type-indicator
ROW, which appeared/disappeared with the hover. That grew/shrank the centered
dialog, moving the Paste button out from under the cursor → hover ended → row
vanished → dialog shrank back → cursor over the button again → rapid flicker.

Keep the in-field preview + verification but make them pure draw-list (no layout
change): the ghost overlay is now coloured by how the clipboard would classify
(green ok / amber wrong-type / red unrecognized), and the type indicator reverts
to the actual field content only. The dialog height is now identical whether or
not Paste is hovered, so there's no shift and no flicker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:52:11 -05:00
DanSandClaude Opus 4.8 0b47134112 feat(settings): labelled Paste/Clear + in-field clipboard preview
Restore text labels on the import dialog's Paste and Clear buttons (the reveal
eye stays a compact icon flush in the field), and replace the paste-hover tooltip
with an in-field preview like the send tab: while Paste is hovered and the Key
field is empty, the trimmed clipboard is drawn as a transparent ghost overlay
inside the field — masked with '*' unless the key is revealed — so the user can
verify it before pasting.

The type indicator now reflects the hovered clipboard (indKey/iRecognized/…),
while the Import/Sweep button guard and the scan panel keep using the actual
field content only, so a hover can never enable or trigger the action. Removes
the now-unused RenderPasteHoverPreview tooltip helper and the dead wrongType local.

Reviewed via an adversarial pass (overlay masking/security, the indicator-vs-action
classification split, button regressions); the one finding (dead wrongType) is fixed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:43:17 -05:00
DanSandClaude Opus 4.8 828018de2b feat(settings): sweep a private key's funds to your own wallet
Add a "Sweep to my wallet (don't keep the key)" option to the Import Private
Key dialog (spending keys only). Rather than keeping the key, it imports the
key, then sends ALL its funds to a destination you control — a freshly
generated shielded address by default, or an existing wallet address you pick.

Flow (App::sweepPrivateKey): import the key (rescan; the stock node has no
address index, so its UTXOs/notes can't be enumerated otherwise) → confirm it
holds spendable funds → resolve the destination → z_sendmany balance-minus-fee
from the key's address to the destination, via the existing tested submitZSendMany
wrapper + async-operation tracker. The imported key is left in the wallet with an
empty balance (there is no remove-key RPC) — stated plainly in the UI caveat.

Design notes forced by this daemon (verified against external/dragonx):
- z_importkey returns null here, so the shielded source address is found by
  diffing z_listaddresses before/after import (aborts if the pre-snapshot fails,
  so a wrong address can never be picked). Transparent uses importprivkey's return.
- z_sendmany, NOT z_mergetoaddress: moves a single-UTXO transparent source (the
  common paper-wallet case) and fails loudly instead of silently leaving a
  remainder. Amount computed in integer satoshis for an exact fixed-decimal string.
- Clear errors for no-funds / unconfirmed / dust-below-fee / already-imported.
- The mode + destination are locked during a running sweep; a sweep that finishes
  while the dialog is closed still shows its Done/Error+txid on reopen; sweep UI
  can't bleed into the watch-only viewing dialog.

i18n: 8 new sweep_* keys (en + 8 langs); dynamic status strings are English,
matching the beginSweepToSeedWallet precedent.

Reviewed across three adversarial multi-agent rounds (fund-safety focus): round 1
caught the feature was broken for shielded keys (z_importkey null) and rejected
single-UTXO transparent sources; round 2 caught reopen/double-submit/precision
issues; round 3 verified the fixes. NOT yet exercised against a live daemon —
needs a small-amount mainnet test before it's trusted, like migrate-to-seed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 17:29:10 -05:00
DanSandClaude Opus 4.8 2a96c41e07 feat(settings): polish the import-key dialog + paste verify preview
Design pass on the Import Private/Viewing Key dialog:

- Group the optional scan-from-height section (label, slider, field, hint) in a
  subtle glass panel via a draw-list splitter so it reads as one distinct,
  clearly-optional block.
- Replace the boxed reveal / Paste / Clear buttons with frameless Material icon
  buttons: the eye toggle sits flush at the Key field's right edge (rounded to
  match the input frame), Paste / Clear are faint icon pills with tooltips.
- Add a centered %-of-chain label to the slider (mirrors the send-tab amount bar).

New: hovering Paste peeks at the clipboard and verifies it before pasting —
a tooltip classifies the clipboard the same way the field indicator does
(✓ valid type for this dialog / ⚠ a key for the other button / ✗ not a key),
with a preview line masked with '*' unless the key is currently revealed (then a
truncated peek). Empty clipboard shows "Clipboard is empty".

i18n: added paste_clip_empty (en + 8 langs); the preview reuses the field
indicator's type / wrong-type keys.

Reviewed via three adversarial multi-agent passes (splitter/panel, icon buttons,
slider math, and the paste preview's correctness/security + i18n); the confirmed
findings (eye hover-pill rounding, unrecognized-state color) are addressed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:38:36 -05:00
DanSandClaude Opus 4.8 c25da0d26d feat(settings): stack the rescan slider above the height field
Lay the optional scan-from-height control out vertically: the send-tab-style
slider spans the full row on top, with the numeric field full-width directly
beneath it (both were previously side by side). Reads more clearly and gives
the slider room; the unsynced fallback (field only) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:04:23 -05:00
DanSandClaude Opus 4.8 0c55ed0fe4 feat(settings): send-tab-style slider for the rescan start height
The optional "scan from block height" control in the Import Private/Viewing
Key dialog now leads with a slider styled like the send-tab amount bar
(rounded track, accent fill, glass thumb) bounded by the chain tip, with the
numeric field beside it for exact entry and the current chain height
right-aligned above. When the node's height is unknown (state_.sync.blocks == 0,
e.g. not yet synced) the slider is omitted and the field spans the full row —
which also fixes the previous cramped 140px field that left the row half empty.

- New static helpers in app.cpp: formatIntWithCommas() and RenderScanHeightControl()
  (mirrors RenderAmountBar in send_tab.cpp). The height decimal string is the single
  source of truth shared by slider + field; the slider reserves/queries its
  InvisibleButton before drawing so a drag has no one-frame lag; the bar height is
  ImGui::GetFrameHeight() so it aligns with the numeric field.
- Import handler clamps the parsed start height to the chain tip.
- i18n: added import_scan_tip ("current height") in en + 8 langs.

Reviewed via an adversarial multi-agent pass (slider-math / state-sync /
dpi-layout / consistency); the one confirmed finding (bar/field height mismatch)
is fixed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:17:15 -05:00
DanSandClaude Opus 4.8 1fc3e03e6f feat(settings): rename to "Import Private Key" + add scan height to it
Follow-up to the key/viewing-key button split:

- Rename the "Import Key…" button to "Import Private Key…" (settings_import_key
  value in en + all 8 langs, reusing the existing import_key_title wording) so it
  reads as the natural counterpart to "Import Viewing Key…". The dialog title was
  already "Import Private Key". The viewing-key wrong-type redirect hint now names
  the renamed button.

- Show the "scan from block height" field in the private-key (spending) dialog too,
  not just the viewing-key one. z_importkey accepts a start height, so a shielded
  spend import of a recent key needn't rescan the whole chain. Transparent WIF
  (importprivkey) has no such param, so once a transparent key is recognized the
  field is disabled, its buffer cleared, and a "Transparent keys always rescan
  fully" note replaces the hint. importPrivateKey already gates the start-height
  append on (viewing || shielded), so transparent imports never receive it.

- i18n: shared scan keys renamed import_viewkey_scan_{label,hint} ->
  import_scan_{label,hint}; added import_scan_transparent (en + 8 langs).

Reviewed via an adversarial multi-agent pass (rpc-routing / i18n / ui-logic /
consistency); only two nits surfaced (stale disabled-field value + a stale
comment), both fixed here.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:38:40 -05:00
DanSandClaude Opus 4.8 f428292ea4 feat(settings): split import into separate key + viewing-key buttons
Backup & Data now has two distinct import actions instead of one
auto-detecting dialog:

- "Import Key" imports a spending key (transparent WIF or shielded
  z-spending key) with the strong "grants access to funds" warning.
- "Import Viewing Key" imports a watch-only shielded viewing key
  (zxviews…) with a milder eye/watch-only note and an optional
  "scan from block height" field — z_importviewingkey accepts a start
  height, so watching a recent address needn't rescan the whole chain.

The shared Material dialog (renderImportKeyDialog) branches on
import_view_mode_: title, warning tone, field label, the live type
indicator, and the recognition guard. A key valid for the *other*
button (e.g. a spending key pasted into the viewing-key dialog) now
shows a redirect hint rather than a generic "unrecognized" error.

importPrivateKey gains a startHeight arg, appended to the shielded
import RPCs (z_importviewingkey / z_importkey) and ignored for
transparent WIF (importprivkey has no start-height param). The
scan-height buffer is wiped on open/close alongside the key buffer.

i18n: 9 new keys (button label + tooltip, viewing title/note/field,
scan label/hint, two wrong-type redirect hints) with translations for
all 8 languages; CJK subset rebuilt (+1 glyph). A modal-import-viewkey
sweep surface is added for the viewing-mode dialog.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:53:35 -05:00
DanSandClaude Opus 4.8 46f3001360 feat(settings): redesign the Import Key dialog
Rework the full-node Import Key dialog into a safer, Material-consistent, more
capable flow:

- Security: the key is masked by default (Password) with a reveal (eye) toggle,
  a DialogWarningHeader "only import a key you own" note, and the buffer is
  wiped with sodium_memzero on close and on successful import.
- Progress: import triggers a blocking full-chain rescan; the dialog now shows
  "Importing & rescanning — this can take several minutes" with LoadingDots and
  disables Import while it runs (no double-submit). Offline shows a "connect a
  running node" note and disables Import up front.
- Viewing keys: the classifier gains isViewingKey / isRecognizedImportKey;
  importPrivateKey auto-detects a shielded viewing key (zxviews…) and routes to
  z_importviewingkey (watch-only) vs z_importkey / importprivkey. The live type
  indicator shows Transparent / Shielded spending / Shielded viewing (watch-only).
- Result: on success the imported address is captured from the RPC result and
  shown via AddressCopyField.
- Material restyle (OverlayDialogSpec/BlurFloat + TactileButton + Esc) and full
  i18n (12 new keys x 8 languages; CJK subset rebuilt).

Verified: rendered on dark + light skins; a seeded zxviews… key shows the masked
field + "Shielded viewing key (watch-only)" indicator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 12:58:36 -05:00
DanSandClaude Opus 4.8 8293f02e36 fix(console): RPC modal — pointer cursor + Material restyle
Two issues from the audit:

- Cursor: hovering the modal showed the console's text-select cursor. The
  output's mouse-interaction guard only suppressed itself under ImGui popups,
  but this modal is a BeginOverlayDialog (not a popup), so the console kept
  setting the text cursor underneath. Suppress output interaction while the
  reference modal is open (!show_commands_popup_).

- Design: the two panes used hard 1px child borders and floated unpolished.
  Restyle to match the portfolio/wallets modals — each pane now sits on a soft
  DrawGlassPanel surface (borderless child + inner padding), category headers
  are subtle accent labels (no heavy filled bars), and the selected/hovered row
  gets a rounded Material fill instead of the sharp Selectable highlight. The
  glass panes are theme-adaptive (light on light skins), so the command-name
  blue and the mono example green are floored to readable contrast (FloorLight)
  and the detail heading uses theme Primary().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 12:11:54 -05:00
DanSandClaude Opus 4.8 b34069f57c fix(console): don't silently drop a filled param after an empty optional
The builder's "omit trailing optionals" loop stopped at the FIRST empty
optional, which discarded any later field the user had filled — e.g. setban
with bantime empty but absolute=true built `setban "ip" "add"`, dropping the
typed `true`, and (worse) left Insert & run enabled so it ran a command that
ignored the input.

Include fields up to the last one that is filled or required; only genuinely
trailing empty optionals are dropped. A gap (an empty field before a later
filled one) keeps its placeholder and marks the command incomplete, so Insert
& run disables and nothing typed is lost. Found by an adversarial pass over all
82 param templates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:38:25 -05:00
DanSandClaude Opus 4.8 4416e01f9c feat(console): parameter builder in the command explorer (phase 3)
The detail pane's Parameters section is now an editable form: each parameter is
a type-hinted input field (string / number / json, optional marked), and a live
"Builds" preview shows the command being assembled. String values are
auto-quoted; a required field left empty keeps its placeholder and disables
"Insert & run"; trailing empty optionals are omitted.

Insert and Insert & run now use the assembled command (not just the template),
so Insert & run works for parameterized commands once the required fields are
filled — destructive commands still confirm first. Param fields reset when the
selected command changes or the modal opens. Keyboard nav (Up/Down/Enter) is
scoped to the search box so typing in a field doesn't move the selection.

A best-effort parseParamSpecs turns the human-readable param templates into
fields; JSON-array params (z_sendmany, createrawtransaction) surface as a single
"json" field the user pastes into. Adds the console_ref_builds label + 8
translations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:32:35 -05:00
DanSandClaude Opus 4.8 44a17f3ad0 feat(console): enrich the remaining RPC commands for the explorer (phase 2)
Add plain-language details, a concrete example, and intent keywords to the
50 commands that Phase 1 left with only the terse description — so every entry
in the command explorer now explains itself and is searchable by task, not just
by name. Destructive flags are preserved.

Content was authored per-category and reviewed for technical accuracy (correct
RPC semantics, valid console example syntax, realistic DragonX placeholders).
Data-only change; command docs stay English (technical), consistent with the
existing policy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:16:55 -05:00
DanSandClaude Opus 4.8 c9e9c9f979 feat(console): redesign the RPC reference as a novice-friendly command explorer
Replace the flat, terse command-reference table with a guided two-pane
explorer built for users who don't know the RPC commands:

- Search by name OR task: keyword synonyms let "balance" find getbalance,
  "send money" find sendtoaddress; results are ranked (name-prefix > name >
  keyword > desc).
- Two panes (mirrors the portfolio editor): a category-grouped master list
  (mono names, a warning dot on consequential commands) + a rich detail pane
  with a plain-language explanation, a per-parameter type breakdown (string /
  number / json, optional dimmed), and a concrete example.
- Insert into console (fills the input with a template to review + run) and,
  for no-parameter commands, Insert & run — with a confirm for destructive
  ones. Deferred via pending_submit_ so the modal needs no executor.
- Keyboard: type to filter, Up/Down to move, Enter to insert; auto-focus and
  auto-select the top result.

Data model: ConsoleCommandEntry gains details/example/keywords/destructive
(C++17 defaults, so the ~60 un-enriched entries are untouched); ~20 common
commands enriched and 18 consequential ones flagged. Command docs stay English
(technical); the UI chrome + 7 category names are translated into all 8
languages (CJK subset rebuilt, +3 glyphs).

Verified: two-pane renders on dark + light skins and at font_scale 1.5; detail
pane shows explanation/params/example; danger dots + safety badge on
destructive commands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 11:07:49 -05:00
DanSandClaude Opus 4.8 ab4dd370c0 fix(console): RPC command-reference modal audit fixes
- Clicking a command now actually closes the modal. It called
  ImGui::CloseCurrentPopup(), a no-op here (BeginOverlayDialog is Begin/
  BeginChild, not an ImGui popup), so the picker stayed open covering the input
  and the insert looked like it did nothing. Set show_commands_popup_=false.
- Search filter is now a member cleared when the modal opens, so it isn't stale
  on reopen after an outside-click / command-click dismiss (was reset only by
  the Close button).
- Esc dismisses the modal (this overlay has no built-in Esc handling), and the
  search box auto-focuses on open so the user can type immediately.
- Scale the 2-vs-3 column breakpoint (cmd-min-width) by dpiScale() — it was a
  logical-px threshold compared against a physical-px available width.
- Give each category's table a unique ImGui id (##cmdsN) instead of sharing
  "##cmds", and translate the 7 category names (Control/Network/... — command
  descriptions stay English) into all 8 languages; CJK subset rebuilt (+2).

(Investigated but rejected: the command-name blue was flagged as low-contrast
on light skins, but the modal card is a dark glass panel on every theme, so the
original color is correct — verified by rendering marble/light/color-pop-light.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:25:51 -05:00
DanSandClaude Opus 4.8 675d434958 fix(console): mop up audit minors — timestamp, filter count, new-line count
- D2: rpcTraceTimestamp used std::localtime (process-wide static tm) guarded by
  a private mutex that can't stop another thread's localtime from clobbering the
  shared buffer. Use localtime_r / localtime_s into a local tm (the codebase
  pattern) and drop the now-useless mutex. Runs on RPC worker threads.
- C1: the toolbar's "<N> matches" label read filter_match_count_ before
  renderOutput recomputed it, so it lagged one frame. Compute the visible/
  filtered set once at the top of render() (before the toolbar) and reuse it in
  renderOutput, so the count and the output share one consistent computation.
- C4: the "N new lines" indicator counted the raw drain count, inflating it with
  lines the active filter hides. Count only newly-added lines that pass the
  current filter, so the badge matches what the user sees on jumping to bottom.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 18:10:27 -05:00
DanSandClaude Opus 4.8 e8055888a5 feat(console): persist the output zoom level across launches
The console zoom (s_console_zoom) was a plain static reset to 1.0 each launch,
unlike the sibling toggles. Wire it through Settings (console_zoom float),
restored into the static at startup in the App ctor and written back via the
existing post-render diff-and-save. Loading clamps to [0.25, 4.0] (also
catching NaN) so a corrupt value can't feed the scanline divisor.

Verified: seeding console_zoom=1.5 survives a relaunch and the console text
renders larger.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:54:47 -05:00
DanSandClaude Opus 4.8 48eceb6593 fix(console): don't coerce quoted or malformed numeric args to JSON numbers
BuildConsoleRpcCall pushed any bare token that "looked numeric" as a JSON
number, and lost the tokenizer's quoting decision — so a genuinely-string
argument that happened to be all digits (e.g. a label) was sent with the wrong
type. std::stoll also silently truncated "1e999" to 1 (stops at the first
non-digit) and std::stod could yield inf/subnormal.

- ParseConsoleCommandArgsTagged returns {text, quoted}; quoted tokens are sent
  verbatim as JSON strings, never coerced. (ParseConsoleCommandArgs kept as a
  text-only wrapper so existing callers/tests are unchanged.)
- Bare tokens become a number only when the WHOLE token parses to a finite
  int/double; otherwise they're sent as a string.

Adds test coverage for quoted-numeric-string, bare-number, 1e999, and 123abc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:49:02 -05:00
DanSandClaude Opus 4.8 4f71c18884 fix(console): audit fixes — HiDPI, fold UX, stop-confirm, RPC-trace UAF
From the console-tab audit:
- HiDPI: the "New output" pill (140x24 + margin/rounding/border) and the JSON
  indent-guide thickness were hand-drawn in raw px; scale them by dpiScale().
- Fold triangle: size it from the DPI/density-scaled gutter width (not the
  zoomed font) and center it in the gutter band so glyph and clickable cell
  stay aligned; only draw/handle it in the unfiltered view (folding_active_) —
  in a filtered flat view a click silently flipped the collapsed flag with no
  visible effect and the glyph disagreed with the rendered block.
- Selection: ignore left-clicks in the gutter (< output_origin_.x) so toggling
  a fold no longer clears the user's active text selection.
- 'stop' confirmation: promote the function-local static to a member and reset
  it in clear(), so a toolbar/context-menu clear between the two 'stop's can't
  leave a stale arm that skips the shutdown warning.
- RPC-trace callback: dereference the console pointer under the mutex (not
  after releasing it) so ~ConsoleTab can't destroy the object mid-call —
  closing a latent use-after-free on shutdown (fires on RPC worker threads).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 17:45:51 -05:00
DanSandClaude Opus 4.8 232b81f8ee feat(settings): surface the console color toggles as checkboxes
Add "Color accents" and "Text colors" checkboxes under the effects section
(both layout variants) so the console output display prefs are discoverable
outside the console toolbar. They mirror the toolbar buttons — bound live to
the ConsoleTab statics and written through to settings on change.

Unlike scanline/theme-effects they carry no GPU cost, so a small helper closes
the effects row's BeginDisabled(low_spec), draws them on their own always-
enabled row, and reopens it for the acrylic sliders (net-balanced). Adds
console_accents / console_text_colors labels + 8 translations; the tooltips
reuse the existing toggle keys.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:40:25 -05:00
DanSandClaude Opus 4.8 c40c252c9a feat(console): persist accent toggle + add monochrome-text toggle
Persist the two console output display preferences and add a second toolbar
toggle for monochrome text.

- Persistence: new Settings fields console_line_accents / console_text_color
  (both default true, matching the ConsoleTab statics so an upgrade re-save
  can't flip visible behavior). Restored into the statics at startup in the App
  constructor, and saved from a diff-and-save after the console renders (only
  when a value actually changed — save() is disk I/O). The startup restore also
  fixes a pre-existing bug: scanline was only synced from settings when the
  Settings page or first-run wizard ran, so an existing wallet ignored a saved
  scanline preference on launch.
- Monochrome text: a new toolbar toggle (FORMAT_COLOR_TEXT, dimmed when off)
  next to the accent toggle. channelTextColor() early-returns the neutral
  COLOR_RESULT for every channel (and JSON syntax role) when off, leaving the
  left accent bars independent. COLOR_RESULT is contrast-floored per theme, so
  text stays readable on light and dark terminals.

Adds console_toggle_text_color to the English source + all 8 translations.
Verified: both toggles independent, persistence round-trips across restart,
monochrome readable on light + dark skins.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 16:26:44 -05:00
DanSandClaude Opus 4.8 d156dddcd0 feat(console): toolbar toggle to hide per-line color accent bars
Adds a paint-bucket toggle button to the console toolbar (in the appearance
cluster next to zoom) that shows/hides the per-line left color accent bars for
a cleaner monochrome gutter. The icon reflects state (FORMAT_COLOR_FILL when on,
a dimmed FORMAT_COLOR_RESET when off) with a tooltip. Session-only, like the
console zoom factor; default on.

Adds console_toggle_accents to the English source and all eight translations
(no new CJK glyphs — the subset font already covers them).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:36:51 -05:00
DanSandClaude Opus 4.8 0e1957c5f9 feat(market): explain why the portfolio Save button is disabled
Hovering the disabled Save button now shows which requirement is missing
(name / at least one address / a manual price above 0), mirroring the
send-tab disabled-submit tooltip idiom (IsItemHovered(AllowWhenDisabled)).
The hint only appears for a validation gap — not the self-evident "nothing
changed" case.

Adds portfolio_save_need_{name,address,price} to the English source and all
eight translations; rebuilds the CJK subset font for the new JA/KO/ZH glyphs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:22:56 -05:00
DanSandClaude Opus 4.8 3f95765dcc fix(balance): unique IDs + scope filter for the Add-to-portfolio menu
The address context menu's "Add to portfolio" submenu built each item with
MenuItem(label), whose ImGui ID is derived from the label — so two groups with
the same label (or two empty labels) collided, routing a click to the wrong
group, and a blank label rendered as an invisible row.

- PushID(pi) per item gives a stable unique ID regardless of label, and an
  empty label now shows the "new entry" placeholder.
- Filter the list to the active wallet's groups (or legacy/unscoped), matching
  the Market summary and editor, so an address can't be added to a different
  wallet's group. The "no entries" placeholder now also covers the case where
  every group is filtered out.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:11:35 -05:00
DanSandClaude Opus 4.8 62c92cc862 fix(market): scope the portfolio editor to the active wallet
The Market summary filters custom groups by wallet scope, but the editor did
not: its master list showed and edited *every* wallet's groups, and "Manage…"
opened raw storage index 0 — which could belong to a different wallet.
Separately, a group could be persisted with an empty scope (a placeholder
added before the wallet identity resolved, then filled in and saved), which the
filter treats as "legacy" and shows in *every* wallet's list.

- The editor master list now filters to the active wallet's groups (plus
  legacy/unscoped), mirroring the summary. Storage indices are preserved so
  selection/delete still target the right entry.
- "Manage…" opens the first in-scope group (or the empty state), never index 0.
- The selection clamp resets to the first visible group if the current one is
  out of range or out of scope.
- On commit, a group with no scope is claimed for the active wallet when an
  identity is available, so it can't linger in the global bucket.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:08:28 -05:00
DanSandClaude Opus 4.8 3aeb847657 fix(market): make portfolio-editor dismiss gestures consistently auto-save
The Close button auto-committed the current group, but Escape, outside-click,
switching groups, adding a group, and deleting another group all *discarded*
uncommitted edits — so whether your work was kept depended on how you happened
to leave, and the prominent unsaved-dot / Save / Revert affordances implied a
save model the exits didn't honor.

Align every dismiss/switch gesture with the Close button's intent (auto-save):
- Escape and outside-click now commit before closing (outside-click is caught
  right after BeginOverlayDialog, which clears open mid-frame).
- Switching to another group, "Add entry", and deleting a *different* group
  commit the current group first. Deleting the selected group still discards
  its own working state.

Save remains "persist now", Revert "undo to last saved", and the dot marks an
in-progress group. Ordering is safe against the placeholder GC: that GC runs
earlier in the frame (mktDrawPortfolio, before RenderPortfolioEditor) and only
while the editor is closed, so it never shifts entry indices under a commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 15:03:17 -05:00
DanSandClaude Opus 4.8 2a9d32e78b fix(market): close portfolio-editor gaps found in re-audit
Adversarial re-audit of the group editor surfaced follow-ups to the prior
hardening pass:

- Dirty-check regression: the empty-currency->"USD" coercion was applied on
  save but not in the dirty comparison, so clearing the currency field left a
  group perpetually "dirty" (unsaved dot + Revert/Save never settling, a
  redundant persist on every Close). Both sides now share pfEditLabel() /
  pfEditCurrency() so they can't disagree — this also trims the label, so a
  trailing space no longer wedges the dirty state.
- Whitespace-only labels ("   ") passed validation and persisted as an
  invisible group; the trimmed label is now required by pfWorkingValid.
- "Add entry" persists an empty placeholder up front; abandoning it (Close/
  Esc/outside-click without adding an address) left a phantom "$0 · 0" group.
  The summary now prunes address-less groups while the editor is closed.
- Manual price accepted inf/NaN (neither is < 0); sanitize with std::isfinite.
- Escape closed the whole editor even when it was only meant to dismiss the
  open custom-color popup, discarding edits; guard on !IsPopupOpen.
- HiDPI: scale the Z/T chip pill's vertical padding and the balance-breakdown
  baseline nudge that the prior pass missed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:51:32 -05:00
DanSandClaude Opus 4.8 97a54f0e0c fix(market): harden portfolio group editor + HiDPI scaling
Portfolio audit fixes:
- Guard the BTC value line behind price_btc > 0 so an empty BTC price no
  longer renders a misleading "≈ 0.00000000 BTC".
- Validate the working group before commit/save (pfWorkingValid: needs a
  label, at least one address, and a positive manual price when the manual
  price basis is selected); the Save button disables to match, so a
  half-filled editor no longer persists a broken group on close/switch.
- Default an empty manual currency to "USD" instead of an empty string.
- Reload the editor after clamping a stale selection index so it can't
  point past the end of the group list.
- Scale hand-drawn geometry by dpiScale(): address-row rounding, chip
  y-offset, preview border, chart toggle buttons, and the shielded/
  transparent ratio-bar corner radii now render correctly on HiDPI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 14:14:45 -05:00
DanSandClaude Opus 4.8 82da4af857 feat(market): per-pair candle cache — instant switch-back, no re-fetch
The per-exchange chart used a single buffer keyed by the loaded pair, so
bouncing between venues (Ourbit <-> NonKYC) re-fetched every switch (loading
spinner each time). Add a per-pair cache: each venue's fetched candles (OHLC +
derived close series) are kept keyed by "<identifier>:<BASE>/<QUOTE>". Switching
back to a recently-viewed pair loads from the cache instantly — no fetch, no
spinner — while entries older than 30 min re-fetch to stay fresh. Bounded to 16
entries (irrelevant for DRGX's 2 venues, but safe if it lists more). Cache is
main-thread-only (populated by the worker's completion MainCb), so no races.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:48:53 -05:00
DanSandClaude Opus 4.8 bfe8b4d77d change(settings): default Windows window opacity to 90%
Raise the Windows window-opacity default (0.75 -> 0.90) so less of the
(often dark) desktop bleeds through the wallpaper behind the panels.
Mac/Linux stay fully opaque. Only affects fresh installs / unset values.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:40:40 -05:00
DanSandClaude Opus 4.8 78c00daf92 change(modals): raise the fixed modal-backdrop blur to 120px
Bump the hardcoded modal blur radius (96 -> 120) for a softer, more
recognizable frost behind dialogs. Still independent of the acrylic slider.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:40:40 -05:00
DanSandClaude Opus 4.8 e82514f46a feat(market): persist chart range + style across sessions
The chart interval (Live/1H/1D/1W/1M) and the line/candle style were
session-only statics. Persist both like the selected exchange/pair: new
chart_interval / chart_style settings (defaults 1M / candlestick),
loaded into the market view on first show and saved on each interval
click / style toggle.

Verified: writing chart_interval=2, chart_style=0 to settings.json and
launching restores them (not reset to defaults) and re-saves them intact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 12:05:36 -05:00
DanSandClaude Opus 4.8 76708e9191 feat(market): line/candle toggle + ~50% taller chart
- Add a chart-style toggle (line vs candlestick) next to the interval buttons,
  shown only when the selected range has per-exchange candles (the aggregate /
  Live view is line-only). It flips s_mkt.chartStyle; candles draw when OHLC
  exists AND the user hasn't switched to the line. Icon reflects the current
  style; tooltip says what a click switches to. Two i18n keys across 8 languages.
- Make the price chart ~50% taller: scale the height floor / desired / viewport
  cap (110->165, x1.5 desired, 0.22->0.33 of available) for a roomier plot.

Verified via a forced-OHLC render: the taller chart, the toggle button, and
candles with a correct week/day x-axis.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:54:08 -05:00
DanSandClaude Opus 4.8 a767702ff6 feat(market): OHLC hover readout on the candlestick chart
Candles had no hover (the line's close tooltip is gated off for them). Add a
candle-aware readout: the candle under the cursor gets a column highlight +
crosshair, and a small box shows its date and open/high/low/close (colored green
up / red down). Verified via a forced-hover render (date + O/H/L/C over the
highlighted candle). Line charts keep their existing single-price tooltip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:46:26 -05:00
DanSandClaude Opus 4.8 40e8128a30 feat(market): candlestick chart for per-exchange OHLC
The exchange candle APIs return full OHLC but we only kept the close (a line).
Now the per-exchange chart draws real candlesticks; the CoinGecko aggregate stays
a line (it's close-only).

- Adapter keeps OHLC: parseExchangeOHLC() returns open/high/low/close candles
  (parseExchangeCandles is now a close-only wrapper over it). New data/candle.h
  holds the dependency-free Candle + bucketOHLC (5-min -> hourly for the 1D view).
- Model stores exchange_ohlc_intraday/daily alongside the close series;
  refreshExchangeChart populates both. market_series::chartCandles() returns the
  bucketed OHLC for the range, empty unless the per-exchange series is active.
- The chart renders wick (low..high) + body (open..close), green up / red down,
  with a low..high y-range; the line-only bits (fill, hi/lo labels, hover tooltip)
  are gated off for candles. Falls back to the line for the aggregate / Live view.

Verified: OHLC parsers + bucketOHLC + chartCandles unit-tested; a forced-state
render shows correct candlesticks; the aggregate still draws a clean line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 11:16:44 -05:00
DanSandClaude Opus 4.8 326192e75e feat(market): per-exchange 24h volume in the hero
The hero's "24H Vol" showed CoinGecko's cross-exchange aggregate even after the
chart/price went per-venue. Capture the per-exchange converted_volume.usd from
the tickers (previously discarded, alongside converted_last) and show the
SELECTED exchange's own 24h volume; fall back to the aggregate when unknown.
Market cap stays aggregate (it's coin-wide, not per-venue).

This is a big real difference — e.g. Ourbit ~$14.3K vs NonKYC ~$253 for DRGX/USDT
— so the header now reflects the venue you're actually looking at.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 10:58:50 -05:00
DanSandClaude Opus 4.8 a107fda9b8 feat(market): loading spinner while the price chart fetches (pair switches)
Switching exchange pairs (or first load) briefly leaves the chart with no series
while the venue's candles fetch, which showed the bare "No price history
available" empty state. When a chart fetch is in flight — App::isMarketChartLoading()
(the CoinGecko aggregate OR the per-exchange fetch) — draw a spinner + animated
"Loading price history…" in the plot area instead; the "no history" text only
shows when genuinely empty and idle. New i18n key market_chart_loading across all
8 languages. Verified via a forced-state render (spinner + label centered).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 09:38:44 -05:00
DanSandClaude Opus 4.8 d1ff58c374 feat(market): per-exchange price chart from each venue's own candle API
The market chart was hardwired to CoinGecko's cross-exchange USD aggregate
(vs_currency=usd), so selecting a trading pair only changed the "Trade" link —
never the chart or price. Now the SELECTED exchange drives both.

CoinGecko gives us which venues list DRGX (the ticker `market.identifier`) but
not their APIs, so add data/exchange_candles.h: a hand-maintained map from that
identifier to each venue's public candle endpoint, with two adapters verified
against the live APIs — Ourbit (MEXC-style /api/v3/klines, array format) and
NonKYC (TradingView-UDF /market/candles, `bars`). Unmapped venues return no URL,
so the chart falls back to the CoinGecko aggregate and nothing regresses.

- App::refreshExchangeChart() resolves the selected pair, fetches its intraday
  (5-min) + daily candles via the TLS-verified httpGetString on the worker, and
  stores them on MarketInfo (exchange_chart_intraday/daily + exchange_chart_active).
  Re-fetches on pair change; self-throttled to ~30 min otherwise; falls back to
  aggregate on any failure.
- chartSeries() draws the per-exchange series when active (portfolio sparklines
  stay on the aggregate). The hero shows the selected venue's real price
  (converted_last) — Ourbit vs NonKYC differ ~7%. parseCoinGeckoTickers now
  captures the identifier + per-exchange USD price (previously discarded).

Adapters unit-tested against real captured responses (URL builder + both parsers
+ the chartSeries switch) and validated against the full live feeds (Ourbit 20d,
NonKYC 370d, ascending, correct closes). Build + ctest + hygiene green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 09:19:55 -05:00
DanSandClaude Opus 4.8 156735eb06 feat(modals): fixed modal-backdrop blur, independent of the acrylic slider
The modal backdrop blur was params.blurRadius (96) scaled by the user's global
acrylic blur-strength slider (blurRadiusMultiplier), so lowering the slider also
weakened every modal's backdrop. Make the modal blur a fixed hardcoded value.

Add AcrylicParams::absoluteBlurRadius: when set, applyBlur() uses the radius
as-is and skips the multiplier (threaded through both the GL and DX11 blur paths
+ the no-op stub). DrawFullWindowBlurBackdrop sets it, so the modal backdrop is
always a 96px blur regardless of the slider. Panels/other acrylic still scale
with the slider as before.

Verified: rendering a modal at blur_multiplier 0.1 vs 2.0 now produces an
identical backdrop (max pixel diff 1/255) — previously those were ~9.6px vs
~192px of blur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 07:41:53 -05:00
DanSandClaude Opus 4.8 2f86bec98e feat(wallets): subtle empty-state hint in the fixed-height list
With the list now pinned at max height, a few wallets leave blank space below
the cards. Fill it — only when there's real room to spare — with a subtle
centered folder glyph + "Scan a folder to find more wallets" (OnSurfaceDisabled),
so the area reads as a gentle nudge toward the scan action rather than dead
space. Purely decorative (draw-list only, clipped to the list); the actions stay
below. New i18n key wallets_empty_hint, translated across all 8 languages.

Verified on the sweep: the hint centers in the gap and reads subtly on both dark
and light themes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 07:17:34 -05:00
DanSandClaude Opus 4.8 6310f51f65 feat(wallets): fixed max-height list + manage scanned folders
Two wallets-modal changes:

- The wallet list now always renders at its max height (kMaxVisibleRows) instead
  of shrinking to the wallet count, so the modal is a consistent size whether you
  have one wallet or many — fewer rows leave empty space, more than 7 scroll.

- Add a "Scanned folders" manager: each user-added scan folder is listed with a
  control to stop scanning it (removeExtraFolder + save + re-scan so its wallets
  drop out). Paths front-elide to keep the identifying leaf visible, with a
  full-path tooltip. The card-height math reserves the manager's rows so nothing
  clips. Two new i18n keys (wallets_scanned_folders / wallets_remove_folder),
  translated across all 8 languages; CJK subset rebuilt.

Verified on the sweep at 1.0 and 1.5x DPI: max-height list on both the few- and
many-wallet surfaces, the folder manager renders and stays clear of the footer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 07:04:43 -05:00
DanSandClaude Opus 4.8 09e0b962c3 i18n(wizard): translate the first-run wizard (was all hardcoded English)
The first-run wizard predated the i18n system — every label, button, and help
line was hardcoded English. Route them all through TR():

- 62 literals in app_wizard.cpp replaced with TR(). Where a wizard string is a
  parallel of existing content, it reuses the existing key (the whole bootstrap_*
  family, plus theme / language / balance_layout / low_spec_mode / ui_opacity /
  console_scanline / download / retry / cancel) so it inherits their translations
  and stays consistent with Settings. Button labels drop their ##id suffix and use
  TR(key) directly (the app's established TactileButton(TR(...)) pattern; the
  wizard's phase buttons never share a frame, so no id collision).
- 55 new wiz_* English keys in i18n.cpp for the wizard-specific strings, and their
  translations back-filled across all 8 languages (es/de/fr/pt/ru/zh/ja/ko);
  format specifiers (%s, %zu) and newlines preserved. CJK subset rebuilt.

Verified on the sweep: English renders with correct labels in place (all keys
resolve), and a German run shows the reused + new keys translated end-to-end
(Darstellung / Acryl-Glaseffekte / Verschlüsseln & Weiter …).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 01:41:18 -05:00
DanSandClaude Opus 4.8 758ea06ab7 feat(wizard): modernize the first-run wizard to the current design language
The first-run wizard predated the modal-unification pass and was the least
on-brand surface left — a flat Surface() slab with a 3-card masonry whose
inactive steps were heavily greyed (alpha 165) and whose focused card used a
hard 2px border + a hand-drawn offset shadow.

Keeping the working phase logic and the "see all steps at once" masonry:
- Reveal the skin backdrop: the app already paints its marble / gradient /
  acrylic backdrop behind every window (drawWindowBackdrop), so drop the opaque
  Surface() fill (transparent WindowBg) and lay a gentle theme-tinted scrim over
  it. The wizard now sits on the same rich backdrop as the rest of the app.
- Focused card lifts with the shared DrawCardDropShadow (the uniform, non-
  clipping card shadow) instead of a bespoke offset rect, and a softer 1.5px
  accent ring.
- Inactive cards get lighter veils (upcoming 165->115, completed 110->70) so
  they read as "waiting" / "done" rather than disabled-grey.
- Warmer header: a one-line subtitle under the welcome title.

Verified on the headless sweep across all four phases (appearance / bootstrap /
encrypt / pin) on marble + obsidian at 1.0 and 1.5x DPI — backdrop shows, cards
float cleanly, geometry scales, nothing clipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:48:22 -05:00
DanSandClaude Opus 4.8 d189ee5ddb fix(ui): readable error text on dark modal backdrops
The Material --error reds are muted by design, so error text drawn with the
theme Error() color read low-contrast on the (now deeper) dark modal backdrops —
the audit flagged the seed-migration warning/error lines on dark / dragonx.

Add material::ReadableError(): the theme error lifted toward a legible light red
on dark themes, unchanged on light themes (where it's already a dark red that
reads on a light background). Apply it to the modal error-TEXT sites (lite
first-run, seed backup, seed migration). This is targeted — it never touches the
app-wide Error() used by toasts / badges / status dots — so nothing else shifts.

Verified on the sweep: the "node too old" warning (dark) and "daemon did not
respond" error (dragonx) now read clearly; light themes are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:27:44 -05:00
DanSandClaude Opus 4.8 32d6e34c45 fix(ui): theme-aware success green for modal status text
Modal success/status lines (import, backup, seed backup, seed migration) used a
hardcoded ImVec4(0.3, 0.8, 0.3) green that ignores the theme — fine on dark, but
faint on the light skins, where the audit flagged it as low-contrast. Switch
those 8 sites to ui::material::SuccessVec4(), so light themes get their tuned
dark green (#3D7A42 etc., readable on white) and dark themes their light green
(#81C784). The 2 status-bar mining indicators keep the vivid attention-green.

Verified on the sweep: the "already has a seed phrase" / "migration complete"
greens are now clearly readable on light, marble, and color-pop-light.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:18:48 -05:00
DanSandClaude Opus 4.8 088e3371d9 feat(modals): stronger backdrop blur + dim, and anchor floating modals on light skins
The modal audit found card-less (floating) dialogs read slightly unanchored on
the light skins: the blur backdrop's dim is a theme-tinted frost, which darkens
on dark themes (content pops) but WHITENS on light themes — so sparse content
floated in a near-white expanse with no figure/ground separation.

- Light themes now get a neutral dark veil instead of the whitening frost, which
  darkens the surround and makes the modal's fields/buttons stand out.
- Dark themes keep the theme-tinted frost, a touch stronger (alpha 70 -> 90).
- Full-window blur deepened (64 -> 96) so the backdrop reads clearly frosted.

Verified on the headless sweep across light + dark: backup / debug-gate now
read as anchored groups; content-heavy modals (daemon-update, settings, wallets)
are unaffected (their own surfaces stand out), and dark-skin text contrast
improves for free from the deeper dim.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 22:18:48 -05:00
DanSandClaude Opus 4.8 49b5ef6b20 fix(wallets): key in-place links on the canonical path (one wallet, one link)
A wallet reached via two different path strings — e.g. a symlinked mount
(/mnt/usb) and its real target (/media/usb) — used to hash to two different
in-place link names and list as two rows, so the same file looked like two
wallets with two datadir links.

Give WalletRow a canonPath (fs::canonical, symlinks resolved, computed once at
scan time so the every-frame render/active-marking path stays cheap) and key
both the link name and the row de-dup on it. Now the same physical wallet maps
to one identity regardless of the path used to reach it: one link name (honors
the "distinct per-target name" invariant), and scan() lists it once — datadir
first, so a datadir wallet wins over the same file seen via an external path.
The link also points straight at the resolved real file instead of through the
symlinked path. Genuinely distinct wallets keep distinct identities (verified
with a standalone symlinked-mount harness).

Closes the last deferred finding from the open-in-place adversarial review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:47:12 -05:00
DanSandClaude Opus 4.8 6aaab9e01f feat(wallets): prefer symlink on Linux/macOS for open-in-place
On Unix a symlink is the better datadir link than a hard link: it needs no
privilege, spans volumes (a wallet on a USB / other partition just works,
where a hard link can't), and is visibly a pointer rather than an
indistinguishable second copy. Windows keeps hard-link-first (a symlink there
needs admin / Developer Mode), falling back to a symlink.

Symlink-first made two latent issues in the pre-clear reachable, fixed here
after an adversarial review:

- Dangling prior symlink: fs::exists follows the link (false when the source
  moved), so it was skipped and create_symlink then tripped on the occupied
  path. Catch it with fs::is_symlink (lstat) and replace it.

- Stale reserved-name occupant: the old "hard_link_count == 1 → reuse" path
  could load the WRONG wallet — a count-1 file at our name is not a valid link
  to the source (a live hard link has count >= 2), it's a stale orphan (source
  deleted, or a copy left by a datadir migrated across filesystems). Gate reuse
  on fs::equivalent(link, src) (same inode) instead, and reclaim the name only
  when the occupant is provably redundant (another hard link still holds the
  data); otherwise refuse rather than risk destroying something unique. So
  hard_link_count is no longer trusted to prove identity — only data-safety.

Also guard against a stale row (source moved/deleted between the scan and the
click): !exists(src) refreshes the list instead of linking a ghost the daemon
would silently swap for wallet.dat. Verified with a standalone harness across
fresh / re-open / valid-hardlink-reuse / stale-orphan / redundant-occupant /
vanished-source cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:39:57 -05:00
DanSandClaude Opus 4.8 b7da6335d1 feat(wallets): open external wallets in place via datadir link (no copy)
Replace the "Import" action — which copied an out-of-datadir wallet into the
datadir and cluttered the list with hard-to-tell-apart duplicates — with an
in-place "Open" that links the real file into the datadir under a stable
per-target name (wallet-ip-<FNV8>.dat) and switches to it. The daemon only
loads a bare filename from its own datadir, so a link is the minimal bridge:
hard link first (no privileges, same volume — covers non-admin Windows),
symlink fallback (cross-volume), then an error. Never a copy (that would fork
the wallet) and never a delete of a real file.

Also:
- show each external wallet's originating sub-directory (…/Backups/2021)
- add a per-row "open folder location" button
- widen the modal (780 → 860) for the extra button
- guard the daemon against a dangling link (external file moved / USB
  unplugged) by falling back to wallet.dat rather than creating an empty one

Per-target link names (not one shared name) make switching between two
external wallets a real -wallet switch, so the rescan/cache index tracks each
correctly. Drops the now-dead wallets_import* i18n keys and back-fills the new
open/folder strings across all 8 languages + rebuilds the CJK subset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:21:30 -05:00
DanSandClaude Opus 4.8 b765d62e00 feat(wallets): show each external wallet's sub-directory
Prepend the containing folder (last 1-2 path components, e.g. "…/Backups/2021")
to an external wallet's metadata line, so several same-named wallet.dat files
surfaced by the recursive scan are distinguishable at a glance (the info-icon
tooltip still shows the full path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:17:45 -05:00
DanSandClaude Opus 4.8 3dc4d2d860 fix(wallets): make the sort-direction toggle a circular icon button
Swap the rounded-rect StyledButton for material::IconButton (square size →
bgRounding = radius, so a circle) with the arrow glyph mathematically centered;
carries its own hover fill + asc/desc tooltip.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:26:59 -05:00
DanSandClaude Opus 4.8 fe40d38f0d feat(wallets): segmented sort control (like the Receive all/z/t toggle)
Replace the sort dropdown with a material::SegmentedControl — Created / Addresses
/ Txs / Size — matching the Receive tab's address-type toggle, with the asc/desc
arrow beside it. Shorten the four sort labels to fit the segments (updated across
all eight languages + the back-fill script). The direction arrow is positioned
explicitly on the segment's baseline (the control is draw-list based and doesn't
advance the layout cursor).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 17:01:51 -05:00
DanSandClaude Opus 4.8 1d3a919ac6 feat(wallets): pin the active wallet to the top of the list
Make "is the active wallet" the primary sort key so the current wallet always
stays at the top regardless of the chosen sort/direction; the remaining wallets
sort by the selected key underneath it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:49:05 -05:00
DanSandClaude Opus 4.8 d549127e41 i18n(wallets): translate created-date + sort keys, rebuild CJK subset
Back-fill the 8 new keys (created label + the four sort options and asc/desc
tooltips) into all eight languages via add_missing_translations.py (+64 keys,
additive), and rebuild res/fonts/NotoSansCJK-Subset.ttf for the new glyphs (升 昇 렬).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:37:36 -05:00
DanSandClaude Opus 4.8 54a14485a5 feat(wallets): show created date + sort the wallet list
Parse the earliest keymeta nCreateTime out of the wallet.dat btree (the daemon's
"wallet birthday") and surface it in each row's metadata line ("created Aug 2025").

Add a sort control above the list — Date created / Address count / Transaction
count / Wallet size, with an ascending/descending toggle (descending default:
newest / most / largest first). Sorting reorders a display-index array
(s_order) rather than s_rows, so the async, index-aligned probe batch is
untouched; sort keys read a single frame-consistent snapshot of the probe
results. Address count prefers the authoritative cached index value, else the
btree key count.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:37:36 -05:00
DanSandClaude Opus 4.8 8eefab99ec i18n(wallets): clarify never-opened wallet label ("Never" -> "Never opened")
Standalone "Never" in the wallet list's last-opened slot was ambiguous; make it
"Never opened" (and update all eight translations). Glyphs already covered by the
CJK subset, so no font rebuild needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:22:13 -05:00
DanSandClaude Opus 4.8 4fdf281230 i18n(wallets): translate the new badge/count keys + rebuild CJK subset
Back-fill the 10 new Wallets-dialog keys (metadata "keys"/"txs" columns and the
Encrypted/Seed/Legacy/Unknown badge labels + tooltips) into all eight languages
via scripts/add_missing_translations.py (additive; +80 keys, no reformatting), and
rebuild res/fonts/NotoSansCJK-Subset.ttf so the new zh/ja/ko glyphs (判 種 類 …)
render instead of tofu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:11:29 -05:00
DanSandClaude Opus 4.8 c4f69b5a85 fix(wallets): probe synchronously during the offline screenshot sweep
The async probe fills badges/counts a frame after the dialog opens, which the
single-frame screenshot sweep captured before it landed (badges/counts missing).
Run the probe synchronously when App::isScreenshotSweeping(), so the captured
modal-wallets frame is populated; interactive use keeps the detached-thread path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 16:03:32 -05:00
DanSandClaude Opus 4.8 171b705429 feat(wallets): smooth wheel scrolling for the wallet list
Adopt the shared ApplySmoothScroll helper (with NoScrollWithMouse) for the
##walletList child, matching the Settings page and the other scroll areas, so
the wallet list eases on wheel input instead of jumping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 15:51:55 -05:00
DanSandClaude Opus 4.8 175950a2ec feat(wallets): exact address/tx counts via a minimal BDB btree parser
parseWalletBtree() walks the wallet.dat Berkeley DB btree directly (no libdb, no
daemon): validate the metapage magic, follow the "main" sub-database (its pgno is
stored big-endian in the master map), traverse internal/leaf pages, and tally
records by their length-prefixed type name — transparent + shielded spendable
keys, address-book, and tx count, plus exact encryption/seed flags. Every offset
is bounds-checked, pages are deduped at push time (stack stays O(npages)), and a
visited-set + page/key caps make it safe on a corrupt/adversarial imported file;
files using DB_CHKSUM/DB_ENCRYPT (which shift the page layout) are rejected so the
byte-scan fallback runs instead.

The async probe now uses this as the primary path (exact badges + counts), falling
back to the byte-scan only when the btree can't be fully parsed. The wallets list
shows "N keys · M txs" for probed rows (labeled "keys", not "addresses", since the
count includes change keys the daemon's address list omits). Validated read-only
against real wallets (counts cross-checked; enc/seed match the byte-scan) and a
hand-built minimal btree fixture in the unit suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 13:20:35 -05:00
DanSandClaude Opus 4.8 36efe99a0f feat(wallets): vertical label+icon badge stack + async probing
Redesign the wallet.dat status badges as a right-aligned vertical stack of
"label icon" rows ("Seed phrase 🌱" over "Encrypted 🔒", "Legacy", or "Unknown"),
each with a hover tooltip; name/metadata reserve the stack width and truncate
before it.

Move the wallet.dat probing off the UI thread: scan() now builds the row list
synchronously (filename/size only) and hands the file reads to a detached
background thread whose results land in a shared, index-aligned batch that
render() reads under a mutex. A re-scan supersedes the old batch (cancel + swap);
the thread touches only its own heap batch (never s_rows/statics), so it's safe
across re-scans and shutdown. The dialog opens instantly even with a large
active wallet; badges fill in over the next few frames.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:47:27 -05:00
DanSandClaude Opus 4.8 fadbd6a879 docs: Overview marble design mockup
Self-contained HTML/CSS mockup of the Overview screen (Marble theme) used as a
pixel reference for tuning the in-app marble background, frosted cards, DragonX
glyph, and card drop shadow. Embeds the real res/img marble texture as a data URI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:33:06 -05:00
DanSandClaude Opus 4.8 112c581247 feat(wallets): recursive subdir scan + wallet.dat encryption/seed badges
Search user-added wallet folders RECURSIVELY (bounded: depth/hit/visit caps,
skip_permission_denied, no symlink-follow, node-junk + node-subtree pruning);
the datadir stays top-level. r.dir is each file's real parent so Open/import
resolve subdir wallets. Auto-suffix the import destination on a name collision.

Add util/wallet_file_probe.h: read encryption/seed/shielded flags straight off
a wallet.dat WITHOUT loading it — validate the Berkeley DB btree magic, then a
bounded streaming byte-scan for the length-prefixed record markers the daemon
writes (mkey -> encrypted; hdseed/hdchain -> seed/HD; zkey/sapzkey -> shielded).
Reads no key material. The Wallets dialog shows Encrypted / Seed / Legacy badges,
plus an Unknown badge when a large file couldn't be fully scanned (so absence of
a lock never falsely reads as unencrypted). Unit-tested + validated read-only
against real wallets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:32:59 -05:00
DanSandClaude Opus 4.8 57147fd06b fix(console): readable theme-colored log text on light themes
The console log text washed out on Marble/Light (~1.2:1 contrast): refreshColors
chose text colors from the stored IsDarkTheme() flag (default true, and only
re-checked on a dark/light toggle) while the terminal surface used IsLightTheme(),
so dark-theme pale text landed on the light surface. Tie both to the same live
background-luminance predicate and refresh on any theme (schema generation) change.
Light themes now derive each channel from that theme's own palette (Primary,
OnSurface, Error, Secondary, ...) nudged to a WCAG contrast floor — on-theme AND
high-contrast (measured 4.6-17.8:1 across the five light skins).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:32:49 -05:00
DanSandClaude Opus 4.8 aa60188665 fix(ui): tighter, darker, non-clipping card drop shadow
Rework DrawCardDropShadow (Light/Marble): a Gaussian-sampled ring stack
matching a `0 0 4px rgb(50 53 58 / 34a)` box-shadow — offset-free, darker,
and tighter than the old faint 14px black halo. Clip to the shadow's own
bounding box (card ± reach) instead of the child window bounds so top/bottom
shadows are no longer chopped where a card sits flush with its container,
while staying on the card's own draw list (correct z-order in modals/popups).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 12:32:42 -05:00
DanSandClaude Opus 4.8 cff958c21e fix(themes): card shadow no longer cut off at child-window edges
Cards are drawn inside clipping child windows, so the shadow (which extends past
the card edge) was cut at the child boundary — most visibly a hard line on the
sidebar's inner edge, which also made the shadow look non-uniform. Widen the
shadow's clip horizontally (card + spread, clamped to the viewport) so the side
shadows reach into the gaps between panels, while keeping the vertical clip at the
child bounds so a card scrolled to a child's top/bottom edge can't bleed its
shadow into the header/footer. Verified: sidebar + settings cards now show an
even shadow on all four sides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 20:42:08 -05:00
DanSandClaude Opus 4.8 c50125523c change(themes): make the card shadow uniform (no directional offset)
Drop the downward offset so the soft shadow is equal on all four sides — an even
halo around each card rather than a directional cast, matching the requested
look. Slightly more rings for a smoother falloff. Still stroke-based (no body
contamination), Light/Marble only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 20:24:56 -05:00
DanSandClaude Opus 4.8 063f1fa4be feat(themes): subtle card drop shadow for Light + Marble
Cards on the pale Light / Marble backgrounds now lift off the surface with a
subtle drop shadow. Drawn in DrawGlassPanel (behind the card fill) as fading
rounded-rect STROKES offset down — strokes never paint across the card body, so
only the outer soft blur remains once the fill covers the interior (the
"render then mask out the UI area" result, no FBO needed). Gated to the Light and
Marble themes (cached per theme-generation); dark themes are unchanged. Verified
across marble / light (shadow present, smooth, body clean) and dark (none).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 20:07:24 -05:00
DanSandClaude Opus 4.8 ad7a150ad6 change(acrylic): lower the blur slider's range to a gentle max
Now that panels frost at full alpha, the slider's old top end (multiplier 4.0)
was far too blurry. Cap the range at multiplier 1.25 (kAcrylicMaxBlur) — the
slider still reads 0–100%, but 100% now maps to a moderate frost instead of a
heavy wash. Snap-to-off threshold scales with the new range, and a value saved
under the old 0–4 range is clamped on load so it lands within 0–100%.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 19:54:48 -05:00
DanSandClaude Opus 4.8 433c7ae46f fix(acrylic): frost glass panels at full strength (slider was a near no-op)
The per-panel acrylic composite drew the blurred capture at
`fallbackColor.w * uiOpacity` alpha. At a lowered UI opacity that's ~24%, so the
SHARP background bled straight through the panels — the acrylic slider changed
almost nothing (measured: blur 0% vs 100% differed in 0.33% of pixels, and the
marbled background was equally crisp at both).

Draw the blurred capture at (near) full strength so it actually FROSTS the panel
(the blurred background replaces the sharp one); UI opacity now scales only the
tint overlay in DrawGlassPanel, which is the correct acrylic model — opacity =
tint, not sharp-vs-blur. Fixed symmetrically in both the GL and DX11 drawRect
paths. Verified on GL: blur 0% now shows a sharp background, 100% a smooth
frosted wash (0.33% -> 8.3% pixels changed), and the default (high-opacity) card
look is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 19:46:18 -05:00
DanSandClaude Opus 4.8 a5efbaf83d change(settings): debug-options gate is once per session
Passing the gate once (confirmation + any re-auth) now unlocks the DEBUG OPTIONS
dropdown for the rest of the session — subsequent expands skip straight to the
options. The flag lives in the session-lifetime page state, so it resets on app
restart. Collapsing still needs no gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 18:49:11 -05:00
DanSandClaude Opus 4.8 46fb0029b8 feat(settings): gate debug options behind a confirmation (+ re-auth if secured)
Revealing the DEBUG OPTIONS dropdown now opens a confirmation modal with a
warning instead of expanding directly. If the wallet is secured, it also requires
re-authentication before the options appear:
- a quick-unlock PIN → verified against the vault (Argon2id derive, off-thread,
  no wallet side effects);
- otherwise an encrypted wallet → the passphrase, verified via walletpassphrase.
Unsecured wallets just confirm. Collapsing needs no gate; each expand re-gates.

App gains debugGateRequiresAuth() + verifyDebugCredential() (cb on the main
thread; secrets wiped). Adds a modal-debug-gate sweep surface. Verified 100% +
150%, dark + light.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 18:46:50 -05:00
DanSandClaude Opus 4.8 aa587d1d84 feat(seed): glow the Migrate-to-seed button for a legacy wallet
A pre-seed-phrase (legacy, non-mnemonic) wallet is exactly the one that benefits
from migrating — so nudge the user by glowing the Settings "Migrate to seed"
button with a soft pulsing accent halo.

Adds a cached wallet mnemonic status (probeWalletSeedStatus), classified once per
connect via z_exportmnemonic (the same signal the migration Intro pre-flight
uses): NoMnemonic = legacy → glow; HasMnemonic / Incapable (old daemon) / while
locked or on lite = no glow. Reset on disconnect so it re-probes after a wallet
switch or a post-migration adopt, with a small attempt cap to avoid re-probing on
a persistent transient error. The sweep forces the status so the glow is
captured (restored after). Verified dark + light.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 18:24:41 -05:00
DanSandClaude Opus 4.8 3a9edf2200 feat(xmrig-update): two-pane version picker matching the node updater
Bring the miner (DRG-XMRig) updater to the same design as the daemon updater:
two-pane layout with tactile version cards (text label + right-aligned icon
badge: installed check / pre-release flask / newest new-releases, hover-only
tooltips) on the left, and the selected version's detail on the right — a unified
rounded info surface holding the version header + markdown-rendered release notes,
with the verify note and a centered, text-fit install button below it, and a
larger Close in the footer.

Fetches the full release list up front; Downloading/Done/Failed render in the
right pane. Preserves the miner specifics: installs are refused while the miner
is running (button disabled + a stop-mining note), and the installed tag is
persisted via setXmrigVersion on success. Adds a modal-xmrig-update sweep surface
(fake releases). Verified 100% + 150%, dark + light.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 17:56:59 -05:00
DanSandClaude Opus 4.8 c25a4e4150 fix(daemon-update): status tooltip only shows on icon hover, not when selected
The status-icon tooltip was gated on (hov || sel), so the newest version — which
is selected by default — kept its "Newest available version" tooltip pinned on
screen. Show it only while the mouse is actually over the icon.

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