Commit Graph

495 Commits

Author SHA1 Message Date
a107fda9b8 feat(market): loading spinner while the price chart fetches (pair switches)
Switching exchange pairs (or first load) briefly leaves the chart with no series
while the venue's candles fetch, which showed the bare "No price history
available" empty state. When a chart fetch is in flight — App::isMarketChartLoading()
(the CoinGecko aggregate OR the per-exchange fetch) — draw a spinner + animated
"Loading price history…" in the plot area instead; the "no history" text only
shows when genuinely empty and idle. New i18n key market_chart_loading across all
8 languages. Verified via a forced-state render (spinner + label centered).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 09:38:44 -05:00
d1ff58c374 feat(market): per-exchange price chart from each venue's own candle API
The market chart was hardwired to CoinGecko's cross-exchange USD aggregate
(vs_currency=usd), so selecting a trading pair only changed the "Trade" link —
never the chart or price. Now the SELECTED exchange drives both.

CoinGecko gives us which venues list DRGX (the ticker `market.identifier`) but
not their APIs, so add data/exchange_candles.h: a hand-maintained map from that
identifier to each venue's public candle endpoint, with two adapters verified
against the live APIs — Ourbit (MEXC-style /api/v3/klines, array format) and
NonKYC (TradingView-UDF /market/candles, `bars`). Unmapped venues return no URL,
so the chart falls back to the CoinGecko aggregate and nothing regresses.

- App::refreshExchangeChart() resolves the selected pair, fetches its intraday
  (5-min) + daily candles via the TLS-verified httpGetString on the worker, and
  stores them on MarketInfo (exchange_chart_intraday/daily + exchange_chart_active).
  Re-fetches on pair change; self-throttled to ~30 min otherwise; falls back to
  aggregate on any failure.
- chartSeries() draws the per-exchange series when active (portfolio sparklines
  stay on the aggregate). The hero shows the selected venue's real price
  (converted_last) — Ourbit vs NonKYC differ ~7%. parseCoinGeckoTickers now
  captures the identifier + per-exchange USD price (previously discarded).

Adapters unit-tested against real captured responses (URL builder + both parsers
+ the chartSeries switch) and validated against the full live feeds (Ourbit 20d,
NonKYC 370d, ascending, correct closes). Build + ctest + hygiene green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 09:19:55 -05:00
156735eb06 feat(modals): fixed modal-backdrop blur, independent of the acrylic slider
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>
2026-07-12 07:41:53 -05:00
2f86bec98e feat(wallets): subtle empty-state hint in the fixed-height list
With the list now pinned at max height, a few wallets leave blank space below
the cards. Fill it — only when there's real room to spare — with a subtle
centered folder glyph + "Scan a folder to find more wallets" (OnSurfaceDisabled),
so the area reads as a gentle nudge toward the scan action rather than dead
space. Purely decorative (draw-list only, clipped to the list); the actions stay
below. New i18n key wallets_empty_hint, translated across all 8 languages.

Verified on the sweep: the hint centers in the gap and reads subtly on both dark
and light themes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 07:17:34 -05:00
6310f51f65 feat(wallets): fixed max-height list + manage scanned folders
Two wallets-modal changes:

- The wallet list now always renders at its max height (kMaxVisibleRows) instead
  of shrinking to the wallet count, so the modal is a consistent size whether you
  have one wallet or many — fewer rows leave empty space, more than 7 scroll.

- Add a "Scanned folders" manager: each user-added scan folder is listed with a
  control to stop scanning it (removeExtraFolder + save + re-scan so its wallets
  drop out). Paths front-elide to keep the identifying leaf visible, with a
  full-path tooltip. The card-height math reserves the manager's rows so nothing
  clips. Two new i18n keys (wallets_scanned_folders / wallets_remove_folder),
  translated across all 8 languages; CJK subset rebuilt.

Verified on the sweep at 1.0 and 1.5x DPI: max-height list on both the few- and
many-wallet surfaces, the folder manager renders and stays clear of the footer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 07:04:43 -05:00
09e0b962c3 i18n(wizard): translate the first-run wizard (was all hardcoded English)
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>
2026-07-12 01:41:18 -05:00
758ea06ab7 feat(wizard): modernize the first-run wizard to the current design language
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>
2026-07-11 22:48:22 -05:00
d189ee5ddb fix(ui): readable error text on dark modal backdrops
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>
2026-07-11 22:27:44 -05:00
32d6e34c45 fix(ui): theme-aware success green for modal status text
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>
2026-07-11 22:18:48 -05:00
088e3371d9 feat(modals): stronger backdrop blur + dim, and anchor floating modals on light skins
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>
2026-07-11 22:18:48 -05:00
49b5ef6b20 fix(wallets): key in-place links on the canonical path (one wallet, one link)
A wallet reached via two different path strings — e.g. a symlinked mount
(/mnt/usb) and its real target (/media/usb) — used to hash to two different
in-place link names and list as two rows, so the same file looked like two
wallets with two datadir links.

