Two issues from the audit:
- Cursor: hovering the modal showed the console's text-select cursor. The
output's mouse-interaction guard only suppressed itself under ImGui popups,
but this modal is a BeginOverlayDialog (not a popup), so the console kept
setting the text cursor underneath. Suppress output interaction while the
reference modal is open (!show_commands_popup_).
- Design: the two panes used hard 1px child borders and floated unpolished.
Restyle to match the portfolio/wallets modals — each pane now sits on a soft
DrawGlassPanel surface (borderless child + inner padding), category headers
are subtle accent labels (no heavy filled bars), and the selected/hovered row
gets a rounded Material fill instead of the sharp Selectable highlight. The
glass panes are theme-adaptive (light on light skins), so the command-name
blue and the mono example green are floored to readable contrast (FloorLight)
and the detail heading uses theme Primary().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The builder's "omit trailing optionals" loop stopped at the FIRST empty
optional, which discarded any later field the user had filled — e.g. setban
with bantime empty but absolute=true built `setban "ip" "add"`, dropping the
typed `true`, and (worse) left Insert & run enabled so it ran a command that
ignored the input.
Include fields up to the last one that is filled or required; only genuinely
trailing empty optionals are dropped. A gap (an empty field before a later
filled one) keeps its placeholder and marks the command incomplete, so Insert
& run disables and nothing typed is lost. Found by an adversarial pass over all
82 param templates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The detail pane's Parameters section is now an editable form: each parameter is
a type-hinted input field (string / number / json, optional marked), and a live
"Builds" preview shows the command being assembled. String values are
auto-quoted; a required field left empty keeps its placeholder and disables
"Insert & run"; trailing empty optionals are omitted.
Insert and Insert & run now use the assembled command (not just the template),
so Insert & run works for parameterized commands once the required fields are
filled — destructive commands still confirm first. Param fields reset when the
selected command changes or the modal opens. Keyboard nav (Up/Down/Enter) is
scoped to the search box so typing in a field doesn't move the selection.
A best-effort parseParamSpecs turns the human-readable param templates into
fields; JSON-array params (z_sendmany, createrawtransaction) surface as a single
"json" field the user pastes into. Adds the console_ref_builds label + 8
translations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add plain-language details, a concrete example, and intent keywords to the
50 commands that Phase 1 left with only the terse description — so every entry
in the command explorer now explains itself and is searchable by task, not just
by name. Destructive flags are preserved.
Content was authored per-category and reviewed for technical accuracy (correct
RPC semantics, valid console example syntax, realistic DragonX placeholders).
Data-only change; command docs stay English (technical), consistent with the
existing policy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the flat, terse command-reference table with a guided two-pane
explorer built for users who don't know the RPC commands:
- Search by name OR task: keyword synonyms let "balance" find getbalance,
"send money" find sendtoaddress; results are ranked (name-prefix > name >
keyword > desc).
- Two panes (mirrors the portfolio editor): a category-grouped master list
(mono names, a warning dot on consequential commands) + a rich detail pane
with a plain-language explanation, a per-parameter type breakdown (string /
number / json, optional dimmed), and a concrete example.
- Insert into console (fills the input with a template to review + run) and,
for no-parameter commands, Insert & run — with a confirm for destructive
ones. Deferred via pending_submit_ so the modal needs no executor.
- Keyboard: type to filter, Up/Down to move, Enter to insert; auto-focus and
auto-select the top result.
Data model: ConsoleCommandEntry gains details/example/keywords/destructive
(C++17 defaults, so the ~60 un-enriched entries are untouched); ~20 common
commands enriched and 18 consequential ones flagged. Command docs stay English
(technical); the UI chrome + 7 category names are translated into all 8
languages (CJK subset rebuilt, +3 glyphs).
Verified: two-pane renders on dark + light skins and at font_scale 1.5; detail
pane shows explanation/params/example; danger dots + safety badge on
destructive commands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Clicking a command now actually closes the modal. It called
ImGui::CloseCurrentPopup(), a no-op here (BeginOverlayDialog is Begin/
BeginChild, not an ImGui popup), so the picker stayed open covering the input
and the insert looked like it did nothing. Set show_commands_popup_=false.
- Search filter is now a member cleared when the modal opens, so it isn't stale
on reopen after an outside-click / command-click dismiss (was reset only by
the Close button).
- Esc dismisses the modal (this overlay has no built-in Esc handling), and the
search box auto-focuses on open so the user can type immediately.
- Scale the 2-vs-3 column breakpoint (cmd-min-width) by dpiScale() — it was a
logical-px threshold compared against a physical-px available width.
- Give each category's table a unique ImGui id (##cmdsN) instead of sharing
"##cmds", and translate the 7 category names (Control/Network/... — command
descriptions stay English) into all 8 languages; CJK subset rebuilt (+2).
(Investigated but rejected: the command-name blue was flagged as low-contrast
on light skins, but the modal card is a dark glass panel on every theme, so the
original color is correct — verified by rendering marble/light/color-pop-light.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- D2: rpcTraceTimestamp used std::localtime (process-wide static tm) guarded by
a private mutex that can't stop another thread's localtime from clobbering the
shared buffer. Use localtime_r / localtime_s into a local tm (the codebase
pattern) and drop the now-useless mutex. Runs on RPC worker threads.
- C1: the toolbar's "<N> matches" label read filter_match_count_ before
renderOutput recomputed it, so it lagged one frame. Compute the visible/
filtered set once at the top of render() (before the toolbar) and reuse it in
renderOutput, so the count and the output share one consistent computation.
- C4: the "N new lines" indicator counted the raw drain count, inflating it with
lines the active filter hides. Count only newly-added lines that pass the
current filter, so the badge matches what the user sees on jumping to bottom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BuildConsoleRpcCall pushed any bare token that "looked numeric" as a JSON
number, and lost the tokenizer's quoting decision — so a genuinely-string
argument that happened to be all digits (e.g. a label) was sent with the wrong
type. std::stoll also silently truncated "1e999" to 1 (stops at the first
non-digit) and std::stod could yield inf/subnormal.
- ParseConsoleCommandArgsTagged returns {text, quoted}; quoted tokens are sent
verbatim as JSON strings, never coerced. (ParseConsoleCommandArgs kept as a
text-only wrapper so existing callers/tests are unchanged.)
- Bare tokens become a number only when the WHOLE token parses to a finite
int/double; otherwise they're sent as a string.
Adds test coverage for quoted-numeric-string, bare-number, 1e999, and 123abc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From the console-tab audit:
- HiDPI: the "New output" pill (140x24 + margin/rounding/border) and the JSON
indent-guide thickness were hand-drawn in raw px; scale them by dpiScale().
- Fold triangle: size it from the DPI/density-scaled gutter width (not the
zoomed font) and center it in the gutter band so glyph and clickable cell
stay aligned; only draw/handle it in the unfiltered view (folding_active_) —
in a filtered flat view a click silently flipped the collapsed flag with no
visible effect and the glyph disagreed with the rendered block.
- Selection: ignore left-clicks in the gutter (< output_origin_.x) so toggling
a fold no longer clears the user's active text selection.
- 'stop' confirmation: promote the function-local static to a member and reset
it in clear(), so a toolbar/context-menu clear between the two 'stop's can't
leave a stale arm that skips the shutdown warning.
- RPC-trace callback: dereference the console pointer under the mutex (not
after releasing it) so ~ConsoleTab can't destroy the object mid-call —
closing a latent use-after-free on shutdown (fires on RPC worker threads).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Persist the two console output display preferences and add a second toolbar
toggle for monochrome text.
- Persistence: new Settings fields console_line_accents / console_text_color
(both default true, matching the ConsoleTab statics so an upgrade re-save
can't flip visible behavior). Restored into the statics at startup in the App
constructor, and saved from a diff-and-save after the console renders (only
when a value actually changed — save() is disk I/O). The startup restore also
fixes a pre-existing bug: scanline was only synced from settings when the
Settings page or first-run wizard ran, so an existing wallet ignored a saved
scanline preference on launch.
- Monochrome text: a new toolbar toggle (FORMAT_COLOR_TEXT, dimmed when off)
next to the accent toggle. channelTextColor() early-returns the neutral
COLOR_RESULT for every channel (and JSON syntax role) when off, leaving the
left accent bars independent. COLOR_RESULT is contrast-floored per theme, so
text stays readable on light and dark terminals.
Adds console_toggle_text_color to the English source + all 8 translations.
Verified: both toggles independent, persistence round-trips across restart,
monochrome readable on light + dark skins.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a paint-bucket toggle button to the console toolbar (in the appearance
cluster next to zoom) that shows/hides the per-line left color accent bars for
a cleaner monochrome gutter. The icon reflects state (FORMAT_COLOR_FILL when on,
a dimmed FORMAT_COLOR_RESET when off) with a tooltip. Session-only, like the
console zoom factor; default on.
Adds console_toggle_accents to the English source and all eight translations
(no new CJK glyphs — the subset font already covers them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hovering the disabled Save button now shows which requirement is missing
(name / at least one address / a manual price above 0), mirroring the
send-tab disabled-submit tooltip idiom (IsItemHovered(AllowWhenDisabled)).
The hint only appears for a validation gap — not the self-evident "nothing
changed" case.
Adds portfolio_save_need_{name,address,price} to the English source and all
eight translations; rebuilds the CJK subset font for the new JA/KO/ZH glyphs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The address context menu's "Add to portfolio" submenu built each item with
MenuItem(label), whose ImGui ID is derived from the label — so two groups with
the same label (or two empty labels) collided, routing a click to the wrong
group, and a blank label rendered as an invisible row.
- PushID(pi) per item gives a stable unique ID regardless of label, and an
empty label now shows the "new entry" placeholder.
- Filter the list to the active wallet's groups (or legacy/unscoped), matching
the Market summary and editor, so an address can't be added to a different
wallet's group. The "no entries" placeholder now also covers the case where
every group is filtered out.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Market summary filters custom groups by wallet scope, but the editor did
not: its master list showed and edited *every* wallet's groups, and "Manage…"
opened raw storage index 0 — which could belong to a different wallet.
Separately, a group could be persisted with an empty scope (a placeholder
added before the wallet identity resolved, then filled in and saved), which the
filter treats as "legacy" and shows in *every* wallet's list.
- The editor master list now filters to the active wallet's groups (plus
legacy/unscoped), mirroring the summary. Storage indices are preserved so
selection/delete still target the right entry.
- "Manage…" opens the first in-scope group (or the empty state), never index 0.
- The selection clamp resets to the first visible group if the current one is
out of range or out of scope.
- On commit, a group with no scope is claimed for the active wallet when an
identity is available, so it can't linger in the global bucket.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Close button auto-committed the current group, but Escape, outside-click,
switching groups, adding a group, and deleting another group all *discarded*
uncommitted edits — so whether your work was kept depended on how you happened
to leave, and the prominent unsaved-dot / Save / Revert affordances implied a
save model the exits didn't honor.
Align every dismiss/switch gesture with the Close button's intent (auto-save):
- Escape and outside-click now commit before closing (outside-click is caught
right after BeginOverlayDialog, which clears open mid-frame).
- Switching to another group, "Add entry", and deleting a *different* group
commit the current group first. Deleting the selected group still discards
its own working state.
Save remains "persist now", Revert "undo to last saved", and the dot marks an
in-progress group. Ordering is safe against the placeholder GC: that GC runs
earlier in the frame (mktDrawPortfolio, before RenderPortfolioEditor) and only
while the editor is closed, so it never shifts entry indices under a commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial re-audit of the group editor surfaced follow-ups to the prior
hardening pass:
- Dirty-check regression: the empty-currency->"USD" coercion was applied on
save but not in the dirty comparison, so clearing the currency field left a
group perpetually "dirty" (unsaved dot + Revert/Save never settling, a
redundant persist on every Close). Both sides now share pfEditLabel() /
pfEditCurrency() so they can't disagree — this also trims the label, so a
trailing space no longer wedges the dirty state.
- Whitespace-only labels (" ") passed validation and persisted as an
invisible group; the trimmed label is now required by pfWorkingValid.
- "Add entry" persists an empty placeholder up front; abandoning it (Close/
Esc/outside-click without adding an address) left a phantom "$0 · 0" group.
The summary now prunes address-less groups while the editor is closed.
- Manual price accepted inf/NaN (neither is < 0); sanitize with std::isfinite.
- Escape closed the whole editor even when it was only meant to dismiss the
open custom-color popup, discarding edits; guard on !IsPopupOpen.
- HiDPI: scale the Z/T chip pill's vertical padding and the balance-breakdown
baseline nudge that the prior pass missed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Portfolio audit fixes:
- Guard the BTC value line behind price_btc > 0 so an empty BTC price no
longer renders a misleading "≈ 0.00000000 BTC".
- Validate the working group before commit/save (pfWorkingValid: needs a
label, at least one address, and a positive manual price when the manual
price basis is selected); the Save button disables to match, so a
half-filled editor no longer persists a broken group on close/switch.
- Default an empty manual currency to "USD" instead of an empty string.
- Reload the editor after clamping a stale selection index so it can't
point past the end of the group list.
- Scale hand-drawn geometry by dpiScale(): address-row rounding, chip
y-offset, preview border, chart toggle buttons, and the shielded/
transparent ratio-bar corner radii now render correctly on HiDPI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chart interval (Live/1H/1D/1W/1M) and the line/candle style were
session-only statics. Persist both like the selected exchange/pair: new
chart_interval / chart_style settings (defaults 1M / candlestick),
loaded into the market view on first show and saved on each interval
click / style toggle.
Verified: writing chart_interval=2, chart_style=0 to settings.json and
launching restores them (not reset to defaults) and re-saves them intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a chart-style toggle (line vs candlestick) next to the interval buttons,
shown only when the selected range has per-exchange candles (the aggregate /
Live view is line-only). It flips s_mkt.chartStyle; candles draw when OHLC
exists AND the user hasn't switched to the line. Icon reflects the current
style; tooltip says what a click switches to. Two i18n keys across 8 languages.
- Make the price chart ~50% taller: scale the height floor / desired / viewport
cap (110->165, x1.5 desired, 0.22->0.33 of available) for a roomier plot.
Verified via a forced-OHLC render: the taller chart, the toggle button, and
candles with a correct week/day x-axis.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Candles had no hover (the line's close tooltip is gated off for them). Add a
candle-aware readout: the candle under the cursor gets a column highlight +
crosshair, and a small box shows its date and open/high/low/close (colored green
up / red down). Verified via a forced-hover render (date + O/H/L/C over the
highlighted candle). Line charts keep their existing single-price tooltip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The exchange candle APIs return full OHLC but we only kept the close (a line).
Now the per-exchange chart draws real candlesticks; the CoinGecko aggregate stays
a line (it's close-only).
- Adapter keeps OHLC: parseExchangeOHLC() returns open/high/low/close candles
(parseExchangeCandles is now a close-only wrapper over it). New data/candle.h
holds the dependency-free Candle + bucketOHLC (5-min -> hourly for the 1D view).
- Model stores exchange_ohlc_intraday/daily alongside the close series;
refreshExchangeChart populates both. market_series::chartCandles() returns the
bucketed OHLC for the range, empty unless the per-exchange series is active.
- The chart renders wick (low..high) + body (open..close), green up / red down,
with a low..high y-range; the line-only bits (fill, hi/lo labels, hover tooltip)
are gated off for candles. Falls back to the line for the aggregate / Live view.
Verified: OHLC parsers + bucketOHLC + chartCandles unit-tested; a forced-state
render shows correct candlesticks; the aggregate still draws a clean line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The hero's "24H Vol" showed CoinGecko's cross-exchange aggregate even after the
chart/price went per-venue. Capture the per-exchange converted_volume.usd from
the tickers (previously discarded, alongside converted_last) and show the
SELECTED exchange's own 24h volume; fall back to the aggregate when unknown.
Market cap stays aggregate (it's coin-wide, not per-venue).
This is a big real difference — e.g. Ourbit ~$14.3K vs NonKYC ~$253 for DRGX/USDT
— so the header now reflects the venue you're actually looking at.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switching exchange pairs (or first load) briefly leaves the chart with no series
while the venue's candles fetch, which showed the bare "No price history
available" empty state. When a chart fetch is in flight — App::isMarketChartLoading()
(the CoinGecko aggregate OR the per-exchange fetch) — draw a spinner + animated
"Loading price history…" in the plot area instead; the "no history" text only
shows when genuinely empty and idle. New i18n key market_chart_loading across all
8 languages. Verified via a forced-state render (spinner + label centered).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The market chart was hardwired to CoinGecko's cross-exchange USD aggregate
(vs_currency=usd), so selecting a trading pair only changed the "Trade" link —
never the chart or price. Now the SELECTED exchange drives both.
CoinGecko gives us which venues list DRGX (the ticker `market.identifier`) but
not their APIs, so add data/exchange_candles.h: a hand-maintained map from that
identifier to each venue's public candle endpoint, with two adapters verified
against the live APIs — Ourbit (MEXC-style /api/v3/klines, array format) and
NonKYC (TradingView-UDF /market/candles, `bars`). Unmapped venues return no URL,
so the chart falls back to the CoinGecko aggregate and nothing regresses.
- App::refreshExchangeChart() resolves the selected pair, fetches its intraday
(5-min) + daily candles via the TLS-verified httpGetString on the worker, and
stores them on MarketInfo (exchange_chart_intraday/daily + exchange_chart_active).
Re-fetches on pair change; self-throttled to ~30 min otherwise; falls back to
aggregate on any failure.
- chartSeries() draws the per-exchange series when active (portfolio sparklines
stay on the aggregate). The hero shows the selected venue's real price
(converted_last) — Ourbit vs NonKYC differ ~7%. parseCoinGeckoTickers now
captures the identifier + per-exchange USD price (previously discarded).
Adapters unit-tested against real captured responses (URL builder + both parsers
+ the chartSeries switch) and validated against the full live feeds (Ourbit 20d,
NonKYC 370d, ascending, correct closes). Build + ctest + hygiene green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With the list now pinned at max height, a few wallets leave blank space below
the cards. Fill it — only when there's real room to spare — with a subtle
centered folder glyph + "Scan a folder to find more wallets" (OnSurfaceDisabled),
so the area reads as a gentle nudge toward the scan action rather than dead
space. Purely decorative (draw-list only, clipped to the list); the actions stay
below. New i18n key wallets_empty_hint, translated across all 8 languages.
Verified on the sweep: the hint centers in the gap and reads subtly on both dark
and light themes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two wallets-modal changes:
- The wallet list now always renders at its max height (kMaxVisibleRows) instead
of shrinking to the wallet count, so the modal is a consistent size whether you
have one wallet or many — fewer rows leave empty space, more than 7 scroll.
- Add a "Scanned folders" manager: each user-added scan folder is listed with a
control to stop scanning it (removeExtraFolder + save + re-scan so its wallets
drop out). Paths front-elide to keep the identifying leaf visible, with a
full-path tooltip. The card-height math reserves the manager's rows so nothing
clips. Two new i18n keys (wallets_scanned_folders / wallets_remove_folder),
translated across all 8 languages; CJK subset rebuilt.
Verified on the sweep at 1.0 and 1.5x DPI: max-height list on both the few- and
many-wallet surfaces, the folder manager renders and stays clear of the footer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A wallet reached via two different path strings — e.g. a symlinked mount
(/mnt/usb) and its real target (/media/usb) — used to hash to two different
in-place link names and list as two rows, so the same file looked like two
wallets with two datadir links.
Give WalletRow a canonPath (fs::canonical, symlinks resolved, computed once at
scan time so the every-frame render/active-marking path stays cheap) and key
both the link name and the row de-dup on it. Now the same physical wallet maps
to one identity regardless of the path used to reach it: one link name (honors
the "distinct per-target name" invariant), and scan() lists it once — datadir
first, so a datadir wallet wins over the same file seen via an external path.
The link also points straight at the resolved real file instead of through the
symlinked path. Genuinely distinct wallets keep distinct identities (verified
with a standalone symlinked-mount harness).
Closes the last deferred finding from the open-in-place adversarial review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Unix a symlink is the better datadir link than a hard link: it needs no
privilege, spans volumes (a wallet on a USB / other partition just works,
where a hard link can't), and is visibly a pointer rather than an
indistinguishable second copy. Windows keeps hard-link-first (a symlink there
needs admin / Developer Mode), falling back to a symlink.
Symlink-first made two latent issues in the pre-clear reachable, fixed here
after an adversarial review:
- Dangling prior symlink: fs::exists follows the link (false when the source
moved), so it was skipped and create_symlink then tripped on the occupied
path. Catch it with fs::is_symlink (lstat) and replace it.
- Stale reserved-name occupant: the old "hard_link_count == 1 → reuse" path
could load the WRONG wallet — a count-1 file at our name is not a valid link
to the source (a live hard link has count >= 2), it's a stale orphan (source
deleted, or a copy left by a datadir migrated across filesystems). Gate reuse
on fs::equivalent(link, src) (same inode) instead, and reclaim the name only
when the occupant is provably redundant (another hard link still holds the
data); otherwise refuse rather than risk destroying something unique. So
hard_link_count is no longer trusted to prove identity — only data-safety.
Also guard against a stale row (source moved/deleted between the scan and the
click): !exists(src) refreshes the list instead of linking a ghost the daemon
would silently swap for wallet.dat. Verified with a standalone harness across
fresh / re-open / valid-hardlink-reuse / stale-orphan / redundant-occupant /
vanished-source cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the "Import" action — which copied an out-of-datadir wallet into the
datadir and cluttered the list with hard-to-tell-apart duplicates — with an
in-place "Open" that links the real file into the datadir under a stable
per-target name (wallet-ip-<FNV8>.dat) and switches to it. The daemon only
loads a bare filename from its own datadir, so a link is the minimal bridge:
hard link first (no privileges, same volume — covers non-admin Windows),
symlink fallback (cross-volume), then an error. Never a copy (that would fork
the wallet) and never a delete of a real file.
Also:
- show each external wallet's originating sub-directory (…/Backups/2021)
- add a per-row "open folder location" button
- widen the modal (780 → 860) for the extra button
- guard the daemon against a dangling link (external file moved / USB
unplugged) by falling back to wallet.dat rather than creating an empty one
Per-target link names (not one shared name) make switching between two
external wallets a real -wallet switch, so the rescan/cache index tracks each
correctly. Drops the now-dead wallets_import* i18n keys and back-fills the new
open/folder strings across all 8 languages + rebuilds the CJK subset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prepend the containing folder (last 1-2 path components, e.g. "…/Backups/2021")
to an external wallet's metadata line, so several same-named wallet.dat files
surfaced by the recursive scan are distinguishable at a glance (the info-icon
tooltip still shows the full path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap the rounded-rect StyledButton for material::IconButton (square size →
bgRounding = radius, so a circle) with the arrow glyph mathematically centered;
carries its own hover fill + asc/desc tooltip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the sort dropdown with a material::SegmentedControl — Created / Addresses
/ Txs / Size — matching the Receive tab's address-type toggle, with the asc/desc
arrow beside it. Shorten the four sort labels to fit the segments (updated across
all eight languages + the back-fill script). The direction arrow is positioned
explicitly on the segment's baseline (the control is draw-list based and doesn't
advance the layout cursor).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make "is the active wallet" the primary sort key so the current wallet always
stays at the top regardless of the chosen sort/direction; the remaining wallets
sort by the selected key underneath it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parse the earliest keymeta nCreateTime out of the wallet.dat btree (the daemon's
"wallet birthday") and surface it in each row's metadata line ("created Aug 2025").
Add a sort control above the list — Date created / Address count / Transaction
count / Wallet size, with an ascending/descending toggle (descending default:
newest / most / largest first). Sorting reorders a display-index array
(s_order) rather than s_rows, so the async, index-aligned probe batch is
untouched; sort keys read a single frame-consistent snapshot of the probe
results. Address count prefers the authoritative cached index value, else the
btree key count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The async probe fills badges/counts a frame after the dialog opens, which the
single-frame screenshot sweep captured before it landed (badges/counts missing).
Run the probe synchronously when App::isScreenshotSweeping(), so the captured
modal-wallets frame is populated; interactive use keeps the detached-thread path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adopt the shared ApplySmoothScroll helper (with NoScrollWithMouse) for the
##walletList child, matching the Settings page and the other scroll areas, so
the wallet list eases on wheel input instead of jumping.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parseWalletBtree() walks the wallet.dat Berkeley DB btree directly (no libdb, no
daemon): validate the metapage magic, follow the "main" sub-database (its pgno is
stored big-endian in the master map), traverse internal/leaf pages, and tally
records by their length-prefixed type name — transparent + shielded spendable
keys, address-book, and tx count, plus exact encryption/seed flags. Every offset
is bounds-checked, pages are deduped at push time (stack stays O(npages)), and a
visited-set + page/key caps make it safe on a corrupt/adversarial imported file;
files using DB_CHKSUM/DB_ENCRYPT (which shift the page layout) are rejected so the
byte-scan fallback runs instead.
The async probe now uses this as the primary path (exact badges + counts), falling
back to the byte-scan only when the btree can't be fully parsed. The wallets list
shows "N keys · M txs" for probed rows (labeled "keys", not "addresses", since the
count includes change keys the daemon's address list omits). Validated read-only
against real wallets (counts cross-checked; enc/seed match the byte-scan) and a
hand-built minimal btree fixture in the unit suite.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the wallet.dat status badges as a right-aligned vertical stack of
"label icon" rows ("Seed phrase 🌱" over "Encrypted 🔒", "Legacy", or "Unknown"),
each with a hover tooltip; name/metadata reserve the stack width and truncate
before it.
Move the wallet.dat probing off the UI thread: scan() now builds the row list
synchronously (filename/size only) and hands the file reads to a detached
background thread whose results land in a shared, index-aligned batch that
render() reads under a mutex. A re-scan supersedes the old batch (cancel + swap);
the thread touches only its own heap batch (never s_rows/statics), so it's safe
across re-scans and shutdown. The dialog opens instantly even with a large
active wallet; badges fill in over the next few frames.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Search user-added wallet folders RECURSIVELY (bounded: depth/hit/visit caps,
skip_permission_denied, no symlink-follow, node-junk + node-subtree pruning);
the datadir stays top-level. r.dir is each file's real parent so Open/import
resolve subdir wallets. Auto-suffix the import destination on a name collision.
Add util/wallet_file_probe.h: read encryption/seed/shielded flags straight off
a wallet.dat WITHOUT loading it — validate the Berkeley DB btree magic, then a
bounded streaming byte-scan for the length-prefixed record markers the daemon
writes (mkey -> encrypted; hdseed/hdchain -> seed/HD; zkey/sapzkey -> shielded).
Reads no key material. The Wallets dialog shows Encrypted / Seed / Legacy badges,
plus an Unknown badge when a large file couldn't be fully scanned (so absence of
a lock never falsely reads as unencrypted). Unit-tested + validated read-only
against real wallets.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The console log text washed out on Marble/Light (~1.2:1 contrast): refreshColors
chose text colors from the stored IsDarkTheme() flag (default true, and only
re-checked on a dark/light toggle) while the terminal surface used IsLightTheme(),
so dark-theme pale text landed on the light surface. Tie both to the same live
background-luminance predicate and refresh on any theme (schema generation) change.
Light themes now derive each channel from that theme's own palette (Primary,
OnSurface, Error, Secondary, ...) nudged to a WCAG contrast floor — on-theme AND
high-contrast (measured 4.6-17.8:1 across the five light skins).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the miner (DRG-XMRig) updater to the same design as the daemon updater:
two-pane layout with tactile version cards (text label + right-aligned icon
badge: installed check / pre-release flask / newest new-releases, hover-only
tooltips) on the left, and the selected version's detail on the right — a unified
rounded info surface holding the version header + markdown-rendered release notes,
with the verify note and a centered, text-fit install button below it, and a
larger Close in the footer.
Fetches the full release list up front; Downloading/Done/Failed render in the
right pane. Preserves the miner specifics: installs are refused while the miner
is running (button disabled + a stop-mining note), and the installed tag is
persisted via setXmrigVersion on success. Adds a modal-xmrig-update sweep surface
(fake releases). Verified 100% + 150%, dark + light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The status-icon tooltip was gated on (hov || sel), so the newest version — which
is selected by default — kept its "Newest available version" tooltip pinned on
screen. Show it only while the mouse is actually over the icon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring back the small status label next to the tag (installed / pre-release /
latest), colored to match, alongside the right-aligned icon badge — both keyed to
the same status. The label is bounded so it never overruns the icon.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Fix the info surface padding: a borderless BeginChild ignores WindowPadding
unless ImGuiChildFlags_AlwaysUseWindowPadding is set, so the content was
touching the surface edges. Add the flag + generous padding, and make the
surface a touch more visible so it reads as a container.
- Version list: replace the inline text badges with a single right-aligned icon
badge per item — check (installed/active), science flask (pre-release), or
new-releases (newest available) — each with a tooltip.
- Center the Download & install button (still sized to its text).
Verified 100% + 150%, dark + light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Design pass on the two-pane node updater:
- Taller card (a generous share of the window) so release notes get vertical
room and usually don't need to scroll.
- Wrap the version header + release notes in ONE rounded Material surface (drop
the divider between them); the notes markdown gets inner padding so it doesn't
touch the edges, and the header stays pinned while long notes scroll within.
- The verify note + install button now sit OUTSIDE that surface; the install
button is sized to its text instead of full width.
- Version cards: slightly smaller radius, accent bar removed, and the currently
installed (active) node is marked with a check on the right of its card.
- Footer: dropped the divider above Close and enlarged the Close button.
Verified 100% + 150%, dark + light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The overlay content child had NoScrollbar (hides the scrollbar) but not
NoScrollWithMouse — so when a fixed-height card's content overflowed by even a
few px, the mouse wheel silently drifted the ENTIRE dialog (title + panes +
footer). Add NoScrollWithMouse to the content child: a modal is a fixed frame;
inner scroll regions (version list, release notes, command list) still scroll on
their own, and auto-height cards resize to content so they never overflow.
Also fix the daemon updater's footer budget, which under-counted the
Spacing/Separator/Spacing + inter-item gaps and produced exactly that few-px
overflow. Verified daemon / console / wallets / portfolio render intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Refine the two-pane node updater:
- Release notes now render as in-app markdown (new src/ui/material/markdown.h):
# / ## / ### headings, **bold**, `inline code` (tinted + subtle pill), and
- / * / + bullets, word-wrapped with mixed fonts via the draw list. cleanNotes
keeps the markdown now (only the trailing checksum table is cut), so the
renderer styles it.
- Each version is a tactile Material card — filled rounded surface with
hover/press/selected states (primary-tinted + outlined when selected), a
rounded status accent bar, tag + latest/installed/pre-release badges + date —
instead of a plain Selectable row.
- Dropped the thin 1px outlines: the version list is borderless and the notes
sit on a filled, rounded Material surface.
The sweep's fake release body gains bold/code/multi-heading markdown to exercise
the renderer. Verified 100% + 150%, dark + light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Redesign the Update Node dialog to mirror the Manage-Portfolio editor: the list
of node versions on the LEFT (with latest / installed / pre-release badges +
date), the selected version's detail on the RIGHT — status vs installed, release
notes (checksum table + markdown heading markers stripped), the verify/downgrade
note, and the install action (Download & install / Install this version /
Reinstall). One Close in the footer.
The dialog now fetches the full release list up front (startListReleases) so the
picker is populated immediately; Downloading/Verifying/Done/Failed render in the
right pane with the list still visible. Layout is GetContentRegionAvail-based so
it adapts to a viewport-capped card (verified 100% + 150%, dark + light).
Adds a sweep seam (sweepSeed/sweepClose) + a modal-daemon-update sweep surface
seeded with fake releases for offline visual verification. Adversarially
reviewed (3 lenses); fixed the real findings — live list was never populated
from getReleases() (only the sweep seeded it), defensive s_rows bounds in the
detail pane, notes box dropped when the pane is too short so the install button
stays visible, and a badge-overrun guard for long tags. DPI "findings" were the
recurring false positives (LegacySize/spacing/GetFrameHeight are all dp-scaled).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
"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>