Design pass on the Import Private/Viewing Key dialog:
- Group the optional scan-from-height section (label, slider, field, hint) in a
subtle glass panel via a draw-list splitter so it reads as one distinct,
clearly-optional block.
- Replace the boxed reveal / Paste / Clear buttons with frameless Material icon
buttons: the eye toggle sits flush at the Key field's right edge (rounded to
match the input frame), Paste / Clear are faint icon pills with tooltips.
- Add a centered %-of-chain label to the slider (mirrors the send-tab amount bar).
New: hovering Paste peeks at the clipboard and verifies it before pasting —
a tooltip classifies the clipboard the same way the field indicator does
(✓ valid type for this dialog / ⚠ a key for the other button / ✗ not a key),
with a preview line masked with '*' unless the key is currently revealed (then a
truncated peek). Empty clipboard shows "Clipboard is empty".
i18n: added paste_clip_empty (en + 8 langs); the preview reuses the field
indicator's type / wrong-type keys.
Reviewed via three adversarial multi-agent passes (splitter/panel, icon buttons,
slider math, and the paste preview's correctness/security + i18n); the confirmed
findings (eye hover-pill rounding, unrecognized-state color) are addressed here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lay the optional scan-from-height control out vertically: the send-tab-style
slider spans the full row on top, with the numeric field full-width directly
beneath it (both were previously side by side). Reads more clearly and gives
the slider room; the unsynced fallback (field only) is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The optional "scan from block height" control in the Import Private/Viewing
Key dialog now leads with a slider styled like the send-tab amount bar
(rounded track, accent fill, glass thumb) bounded by the chain tip, with the
numeric field beside it for exact entry and the current chain height
right-aligned above. When the node's height is unknown (state_.sync.blocks == 0,
e.g. not yet synced) the slider is omitted and the field spans the full row —
which also fixes the previous cramped 140px field that left the row half empty.
- New static helpers in app.cpp: formatIntWithCommas() and RenderScanHeightControl()
(mirrors RenderAmountBar in send_tab.cpp). The height decimal string is the single
source of truth shared by slider + field; the slider reserves/queries its
InvisibleButton before drawing so a drag has no one-frame lag; the bar height is
ImGui::GetFrameHeight() so it aligns with the numeric field.
- Import handler clamps the parsed start height to the chain tip.
- i18n: added import_scan_tip ("current height") in en + 8 langs.
Reviewed via an adversarial multi-agent pass (slider-math / state-sync /
dpi-layout / consistency); the one confirmed finding (bar/field height mismatch)
is fixed here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the key/viewing-key button split:
- Rename the "Import Key…" button to "Import Private Key…" (settings_import_key
value in en + all 8 langs, reusing the existing import_key_title wording) so it
reads as the natural counterpart to "Import Viewing Key…". The dialog title was
already "Import Private Key". The viewing-key wrong-type redirect hint now names
the renamed button.
- Show the "scan from block height" field in the private-key (spending) dialog too,
not just the viewing-key one. z_importkey accepts a start height, so a shielded
spend import of a recent key needn't rescan the whole chain. Transparent WIF
(importprivkey) has no such param, so once a transparent key is recognized the
field is disabled, its buffer cleared, and a "Transparent keys always rescan
fully" note replaces the hint. importPrivateKey already gates the start-height
append on (viewing || shielded), so transparent imports never receive it.
- i18n: shared scan keys renamed import_viewkey_scan_{label,hint} ->
import_scan_{label,hint}; added import_scan_transparent (en + 8 langs).
Reviewed via an adversarial multi-agent pass (rpc-routing / i18n / ui-logic /
consistency); only two nits surfaced (stale disabled-field value + a stale
comment), both fixed here.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backup & Data now has two distinct import actions instead of one
auto-detecting dialog:
- "Import Key" imports a spending key (transparent WIF or shielded
z-spending key) with the strong "grants access to funds" warning.
- "Import Viewing Key" imports a watch-only shielded viewing key
(zxviews…) with a milder eye/watch-only note and an optional
"scan from block height" field — z_importviewingkey accepts a start
height, so watching a recent address needn't rescan the whole chain.
The shared Material dialog (renderImportKeyDialog) branches on
import_view_mode_: title, warning tone, field label, the live type
indicator, and the recognition guard. A key valid for the *other*
button (e.g. a spending key pasted into the viewing-key dialog) now
shows a redirect hint rather than a generic "unrecognized" error.
importPrivateKey gains a startHeight arg, appended to the shielded
import RPCs (z_importviewingkey / z_importkey) and ignored for
transparent WIF (importprivkey has no start-height param). The
scan-height buffer is wiped on open/close alongside the key buffer.
i18n: 9 new keys (button label + tooltip, viewing title/note/field,
scan label/hint, two wrong-type redirect hints) with translations for
all 8 languages; CJK subset rebuilt (+1 glyph). A modal-import-viewkey
sweep surface is added for the viewing-mode dialog.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the full-node Import Key dialog into a safer, Material-consistent, more
capable flow:
- Security: the key is masked by default (Password) with a reveal (eye) toggle,
a DialogWarningHeader "only import a key you own" note, and the buffer is
wiped with sodium_memzero on close and on successful import.
- Progress: import triggers a blocking full-chain rescan; the dialog now shows
"Importing & rescanning — this can take several minutes" with LoadingDots and
disables Import while it runs (no double-submit). Offline shows a "connect a
running node" note and disables Import up front.
- Viewing keys: the classifier gains isViewingKey / isRecognizedImportKey;
importPrivateKey auto-detects a shielded viewing key (zxviews…) and routes to
z_importviewingkey (watch-only) vs z_importkey / importprivkey. The live type
indicator shows Transparent / Shielded spending / Shielded viewing (watch-only).
- Result: on success the imported address is captured from the RPC result and
shown via AddressCopyField.
- Material restyle (OverlayDialogSpec/BlurFloat + TactileButton + Esc) and full
i18n (12 new keys x 8 languages; CJK subset rebuilt).
Verified: rendered on dark + light skins; a seeded zxviews… key shows the masked
field + "Shielded viewing key (watch-only)" indicator.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two issues from the audit:
- Cursor: hovering the modal showed the console's text-select cursor. The
output's mouse-interaction guard only suppressed itself under ImGui popups,
but this modal is a BeginOverlayDialog (not a popup), so the console kept
setting the text cursor underneath. Suppress output interaction while the
reference modal is open (!show_commands_popup_).
- Design: the two panes used hard 1px child borders and floated unpolished.
Restyle to match the portfolio/wallets modals — each pane now sits on a soft
DrawGlassPanel surface (borderless child + inner padding), category headers
are subtle accent labels (no heavy filled bars), and the selected/hovered row
gets a rounded Material fill instead of the sharp Selectable highlight. The
glass panes are theme-adaptive (light on light skins), so the command-name
blue and the mono example green are floored to readable contrast (FloorLight)
and the detail heading uses theme Primary().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The builder's "omit trailing optionals" loop stopped at the FIRST empty
optional, which discarded any later field the user had filled — e.g. setban
with bantime empty but absolute=true built `setban "ip" "add"`, dropping the
typed `true`, and (worse) left Insert & run enabled so it ran a command that
ignored the input.
Include fields up to the last one that is filled or required; only genuinely
trailing empty optionals are dropped. A gap (an empty field before a later
filled one) keeps its placeholder and marks the command incomplete, so Insert
& run disables and nothing typed is lost. Found by an adversarial pass over all
82 param templates.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The detail pane's Parameters section is now an editable form: each parameter is
a type-hinted input field (string / number / json, optional marked), and a live
"Builds" preview shows the command being assembled. String values are
auto-quoted; a required field left empty keeps its placeholder and disables
"Insert & run"; trailing empty optionals are omitted.
Insert and Insert & run now use the assembled command (not just the template),
so Insert & run works for parameterized commands once the required fields are
filled — destructive commands still confirm first. Param fields reset when the
selected command changes or the modal opens. Keyboard nav (Up/Down/Enter) is
scoped to the search box so typing in a field doesn't move the selection.
A best-effort parseParamSpecs turns the human-readable param templates into
fields; JSON-array params (z_sendmany, createrawtransaction) surface as a single
"json" field the user pastes into. Adds the console_ref_builds label + 8
translations.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add plain-language details, a concrete example, and intent keywords to the
50 commands that Phase 1 left with only the terse description — so every entry
in the command explorer now explains itself and is searchable by task, not just
by name. Destructive flags are preserved.
Content was authored per-category and reviewed for technical accuracy (correct
RPC semantics, valid console example syntax, realistic DragonX placeholders).
Data-only change; command docs stay English (technical), consistent with the
existing policy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the flat, terse command-reference table with a guided two-pane
explorer built for users who don't know the RPC commands:
- Search by name OR task: keyword synonyms let "balance" find getbalance,
"send money" find sendtoaddress; results are ranked (name-prefix > name >
keyword > desc).
- Two panes (mirrors the portfolio editor): a category-grouped master list
(mono names, a warning dot on consequential commands) + a rich detail pane
with a plain-language explanation, a per-parameter type breakdown (string /
number / json, optional dimmed), and a concrete example.
- Insert into console (fills the input with a template to review + run) and,
for no-parameter commands, Insert & run — with a confirm for destructive
ones. Deferred via pending_submit_ so the modal needs no executor.
- Keyboard: type to filter, Up/Down to move, Enter to insert; auto-focus and
auto-select the top result.
Data model: ConsoleCommandEntry gains details/example/keywords/destructive
(C++17 defaults, so the ~60 un-enriched entries are untouched); ~20 common
commands enriched and 18 consequential ones flagged. Command docs stay English
(technical); the UI chrome + 7 category names are translated into all 8
languages (CJK subset rebuilt, +3 glyphs).
Verified: two-pane renders on dark + light skins and at font_scale 1.5; detail
pane shows explanation/params/example; danger dots + safety badge on
destructive commands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Clicking a command now actually closes the modal. It called
ImGui::CloseCurrentPopup(), a no-op here (BeginOverlayDialog is Begin/
BeginChild, not an ImGui popup), so the picker stayed open covering the input
and the insert looked like it did nothing. Set show_commands_popup_=false.
- Search filter is now a member cleared when the modal opens, so it isn't stale
on reopen after an outside-click / command-click dismiss (was reset only by
the Close button).
- Esc dismisses the modal (this overlay has no built-in Esc handling), and the
search box auto-focuses on open so the user can type immediately.
- Scale the 2-vs-3 column breakpoint (cmd-min-width) by dpiScale() — it was a
logical-px threshold compared against a physical-px available width.
- Give each category's table a unique ImGui id (##cmdsN) instead of sharing
"##cmds", and translate the 7 category names (Control/Network/... — command
descriptions stay English) into all 8 languages; CJK subset rebuilt (+2).
(Investigated but rejected: the command-name blue was flagged as low-contrast
on light skins, but the modal card is a dark glass panel on every theme, so the
original color is correct — verified by rendering marble/light/color-pop-light.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- D2: rpcTraceTimestamp used std::localtime (process-wide static tm) guarded by
a private mutex that can't stop another thread's localtime from clobbering the
shared buffer. Use localtime_r / localtime_s into a local tm (the codebase
pattern) and drop the now-useless mutex. Runs on RPC worker threads.
- C1: the toolbar's "<N> matches" label read filter_match_count_ before
renderOutput recomputed it, so it lagged one frame. Compute the visible/
filtered set once at the top of render() (before the toolbar) and reuse it in
renderOutput, so the count and the output share one consistent computation.
- C4: the "N new lines" indicator counted the raw drain count, inflating it with
lines the active filter hides. Count only newly-added lines that pass the
current filter, so the badge matches what the user sees on jumping to bottom.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The console zoom (s_console_zoom) was a plain static reset to 1.0 each launch,
unlike the sibling toggles. Wire it through Settings (console_zoom float),
restored into the static at startup in the App ctor and written back via the
existing post-render diff-and-save. Loading clamps to [0.25, 4.0] (also
catching NaN) so a corrupt value can't feed the scanline divisor.
Verified: seeding console_zoom=1.5 survives a relaunch and the console text
renders larger.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BuildConsoleRpcCall pushed any bare token that "looked numeric" as a JSON
number, and lost the tokenizer's quoting decision — so a genuinely-string
argument that happened to be all digits (e.g. a label) was sent with the wrong
type. std::stoll also silently truncated "1e999" to 1 (stops at the first
non-digit) and std::stod could yield inf/subnormal.
- ParseConsoleCommandArgsTagged returns {text, quoted}; quoted tokens are sent
verbatim as JSON strings, never coerced. (ParseConsoleCommandArgs kept as a
text-only wrapper so existing callers/tests are unchanged.)
- Bare tokens become a number only when the WHOLE token parses to a finite
int/double; otherwise they're sent as a string.
Adds test coverage for quoted-numeric-string, bare-number, 1e999, and 123abc.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
From the console-tab audit:
- HiDPI: the "New output" pill (140x24 + margin/rounding/border) and the JSON
indent-guide thickness were hand-drawn in raw px; scale them by dpiScale().
- Fold triangle: size it from the DPI/density-scaled gutter width (not the
zoomed font) and center it in the gutter band so glyph and clickable cell
stay aligned; only draw/handle it in the unfiltered view (folding_active_) —
in a filtered flat view a click silently flipped the collapsed flag with no
visible effect and the glyph disagreed with the rendered block.
- Selection: ignore left-clicks in the gutter (< output_origin_.x) so toggling
a fold no longer clears the user's active text selection.
- 'stop' confirmation: promote the function-local static to a member and reset
it in clear(), so a toolbar/context-menu clear between the two 'stop's can't
leave a stale arm that skips the shutdown warning.
- RPC-trace callback: dereference the console pointer under the mutex (not
after releasing it) so ~ConsoleTab can't destroy the object mid-call —
closing a latent use-after-free on shutdown (fires on RPC worker threads).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add "Color accents" and "Text colors" checkboxes under the effects section
(both layout variants) so the console output display prefs are discoverable
outside the console toolbar. They mirror the toolbar buttons — bound live to
the ConsoleTab statics and written through to settings on change.
Unlike scanline/theme-effects they carry no GPU cost, so a small helper closes
the effects row's BeginDisabled(low_spec), draws them on their own always-
enabled row, and reopens it for the acrylic sliders (net-balanced). Adds
console_accents / console_text_colors labels + 8 translations; the tooltips
reuse the existing toggle keys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Persist the two console output display preferences and add a second toolbar
toggle for monochrome text.
- Persistence: new Settings fields console_line_accents / console_text_color
(both default true, matching the ConsoleTab statics so an upgrade re-save
can't flip visible behavior). Restored into the statics at startup in the App
constructor, and saved from a diff-and-save after the console renders (only
when a value actually changed — save() is disk I/O). The startup restore also
fixes a pre-existing bug: scanline was only synced from settings when the
Settings page or first-run wizard ran, so an existing wallet ignored a saved
scanline preference on launch.
- Monochrome text: a new toolbar toggle (FORMAT_COLOR_TEXT, dimmed when off)
next to the accent toggle. channelTextColor() early-returns the neutral
COLOR_RESULT for every channel (and JSON syntax role) when off, leaving the
left accent bars independent. COLOR_RESULT is contrast-floored per theme, so
text stays readable on light and dark terminals.
Adds console_toggle_text_color to the English source + all 8 translations.
Verified: both toggles independent, persistence round-trips across restart,
monochrome readable on light + dark skins.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a paint-bucket toggle button to the console toolbar (in the appearance
cluster next to zoom) that shows/hides the per-line left color accent bars for
a cleaner monochrome gutter. The icon reflects state (FORMAT_COLOR_FILL when on,
a dimmed FORMAT_COLOR_RESET when off) with a tooltip. Session-only, like the
console zoom factor; default on.
Adds console_toggle_accents to the English source and all eight translations
(no new CJK glyphs — the subset font already covers them).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hovering the disabled Save button now shows which requirement is missing
(name / at least one address / a manual price above 0), mirroring the
send-tab disabled-submit tooltip idiom (IsItemHovered(AllowWhenDisabled)).
The hint only appears for a validation gap — not the self-evident "nothing
changed" case.
Adds portfolio_save_need_{name,address,price} to the English source and all
eight translations; rebuilds the CJK subset font for the new JA/KO/ZH glyphs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The address context menu's "Add to portfolio" submenu built each item with
MenuItem(label), whose ImGui ID is derived from the label — so two groups with
the same label (or two empty labels) collided, routing a click to the wrong
group, and a blank label rendered as an invisible row.
- PushID(pi) per item gives a stable unique ID regardless of label, and an
empty label now shows the "new entry" placeholder.
- Filter the list to the active wallet's groups (or legacy/unscoped), matching
the Market summary and editor, so an address can't be added to a different
wallet's group. The "no entries" placeholder now also covers the case where
every group is filtered out.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Market summary filters custom groups by wallet scope, but the editor did
not: its master list showed and edited *every* wallet's groups, and "Manage…"
opened raw storage index 0 — which could belong to a different wallet.
Separately, a group could be persisted with an empty scope (a placeholder
added before the wallet identity resolved, then filled in and saved), which the
filter treats as "legacy" and shows in *every* wallet's list.
- The editor master list now filters to the active wallet's groups (plus
legacy/unscoped), mirroring the summary. Storage indices are preserved so
selection/delete still target the right entry.
- "Manage…" opens the first in-scope group (or the empty state), never index 0.
- The selection clamp resets to the first visible group if the current one is
out of range or out of scope.
- On commit, a group with no scope is claimed for the active wallet when an
identity is available, so it can't linger in the global bucket.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Close button auto-committed the current group, but Escape, outside-click,
switching groups, adding a group, and deleting another group all *discarded*
uncommitted edits — so whether your work was kept depended on how you happened
to leave, and the prominent unsaved-dot / Save / Revert affordances implied a
save model the exits didn't honor.
Align every dismiss/switch gesture with the Close button's intent (auto-save):
- Escape and outside-click now commit before closing (outside-click is caught
right after BeginOverlayDialog, which clears open mid-frame).
- Switching to another group, "Add entry", and deleting a *different* group
commit the current group first. Deleting the selected group still discards
its own working state.
Save remains "persist now", Revert "undo to last saved", and the dot marks an
in-progress group. Ordering is safe against the placeholder GC: that GC runs
earlier in the frame (mktDrawPortfolio, before RenderPortfolioEditor) and only
while the editor is closed, so it never shifts entry indices under a commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial re-audit of the group editor surfaced follow-ups to the prior
hardening pass:
- Dirty-check regression: the empty-currency->"USD" coercion was applied on
save but not in the dirty comparison, so clearing the currency field left a
group perpetually "dirty" (unsaved dot + Revert/Save never settling, a
redundant persist on every Close). Both sides now share pfEditLabel() /
pfEditCurrency() so they can't disagree — this also trims the label, so a
trailing space no longer wedges the dirty state.
- Whitespace-only labels (" ") passed validation and persisted as an
invisible group; the trimmed label is now required by pfWorkingValid.
- "Add entry" persists an empty placeholder up front; abandoning it (Close/
Esc/outside-click without adding an address) left a phantom "$0 · 0" group.
The summary now prunes address-less groups while the editor is closed.
- Manual price accepted inf/NaN (neither is < 0); sanitize with std::isfinite.
- Escape closed the whole editor even when it was only meant to dismiss the
open custom-color popup, discarding edits; guard on !IsPopupOpen.
- HiDPI: scale the Z/T chip pill's vertical padding and the balance-breakdown
baseline nudge that the prior pass missed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Portfolio audit fixes:
- Guard the BTC value line behind price_btc > 0 so an empty BTC price no
longer renders a misleading "≈ 0.00000000 BTC".
- Validate the working group before commit/save (pfWorkingValid: needs a
label, at least one address, and a positive manual price when the manual
price basis is selected); the Save button disables to match, so a
half-filled editor no longer persists a broken group on close/switch.
- Default an empty manual currency to "USD" instead of an empty string.
- Reload the editor after clamping a stale selection index so it can't
point past the end of the group list.
- Scale hand-drawn geometry by dpiScale(): address-row rounding, chip
y-offset, preview border, chart toggle buttons, and the shielded/
transparent ratio-bar corner radii now render correctly on HiDPI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-exchange chart used a single buffer keyed by the loaded pair, so
bouncing between venues (Ourbit <-> NonKYC) re-fetched every switch (loading
spinner each time). Add a per-pair cache: each venue's fetched candles (OHLC +
derived close series) are kept keyed by "<identifier>:<BASE>/<QUOTE>". Switching
back to a recently-viewed pair loads from the cache instantly — no fetch, no
spinner — while entries older than 30 min re-fetch to stay fresh. Bounded to 16
entries (irrelevant for DRGX's 2 venues, but safe if it lists more). Cache is
main-thread-only (populated by the worker's completion MainCb), so no races.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Raise the Windows window-opacity default (0.75 -> 0.90) so less of the
(often dark) desktop bleeds through the wallpaper behind the panels.
Mac/Linux stay fully opaque. Only affects fresh installs / unset values.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bump the hardcoded modal blur radius (96 -> 120) for a softer, more
recognizable frost behind dialogs. Still independent of the acrylic slider.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The chart interval (Live/1H/1D/1W/1M) and the line/candle style were
session-only statics. Persist both like the selected exchange/pair: new
chart_interval / chart_style settings (defaults 1M / candlestick),
loaded into the market view on first show and saved on each interval
click / style toggle.
Verified: writing chart_interval=2, chart_style=0 to settings.json and
launching restores them (not reset to defaults) and re-saves them intact.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Add a chart-style toggle (line vs candlestick) next to the interval buttons,
shown only when the selected range has per-exchange candles (the aggregate /
Live view is line-only). It flips s_mkt.chartStyle; candles draw when OHLC
exists AND the user hasn't switched to the line. Icon reflects the current
style; tooltip says what a click switches to. Two i18n keys across 8 languages.
- Make the price chart ~50% taller: scale the height floor / desired / viewport
cap (110->165, x1.5 desired, 0.22->0.33 of available) for a roomier plot.
Verified via a forced-OHLC render: the taller chart, the toggle button, and
candles with a correct week/day x-axis.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Candles had no hover (the line's close tooltip is gated off for them). Add a
candle-aware readout: the candle under the cursor gets a column highlight +
crosshair, and a small box shows its date and open/high/low/close (colored green
up / red down). Verified via a forced-hover render (date + O/H/L/C over the
highlighted candle). Line charts keep their existing single-price tooltip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The exchange candle APIs return full OHLC but we only kept the close (a line).
Now the per-exchange chart draws real candlesticks; the CoinGecko aggregate stays
a line (it's close-only).
- Adapter keeps OHLC: parseExchangeOHLC() returns open/high/low/close candles
(parseExchangeCandles is now a close-only wrapper over it). New data/candle.h
holds the dependency-free Candle + bucketOHLC (5-min -> hourly for the 1D view).
- Model stores exchange_ohlc_intraday/daily alongside the close series;
refreshExchangeChart populates both. market_series::chartCandles() returns the
bucketed OHLC for the range, empty unless the per-exchange series is active.
- The chart renders wick (low..high) + body (open..close), green up / red down,
with a low..high y-range; the line-only bits (fill, hi/lo labels, hover tooltip)
are gated off for candles. Falls back to the line for the aggregate / Live view.
Verified: OHLC parsers + bucketOHLC + chartCandles unit-tested; a forced-state
render shows correct candlesticks; the aggregate still draws a clean line.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The hero's "24H Vol" showed CoinGecko's cross-exchange aggregate even after the
chart/price went per-venue. Capture the per-exchange converted_volume.usd from
the tickers (previously discarded, alongside converted_last) and show the
SELECTED exchange's own 24h volume; fall back to the aggregate when unknown.
Market cap stays aggregate (it's coin-wide, not per-venue).
This is a big real difference — e.g. Ourbit ~$14.3K vs NonKYC ~$253 for DRGX/USDT
— so the header now reflects the venue you're actually looking at.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switching exchange pairs (or first load) briefly leaves the chart with no series
while the venue's candles fetch, which showed the bare "No price history
available" empty state. When a chart fetch is in flight — App::isMarketChartLoading()
(the CoinGecko aggregate OR the per-exchange fetch) — draw a spinner + animated
"Loading price history…" in the plot area instead; the "no history" text only
shows when genuinely empty and idle. New i18n key market_chart_loading across all
8 languages. Verified via a forced-state render (spinner + label centered).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The market chart was hardwired to CoinGecko's cross-exchange USD aggregate
(vs_currency=usd), so selecting a trading pair only changed the "Trade" link —
never the chart or price. Now the SELECTED exchange drives both.
CoinGecko gives us which venues list DRGX (the ticker `market.identifier`) but
not their APIs, so add data/exchange_candles.h: a hand-maintained map from that
identifier to each venue's public candle endpoint, with two adapters verified
against the live APIs — Ourbit (MEXC-style /api/v3/klines, array format) and
NonKYC (TradingView-UDF /market/candles, `bars`). Unmapped venues return no URL,
so the chart falls back to the CoinGecko aggregate and nothing regresses.
- App::refreshExchangeChart() resolves the selected pair, fetches its intraday
(5-min) + daily candles via the TLS-verified httpGetString on the worker, and
stores them on MarketInfo (exchange_chart_intraday/daily + exchange_chart_active).
Re-fetches on pair change; self-throttled to ~30 min otherwise; falls back to
aggregate on any failure.
- chartSeries() draws the per-exchange series when active (portfolio sparklines
stay on the aggregate). The hero shows the selected venue's real price
(converted_last) — Ourbit vs NonKYC differ ~7%. parseCoinGeckoTickers now
captures the identifier + per-exchange USD price (previously discarded).
Adapters unit-tested against real captured responses (URL builder + both parsers
+ the chartSeries switch) and validated against the full live feeds (Ourbit 20d,
NonKYC 370d, ascending, correct closes). Build + ctest + hygiene green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The modal backdrop blur was params.blurRadius (96) scaled by the user's global
acrylic blur-strength slider (blurRadiusMultiplier), so lowering the slider also
weakened every modal's backdrop. Make the modal blur a fixed hardcoded value.
Add AcrylicParams::absoluteBlurRadius: when set, applyBlur() uses the radius
as-is and skips the multiplier (threaded through both the GL and DX11 blur paths
+ the no-op stub). DrawFullWindowBlurBackdrop sets it, so the modal backdrop is
always a 96px blur regardless of the slider. Panels/other acrylic still scale
with the slider as before.
Verified: rendering a modal at blur_multiplier 0.1 vs 2.0 now produces an
identical backdrop (max pixel diff 1/255) — previously those were ~9.6px vs
~192px of blur.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With the list now pinned at max height, a few wallets leave blank space below
the cards. Fill it — only when there's real room to spare — with a subtle
centered folder glyph + "Scan a folder to find more wallets" (OnSurfaceDisabled),
so the area reads as a gentle nudge toward the scan action rather than dead
space. Purely decorative (draw-list only, clipped to the list); the actions stay
below. New i18n key wallets_empty_hint, translated across all 8 languages.
Verified on the sweep: the hint centers in the gap and reads subtly on both dark
and light themes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two wallets-modal changes:
- The wallet list now always renders at its max height (kMaxVisibleRows) instead
of shrinking to the wallet count, so the modal is a consistent size whether you
have one wallet or many — fewer rows leave empty space, more than 7 scroll.
- Add a "Scanned folders" manager: each user-added scan folder is listed with a
control to stop scanning it (removeExtraFolder + save + re-scan so its wallets
drop out). Paths front-elide to keep the identifying leaf visible, with a
full-path tooltip. The card-height math reserves the manager's rows so nothing
clips. Two new i18n keys (wallets_scanned_folders / wallets_remove_folder),
translated across all 8 languages; CJK subset rebuilt.
Verified on the sweep at 1.0 and 1.5x DPI: max-height list on both the few- and
many-wallet surfaces, the folder manager renders and stays clear of the footer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first-run wizard predated the i18n system — every label, button, and help
line was hardcoded English. Route them all through TR():
- 62 literals in app_wizard.cpp replaced with TR(). Where a wizard string is a
parallel of existing content, it reuses the existing key (the whole bootstrap_*
family, plus theme / language / balance_layout / low_spec_mode / ui_opacity /
console_scanline / download / retry / cancel) so it inherits their translations
and stays consistent with Settings. Button labels drop their ##id suffix and use
TR(key) directly (the app's established TactileButton(TR(...)) pattern; the
wizard's phase buttons never share a frame, so no id collision).
- 55 new wiz_* English keys in i18n.cpp for the wizard-specific strings, and their
translations back-filled across all 8 languages (es/de/fr/pt/ru/zh/ja/ko);
format specifiers (%s, %zu) and newlines preserved. CJK subset rebuilt.
Verified on the sweep: English renders with correct labels in place (all keys
resolve), and a German run shows the reused + new keys translated end-to-end
(Darstellung / Acryl-Glaseffekte / Verschlüsseln & Weiter …).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first-run wizard predated the modal-unification pass and was the least
on-brand surface left — a flat Surface() slab with a 3-card masonry whose
inactive steps were heavily greyed (alpha 165) and whose focused card used a
hard 2px border + a hand-drawn offset shadow.
Keeping the working phase logic and the "see all steps at once" masonry:
- Reveal the skin backdrop: the app already paints its marble / gradient /
acrylic backdrop behind every window (drawWindowBackdrop), so drop the opaque
Surface() fill (transparent WindowBg) and lay a gentle theme-tinted scrim over
it. The wizard now sits on the same rich backdrop as the rest of the app.
- Focused card lifts with the shared DrawCardDropShadow (the uniform, non-
clipping card shadow) instead of a bespoke offset rect, and a softer 1.5px
accent ring.
- Inactive cards get lighter veils (upcoming 165->115, completed 110->70) so
they read as "waiting" / "done" rather than disabled-grey.
- Warmer header: a one-line subtitle under the welcome title.
Verified on the headless sweep across all four phases (appearance / bootstrap /
encrypt / pin) on marble + obsidian at 1.0 and 1.5x DPI — backdrop shows, cards
float cleanly, geometry scales, nothing clipped.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Material --error reds are muted by design, so error text drawn with the
theme Error() color read low-contrast on the (now deeper) dark modal backdrops —
the audit flagged the seed-migration warning/error lines on dark / dragonx.
Add material::ReadableError(): the theme error lifted toward a legible light red
on dark themes, unchanged on light themes (where it's already a dark red that
reads on a light background). Apply it to the modal error-TEXT sites (lite
first-run, seed backup, seed migration). This is targeted — it never touches the
app-wide Error() used by toasts / badges / status dots — so nothing else shifts.
Verified on the sweep: the "node too old" warning (dark) and "daemon did not
respond" error (dragonx) now read clearly; light themes are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Modal success/status lines (import, backup, seed backup, seed migration) used a
hardcoded ImVec4(0.3, 0.8, 0.3) green that ignores the theme — fine on dark, but
faint on the light skins, where the audit flagged it as low-contrast. Switch
those 8 sites to ui::material::SuccessVec4(), so light themes get their tuned
dark green (#3D7A42 etc., readable on white) and dark themes their light green
(#81C784). The 2 status-bar mining indicators keep the vivid attention-green.
Verified on the sweep: the "already has a seed phrase" / "migration complete"
greens are now clearly readable on light, marble, and color-pop-light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The modal audit found card-less (floating) dialogs read slightly unanchored on
the light skins: the blur backdrop's dim is a theme-tinted frost, which darkens
on dark themes (content pops) but WHITENS on light themes — so sparse content
floated in a near-white expanse with no figure/ground separation.
- Light themes now get a neutral dark veil instead of the whitening frost, which
darkens the surround and makes the modal's fields/buttons stand out.
- Dark themes keep the theme-tinted frost, a touch stronger (alpha 70 -> 90).
- Full-window blur deepened (64 -> 96) so the backdrop reads clearly frosted.
Verified on the headless sweep across light + dark: backup / debug-gate now
read as anchored groups; content-heavy modals (daemon-update, settings, wallets)
are unaffected (their own surfaces stand out), and dark-skin text contrast
improves for free from the deeper dim.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A wallet reached via two different path strings — e.g. a symlinked mount
(/mnt/usb) and its real target (/media/usb) — used to hash to two different
in-place link names and list as two rows, so the same file looked like two
wallets with two datadir links.
Give WalletRow a canonPath (fs::canonical, symlinks resolved, computed once at
scan time so the every-frame render/active-marking path stays cheap) and key
both the link name and the row de-dup on it. Now the same physical wallet maps
to one identity regardless of the path used to reach it: one link name (honors
the "distinct per-target name" invariant), and scan() lists it once — datadir
first, so a datadir wallet wins over the same file seen via an external path.
The link also points straight at the resolved real file instead of through the
symlinked path. Genuinely distinct wallets keep distinct identities (verified
with a standalone symlinked-mount harness).
Closes the last deferred finding from the open-in-place adversarial review.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On Unix a symlink is the better datadir link than a hard link: it needs no
privilege, spans volumes (a wallet on a USB / other partition just works,
where a hard link can't), and is visibly a pointer rather than an
indistinguishable second copy. Windows keeps hard-link-first (a symlink there
needs admin / Developer Mode), falling back to a symlink.
Symlink-first made two latent issues in the pre-clear reachable, fixed here
after an adversarial review:
- Dangling prior symlink: fs::exists follows the link (false when the source
moved), so it was skipped and create_symlink then tripped on the occupied
path. Catch it with fs::is_symlink (lstat) and replace it.
- Stale reserved-name occupant: the old "hard_link_count == 1 → reuse" path
could load the WRONG wallet — a count-1 file at our name is not a valid link
to the source (a live hard link has count >= 2), it's a stale orphan (source
deleted, or a copy left by a datadir migrated across filesystems). Gate reuse
on fs::equivalent(link, src) (same inode) instead, and reclaim the name only
when the occupant is provably redundant (another hard link still holds the
data); otherwise refuse rather than risk destroying something unique. So
hard_link_count is no longer trusted to prove identity — only data-safety.
Also guard against a stale row (source moved/deleted between the scan and the
click): !exists(src) refreshes the list instead of linking a ghost the daemon
would silently swap for wallet.dat. Verified with a standalone harness across
fresh / re-open / valid-hardlink-reuse / stale-orphan / redundant-occupant /
vanished-source cases.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the "Import" action — which copied an out-of-datadir wallet into the
datadir and cluttered the list with hard-to-tell-apart duplicates — with an
in-place "Open" that links the real file into the datadir under a stable
per-target name (wallet-ip-<FNV8>.dat) and switches to it. The daemon only
loads a bare filename from its own datadir, so a link is the minimal bridge:
hard link first (no privileges, same volume — covers non-admin Windows),
symlink fallback (cross-volume), then an error. Never a copy (that would fork
the wallet) and never a delete of a real file.
Also:
- show each external wallet's originating sub-directory (…/Backups/2021)
- add a per-row "open folder location" button
- widen the modal (780 → 860) for the extra button
- guard the daemon against a dangling link (external file moved / USB
unplugged) by falling back to wallet.dat rather than creating an empty one
Per-target link names (not one shared name) make switching between two
external wallets a real -wallet switch, so the rescan/cache index tracks each
correctly. Drops the now-dead wallets_import* i18n keys and back-fills the new
open/folder strings across all 8 languages + rebuilds the CJK subset.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Prepend the containing folder (last 1-2 path components, e.g. "…/Backups/2021")
to an external wallet's metadata line, so several same-named wallet.dat files
surfaced by the recursive scan are distinguishable at a glance (the info-icon
tooltip still shows the full path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swap the rounded-rect StyledButton for material::IconButton (square size →
bgRounding = radius, so a circle) with the arrow glyph mathematically centered;
carries its own hover fill + asc/desc tooltip.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the sort dropdown with a material::SegmentedControl — Created / Addresses
/ Txs / Size — matching the Receive tab's address-type toggle, with the asc/desc
arrow beside it. Shorten the four sort labels to fit the segments (updated across
all eight languages + the back-fill script). The direction arrow is positioned
explicitly on the segment's baseline (the control is draw-list based and doesn't
advance the layout cursor).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make "is the active wallet" the primary sort key so the current wallet always
stays at the top regardless of the chosen sort/direction; the remaining wallets
sort by the selected key underneath it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Back-fill the 8 new keys (created label + the four sort options and asc/desc
tooltips) into all eight languages via add_missing_translations.py (+64 keys,
additive), and rebuild res/fonts/NotoSansCJK-Subset.ttf for the new glyphs (升 昇 렬).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Standalone "Never" in the wallet list's last-opened slot was ambiguous; make it
"Never opened" (and update all eight translations). Glyphs already covered by the
CJK subset, so no font rebuild needed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Back-fill the 10 new Wallets-dialog keys (metadata "keys"/"txs" columns and the
Encrypted/Seed/Legacy/Unknown badge labels + tooltips) into all eight languages
via scripts/add_missing_translations.py (additive; +80 keys, no reformatting), and
rebuild res/fonts/NotoSansCJK-Subset.ttf so the new zh/ja/ko glyphs (判 種 類 …)
render instead of tofu.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Self-contained HTML/CSS mockup of the Overview screen (Marble theme) used as a
pixel reference for tuning the in-app marble background, frosted cards, DragonX
glyph, and card drop shadow. Embeds the real res/img marble texture as a data URI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Rework DrawCardDropShadow (Light/Marble): a Gaussian-sampled ring stack
matching a `0 0 4px rgb(50 53 58 / 34a)` box-shadow — offset-free, darker,
and tighter than the old faint 14px black halo. Clip to the shadow's own
bounding box (card ± reach) instead of the child window bounds so top/bottom
shadows are no longer chopped where a card sits flush with its container,
while staying on the card's own draw list (correct z-order in modals/popups).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cards are drawn inside clipping child windows, so the shadow (which extends past
the card edge) was cut at the child boundary — most visibly a hard line on the
sidebar's inner edge, which also made the shadow look non-uniform. Widen the
shadow's clip horizontally (card + spread, clamped to the viewport) so the side
shadows reach into the gaps between panels, while keeping the vertical clip at the
child bounds so a card scrolled to a child's top/bottom edge can't bleed its
shadow into the header/footer. Verified: sidebar + settings cards now show an
even shadow on all four sides.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the downward offset so the soft shadow is equal on all four sides — an even
halo around each card rather than a directional cast, matching the requested
look. Slightly more rings for a smoother falloff. Still stroke-based (no body
contamination), Light/Marble only.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Cards on the pale Light / Marble backgrounds now lift off the surface with a
subtle drop shadow. Drawn in DrawGlassPanel (behind the card fill) as fading
rounded-rect STROKES offset down — strokes never paint across the card body, so
only the outer soft blur remains once the fill covers the interior (the
"render then mask out the UI area" result, no FBO needed). Gated to the Light and
Marble themes (cached per theme-generation); dark themes are unchanged. Verified
across marble / light (shadow present, smooth, body clean) and dark (none).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Now that panels frost at full alpha, the slider's old top end (multiplier 4.0)
was far too blurry. Cap the range at multiplier 1.25 (kAcrylicMaxBlur) — the
slider still reads 0–100%, but 100% now maps to a moderate frost instead of a
heavy wash. Snap-to-off threshold scales with the new range, and a value saved
under the old 0–4 range is clamped on load so it lands within 0–100%.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-panel acrylic composite drew the blurred capture at
`fallbackColor.w * uiOpacity` alpha. At a lowered UI opacity that's ~24%, so the
SHARP background bled straight through the panels — the acrylic slider changed
almost nothing (measured: blur 0% vs 100% differed in 0.33% of pixels, and the
marbled background was equally crisp at both).
Draw the blurred capture at (near) full strength so it actually FROSTS the panel
(the blurred background replaces the sharp one); UI opacity now scales only the
tint overlay in DrawGlassPanel, which is the correct acrylic model — opacity =
tint, not sharp-vs-blur. Fixed symmetrically in both the GL and DX11 drawRect
paths. Verified on GL: blur 0% now shows a sharp background, 100% a smooth
frosted wash (0.33% -> 8.3% pixels changed), and the default (high-opacity) card
look is unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Passing the gate once (confirmation + any re-auth) now unlocks the DEBUG OPTIONS
dropdown for the rest of the session — subsequent expands skip straight to the
options. The flag lives in the session-lifetime page state, so it resets on app
restart. Collapsing still needs no gate.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Revealing the DEBUG OPTIONS dropdown now opens a confirmation modal with a
warning instead of expanding directly. If the wallet is secured, it also requires
re-authentication before the options appear:
- a quick-unlock PIN → verified against the vault (Argon2id derive, off-thread,
no wallet side effects);
- otherwise an encrypted wallet → the passphrase, verified via walletpassphrase.
Unsecured wallets just confirm. Collapsing needs no gate; each expand re-gates.
App gains debugGateRequiresAuth() + verifyDebugCredential() (cb on the main
thread; secrets wiped). Adds a modal-debug-gate sweep surface. Verified 100% +
150%, dark + light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A pre-seed-phrase (legacy, non-mnemonic) wallet is exactly the one that benefits
from migrating — so nudge the user by glowing the Settings "Migrate to seed"
button with a soft pulsing accent halo.
Adds a cached wallet mnemonic status (probeWalletSeedStatus), classified once per
connect via z_exportmnemonic (the same signal the migration Intro pre-flight
uses): NoMnemonic = legacy → glow; HasMnemonic / Incapable (old daemon) / while
locked or on lite = no glow. Reset on disconnect so it re-probes after a wallet
switch or a post-migration adopt, with a small attempt cap to avoid re-probing on
a persistent transient error. The sweep forces the status so the glow is
captured (restored after). Verified dark + light.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
An ImGui auto-resize child reports a too-small height on its first render and
the true height a frame later, so centering on last frame's height made the card
visibly slide for ~2 frames as it converged. Hiding only the very first frame
wasn't enough — the next frame or two still moved.
Track per-dialog height stability: keep the card hidden (laid out + measured, but
clipped) until its measured height holds steady across two frames, then reveal it
already centered. A dialog re-arms on (re)open so each open reveals stable; once
shown it won't re-hide on a mid-dialog content change (just repositions); a
frame cap prevents an ever-changing height from hiding it forever. Fixed-height
dialogs are unaffected. Verified migrate / about / seed-backup / import-key
reveal centered at the settled frame.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Auto-height dialogs need one frame to measure their content height before they
can be centered. Previously that first frame was drawn near the top, so a
first-ever open flashed off-center for a frame before snapping to center.
On the measuring frame, lay the content out (so the child's auto-height is still
measured) but clip the whole card to a zero rect and skip the glass panel — the
card draws nothing while the blur backdrop still shows, then it appears already
centered on the next frame. Clip covers both ImGui widgets and hand-drawn
content; layout/measurement is unaffected (verified migrate / about / import-key
still render centered at the settled frame).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fixed-height overlay cards were already centered, but auto-height cards
hard-anchored at 15% from the top, so shorter dialogs (e.g. Migrate to a seed
wallet, Import Key, About) rendered top-heavy with a large empty area below.
Center auto-height cards on last frame's measured content height (already cached
per dialog id in g_overlayCardHeights). A re-opened dialog centers from frame 1;
only the very first open of a given dialog this session briefly anchors near the
top before it re-centers. Content taller than the window top-anchors at a 16px
margin. One framework change centers every modal (verified across migrate /
import-key / about / seed-backup at 100%).
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>
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>
The hardware [WINLOG] trace showed the DPI-transition target computed correctly
(min height 720) but the window stuck ~5% larger (759), with every deferred retry
failing to shrink it. Root cause: the DX11 window is a borderless native HWND
(WS_CAPTION/WS_THICKFRAME, for native move/resize) wrapped by SDL. SDL_SetWindowSize
converts the requested CLIENT size to a window rect via AdjustWindowRectEx (adding
the caption+border frame), but our WM_NCCALCSIZE handler folds that whole frame
back into the client area — so every SDL resize inflates the window by ~one frame
(~39px height, ~16px width) and the 720 minimum becomes an unreachable 759. That,
not the earlier min-ordering/settle logic, is why the height wouldn't stay at min.
resizeWindowExact() resizes the native HWND directly via SetWindowPos on the DX11
path (window rect == client for a borderless window → exact requested size); other
platforms keep SDL_SetWindowSize. Used for the DPI-transition resize and the
deferred retry, so the window now lands on the true minimum in both directions.
Keeps the [WINLOG] native-rect + min-track tracing so the next capture confirms the
fix (or surfaces any remaining gap).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The first hardware capture showed handleDisplayScaleChange computing the correct
target (e.g. 1353x720, height = min) but the window never reaching it across all
30 deferred retries — stuck ~5% above target in BOTH DPI directions (759 vs 720
at 100%, 1136 vs 1080 at 150%). That rules out the earlier min-ordering / settle
logic and points at either the borderless WM_GETMINMAXINFO min-track
(720 * g_borderlessDpi) or a native-HWND non-client frame discrepancy on the
externally-created, SDL-wrapped DirectComposition window.
Add the data to disambiguate:
- winlogState now also logs the native GetWindowRect vs GetClientRect (+ pos),
exposing any window-vs-client size mismatch that SDL_GetWindowSize hides.
- WM_GETMINMAXINFO logs g_borderlessDpi and the resulting min-track (once per
change) so we can see the actual OS-enforced minimum during the transition.
- Forward-declared winlog so the WndProc can use it.
Windows-only, no behavior change. Grep the log for "[WINLOG]".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Always-on stderr tracing (visible in release builds via the Windows
dragonx-debug.log redirect, unlike DRAGONX_DEBUG-gated DEBUG_LOGF) to capture
exactly how the window size / display-scale / minimum change when dragging
between monitors of different DPI. Logs:
- every WINDOW_RESIZED: size, actual vs stored scale, dpiPending/settle/retries,
and the DPI-vs-user classification (IGNORED/RECORDED);
- every DISPLAY_SCALE_CHANGED + the full handleDisplayScaleChange trace
(saved-under-old-scale, restore/proportional, min-clamp, display-clamp, the
SetWindowMinimumSize+SetWindowSize, and the resulting window state);
- each deferred-resize retry;
- monitor crossings (WINDOW_MOVED, throttled to display changes);
- startup and shutdown size.
Low volume — only fires on window events. Temporary diagnostic; remove once the
multi-monitor sizing bug is fixed. Grep the log for "[WINLOG]".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Moving the window between monitors of different scaling (150% -> 100% -> 150%)
left the height off the minimum in BOTH directions, forcing a manual resize each
time. A deep trace against the vendored SDL3 source found two distinct causes:
- Shrink leg (higher -> lower scale): handleDisplayScaleChange called
SDL_SetWindowSize BEFORE SDL_SetWindowMinimumSize. SDL clamps a size request up
to the current minimum (SDL_video.c:3184-3191), so the smaller target was
clamped back to the stale larger min; SetWindowMinimumSize only grows a window,
never shrinks it, so lowering the min afterwards didn't pull it back. The
deferred retry masked it only on rendered frames, so it leaked when the app was
idle. Fix: set the new minimum BEFORE resizing.
- Return leg (lower -> higher scale): the single-shot dpiResizePending flag let a
later settling WINDOW_RESIZED in the same burst be misclassified as a user
resize and recorded, corrupting savedSizeForScale for that scale so the
remembered min was lost. Fix: a frame-counted settle window (dpiSettleFrames)
suppresses resize-recording for the whole transition burst regardless of event
ordering; also clamp any restored/scaled size up to the new scale's minimum.
Only user resizes outside the ~40-frame settle burst are recorded, so normal
resizing is unaffected. SDL clamp semantics verified in-repo; build clean. The
transition needs real multi-monitor hardware to exercise end-to-end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The only startup clamp to the display's usable bounds lived inside the
`currentDpiScale > 1.01f` HiDPI branch, so on a standard-DPI (100%) cold start it
never ran. A window size saved on a larger or since-disconnected monitor then
opened oversized / partly off-screen (title bar under the taskbar), since the
window is also re-centered at the saved size.
Hoist the clamp-to-usable-bounds + re-center out of the HiDPI branch so it runs
on every startup; the DPI multiply and scaled minimum-size stay HiDPI-only. It
resizes only when the clamp actually changes the size, so a window that fits its
display is left untouched — only genuinely-oversized ones clamp. Also log when a
persisted size is rejected on load/save (below the 1024x720 floor) so "my window
keeps resetting" reports become traceable.
Root-caused via a multi-agent audit of the window-size/DPI logic, which also
confirmed the save/restore DPI round-trip is otherwise correct (the restore-side
auto-scale multiply was mistaken by the investigators for a unit bug). Verified:
a 9999x9999 saved size now clamps to the display at scale 1.00 (previously the
clamp was skipped on a same-DPI cold start).
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>
The send-confirm surface renders via RenderSendConfirmPopup, which calls
ImGui::OpenPopup() every frame — but BeginOverlayDialog is a plain window,
so that popup is never consumed by a BeginPopup and is normally cleared
only when a mouse click triggers ImGui's click-to-dismiss. The headless
sweep never clicks and tears the surface down by just clearing its flag,
so the orphaned popup lingered on OpenPopupStack past the end of the sweep.
A stuck popup makes IsPopupOpen(AnyPopup) permanently true, which disables
the smooth-scroll wheel capture in draw_helpers::ApplySmoothScroll — so
after a sweep the mouse wheel stopped scrolling Settings / Explorer /
Console (the tabs that use ApplySmoothScroll; native-scroll tabs were
unaffected).
Flush any lingering popup (ClosePopupToLevel) right after each surface's
teardown, so it can't bleed into the next capture or survive the sweep.
Confirmed via instrumentation: only 'popup-send-confirm' leaked (stack=1,
once per skin); now flushed.
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>