Commit Graph

285 Commits

Author SHA1 Message Date
3fbb64d14c feat(contacts): switchable Cards / List / Table view for the address list
Add a persisted view toggle so the contacts address list can be rendered three
ways, moving it away from a bare data-grid toward a Material look:

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

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

Build + ctest + hygiene clean.

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

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

Build + ctest + hygiene clean.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Build + hygiene clean.

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 16:20:02 -05:00
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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