The revamped edit dialog derives its input widths from the card content
width, so the address-input schema config is no longer read.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rebuild the add/edit contact dialog on the portfolio-editor design language:
- A live preview card at the top shows the contact exactly as it renders in the
list (avatar + name + address), updating as you type and pick an avatar.
- An avatar picker (Badge / Icon / Image segmented control) lets you keep the
default Z/T type badge, choose a Material wallet icon from a searchable grid,
or set a custom image. The picker area is fixed-height so the modal doesn't
jump when switching modes.
- Custom images go through a new in-app ImagePicker (image_picker.h): a
Material overlay that browses the filesystem starting at the user's Pictures
folder, shows a thumbnail grid (decoded to raw pixels, box-downscaled to a
small texture, cached per directory and freed on navigate/close, budgeted a
few decodes per frame so large folders don't hitch), and returns the chosen
path. The chosen image is copied into <config>/contact-avatars/ (named by an
FNV hash of the source path, so re-picking is idempotent) and stored as
"img:<path>". Like FolderPicker, it takes over the modal surface while open.
- Paste is now available on both add and edit; buttons are content-sized.
Adds three sweep surfaces (contacts-edit-{icon,badge,image}) that open the
dialog on a seeded contact in each avatar mode, plus i18n keys (+ 8-language
translations; reworded two zh/ja strings to stay within the existing CJK
subset, so no font rebuild).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an `avatar` field to AddressBookEntry ("" = default Z/T type badge,
"icon:<name>" = a Material wallet-icon, "img:<path>" = a custom image),
serialized additively in addressbook.json (only written when non-empty, so
existing books are untouched).
Render it in the Cards/List views via a new drawContactAvatar helper: custom
images are loaded once through a path-keyed texture cache and drawn
circular-cropped (centre-cropped UVs + a thin border ring); icons reuse the
project-icon set (incl. the special pickaxe font path) in a tinted circle;
everything else falls back to the existing Z/T badge (also the fallback when
an image fails to load or an icon name is unknown).
Seed one sweep contact with an icon avatar to exercise the icon-badge path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three interaction refinements to the address list:
- The globe badge now stays pinned far-right; the per-row copy/edit/delete actions
appear to its LEFT on hover/selection instead of replacing it.
- A left-click on empty space in the Cards/List area clears the current selection
(no row/action hovered -> deselect).
- Right-clicking a row (any view) selects it and opens a shared context menu
(Copy address / Edit / Delete), rendered once after the list.
Build + hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-ups from the contacts audit:
- Per-row actions: in the Cards/List views, copy/edit/delete icon buttons now appear
on the right of a row on hover or selection (the globe badge shows otherwise). The
row Selectable uses SetNextItemAllowOverlap so the action InvisibleButtons take
click priority; whole-row hover (IsMouseHoveringRect) drives the highlight so it
survives hovering an icon; the delete icon turns red while armed (two-click confirm);
each has a tooltip. Trailing space is reserved so the text never reflows on hover,
and the cursor is restored after the manual action layout.
- Add/edit modal HiDPI: the Layout::kDialog* helpers fold dpiScale() (physical px)
while raw schema widths are logical, so the schema-path formW/actionW/actionGap/
notesH were unscaled vs their scaled fallbacks. Scale the schema-path values; and
since BeginOverlayDialog re-applies dpiScale to cardWidth, divide the already-scaled
dialogW back out (it was double-scaled). No visible change at 100%; correct at 150%.
Build + ctest + hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add three sweep surfaces (contacts-cards / -list / -table) that force each Contacts
address-list view mode and seed a few demo contacts (Z + T types, some global) so
the modes render with data instead of the empty state. Teardown restores the real
book and resets the mode.
To avoid touching the user's persisted address book (the Windows sweep can run on
the real HOME, and addEntry/removeEntry call save()), add AddressBook::sweepSetEntries
— a no-save in-memory setter used only here: the surface snapshots the real entries,
swaps in the demo set, and restores the snapshot on teardown, so nothing is written
to disk even if the sweep is interrupted.
Build + hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a persisted view toggle so the contacts address list can be rendered three
ways, moving it away from a bare data-grid toward a Material look:
- Cards: tactile rounded cards with a circular Z/T type-avatar, label over a muted
truncated address, trailing globe badge, primary-tint + outline selection, hover
fill, and a centred Material empty state.
- List: borderless two-line rows (same avatar + label/address) with hover tint and a
thin per-row divider — denser than cards.
- Table: the previous 3-column table, material-ized (grid borders dropped, row
backgrounds + interactive sort kept).
The toggle is an icon SegmentedControl (view-agenda / view-list / table-rows)
right-aligned on the toolbar; the choice persists via a new contacts_view_mode_
setting (0 cards / 1 list / 2 table, mirroring portfolio_style_). Cards/List sort by
label; Table keeps its sortable headers. All geometry is dpiScale()-aware; the globe
badge is drawn in the icon font in every mode. Default is Cards.
Build + ctest + hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the confirmed findings from a workflow audit of the Contacts tab body:
- Label clip: the Label column was WidthFixed 150px and clipped long labels
mid-word ("drgx pool payout a…"). Make all three columns WidthStretch (label 1.5,
address 2.6, notes 1.0) so the label grows with the tab and the notes column no
longer reserves a fixed empty block on the right (notes = low weight, per request).
- Globe badge tofu: the global-contact ICON_MD_PUBLIC badge was drawn with the text
font (no Material glyphs) → rendered as "?". Push Type().iconSmall() around it.
- Plural: "1 addresses saved" -> add address_book_count_one ("%zu address saved",
8 langs, %zu kept so the format signature matches) and branch on count == 1.
- DPI: the SameLine badge gaps (6/8px) and the table-height floor (120px) are now
* Layout::dpiScale(); the WidthStretch columns are relative so need no scaling.
- Toolbar: give the four actions leading Material icons (person-add / edit / delete /
content-copy) and accent the primary "Add New" — via a small local icon+label
tactile-button helper (ImGui has no two-font button), icons inherit the
disabled-aware text alpha so they gray out with BeginDisabled().
Build + ctest + hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The QR popup used the same window-relative card width the Export-Key dialog just
moved away from (its comment even said "Match the key-export modal: 85% of the
window width"). On a large monitor that left the centered AddressCopyField floating
far from its left-aligned "Address:" label and inflated the QR's empty margins.
Derive the card width from the address field's own natural (chunked) box width
(mirroring the key-export fix, incl. the overlay card's 28px content inset so the
address stays on one line), bounded to 85% of the window as an upper limit. Also
scale the responsive-QR size literals (280 fallback / 420 cap) by dpiScale() so the
QR renders at the intended size on HiDPI/font-scale. No change to the QR texture /
clipboard logic.
Found via a workflow audit of all 27 redesigned modals for the same anti-pattern —
the QR popup was the only other occurrence (0 false positives).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Export-Key dialog sized its card to a fixed 85% of the window width. On a large
monitor that's ~1570px, but the content is ~900px, and AddressCopyField centers its
text-clamped box within the available width — so the address/key fields floated far
from their left-aligned "Address:"/"Viewing Key:" labels and read as off-center
(more visible after the warning box became a left-aligned DialogWarningHeader).
Derive the card width from the address field's own natural (chunked) box width —
the widest deterministic element before the key is revealed — mirroring
AddressCopyField's boxW math, plus the overlay card's 28px-per-side content inset so
the address sits on one line under its label. Adapts to address type (z vs t) and
font scale with no magic width; bounded to 85% of the window as an upper safety
limit; the revealed key+QR layout adapts to whatever width results. Width-only
change — the key fetch/wipe/mask/close logic is untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register sweep surfaces for the redesigned Wave-2 modals so they get captured for
visual review: modal-shield + modal-merge (ShieldDialog's two modes),
modal-transfer (a z->t transfer so the converted deshielding DialogWarningHeader
renders), and modal-key-export (KeyExportDialog — named distinctly from the
settings modal-export-key). Each setup opens the dialog through its public show()
without clicking any button, so no async RPC fires; teardown closes it.
Adds a static hide() to ShieldDialog and KeyExportDialog for clean sweep teardown
(AddressTransferDialog already had close()). KeyExportDialog::hide() also clears the
revealed key + frees its QR, mirroring the dialog's own close/dismiss secret-wipe.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Presentation-only migration of KeyExportDialog::render (reveals a private or
viewing key) — a secret-handling modal:
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth cardW,
idSuffix keyexport}) replacing the legacy positional overload
- replace the bespoke red-bordered "WARNING" box with material::DialogWarningHeader
(user-approved) so the key-export warning matches the import-key reference; passes
the correct per-key-type body key (private vs viewing)
- remove the two dividers (after the warning, above the Close footer)
- 4 StyledButton -> TactileButton (retry / reveal / show-hide / close)
The secret path is byte-for-byte unchanged: the z_exportkey/dumpprivkey/
z_exportviewingkey (and lite exportPrivateKeys) fetch + their secureWipeLiteSecret
calls, the "*"-masking when hidden, the Show/Hide toggle, copySecretToClipboard
(auto-clearing), the QR generate/cache/releaseQr, and the secret-clear on BOTH exit
paths (Close button + the post-EndOverlayDialog dismiss teardown).
Verified: build + ctest + hygiene clean; a 2-lens adversarial review
(secret-hygiene / presentation-fund) returned zero findings, proving every
secret-handling path diffs byte-for-byte identical to HEAD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Presentation-only migration of AddressTransferDialog::render (drag-one-address-
onto-another transfer) — a fund-moving modal:
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 620,
idSuffix addrtransfer}) replacing the legacy positional overload
- the top-of-dialog deshielding warning -> material::DialogWarningHeader; the green
shielding notice stays colored text (it's a positive notice, not a warning)
- remove the footer divider and drop its "+1.0f" footerH reserve term (keep the
form->result-preview divider)
- reuse the existing shield_operation_id key for the "Operation ID: %s" label (no
new i18n key)
The fund path is byte-for-byte unchanged: the amountValid guard, totalDeduct/
newFromBal/newToBal math, maxSendableAmount, the Max button, s_app->sendTransaction
+ its ok/fail callback, and the BeginDisabled(!amountValid || s_sending) gate.
Verified: build + ctest + hygiene clean; a 2-lens adversarial review returned zero
findings, proving the fund path diffs byte-for-byte identical to HEAD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Presentation-only migration of ShieldDialog::render (Shield coinbase / Merge funds)
— a fund-moving modal:
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth win.width,
idSuffix shielddialog}) replacing the legacy positional overload
- drop the description divider and the form->footer divider (keep the post-submit
operation-id section divider)
- 3 StyledButton -> TactileButton (submit / cancel / check-status)
- TR() the hardcoded "No shielded (z) address yet …" note -> new shield_no_zaddr_hint
key, 8 langs (em-dash preserved); the "DRGX" ticker stays hardcoded per convention
The fund path is byte-for-byte unchanged: the can_submit guard, fee/utxo clamps, the
"ANY_TADDR" setup, the z_shieldcoinbase / z_mergetoaddress worker->post calls + their
opid/status/Notifications/trackOperation callbacks, and the BeginDisabled/tooltip logic.
Verified: build + ctest + hygiene clean; a 2-lens adversarial review
(fund-path-untouched / presentation-i18n) returned zero findings, proving the fund
path diffs byte-for-byte identical to HEAD via isolation diffs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Presentation-only migration of RenderSendConfirmPopup (the confirm-before-broadcast
popup) — a fund-moving modal:
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth popupW,
idSuffix confirmsend}) replacing the legacy positional overload; p_open stays
nullptr (the popup keeps its own Escape + Cancel handling)
- TR() the one hardcoded status string in the synchronous live-revalidation guard
("Cannot send now …") -> new send_cannot_send_now key, 8 langs (em-dash preserved)
The fund path is byte-for-byte unchanged: the live re-validation guard, the
app->sendTransaction call + its success/failure callback, the lite-locked
requestLiteUnlock branch, the s_sending/s_show_confirm state, the Escape handler,
and the FROM/TO cards + fee-tier/summary rendering.
Verified: build + ctest + hygiene clean; a 2-lens adversarial review
(fund-path-untouched / ui-i18n) returned zero findings, with the fund-path lens
proving the send path diffs byte-for-byte identical to HEAD.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two sweep-driven fixes:
- Validate Address: remove the input->results Separator. When no result is shown
yet it sat directly above the Close button and read as a footer divider,
inconsistent with the reference design (the description divider was already gone).
- Drop the modal-daemon-prompt sweep surface: renderDaemonUpdatePrompt is gated
behind !capture_mode_, so it never renders during a sweep (the capture showed the
Settings page behind it). Its migration stays verified via build + the shared
BlurFloat/TactileButton pattern.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Register sweep surfaces so the redesigned Wave-1 modals get captured for visual
review: modal-qr-popup, modal-request-payment, modal-validate-address,
modal-address-label, modal-daemon-prompt, and modal-antivirus (the last renders
only on Windows, where its #ifdef body compiles). Each is driven through the
dialog's public show()/flag; teardown closes it. Adds a static hide() to
RequestPaymentDialog / ValidateAddressDialog / AddressLabelDialog for clean sweep
teardown (they had no public close). The contact add/edit and explorer block-detail
modals are tab-embedded and not yet surfaced.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring renderAntivirusHelpDialog onto the reference design and remove its hardcoded
English (the last Wave-1 modal):
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth 560,
idSuffix antivirus}) replacing the legacy positional overload
- the hardcoded "Windows Defender Blocked Miner" title and all 10 step/intro/bullet
strings -> TR() (11 new av_* keys + reuse `close`; 8-lang translations, Windows
env vars / xmrig.exe kept literal, menu names localized)
- 2 StyledButton -> TactileButton, drop the footer divider, dp-scale the button widths
- CJK subset rebuilt (glyphs 1741->1751; kept — new glyphs are used)
NOTE: this dialog is inside #ifdef _WIN32, so the Linux build compiles it out and
cannot verify it — needs a Windows build to compile-check. The edits mirror the
already-verified daemon-update-prompt / migrate-to-seed struct-overlay pattern and
leave the ShellExecuteA call untouched. i18n.cpp + the daemon prompt (cross-platform)
build + ctest + hygiene clean on Linux.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Presentation-only reference-design migration of three more modals:
- Validate Address: struct-form overlay (idSuffix validateaddr), 3 StyledButton ->
TactileButton, drop the description divider, and TR() the hardcoded "Error: %s"
(reuse the existing error_format key). The validateaddress RPC/worker path is untouched.
- Address label: struct-form overlay (idSuffix addrlabel); drop the footer divider and
zero its layout-reserve term (separatorH) so the fixed-height icon-picker card math
stays consistent. Buttons were already TactileButton.
- Contacts add/edit: struct-form overlay (keeps its idSuffix + BeginOverlayDialogFooter);
StyledButton -> TactileButton across the file (3 modal + 4 contact-list buttons) for a
consistent Contacts tab. The address-book add/update/notify logic is untouched.
Build + hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the two Receive-tab modals onto the settings-modal reference language
(presentation-only):
- struct-form BeginOverlayDialog(OverlayDialogSpec{BlurFloat, cardWidth, idSuffix})
replacing the legacy positional overload (idSuffix qrpopup / requestpay)
- StyledButton -> TactileButton (2 + 3 sites)
- drop Request-payment's description divider
No logic touched: the QR texture lifecycle, payment-URI builder, address combo,
and clipboard/notification calls are unchanged. The "DRGX" ticker stays hardcoded
per the existing codebase convention (7 sites, no currency TR key).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BlurFloat overlays draw a full-window veil over the live blur so the card-less
floating modal content reads as foreground. The light-theme branch used a strong
neutral DARK veil (IM_COL32(16,18,24,200)) on the theory that "light content pops
on a dark veil" — but on a light theme the modal chrome is dark-on-light, so it
sank INTO the veil: the heading, description, Sort row, and the create/scan/
reveal/close buttons all washed out to low-contrast dark-on-dark, and a "light"
theme read dark whenever a modal opened.
UI controls already carry a dropshadow on light themes for figure/ground, so the
backdrop no longer needs to supply contrast by going dark. Switch the light-theme
veil to a theme-tinted LIGHT frost (WithAlpha(Background(), 205)); the dark-theme
branch is untouched. Fixes every BlurFloat modal at once.
Verified via full sweep: marble / light read as clean light frost with all chrome
legible, dune gets a theme-appropriate warm frost, and obsidian (dark) is
unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>