Give WalletRow a canonPath (fs::canonical, symlinks resolved, computed once at
scan time so the every-frame render/active-marking path stays cheap) and key
both the link name and the row de-dup on it. Now the same physical wallet maps
to one identity regardless of the path used to reach it: one link name (honors
the "distinct per-target name" invariant), and scan() lists it once — datadir
first, so a datadir wallet wins over the same file seen via an external path.
The link also points straight at the resolved real file instead of through the
symlinked path. Genuinely distinct wallets keep distinct identities (verified
with a standalone symlinked-mount harness).

Closes the last deferred finding from the open-in-place adversarial review.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:47:12 -05:00
6aaab9e01f feat(wallets): prefer symlink on Linux/macOS for open-in-place
On Unix a symlink is the better datadir link than a hard link: it needs no
privilege, spans volumes (a wallet on a USB / other partition just works,
where a hard link can't), and is visibly a pointer rather than an
indistinguishable second copy. Windows keeps hard-link-first (a symlink there
needs admin / Developer Mode), falling back to a symlink.

Symlink-first made two latent issues in the pre-clear reachable, fixed here
after an adversarial review:

- Dangling prior symlink: fs::exists follows the link (false when the source
  moved), so it was skipped and create_symlink then tripped on the occupied
  path. Catch it with fs::is_symlink (lstat) and replace it.

- Stale reserved-name occupant: the old "hard_link_count == 1 → reuse" path
  could load the WRONG wallet — a count-1 file at our name is not a valid link
  to the source (a live hard link has count >= 2), it's a stale orphan (source
  deleted, or a copy left by a datadir migrated across filesystems). Gate reuse
  on fs::equivalent(link, src) (same inode) instead, and reclaim the name only
  when the occupant is provably redundant (another hard link still holds the
  data); otherwise refuse rather than risk destroying something unique. So
  hard_link_count is no longer trusted to prove identity — only data-safety.

Also guard against a stale row (source moved/deleted between the scan and the
click): !exists(src) refreshes the list instead of linking a ghost the daemon
would silently swap for wallet.dat. Verified with a standalone harness across
fresh / re-open / valid-hardlink-reuse / stale-orphan / redundant-occupant /
vanished-source cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:39:57 -05:00
b7da6335d1 feat(wallets): open external wallets in place via datadir link (no copy)
Replace the "Import" action — which copied an out-of-datadir wallet into the
datadir and cluttered the list with hard-to-tell-apart duplicates — with an
in-place "Open" that links the real file into the datadir under a stable
per-target name (wallet-ip-<FNV8>.dat) and switches to it. The daemon only
loads a bare filename from its own datadir, so a link is the minimal bridge:
hard link first (no privileges, same volume — covers non-admin Windows),
symlink fallback (cross-volume), then an error. Never a copy (that would fork
the wallet) and never a delete of a real file.

Also:
- show each external wallet's originating sub-directory (…/Backups/2021)
- add a per-row "open folder location" button
- widen the modal (780 → 860) for the extra button
- guard the daemon against a dangling link (external file moved / USB
  unplugged) by falling back to wallet.dat rather than creating an empty one

