"Scan another folder" now anchors the picker at getDragonXDataDir() (where the
datadir wallets live) instead of home, so the user starts from a familiar spot
and navigates up/out to find wallets elsewhere.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The wallets dialog hand-computed a content-sized card height. With many wallets
on a small / HiDPI (150%) window the computed height could exceed the viewport,
and the framework clips a too-tall card — footer and all.
Cap the card at 0.86 of the viewport height. When capped, the wallet list flexes
to the space left above the create/scan/footer controls and scrolls internally,
so those controls stay pinned and visible; uncapped (the common case, few
wallets) nothing changes and there's no spurious scrollbar. Add a
modal-wallets-many sweep surface (8 wallets, self-cleaning teardown) to exercise
the capped-scroll path. Verified at 100% + 150%.
This is the one real fixed-height clip risk from the modal audit; the other 44
modals use auto-height (grow-to-fit, capped at a viewport ratio) and were
confirmed non-clipping — e.g. the 24-word seed-backup grid renders fully at 150%
in a 720px window.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The picker used a fixed-height card whose budget under-counted ImGui's
inter-item spacing, so on larger windows / at 150% DPI the footer buttons
overflowed the card's clip rect and got cut off.
Rework it as a flex column: the card takes ~82% of the viewport height (like
the console command-reference modal) and the directory list fills whatever
space is left above a reserved footer block. The status line + Scan/Cancel
buttons are now always pinned at the bottom and visible at any window size or
DPI, and the browsing area is generous instead of a cramped fixed row count.
Verified headless at 100% + 150%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wallets dialog refinements:
- More space between wallet cards (cardGap: spacingSm -> spacingMd).
- "Scan another folder" is now always visible as a full-width button (no
expandable toggle) that opens the new picker.
- The Create-wallet row and the scan button span the full list width, so their
right edges align with the wallet cards / Open|Import buttons.
New in-app folder picker (folder_picker.h): a Material-styled modal replacing
the raw path input. Browse the filesystem (Up / Home / click a sub-folder),
see the *.dat wallet files present + a count, and pick a folder to scan. The
picker takes over the modal surface while open (the overlay framework doesn't
nest); it remembers the last browsed location across opens. Fixed-height card
(no auto-height feedback), UTF-8-aware truncation, full-node icons confirmed
in the embedded MaterialIcons range. Added a modal-folder-picker sweep surface;
verified at 100% + 150%, dark + light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues on the card list: it showed a scrollbar (and clipped the last card)
with only a few wallets, and the cards felt cramped.
- Scrollbar: the per-card advance added a trailing gap after EVERY card (incl.
the last) and ImGui inserted its default ItemSpacing between the Dummy items,
so the content ran ~1 gap taller than the child's height budget -> overflow ->
scrollbar + clipped card. Zero ItemSpacing inside the list child, register each
card's footprint with a Dummy, and gap only BETWEEN cards (no trailing gap), so
the content matches the budget exactly. Sized the list to fit up to 7 cards
before it scrolls.
- Roominess: bigger name (subtitle1), more vertical padding (spacingMd), and a
touch more space after the icon.
- Removed the full-card InvisibleButton (hover is IsMouseHoveringRect again): its
overlap with the action button tripped ImGui's ConfigDebugHighlightIdConflicts,
drawing a red debug border around the list at 150% DPI.
Verified via the headless sweep at 100% and font_scale 1.5 — no scrollbar, no red
border, cards fully visible with breathing room.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the wallet table with a stack of Material-style cards inside a scrolling
child. Each wallet is a rounded card: a leading wallet icon, the filename as the
primary line, and a muted metadata subtitle (size · N addresses · balance DRGX ·
last opened). The action sits on the right — Open / Import buttons, or a green
"Active" chip for the current wallet. The current wallet's card gets a green tint,
border, and icon; external files show an info icon + tooltip; cards highlight on
hover. The dialog stays content-sized + fixed-height (computed from the wallet
count, up to 6 before it scrolls), so the monitor-drag stability is unaffected.
Adversarially reviewed (two-lens workflow + synthesis): the reviewers' DPI
unit-mixing and broken-scroll findings were false positives (LegacySize is the
DPI-scaled atlas size, and BeginChild takes physical px while the framework's
cardHeight takes logical) — the only real issue was byte-wise truncation that
could split a multibyte UTF-8 char, now fixed to pop whole code points. Verified
via the headless sweep at 100%, 150%, and light skin.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Act on the design review of the Wallets modal (all six points):
1. Content-sized card — size the card to the wallet count (up to 8 rows, then the
list scrolls) instead of a fixed 620px, killing the huge empty void a short
list floated in. Computed from the KNOWN row count, so it stays a fixed-height
dialog (no auto-height feedback — that's what slid content off-screen on a
monitor move).
2. Framed wallet list — BordersOuter + PadOuterX so the list reads as a contained
panel with room to grow, not loose rows on the backdrop.
3. Right-aligned numeric columns (Size / Addresses / Balance) and their headers,
via a custom header row + a right-aligned cell helper, so the numbers line up.
4. Consistent action column — the active wallet's row gets a subtle green tint and
an "Active" chip (badge), distinct from the Open/Import buttons.
5. De-cluttered bottom — "Create wallet" stays the primary action; the folder-scan
is tucked behind a "+ Scan another folder…" toggle.
6. Polish — the external-file marker is now a real info icon + tooltip (was
ICON_MD_FOLDER rendering as a "?" tofu, since that glyph isn't in the subset);
Refresh is an icon-only button, de-emphasized next to Close.
New i18n keys (wallets_add_folder_toggle, wallets_external_tt) + 8 langs; CJK
subset rebuilt. Verified via the headless sweep at 100% and font_scale 1.5.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-ups to the modal float work:
- Console RPC command reference (renderCommandsPopup) had the SAME auto-height
feedback bug as Wallets: BlurFloat with no cardHeight, and its fill-height
command list read GetContentRegionAvail() inside the AutoResizeY child ->
self-referential, sliding content off-screen after a monitor move. Give it a
FIXED viewport-relative cardHeight (fills ~84% of the viewport). Also removed
its duplicate body H6 heading (the framework h6 title already shows it) —
missed earlier because it's spec-form, not the positional overload.
- Wallets footer clipping: after the fixed-height conversion the content child
clips overflow (NoScrollbar), and the hardcoded 130*dp reserve below the table
was too small for the create/add-folder rows + separator + footer (~160*dp),
so the buttons clipped — worse at 150%. Compute the reserve from real widget
metrics (GetFrameHeightWithSpacing + caption height) so it holds at any DPI.
Added a modal-console-commands sweep surface (debug-gated ConsoleTab setter) so
the popup is captured. Verified both at 100% and font_scale 1.5 (150%): footers
on-screen, single headings, lists fill, nothing clipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Wallets modal used the auto-height BeginOverlayDialog overload, whose content
child is AutoResizeY. It sized its table with `tableH = GetContentRegionAvail().y
- 130*dp` INSIDE that child — but on an AutoResizeY child GetContentRegionAvail().y
derives from the child's Size.y, carried over from the previous frame's content
height. That makes tableH self-referential: a fixed point on one monitor, but a
monitor move jumps dpiScale() (the 130*dp reserve + table column/row sizes switch
to new units while the child's Size.y is still old units), knocking it off
equilibrium. With only a lower clamp and no upper bound, the excursion runs
unbounded and pushes the create/folder/footer rows off the bottom.
Give the dialog a FIXED cardHeight (620) via the spec form — the pattern Manage
Portfolio already uses. The content child is then fixed-height (not AutoResizeY),
so GetContentRegionAvail().y is a stable, DPI-current "space left in the card",
tableH stops feeding back, and the fixed-height path already clamps the card to
the viewport (min(cardHeight*dp, vp_size.y-32)). Root-caused via a multi-agent
audit against the ImGui internals.
Verified via the headless sweep at 100% and font_scale 1.5 (150%): the table
absorbs the slack and the create/folder inputs + footer stay on-screen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the glass card + boxed title bar from every dialog so content floats
directly on the blur backdrop under a plain h6 heading — the Manage-Portfolio
look — unifying the whole modal system on one visual language (reverses the
earlier card-for-small / float-for-large split).
- Framework: the positional BeginOverlayDialog overload (used by all ~34 card
dialogs) now defaults to floatingContent + plainHeading (draw_helpers.h). The
9 spec-form BlurFloat dialogs are unchanged; any dialog can still opt back to a
card via an explicit OverlayStyle::GlassCard spec. Authored widths are kept
(floating never resizes), so width-coupled layouts are unaffected.
- Dedup: the framework h6 title made each dialog's own repeated title heading
redundant. Removed the duplicate body heading from Import/Export key, Backup,
daemon update, seed backup, seed migration, Defender-blocked (app.cpp),
Wallets, and the send-confirm popup.
- Close paths: dropping the title-bar close X is covered — every dialog has a
footer button and/or the framework's outside-click dismiss (all pass p_open);
send-confirm keeps Esc + Confirm/Cancel.
Verified via the headless full UI sweep on dark + light: 17 modal surfaces float
card-less with a single clean heading, nothing clipped, close controls present.
Build clean, ctest passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Act on the design review of the row layouts:
- Fixed right-aligned columns (DRGX | value | 24h | sparkline) so the numbers
line up across rows and the value no longer jumps rightward when a group has
no sparkline/24h.
- Emphasize the fiat value (larger + brighter than the muted DRGX amount) — the
number users actually scan for.
- Graceful fallback: on-but-absent fields (e.g. price not loaded yet) show a
muted em-dash instead of a hole; deliberately-hidden fields stay blank.
- Detailed style reworked into a 2x2 grid (label/value on top, DRGX/24h below)
with a tall sparkline strip spanning both lines, so its extra height earns its
keep; when a group has no sparkline the value/DRGX fill the space.
- Featured hero value is now neutral bold (OnSurface) instead of the accent
color, so it can't collide with the green 24h; DRGX right-aligns to fill the
right when there is no sparkline.
- Stronger row separation (more card fill) and REMOVED the left accent strip —
group identity now reads from the tinted icon + a subtle accent border.
Verified via the headless sweep across all three styles on dark + light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Inside the portfolio row child each row emits two items (the InvisibleButton
hit target + a gap Dummy), and ImGui inserts its default ItemSpacing.y between
every item — so the real content ran ~4x ItemSpacing taller than the child's
height budget, clipping the bottom row (e.g. "Cold storage" lost its second
line) and widening the inter-row gaps.
Zero ItemSpacing inside the row child (the inter-row gap is set explicitly by
the Dummy) and size the child to the visible rows plus one gap of bottom
breathing room, so the last row never sits on the clip edge. Scale-independent
(rowH/gap both scale by dpiScale). Verified via the headless sweep across all
three row styles — every row now renders fully and the gaps tighten to rowGap.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the drag/resize dashboard grid on the Market tab's portfolio with
three full-width row styles — compact (single-line), detailed (two-line),
and value-hero — cycled with Left/Right arrows like the Overview layouts.
Rows live in an internal scroll region bounded to the portfolio's height
budget, so the section stays put as groups are added.
- pfDrawRow renders each group in the selected style, reusing the icon /
value / DRGX / 24h / sparkline pieces; per-entry field toggles honored.
- portfolio_style setting (0/1/2) persisted; arrow-key switcher + toast.
- Portfolio groups gain a per-wallet scope: new groups default to the
active wallet's identity hash; legacy groups (empty scope) stay visible
in every wallet. Scope filter drives both the height budget and the rows.
- Remove the now-dead grid machinery (mktDrawCard, pfComputeGridLayout,
PfCell, PfGridDrag, MktCardCtx + MktCtx grid fields) — net code drop.
- Full UI sweep: seed demo portfolio groups + wavy price history and add
market-rows-{compact,detailed,featured} surfaces so the styles are
captured per skin (snapshot/restore the real portfolio + market data).
- i18n: portfolio_style_{label,compact,detailed,featured} + 8 langs; CJK
subset rebuilt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Completes the wallet-files dialog and localizes the feature.
- Create a new wallet: a name is normalized to wallet-<name>.dat and opened
via switchToWallet; the daemon mints it (seed-backed, -usemnemonic=1) on
first load. Rejects blank names + name collisions.
- Import: out-of-datadir wallets get an Import action that copies the file
into the datadir (never overwriting) under a wallet-*.dat name, then opens
it — the daemon only loads plain filenames from the datadir.
- i18n: the 32 new multi-wallet / per-wallet-contacts / mining-payout strings
translated into all 8 languages (de/es/fr/ja/ko/pt/ru/zh), CJK subset font
rebuilt. Verified: the Chinese Wallets dialog renders with no missing glyphs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second phase of multi-wallet: list wallet files and switch the active one.
- Daemon plumbing: EmbeddedDaemon::setWalletFile -> start() passes -wallet=<name>
(non-default only; skipped during the isolated seed-migration start), and
DaemonController::syncSettings pushes active_wallet_file on each start.
- App::switchToWallet: persist the new active wallet, then stop + restart the
node on -wallet=<name> (rescan only if it was never synced in this datadir).
Reuses the seed-adopt restart coordination; WalletState clears on disconnect
and the P1 identity-scoped caches re-key, so no previous-wallet data leaks.
Guarded: full-node, embedded daemon, not mid-restart, no pending send.
- Wallets dialog (Settings -> Backup & Data -> "Wallets…"): a table of wallet
files (datadir wallet*.dat + user-added folders' *.dat) with size (disk) and
cached balance / address count / last-opened (wallet index), a current/Active
badge, Open (switch), and Add folder. Out-of-datadir wallets show "import to
open" (P3). Added as a sweep surface.
- Mining payout safety: non-destructive warning if the pool-mode worker looks
like a DragonX address not in the current wallet (stale after a switch).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First phase of multi-wallet support: keep each wallet's data separate so loading
a different wallet no longer shows the previous one's data, and lay the metadata
groundwork for the wallet-files list.
- Address book scoped per wallet: AddressBookEntry gains a "scope" ("global" or a
wallet-identity hash); the Contacts tab shows global + current-wallet contacts
(App::activeWalletIdentityHash) with a "show in every wallet" toggle + globe
badge. Legacy entries migrate to "global". Scope-aware de-dup allows the same
address across different wallets.
- Wallet metadata index (data/wallet_index -> wallets.json): file-keyed cache of
balance / address count / identity / size / last-opened / synced-here, since
those can't be read off a wallet.dat without loading it. Populated on connect +
address refresh (change-detecting upsert). Plus an active_wallet_file setting
for the -wallet=<name> switch coming in P2.
Unit-tested (testAddressBookScope, testWalletIndex): migration, visibility filter,
scope-aware de-dup, upsert change-detection, save/reload round-trip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hand-drawn absolute geometry (dl->AddText offsets, SetNextWindowSize,
explicit ImVec2 button widths, SameLine strides) is immune to the app's
DPI machinery (font-atlas rebuild + ScaleAllSizes), so several overlays
rendered native-size (tiny) on HiDPI / Windows 150% displays. Multiply the
raw logical-px geometry by Layout::dpiScale():
- seed_display: content-aware, DPI-scaled seed-grid column stride (a raw
130px stride let words collide with the next column's index at 150%)
- lock screen: card/logo/input/unlock-button + all vertical offsets
- migrate-to-seed + seed-backup dialogs: button widths
- send-confirm: confirm/cancel button widths
- about dialog: value-column x, edition inset, link/close button widths
- chat conversation list: row height + padding (fixes the preview clip)
Also freeze demo state during the full UI sweep: guard App::update() on
capture_mode_ so a running daemon's refresh can't clobber the injected
demo state (it blanked Send/Receive/History with a CDB error mid-sweep).
Verified on Windows 4K@150% and locally at font_scale=1.5 (same
dpiScale()==1.5 code path). Adds a DPI-scaling convention note to CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The existing sweep only captures tabs. This adds a "Full UI sweep" (DEBUG
OPTIONS) that also drives every normally-hidden surface into view and captures
it under every skin, so all the UI a normal sweep misses can be reviewed at
once.
- The capture unit becomes a "surface" (SweepTarget: a base tab, optionally
with a setup()/teardown() that forces a modal / multi-step flow / state
overlay on top). Tabs are surfaces with a null setup. The existing tab sweep
is folded into the same state machine (one code path). All sweep logic +
the catalog + demo data move into a new src/app_sweep.cpp.
- Runs OFFLINE against injected demo data (installDemoWalletData snapshots then
restores the real state_ — WalletState isn't copy-assignable, so a targeted
field snapshot is used) and fires NO live op: capture_mode_ guards the
seed-backup RPC, auto-lock, and the migration pumps; the async-firing dialogs
are neutralized by pre-setting their state (fetch_started_ + demo phrase;
seed_migration_step_ set directly). Verified: 0 RPC/secret/send calls.
- Catalog (v1): tabs + the bool-flag modals (import/export key, backup,
seed-backup, about, settings) + the 6 migrate-to-seed steps + 4 wizard steps
+ lock/warmup/not-ready overlays + the send-confirm popup (via a new
ui::SweepShowSendConfirm debug hook, since its state is send_tab-static).
- Overlay/blur surfaces settle 8 frames (blur backdrop freeze); setup re-runs
each frame (OpenPopup popups must re-fire). updateScreenshotSweep now runs
before the first-run-wizard early-return in render() so the sweep advances
while a wizard step is shown. main.cpp drops vsync during a sweep so it isn't
throttled to the compositor's unfocused rate. Output:
<config>/screenshots-full/<surface>/<skin>.png + index.md.
- DRAGONX_FULL_SWEEP=1 runs the sweep headlessly then quits (CI / verification).
Verified end-to-end under WSLg: 288 PNGs (9 skins x 32 surfaces), all 1200x720,
[Sweep] done, clean quit, no daemon. Byte-identity across runs holds only for
static skins on data-free surfaces — the rest vary by animated theme effects /
live price / relative timestamps (expected for a review tool, not a bug).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New full-node wallets now get a real 24-word recovery phrase, and users can
reveal/back it up from Settings.
- Pass -usemnemonic=1 to dragonxd. The daemon reads it only inside
GenerateNewSeed() when a wallet has no seed yet, so it is inert on existing
wallets (safe to pass unconditionally) and makes every freshly-created
wallet mnemonic-backed — its phrase is then exportable via z_exportmnemonic
and portable to SDXLite/ObsidianDragonLite. (Existing/legacy wallets are
unaffected and keep using the chat identity's z_exportkey fallback.)
- App::exportSeedPhrase() wraps z_exportmnemonic on the RPC worker with
sodium_memzero wiping on every path; it flags the daemon's "not derived
from a mnemonic" error as a legacy wallet rather than a failure.
- New "Back up seed phrase" modal (Settings -> Backup & Data, full-node
gated): reveals the phrase in a numbered word grid, copy (45s clipboard
auto-clear) + save-to-file (0600), wipes the secret on every close path
incl. dismiss-mid-fetch. Shared ui/windows/seed_display.h grid helper.
- One-time nudge (settings flag seed_backup_reminded) toasts mnemonic-wallet
users to back up their phrase; legacy wallets are not nagged.
- 15 new strings translated into all 8 languages (additive, 957->972 keys);
CJK subset font rebuilt for the new glyphs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Outgoing echoes were recorded optimistically regardless of whether the broadcast
was actually submitted. Now broadcastChatMemos / broadcastChatMemosLite return
whether the send was submitted (false on immediate failures: not connected, no
spendable z-address, or a lite send already in progress), and the echo records a
ChatDelivery status (Sent / Failed). The Chat tab shows a "not sent" marker in
the error color on failed messages. The status persists in the DB (backward-
compatible: old rows deserialize as Sent).
(A later async failure — an opid that fails after submission — still surfaces via
the existing send-progress notification; only the submit gate is reflected here.)
Also removes the now-unused chat_readonly_note string (the composer replaced the
read-only footer). Test: delivery status round-trips through the DB. Verified:
Linux + Windows build with chat ON, ctest 100%, hygiene clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the outgoing side of HushChat: compose messages and start conversations.
The wire construction is the byte-exact inverse of the receive parser and is
proven by a self-consistent round-trip (build → parse → decrypt).
- src/chat/chat_outgoing.{h,cpp} (pure): buildOutgoingMessage encrypts via
encryptOutgoing and buildOutgoingContactRequest carries plaintext; both emit
the header memo JSON ({h,v,z,cid,t,e,p}, which nlohmann serializes
alphabetically to match SilentDragonXLite) + the payload memo, validating the
512-byte memo limit, the 64-hex peer key, and the "no leading '{'" rule for
request text. My public key goes in header "p"; the peer's key is the
encryption recipient.
- ChatService: composeMessage/composeContactRequest (encrypt with the held
identity) + recordOutgoing (echo an Outgoing ChatMessage into the store + DB —
we never harvest our own sends, so this is the only local record).
- App: sendChatMessage(cid,text) sources the peer z-addr + public key from the
conversation, composes, and records a random-id echo; startChatConversation
(zaddr,text) mints a random cid + composes a plaintext contact request;
chatReplyZaddr() picks a spendable z-addr. broadcastChatMemos() is the Phase-5
transport seam (network delivery + real-SDXL interop verification land there).
- Chat tab: a message composer (shown once the peer's key is known, else a
"waiting for reply" hint) + a "New conversation" modal that sends a contact
request to a z-address.
- Tests: outgoing round-trip through the receive parser + decrypt, contact-request
passthrough, validation guards, and ChatService compose + recordOutgoing echo.
Adversarially reviewed (crypto/interop, secret hygiene, logic, UI) — 0 findings.
Gated by DRAGONX_ENABLE_CHAT (default OFF). Verified: Linux + Windows(mingw)
build with chat ON, ctest 100%, hygiene clean; caches restored to the OFF default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the first user-visible HushChat surface: a Chat tab in the sidebar with a
two-pane, read-only conversation view — a conversation list on the left and the
selected thread on the right — reading from the App-owned ChatService store.
- New src/ui/windows/chat_tab.{h,cpp} (RenderChatTab(App*), mirroring the
Contacts tab). Conversations are sorted by most-recent activity; peer
z-addresses resolve to contact names via the address book when known; each
message shows sender + timestamp + wrapped body, with a contact-request tag
and a read-only footer (composing arrives in a later phase). Empty states
cover "wallet locked / identity not ready" and "no conversations yet".
- Sidebar wiring: NavPage::Chat + registry entry (static_assert stays balanced),
NavPageSurface + GetNavIconMD (ICON_MD_CHAT), dispatch in app.cpp, trace/sweep
page names, and App::chatService() accessor. The tab is gated on the new
WalletUiSurface::Chat, which returns DRAGONX_ENABLE_CHAT != 0 — so it only
appears in chat-enabled builds and is hidden (and unreachable) by default.
- i18n: English defaults for the chat nav label + hint strings.
Verified: Linux + Windows(mingw) build with chat ON, ctest 100%, hygiene clean;
caches restored to the OFF default. The screenshot sweep now includes the Chat
tab (sweepPageName "chat") for per-theme visual review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Screenshot-sweep review follow-ups:
- sweepPageName() had its own page-name switch that lacked a Contacts case,
so the tab's screenshots landed under the fallback "page/" dir. Add the
case so they capture under "contacts/".
- The Z/T type badge (green/amber) washed out on light skins. Use darker,
more-saturated variants when IsLightTheme(); keep the brighter ones on dark.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 0d — the accessibility pass on the Contacts tab.
- Search box filters by label/address/notes (case-insensitive); a distinct
"no matching contacts" empty state.
- Sortable columns (ImGuiTableFlags_Sortable) — click a header to sort the
view by label/address/notes, ascending or descending.
- Keyboard nav (when the tab owns focus, no field/modal active): Up/Down move
the selection through the *visible* order, Enter edits, Delete deletes
(feeding the two-click confirm), Ctrl+C copies.
- Contrast: the address is no longer rendered as muted TextDisabled (it's the
row's key data) — normal legible text, with a coloured Z/T type badge.
Notes are legible too.
- The add/edit form focuses its first field on open (SetKeyboardFocusHere).
- The delete confirm is now visible: the Delete button relabels to
"Confirm delete?" while armed, instead of a transient toast.
Correctness: selection is tracked by STORAGE index, decoupled from the
filtered/sorted visible order, so edit/delete/copy always target the right
entry. (ImGui renders to a canvas with no OS accessibility tree, so this is a
keyboard/contrast/findability pass, not screen-reader support.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 0c. The Send recipient field only offered Paste and a tx-history
suggestion list — no way to pick a saved contact. Add a compact contacts
icon button next to Paste that opens a picker popup over the App-owned
address book; selecting a contact fills the recipient (re-validated next
frame).
- New header-only src/ui/windows/contact_picker.h: ContactPickerPopup(id,
book, outBuf, outSz) — a reusable popup listing contacts ([Z]/[T] tag +
label + short address) that copies the chosen address into the caller's
buffer. Kept out of the material layer so the design-system headers stay
free of data/ deps; reserved for Chat "new conversation" later.
- send_tab: shrink the recipient input to make room for the icon button
(ICON_MD_CONTACTS), wire the popup to app->addressBook(). Coexists with the
existing Paste + suggestion affordances.
- i18n: send_contacts_button tooltip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 0b. The address book was buried in Settings behind a modal dialog.
Make it a first-class "Contacts" tab (which will also become the chat
roster), rendered inline in the main content area.
- New NavPage::Contacts (after History; ICON_MD_CONTACTS) +
WalletUiSurface::Contacts. isUiSurfaceAvailable's `default: return true`
shows it in BOTH variants; uiSurfaceNeedsWalletData's default keeps it
usable before wallet data loads. All the touchpoints wired: NavPageSurface,
GetNavIconMD, the app.cpp dispatch case, the app_network.cpp tracePageName
case, and the `contacts` i18n label.
- New src/ui/windows/contacts_tab.{h,cpp}: RenderContactsTab lifts the
toolbar + table + add/edit modal out of address_book_dialog, rendered in a
BeginChild scroll region (peers_tab pattern) instead of an overlay; the
add/edit form stays a modal layered over the tab. Reuses the existing
address_book_* i18n keys and the dialogs.address-book schema.
- Delete address_book_dialog.{h,cpp}; remove its app.cpp render pump and the
dead App::show_address_book_. The Settings "Address Book…" button now
navigates to the tab (setCurrentPage) instead of opening the modal, so the
Tools & Actions grid layout is untouched.
- CMake: swap the dialog sources for contacts_tab.
Behavior-preserving move; search/sort/keyboard/contrast land in 0d. Visual
check pending the per-theme screenshot sweep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 0a of the Contacts/Chat plan. The address book was a file-scoped
singleton (`s_address_book` + `getAddressBook()`) trapped inside
address_book_dialog.cpp, reachable only from that TU. Promote it to an
App-owned member so the upcoming Contacts tab, the Send contact picker,
and a future Chat roster all read one source of truth.
- app.h: add `data::AddressBook address_book_;` + `App::addressBook()`
accessors, next to the other owned data models.
- app.cpp: load it once in App::init() (idempotent; missing file is fine;
purely local, no daemon dependency).
- address_book_dialog.cpp: delete the singleton + getAddressBook(); read
through the App* the dialog already carries (dropping the dead
`(void)app;`). show() is static so it can't reach the App — drop its
per-open reload (the book is authoritative for the app lifetime and
self-saves on mutation). Drop the now-unused <memory> include.
No behavior change; groundwork for the Contacts tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Centralise the remaining hardcoded English literals in two otherwise
fully-translated files (explorer_tab: 40 TR calls, shield_dialog: 23) so
they join the i18n system with an English source key + fallback:
- explorer_tab: the three search-error messages (invalid response, hash
not found, not-connected) now use explorer_* keys. Also guards a hash
lookup behind an active daemon connection and disables the block-detail
prev/next nav while a fetch is in flight (both were the reason these
error paths could fire).
- shield_dialog: the operation status/error strings (submitting, submitted,
failed, status, error-checking-status, shield/merge failed) now use
shield_* keys.
- i18n: add the new explorer_* and shield_* English source keys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- mining_benchmark: when a thread benchmark finishes with no nonzero
hashrate sample (e.g. the pool never reported a rate), don't finish
silently — set a new `inconclusive` flag on ThreadBenchmarkUpdate and
let mining_tab warn. Keeps the state-machine core pure/no-I/O (it is
unit-tested), rather than calling the Notifications singleton from it.
- mining_controls: fold the two byte-identical idle-delay combo blocks
(non-scaling + thread-scaling branches) into one lambda parameterised
by combo id; removes ~30 lines of duplication.
- request_payment: drop the dead s_selected_addr_idx state and write the
address-selected comparisons as std::string == char* consistently.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tighten input handling across several dialogs so bad/edge input can't
produce a doomed request or a confusing display:
- app_security: passphrase strength meter now factors character-class
diversity — an all-one-class string downgrades one tier so "aaaaaaaa"
no longer scores as high as a mixed one.
- block_info: clamp the height field to the chain tip and hide/deny
"Next" at the tip (was only yielding a raw RPC error).
- address_label: trim surrounding whitespace before saving; a
whitespace-only label clears it (mirrors clearing the icon).
- send_tab: re-clamp the amount when a Max send has its fee bumped in the
confirm popup (kept the total within budget) and NUL-terminate the
strncpy'd address/memo when a payment URI overwrites the form.
- settings_page: reject an empty/whitespace-only lite wallet path before
dispatching an unusable open/restore request.
- peers_tab: guard ExtractIP against an empty address.
- transaction_details: disable "View in explorer" when the configured
explorer URL is empty/whitespace so we never open a garbage link.
- app: seed-backup "Skip" now requires a second, deliberate click with a
fund-loss warning before it dismisses.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address a cluster of low-severity robustness nits where an async
callback could be skipped (leaving a spinner spinning forever) or a
synchronous action could be re-fired mid-flight:
- app_network: add catch(...) fallbacks around the importPrivateKey and
submitZSendMany worker lambdas so a non-std throw can't escape the
worker and skip the main-thread callback (stuck "Importing…"/"Sending…").
- export_transactions: re-entrancy guard disabling Export while a write
is in flight.
- bootstrap_download: disable Cancel once clicked so the request is
visibly acknowledged and can't be re-fired before the worker stops.
- network_tab: give clear "already in list" feedback on a duplicate
server (keep the inputs, clear only the stale invalid-URL error) and
disable/relabel Refresh while a probe is in flight.
- key_export: offer Retry from the error branch (falls back to Reveal)
and guard against an empty/malformed address before dispatching.
- receive_tab: only enter the "generating…" state when a dispatch is
actually possible (guards a stuck spinner when disconnected).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Address-transfer dialog closed itself on submit (s_open=false the same frame),
so its in-dialog result screen was dead code. Keep the dialog open on submit:
s_sending drives the button to a disabled "Sending…" state, and the async
callback's result (success txid / error) now shows in the result screen with
its own Close button.
- Settings RPC connection editor was inert AND showed compile-time defaults.
Populate Host/Port/Username/Password from the auto-detected daemon config
(rpc::Connection::autoDetectConfig) and make the fields read-only — the RPC
credentials come from the daemon's DRAGONX.conf, so these now accurately
DISPLAY the live connection instead of pretending to be an editor (which did
nothing). The existing "auto-detected" note now matches the behaviour.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The last four (more involved) robustness items from the audit:
- deleteBlockchainData: post the deleted-item count to the main thread (via an
atomic, since Notifications isn't thread-safe) and show a completion toast
("Blockchain data deleted (N items). Daemon restarting…") — previously the
result was only logged.
- Pool mining: pool start has a connect delay with no feedback; announce
"Starting pool miner — connecting…" on a successful start and "Pool miner
connected and hashing." once the poll confirms it (contained pool_starting_
flag; the shared mining-toggle state machine is untouched).
- Force Quit (shutdown screen): gate it on the status text having STALLED (a
genuine hang) rather than a bare 10s timer — with a 20s hard-ceiling backstop —
and show a state-aware caution naming the stuck step (force-quitting mid daemon
flush risks the chainstate).
- Benchmark: require a confirming second click that first builds candidates to
estimate the duration ("Benchmark takes ~Ns and interrupts mining"), instead
of interrupting mining immediately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Send: re-validate against LIVE state at the "Confirm & Send" click (connected,
not syncing, amount > 0, total <= available). The confirm dialog persists
across frames, so a balance drop / sync restart / fee bump after Review could
otherwise broadcast a now-invalid transaction; it now shows a clear message
instead of sending.
- Console: a malformed JSON argument ({...}/[...] that fails to parse) was
silently sent to the daemon as a plain string. Now the command fails with a
clear "Argument N is not valid JSON" line instead of being silently mangled.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Send: block Review from a view-only source (no spending key) with a tooltip,
instead of failing at submit; label the memo counter as bytes and warn in
colour once the field hits its cap (Sapling memos are byte-limited).
- Shield: show "No shielded (z) address yet — create one on the Receive tab
first" when the destination combo is empty, instead of an empty dropdown.
- Updater dialogs (xmrig/daemon): suppress the window X during an active
download/verify/extract so closing can't orphan/block on the worker — the
in-dialog Cancel is the way to abort.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
More robustness fixes from the audit (the import-key pattern, lower severity):
- Trim pasted input before use/validation: network add-server URL/label,
lite-wallet key import (+ block empty), validate-address, address-book entry.
- Confirmations for destructive/irreversible actions:
- Address-book Delete now needs a confirming second click (no undo).
- Console `stop` needs a confirming second `stop` (it shuts down the node).
- Wizard "Skip" encryption needs a confirming second click (keys stored
unencrypted).
- Send amount: normalize to 8dp (satoshi precision) on both the DRGX and USD
inputs so digits past 8dp aren't silently dropped between the preview/review
and what's actually sent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The app-wide "import-key pattern" fixes (missing guards / untrimmed input / no
re-entrancy protection), from the robustness audit:
- Import-key dialog: unify the type indicator and the RPC dispatch on one
classifier (classifyPrivateKey is now prefix-aware — an "SK..." shielded key no
longer misroutes to the transparent RPC), trim manual input (not just Paste),
and gate both the indicator and the Import button on a shared
isRecognizedPrivateKey() so unrecognized/empty input can't be submitted.
- Shield: guard the submit button on connected + !syncing (like Send), with a
disabled tooltip, instead of firing into a raw daemon error.
- Mining: disable the pool Mine button when the payout address is empty
("enter a payout address first"), and trim the pool URL/worker on persist.
- Console: track in-flight RPC commands (atomic counter + busy() override) so the
input is disabled while a command runs instead of piling up on the worker.
- Maintenance (rescan/repair/delete-chain/reinstall-daemon): re-entrancy guard —
bail with "already in progress" instead of launching a duplicate destructive op.
- Bootstrap dialog: re-attach to an in-flight download on reopen instead of
resetting state and orphaning the running worker.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From the app-wide robustness audit (calibrated to the import-key dialog). These
are the safety-critical fixes; several could cost users funds:
- Backup/export false success (FUND LOSS): exportAllKeys pre-seeded a header, so
a keyless result (the usual case when the wallet is encrypted+locked) still
looked non-empty and backupWallet wrote a private-key-less file and reported
"Backup saved". Now exportAllKeys returns an exported count; backupWallet and
the export-all dialog refuse to write / report success on 0 keys, disclose the
count, flag partial results as INCOMPLETE, and the backup dialog confirms
before overwriting an existing file (+ trims the path).
- Bootstrap: fail CLOSED — refuse to install an unverified multi-GB archive when
no checksum is published (was `return true`), mirroring the xmrig/daemon updaters.
- Restore-from-seed: require a valid BIP39 word count (12/15/18/21/24) with a live
"should be 24 words — you have N" hint instead of accepting any non-empty text.
- Encryption passphrase: detect leading/trailing whitespace and BLOCK with a
warning (not a silent trim, which would change the passphrase and lock the user
out).
- Receive payment QR: emit the canonical `drgx:` scheme with a URL-encoded memo
via a new shared util::buildPaymentUri (the request-payment dialog now routes
through it too, so they can't diverge); the old "dragonx:" + raw memo was
unparseable by the wallet's own scanner.
Also clamps the receive "Recent Received" list to the 4 most recent (companion to
the recent-lists commit).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Each pool row showed "<hashrate> N%" with nothing saying the % is the pool fee.
Make it "<hashrate> N% fee" and widen the left auto-balance card (25%/180dp ->
30%/210dp min) so the added text fits.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Lower the zebra-stripe contrast (scanline-line-alpha 4 -> 2; the derived dark
band softens with it) so the alternating rows are subtler.
- Give the input's terminal bar a 1px outline along its own edges (theme-aware),
matching the output panel's glass rim, and a near-white surface on light skins.
- Add a blinking terminal caret at the end of the input when it isn't focused.
- Report a daemon started before the wallet ("no wallet-managed EmbeddedDaemon"
but RPC connected) as "Running" instead of the misleading "no daemon".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The overview "Recent Transactions" and the send tab "Recent Sends" lists showed
every matching transaction in a scroll region. Cap them at the 4 most recent
(state.transactions is sorted newest-first, so a simple head-limit). The receive
"Recent Received" list gets the same clamp in the Tier-1 robustness commit
(same file as the QR fix).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lower-severity polish from the per-skin review:
- Market portfolio ratio bar: was full-saturation Success/Warning at a208 over a
white a10 track — the green read as "neon" on dark skins and the track was
invisible on light skins. Make it theme-aware: muted fills (a165) on dark, a
dark track on light so the bar is defined on white/pastel surfaces.
- Light-skin muted text: bump --on-surface-disabled 0.52 -> 0.62 on light/
color-pop-light/dune/marble/iridescent so secondary text (addresses, category
labels) clears the pastel/gradient backgrounds (analog of the dark-skin bump).
Remaining review nits are the "white-overlay-assumes-dark" track/hover pattern
(a broader separate sweep) and per-skin aesthetic trade-offs; deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address the high-severity, systemic legibility issues found in the per-skin
screenshot review (verified across all 9 skins):
- Status pills (history): "Confirmed" text was 55%-alpha (dim); make it full
opacity, and give status/shield pills a visible tinted fill (a48) + 1px border
(a90) so they stop vanishing on dark-red / near-white / gradient skins.
- Console light theming: light skins now use a near-white terminal surface, and
the log text/JsonBrace colors switch to the dark defaults in light themes (the
schema colors are dark-terminal-authored — honor them only in dark themes) so
text isn't pale-on-white.
- Dark-skin muted text: bump --on-surface-medium 0.85 / --on-surface-disabled
0.58 on dragonx/dark/color-pop-dark/obsidian (obsidian addresses were ~1.1:1).
- Disabled Send button: the disabled fill was hardcoded white (invisible on
light skins) — use a dark overlay in light themes.
- Light-skin inputs: stronger frame fill + border color and a 1px frame border
for light themes so fields (and buttons) have defined boundaries on white/
pastel surfaces.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extend the inputs layer with the two remaining form-field idioms, pixel-identical:
- material::LabeledInputRow(label, labelPos, ...) — horizontal label-beside-input
(Text + SameLine + SetNextItemWidth + InputText). Adopt at the settings RPC
host/port/user/pass rows.
- material::LabeledInputMultiline(label, id, buf, size, flags) — label above a
sized multiline input. Adopt at the address-book notes and request-payment
memo (drops the redundant SetNextItemWidth that InputTextMultiline's explicit
size already governs).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UI-standardization audit: the labeled single-line form field (a plain
ImGui::Text label above a fixed-width InputText/InputTextWithHint) was
open-coded across dialogs. Add material::LabeledInput(label, id, buf, size,
width=-1, hint=nullptr, flags=0) mirroring that idiom exactly — pixel-identical,
but a single chokepoint so input styling (label typography, frame, spacing) can
later evolve in one place. Returns the input's edited bool.
Adopt at the clean vertical (label-above) single-line sites: address-book
add/edit (label + address), shield from-address, export-all-keys + export-
transactions filename, validate-address input, request-payment label + URI.
Left as-is: horizontal label-beside-input rows (settings RPC host/port/user/
pass use Text + SameLine), multiline (InputTextMultiline), numeric (InputDouble),
combos, and hint-only inputs — none match the vertical single-line idiom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UI-standardization audit: the receive address-type filter (All/Z/T) was three
hand-rolled gapped ImGui::Button pills with per-segment PushStyleColor. Replace
with the shared material::SegmentedControl (rounded track + Primary inset-pill
on the active cell + centered caption labels), the same control the market
portfolio editor uses. Keeps the same right-aligned footprint (toggleTotalW)
and drives s_addr_type_filter from the returned clicked index. Verified across
all 9 skins via the screenshot sweep.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to c83348a: convert the three mining solo-mode header toggles
(idle-mining, thread-scaling, gpu-aware) from the hand-rolled "draw active
pill + glyph, then hit-test" idiom to material::IconButton using the restBg
(active-pill) path. Each keeps its exact behavior pixel-for-pixel: a circular
Primary-tinted pill when active (alpha 60 idle / 40 scale+gpu), a state-colored
glyph (Primary when on, muted when off), hand cursor + tooltip on hover, and
the idle button's cursor save/restore (savedCur, restored at block end) is
preserved. Completes the clean icon-button adoption; the raw-rect IsRectHovered
sites remain intentionally separate (different hit mechanism).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UI-standardization audit: ~66 call sites still used raw ImGui::Button /
ImGui::SmallButton instead of the app's canonical TactileButton, so those
buttons missed the standard glass fill + rim + tactile overlay (and the
light-theme flat treatment). Convert 59 plain action-button sites across the
wizard, security dialogs, settings, market/console/explorer tabs, and the
address dialogs to material::TactileButton / TactileSmallButton (drop-in:
same signature/return, args preserved verbatim). Danger buttons keep their
PushStyleColor wrappers — Tactile renders through them then overlays.
Deliberately left raw (not plain buttons): InvisibleButton hit-targets, the
frameless transparent-bg collapsible-header toggles (RPC/Debug), the icon-only
pagination chevrons, and the receive All/Z/T segmented filter — a glass rim
would clash with those intentionally borderless/segmented looks.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
UI-standardization audit: three draw patterns were copy-pasted across tabs and
dialogs. Promote each to a material:: helper and adopt at every verbatim site.
Output is pixel-identical (same colors/geometry threaded through as params).
- material::DrawProgressBar / ProgressBar — replaces 3 byte-identical rounded
fill bars in the daemon/xmrig/bootstrap download dialogs.
- material::CopyableTextOverlay — the click-to-copy affordance (hit button +
hand cursor + tooltip + hover underline + clipboard) shared by the peers
best-block hash and the 3 mining stats (difficulty/block/address). Returns
"copied this frame" so callers keep their own toast (no ui-layer dependency).
- material::DrawPill / PillSize — rounded bg + optional border + inset text
badge, shared by the explorer status pill, the market Z/T chip, and the
network official/custom tag.
Divergent one-offs left in place (transactions status pills use schema rounding
around pre-positioned multi-line text; the mining filter-pill is a button bg).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>