Per-target link names (not one shared name) make switching between two
external wallets a real -wallet switch, so the rescan/cache index tracks each
correctly. Drops the now-dead wallets_import* i18n keys and back-fills the new
open/folder strings across all 8 languages + rebuilds the CJK subset.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 21:21:30 -05:00
b765d62e00 feat(wallets): show each external wallet's sub-directory
Prepend the containing folder (last 1-2 path components, e.g. "…/Backups/2021")
to an external wallet's metadata line, so several same-named wallet.dat files
surfaced by the recursive scan are distinguishable at a glance (the info-icon
tooltip still shows the full path).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 19:17:45 -05:00
3dc4d2d860 fix(wallets): make the sort-direction toggle a circular icon button
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>
2026-07-11 17:26:59 -05:00
fe40d38f0d feat(wallets): segmented sort control (like the Receive all/z/t toggle)
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>
2026-07-11 17:01:51 -05:00
1d3a919ac6 feat(wallets): pin the active wallet to the top of the list
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>
2026-07-11 16:49:05 -05:00
d549127e41 i18n(wallets): translate created-date + sort keys, rebuild CJK subset
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>
2026-07-11 16:37:36 -05:00
54a14485a5 feat(wallets): show created date + sort the wallet list
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>
2026-07-11 16:37:36 -05:00
8eefab99ec i18n(wallets): clarify never-opened wallet label ("Never" -> "Never opened")
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>
2026-07-11 16:22:13 -05:00
4fdf281230 i18n(wallets): translate the new badge/count keys + rebuild CJK subset
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>
2026-07-11 16:11:29 -05:00
c4f69b5a85 fix(wallets): probe synchronously during the offline screenshot sweep
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>
2026-07-11 16:03:32 -05:00
171b705429 feat(wallets): smooth wheel scrolling for the wallet list
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>
2026-07-11 15:51:55 -05:00
175950a2ec feat(wallets): exact address/tx counts via a minimal BDB btree parser
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>
2026-07-11 13:20:35 -05:00
36efe99a0f feat(wallets): vertical label+icon badge stack + async probing
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>
2026-07-11 12:47:27 -05:00
fadbd6a879 docs: Overview marble design mockup
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>
2026-07-11 12:33:06 -05:00
112c581247 feat(wallets): recursive subdir scan + wallet.dat encryption/seed badges
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>
2026-07-11 12:32:59 -05:00
57147fd06b fix(console): readable theme-colored log text on light themes
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>
2026-07-11 12:32:49 -05:00
aa60188665 fix(ui): tighter, darker, non-clipping card drop shadow
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>
2026-07-11 12:32:42 -05:00
cff958c21e fix(themes): card shadow no longer cut off at child-window edges
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>
2026-07-10 20:42:08 -05:00
c50125523c change(themes): make the card shadow uniform (no directional offset)
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>
2026-07-10 20:24:56 -05:00
063f1fa4be feat(themes): subtle card drop shadow for Light + Marble
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>
2026-07-10 20:07:24 -05:00
ad7a150ad6 change(acrylic): lower the blur slider's range to a gentle max
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>
2026-07-10 19:54:48 -05:00
433c7ae46f fix(acrylic): frost glass panels at full strength (slider was a near no-op)
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>
2026-07-10 19:46:18 -05:00
a5efbaf83d change(settings): debug-options gate is once per session
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>
2026-07-10 18:49:11 -05:00
46fb0029b8 feat(settings): gate debug options behind a confirmation (+ re-auth if secured)
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>
2026-07-10 18:46:50 -05:00
aa587d1d84 feat(seed): glow the Migrate-to-seed button for a legacy wallet
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>
2026-07-10 18:24:41 -05:00
3a9edf2200 feat(xmrig-update): two-pane version picker matching the node updater
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>
2026-07-10 17:56:59 -05:00
c25a4e4150 fix(daemon-update): status tooltip only shows on icon hover, not when selected
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>
2026-07-10 17:42:57 -05:00
54945c94bf refine(daemon-update): version cards show a text label + the icon badge
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>
2026-07-10 17:37:12 -05:00
96a1c54556 refine(daemon-update): pad info surface, right-aligned icon badges, centered install
- 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>
2026-07-10 17:33:18 -05:00
bedb482855 refine(daemon-update): taller card, unified info surface, tactile card polish
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>
2026-07-10 17:18:13 -05:00
c352364bdc fix(modals): a fixed-card modal can no longer wheel-scroll as a whole
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>
2026-07-10 16:52:43 -05:00
6b7438b529 feat(daemon-update): render release notes as markdown + tactile Material cards
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>
2026-07-10 16:44:25 -05:00
c6624913de feat(daemon-update): two-pane version picker (versions left, detail right)
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>
2026-07-10 16:28:33 -05:00
e6bea8eb04 fix(modals): hold dialog hidden until its height settles (no reveal-time slide)
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>
2026-07-10 15:29:13 -05:00
5e5f167fb0 fix(modals): hide the auto-height measuring frame so dialogs only appear centered
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>
2026-07-10 15:09:05 -05:00
1fd794ccc1 fix(modals): vertically center auto-height dialogs in the window
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>
2026-07-10 15:00:15 -05:00
ed4d61c364 fix(wallets): folder picker opens in the DRAGONX data directory
"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>
2026-07-10 14:53:18 -05:00
3df516be70 fix(wallets): cap card at viewport + flex list so many wallets never clip
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>
2026-07-10 14:47:59 -05:00