Commit Graph
1007 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 3fd52fbdaf perf(chat): ingest a message batch in one transaction
Ingesting a scan's worth of chat messages persisted each new message
with its own append() call, so a batch of fresh messages opened and
committed one SQLite transaction per row.

- A new appendBatch persists many messages in a single transaction,
  using the same INSERT OR IGNORE dedup key as append() and preserving
  input order so load()'s rowid ordering is unchanged.
- Every message is serialized and encrypted before the transaction
  opens, so a message with a non-UTF-8 body is skipped on its own
  without leaving a transaction open; the valid siblings still persist.
- Ingest collects the newly stored messages and writes them once after
  the loop instead of per message. It is inert (returns 0) while locked
  or on empty input.

Tests cover a batch insert, dedup on re-insert, empty input, bad-UTF-8
isolation with a valid sibling and a following append(), insert order,
and the locked no-op.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 22:03:00 -05:00
DanSandClaude Opus 4.8 1416654385 perf(tx-cache): skip Argon2id for unencrypted wallets
Opening the transaction-history cache always derived its encryption key
with Argon2id, even for an unencrypted wallet. For those wallets the
passphrase is just a value derivable from the database itself, so the
~50-100 ms Argon2id stall bought no additional secrecy and ran on the
UI thread at open.

- Unencrypted wallets now derive the cache key with a cheap,
  domain-separated BLAKE2b KDF over the wallet identity, keyed by the
  per-wallet salt. The secret is derivable from the DB, so this is
  security-equivalent for the unencrypted case and avoids the stall.
- Passphrase-encrypted wallets are unchanged: they keep the Argon2id
  key derivation. The two paths use separate domain labels, so the
  fast-path key and the Argon2id key never collide for the same DB and
  salt; a cache written under one cannot be read under the other.

Tests cover the fast-path round-trip and reopen, and assert that an
Argon2id unlock of the same DB produces a different key and fails to
read the fast-path snapshot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 22:03:00 -05:00
DanSandClaude Opus 4.8 3554ba1e2d perf(noise): tile the resident noise texture on GL instead of a copy
Drawing the tiled background noise built a viewport-sized pre-tiled
texture and re-created it whenever the viewport changed, copying the
256x256 noise tile across the whole screen in memory.

- On GL the draw now samples the resident 256x256 tile directly with
  GL_REPEAT, computing UVs from the screen rect over the tile size with
  the phase anchored at the viewport origin. The result is pixel-
  identical to the old pre-tiled texture (same generator, same origin)
  with no per-viewport copy.
- DX11 keeps the pre-tiled viewport-sized texture, since its sampler is
  CLAMP and cannot repeat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 22:03:00 -05:00
DanSandClaude Opus 4.8 aa486266c6 perf(texture): cap and downscale theme images on load
Theme images were decoded straight to RGBA at their full source size
and uploaded to a GPU texture as-is. A large or over-sized image wasted
VRAM and could exceed the GL maximum texture size, and each code path
also hand-managed the decoded pixel buffer with its own free calls.

- Loading from file and from memory now goes through the shared
  size-aware decoder with a 2048-pixel cap on the long side. An image
  larger than that is downscaled with its aspect ratio kept; an image
  whose area is absurd is rejected; a small image is left untouched.
- The decoder owns the pixel buffer for the life of the upload, so the
  manual free calls are gone.

Tests cover downscaling a wide image to a 2048 long side with the aspect
kept, rejecting an over-area image, and leaving an under-cap image as-is,
plus the fit-dims math at the theme-art cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 22:03:00 -05:00
DanSandClaude Opus 4.8 8c5bd485dd fix(lite): probe servers without joining the thread on the UI thread
Adding a lite server from the network tab restarted the background
server probe immediately. Starting a probe while one is still running
joins the previous worker thread, and that join happened on the UI
thread, so the frame could stall until the outstanding probe finished.

- Adding a server now sets a dirty flag instead of restarting the probe
  inline; the next frame starts a fresh probe once the current pass has
  finished, so the UI thread never blocks on a join.
- The probe's libcurl transfers honor the cancel flag through a progress
  callback and abort cleanly, so a probe in flight stops promptly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 22:03:00 -05:00
DanSandClaude Opus 4.8 8168e26d93 perf(updater): read the downloaded archive in one bounded allocation
Before verifying a downloaded archive, both updaters slurped it into a
string with an istreambuf_iterator, which grows the buffer repeatedly
and reallocates as it reads a multi-megabyte file. It also placed no
upper bound on the size it would read into memory.

- A shared readFileExact helper sizes the file first and reads it in a
  single allocation, failing closed if the file is missing, empty,
  larger than a caller-supplied cap, or comes up short. A truncated read
  can never reach the SHA-256 / signature verifier.
- Both updaters use it with the existing max-archive cap.

Tests cover a good read, a read exactly at the cap, over the cap, an
empty file, and a missing file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 22:03:00 -05:00
DanSandClaude Opus 4.8 e685d425ed fix(updater): cancel the release-metadata request on dialog close
The daemon and miner updaters fetch release metadata with a blocking
HTTP GET that ignored the updater's cancel flag. Closing the dialog set
the flag, but an in-flight metadata request kept running to completion
on the worker thread, so the dialog could not tear down promptly on a
slow or stalled connection.

- The metadata GET now passes the updater's cancel flag through to the
  HTTP helper, so closing the dialog aborts the request in progress.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 22:02:59 -05:00
DanSandClaude Opus 4.8 2396ed33db fix(settings): tolerate wrongly-typed values instead of quarantining
Several settings were read with json's value()/get<T>() helpers that
assume the stored type. A hand-edited settings file, or one re-saved
after an inf/NaN float was written back as JSON null, could therefore
hold a value of the wrong type and throw during load. The whole load is
wrapped so a throw quarantines the entire settings file to a
.corrupt- copy, meaning one bad field reset every unrelated setting.

- ui_opacity is accepted only when it is actually a number, and it is
  clamped to the same 0.3..1.0 range the setter enforces; the legacy
  reduced_transparency migration likewise runs only when that key is a
  boolean.
- A non-string name in a blocked chat conversation, and a non-string
  scope in a portfolio entry, now fall back to an empty string instead
  of throwing.

The file loads with the offending field defaulted, and the rest of the
settings survive.

Tests cover ui_opacity as a string/null/int/float/bool, a non-string
blocked-conversation name and portfolio scope, and the legacy
reduced_transparency migration, asserting no quarantine file is written.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 22:02:59 -05:00
DanSandClaude Opus 4.8 f47528de53 fix(startup): move the single-instance lock to a private per-user file
The single-instance lock lived in /tmp, which is world-writable. Creating
it with mode 0666 there gets masked to 0644, so a second local user
cannot open it O_RDWR and is wrongly blocked from running the wallet at
all. A world-writable directory also lets another user pre-create or
symlink the lock path.

- The lock now lives in a per-user directory: $XDG_RUNTIME_DIR when it is
  an absolute directory the user owns, otherwise the per-user, per-variant
  config directory that startup has already created. Never /tmp.
- It is created mode 0600 with O_NOFOLLOW, and after opening we fstat it
  and refuse anything that is not a regular file we own, rejecting a
  planted symlink or a foreign-owned file.
- The lock file is no longer unlinked on release; flock drops on close,
  and leaving the file in place avoids an unlink/relock race (a stale
  file is harmless).

Tests cover acquiring and conflicting on the lock, the 0600 mode, that
release leaves the file in place, and that a symlinked lock path is
refused.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 20:57:21 -05:00
DanSandClaude Opus 4.8 bf5cb39566 fix(bootstrap): restrict download redirects to https
Both the HEAD size probe and the archive download followed redirects
with no restriction on where they could go. A redirect could therefore
downgrade the transfer to plain http or point at a file:// URL, defeating
the TLS verification the initial https request was relying on.

- Redirects on both requests are now limited to https, so a redirect can
  no longer leave TLS or reach a local path.
- The limit is set with the string-based curl option where available and
  the protocol-mask option on older libcurl.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 20:57:21 -05:00
DanSandClaude Opus 4.8 667c89ba51 fix(bootstrap): resume downloads without discarding a valid archive
Resuming a partial download leaned entirely on the remote size from a
HEAD request. When that size could not be determined the code played it
safe by deleting the partial file and starting over, so a flaky HEAD (or
an offline mirror) threw away gigabytes that were fine to resume. It also
could not tell a real size from an error page, and if the server answered
a ranged request with the whole file (200, not 206) libcurl could append
it to the partial and silently corrupt the archive.

- The HEAD now returns transport status, HTTP code, and size together,
  and a size counts as authoritative only on a 2xx reply with a positive
  Content-Length.
- A pure helper decides what to do from the existing bytes and that
  authoritative flag: start fresh, truncate a stale oversized file, reuse
  a complete one, or resume with a ranged GET. When the size is not
  authoritative the partial is kept and a ranged GET is attempted rather
  than discarded.
- A ranged request that comes back as 200 instead of 206 now restarts
  cleanly from scratch instead of appending to the partial.
- The free-space pre-check runs only when the size is authoritative, and
  a failed download reports how many bytes were kept for the retry.

Tests cover the resume-decision helper across the fresh, stale, complete,
resuming, and non-authoritative cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 20:57:21 -05:00
DanSandClaude Opus 4.8 4d9d528566 fix(bootstrap): keep the archive unless a checksum truly mismatches
Any verification failure deleted the downloaded archive and forced a
full multi-GB re-download on the next attempt. But most failures are not
a corrupt archive: a missing or unreadable checksum sidecar, or a fetch
blip while pulling the checksum, would throw away a perfectly good file.
On top of that the checksum parser took the first token of whatever came
back, so an HTTP error page could be mistaken for a digest.

- The archive is now removed only on a genuine digest mismatch. A
  missing sidecar, an unusable one, or a transient fetch error keeps the
  file so the next attempt can reuse or resume it; the no-checksum case
  still fails closed and keeps the archive.
- The checksum parser now accepts a token only if it is exactly the
  expected length and all hex, lowercasing it; anything else (an error
  page, a truncated or wrong-width digest) is rejected.
- The small-file fetch moved onto the shared capped HTTP helper, and MD5
  is consulted only as a fallback when no SHA-256 is published.

Tests cover the checksum parser across good, wrong-length, non-hex, and
error-page inputs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 20:57:21 -05:00
DanSandClaude Opus 4.8 b3a1a9e654 fix(bootstrap): report verify progress correctly past 2 GiB
The checksum and MD5 passes measured the file for their progress bar
with fseek/ftell. On Windows a long is 32-bit, so ftell overflows on
any archive larger than 2 GiB and hands back a wrong (often negative)
size, which makes the verify progress percentage meaningless on exactly
the multi-gigabyte downloads this code exists for.

- Both passes now take the size from std::filesystem::file_size, which
  is 64-bit on every platform, through a small helper that returns 0 if
  the size can't be read.
- No seek is performed just to size the file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 20:57:21 -05:00
DanSandClaude Opus 4.8 e1003cba43 perf(migrate): gate the seed-migration confirming balance scan
While the seed migration sits on the Confirming gate it polled the
sweep's confirmations and a full wallet balance together on every pass.
That balance call is the expensive one, and it only matters once the
sweep is mined and we do not yet have a post-mine figure, yet it ran
every poll even mid-sync when it cannot move the gate.

- The confirmation count is still read on every poll.
- The balance scan now runs only when it can actually advance the adopt
  gate: the sweep is mined, no post-mine balance has been recorded yet,
  and no sync is in progress. The manual Refresh button forces it
  regardless.
- The decision lives in a small pure helper, and the poller snapshots
  the main-thread state it needs rather than reading members off the
  worker thread.
- The Confirming screen now claims a leftover remainder (and offers the
  re-sweep button) only once the balance has actually been measured; while
  it is still unknown it keeps waiting instead of prompting on a sentinel.

Tests cover the gate helper across the forced, syncing, and mined cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 19:24:12 -05:00
DanSandClaude Opus 4.8 f1b0ae5ce7 fix(net): cap market and bootstrap HTTP response bodies
Several HTTP fetches read a remote body into memory without a tight
bound. The price and exchange-ticker calls shared one large 16 MiB
metadata cap, and the bootstrap checksum download had no size limit at
all, so a hostile or MITM'd server streaming an unbounded (and
Content-Length-free) response could grow memory far past anything these
endpoints legitimately return.

- The HTTP GET helper now takes a per-call byte cap and redirect limit.
  The write callback refuses bytes past the cap, which aborts the
  transfer, and the same value is set as curl's max file size.
- The price call is capped at 256 KiB and the ticker list at 2 MiB,
  each sized to its real payload rather than the shared default.
- The bootstrap checksum fetch caps its body at 64 KiB in both the
  write callback and curl's max file size.

Tests cover the sink accept/abort boundary.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 19:24:12 -05:00
DanSandClaude Opus 4.8 edbb98d9ec perf(chat): single-pass watermark and move-append on chat load
Loading chat history did more work than it needed to. Seeding the unread
watermark walked every conversation and copied each one's full message
list out of the store just to read the last timestamp, and the database
reload copied every message into the store one at a time with no
reservation.

- The watermark now comes from one pass over the stored messages that
  returns the latest timestamp per conversation, with no per-
  conversation copy or sort.
- The store gained a move-append and a reserve, so the reload pre-sizes
  its vector and dedup set once and moves each row in instead of copying
  it. The dedup key is computed before the move.

Behaviour is unchanged: the watermark values and the blocked-conversation
filter on reload are the same as before.

Tests cover the per-conversation latest-timestamp pass and move-append.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 19:24:12 -05:00
DanSandClaude Opus 4.8 2a535c844d fix(security): overwrite key/PIN files with fsync before deleting them
Two secure-delete paths overwrote a sensitive file and then unlinked it,
but neither reliably forced the overwrite to stable storage. The
plaintext key export used a buffered stream with only a flush, and the
PIN vault removal opened a second descriptor to fsync only on non-
Windows. On a crash, or on Windows, the zeroing write could stay in the
OS cache and the original bytes could survive on disk.

Both now go through one shared best-effort secure-delete helper:

- It overwrites the file in place in chunks, never truncating first, so
  the original blocks are the ones rewritten.
- It flushes the data to the device before unlinking (full fsync on
  macOS, write-through on Windows), then removes the file.
- It is safe on a missing file and reports whether the file is gone.

This is still best-effort: SSD wear-levelling and copy-on-write or
journaling filesystems may retain copies.

Tests exercise the overwrite-and-remove helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 19:24:12 -05:00
DanSandClaude Opus 4.8 63a422a615 fix(security): only a real wrong passphrase counts toward unlock lockout
A failed wallet unlock was always treated as a wrong guess: the lockout
counter was bumped and the back-off curve armed no matter why the RPC
failed. A busy or warming-up node, a transport timeout, or a wallet that
is not actually encrypted would all eat an attempt and could push a user
into a lockout without a single mistyped passphrase.

The failed call is now classified before it is acted on:

- Only the daemon's "passphrase incorrect" error counts as a wrong
  guess and advances the lockout.
- A warming-up or unreachable node, a reply timeout, or a full queue is
  reported as a transient busy-node condition with no attempt charged.
- A "wallet is not encrypted" reply refreshes the local encryption state
  instead of penalising the user.
- Any unknown error surfaces its message without touching the counter.

The PIN path got the same treatment: when the vault decrypts correctly
but the unlock RPC then fails for a non-credential reason, the attempt
is no longer counted.

Tests cover the classification of each failure kind.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 19:24:12 -05:00
DanSandClaude Opus 4.8 100d76b88f fix(input): handle the first Alt+wheel while idle in the wait path
Alt + mouse wheel adjusts the font scale, but it was only handled in the
poll loop that runs after the app wakes. When the app was idle it blocks
in a single timed wait, and that first wheel event went straight to
ImGui, scrolling the window instead of changing the scale until a later
event arrived.

- The Alt+wheel handling is pulled into a shared helper used by both the
  idle wait path and the poll loop, so the first event while idle is
  consumed and triggers a redraw.
- Behaviour is otherwise unchanged: the visual scale updates smoothly and
  the font atlas is rebuilt once scrolling stops.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 18:16:50 -05:00
DanSandClaude Opus 4.8 fa1ba97286 perf(win): skip DWM blur/acrylic when the window starts opaque
The window always asked DWM for a full-window blur region and, on
Windows 11, an acrylic backdrop. When the user runs with an opaque
window (low-spec mode or full opacity) that desktop blur is never seen
but DWM still composites it every frame.

- An early read of the saved settings decides whether the window starts
  opaque before the backdrop is configured.
- When it does, the full-window blur region is left as SDL's zero-area
  default and the backdrop type is set to none instead of acrylic.

Windows-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 18:16:50 -05:00
DanSandClaude Opus 4.8 53da160546 fix(window): defer caption-button window ops out of the ImGui frame
The custom minimize / maximize / restore buttons are drawn from inside
app.render(), while an ImGui frame is open. Calling SDL to change the
window state there pushes resize events synchronously, which re-enters
the resize watcher and can nest a new frame inside the open one.

- A click now records the requested op instead of acting on it, and the
  window state change is applied once the frame has closed, after the
  pacing block so the follow-up redraw is not dropped.

Windows-only; it rides the borderless caption controls.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 18:16:50 -05:00
DanSandClaude Opus 4.8 49240facff perf(resize): dedupe the live-resize watcher, early-out DX11 resize
The live-resize event watcher redrew for every resize event, and each
resize tore down and recreated the DX11 render target even when the
pixel size had not actually changed. A single drag fires both a RESIZED
and a PIXEL_SIZE_CHANGED event reporting the same final size, so the
work ran twice.

- The watcher keys off the real pixel size and returns early when it has
  not moved since the last pass, collapsing the duplicate event pair into
  one redraw. It also bails out when called from inside an open ImGui
  frame, which the caption buttons can trigger.
- The DX11 resize now checks the swap chain's current dimensions up
  front and does nothing when the size already matches and the target is
  live, skipping the destroy/recreate churn.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 18:16:50 -05:00
DanSandClaude Opus 4.8 13059412ce fix(log): share one handle between stdout and stderr on Windows
On Windows stdout and stderr were reopened onto the same log path with
two separate freopen() calls. Each stream then had its own file handle
and its own write offset into the one file, so the two writers
overwrote each other and interleaved into a garbled log.

- stdout is reopened (truncating the file per run), stderr is pointed at
  the same underlying handle via _dup2 so both share one offset and
  append in order.
- stderr is first given a valid descriptor, since a GUI-subsystem
  process may start with no fd 2, and is left unbuffered so crash
  diagnostics are not lost.

Also corrects a stale comment that named the old log file.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 18:16:50 -05:00
DanSandClaude Opus 4.8 45d8bdccf0 perf(startup): drop the unused SDL gamepad subsystem and nav flag
Startup initialised SDL's gamepad subsystem and turned on ImGui gamepad
navigation, but the wallet is never driven by a gamepad. Initialising
the subsystem enumerates attached controllers and loads the mapping
database for nothing.

- SDL now starts with the video subsystem only.
- ImGui keeps keyboard navigation; the gamepad navigation flag is
  removed from both the DX11 and the OpenGL setup paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 18:16:50 -05:00
DanSandClaude Opus 4.8 a24ffea86e perf(startup): let Windows relax the system timer resolution
On Windows SDL3 raises the global timer resolution with
timeBeginPeriod(1) and holds it for the whole process lifetime, which
keeps the system on a 1 ms tick and costs power for no benefit here.

- A startup hint now asks SDL to leave the system timer alone. Nothing
  in the app needs sub-15 ms waits: vsync paces drawing, the idle wait
  is 200 ms, and background sleeps are 50 ms or longer.

The hint is Windows-only in effect and is ignored elsewhere; an
environment override that turns it off is harmless.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 18:16:50 -05:00
DanSandClaude Opus 4.8 5d9bd1688c fix(security): clear the secret clipboard on a monotonic clock
A secret copied to the clipboard auto-clears after a timeout, but the
deadline was stored as an ImGui::GetTime() value. That clock only
advances while frames are being drawn, so while the app idled (no
redraws) it effectively stood still and the pending clear could be put
off well past the intended window.

- The clear deadline is now a steady_clock time_point, which keeps
  counting whether or not the UI is rendering.
- The pump uses the armed hash as its "something is pending" flag rather
  than a sentinel deadline value, and compares the real wall time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 18:16:50 -05:00
DanSandClaude Opus 4.8 f3f85d602d fix(mining): stamp hashrate history and label the chart by real time
The hashrate sparkline guessed the age of its oldest sample from the
sample count, labelling the left edge "5m ago", "1m ago" or "start"
regardless of how fast samples actually arrived. Solo and pool samples
come in at different intervals, and a slow or stalled poll made those
labels wrong.

- Each hashrate sample now carries a wall-clock timestamp, kept in
  lockstep with the value as the history fills and rolls over, for both
  the solo and pool histories.
- The chart's left-edge label is derived from the oldest sample's actual
  timestamp, falling back to "start" only when no stamp is available.

Tests: a mining refresh records a timestamp per sample, and the
timestamp history stays the same length as the value history once the
history is capped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 17:42:30 -05:00
DanSandClaude Opus 4.8 2c9e99c022 perf(daemon): memoize the external debug.log miss
When following the debug.log of a node this session did not launch, every
poll re-read the last 2 MB of the file and re-scanned it for the process
banner. If that node logs to its console instead, the banner is never
there, so the full re-scan repeated on every poll for the life of the
process.

- A miss is now remembered by pid and file size: if nothing has changed
  since the last miss, or the file is growing but the backoff has not
  elapsed, the scan is skipped.
- The retry schedule re-scans quickly during a fresh-node grace window
  (the banner flushes a beat after the pid appears) and then backs off
  from 4 s, doubling to a 60 s cap. The memo is cleared on a successful
  tie and when the followed process goes away.

Tests: the retry delay is fast within the grace window and then steps
4, 8, 16 s up to the 60 s cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 17:42:30 -05:00
DanSandClaude Opus 4.8 1cd4ea4026 fix(security): throttle and wipe the import dialog clipboard preview
Hovering the Paste button in the import-key dialog read the OS clipboard
every frame to draw a ghost preview of the key, and the trimmed
plaintext was rebuilt each frame and never wiped. SDL's clipboard read
is a synchronous round-trip, and the pasted value is a secret private or
viewing key.

- The clipboard is now read only on hover entry and then at most twice a
  second, and the classified preview is cached rather than recomputed
  every frame.
- The cached preview is a dedicated secret buffer: it is wiped the moment
  it stops being shown, wiped on secret wipe, and the transient trimmed
  copy is zeroed right after it is classified.
- Trimming moves to a shared clipboard helper shared with the paste path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 17:42:30 -05:00
DanSandClaude Opus 4.8 d601f786e2 perf(lite): cache walletExists() to stop per-frame FFI
While no lite wallet is open the startup auto-open check called
walletExists() every frame. That call crosses into the backend and
touches the filesystem (and prints), so it ran a disk stat on every
frame the app idled at the first-run prompt.

- walletExists() now caches its result for about two seconds on the main
  thread, and the cache is invalidated whenever an open or lifecycle
  result is consumed, since the wallet file may have changed.
- The auto-open condition is reordered so the cheap timer checks gate the
  call, and the click-time overwrite decision uses a new
  walletExistsFresh() that bypasses the cache.

Tests: the cache returns the first queried value within the TTL and the
fresh read re-queries the backend and refreshes the cache.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 17:42:30 -05:00
DanSandClaude Opus 4.8 29c1674094 fix(mining): honour the pool Refresh button in Manual mode
The pool auto-balance step returned early whenever the pool selector was
in Manual mode, so a pending manual Refresh of the pool display stats was
dropped and the button did nothing unless Auto-balance was active.

- A pending refresh is now serviced in Manual mode too, re-querying the
  pool display stats without auto-switching the pool or restarting xmrig.
- Auto-balance behaviour is unchanged; the manual path is display-only,
  so it runs with no benchmark gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 17:42:30 -05:00
DanSandClaude Opus 4.8 95605dd7e9 fix(ui): stop global shortcuts auto-repeating; skip invalid skins
The global keyboard shortcuts tested keys without disabling key-repeat,
so holding one down fired it every frame: the theme cycle, balance and
market layout cycles, the effects and background toggles, the settings
and sidebar-collapse keys all re-triggered on a held key. Cycling themes
also stepped onto skins that had failed to load and wrote the broken
pick into settings.

- Every one-shot shortcut now checks for a fresh key press only, so a
  held key fires once.
- Theme cycling skips over invalid skins and only applies, persists, and
  shows the toast when a skin actually loads; if none can load, settings
  are left untouched and no toast appears.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 17:42:29 -05:00
DanSandClaude Opus 4.8 96352ce9ad fix(daemon): guard start/connect during shutdown
Once a shutdown was under way the app could still launch or restart the
full node and keep running the RPC connection state machine. That could
spawn a daemon process on the way out and surface a spurious connection
failure toast as the window tore down.

- Starting the embedded daemon now bails out immediately while a
  shutdown is in progress, so the node is never (re)launched on exit.
- The connect attempt returns early during shutdown, so there is no
  start/connect try and no stray failure toast on the exit screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 17:42:29 -05:00
DanSandClaude Opus 4.8 bbe2bf94dc fix(export): write the transaction CSV atomically and report failures
The transaction export streamed rows straight into an ofstream. A write
that failed partway, on a full disk or removable media, left a truncated
CSV on disk behind a success toast. The CSV formatting also lived inline
in the dialog with no test coverage.

- The CSV is now built in memory and written atomically (temp file plus
  rename), so a write failure is reported with an error toast and leaves
  no partial file.
- Formatting moves into a reusable helper, and the timestamp conversions
  use the thread-safe localtime variants and tolerate a conversion
  failure rather than dereferencing a null result.

Tests: field escaping neutralises formula-injection prefixes and quotes
special characters, and the full CSV has the expected header and rows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 f76d71fede fix(chat): drop a redundant save and report a real contact-save result
Renaming or adding a contact from the chat tab called the address-book
mutator and then saved again, even though the mutator already persists.
It also reported success unconditionally, so a failed rename still
showed a success toast.

- The redundant explicit save is removed; the mutator's own save stands.
- A rename now shows success only when the mutator succeeds and an error
  toast when it does not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 2a3e88c8dc fix(network): do not re-pin a lite server while a connect is in progress
Pinning a lite server while a connection to it was already being
established tore the in-flight connect down and restarted it, slowing
the very connection the user was waiting on.

- Pinning is now skipped while a connect is already underway, so the
  existing attempt is left to finish.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 b4ff9a10a2 fix(request): clear the stale payment URI, show it only for an address
The request-payment dialog kept its generated payment URI across opens
and closes. A dialog opened for an address with no payable shielded
address could still show the previous request's URI, and the string
lingered after the dialog closed.

- The payment URI is cleared when the dialog opens, when it closes, and
  on the close cleanup path.
- The URI field is shown only when there is both a URI and a real
  address to pay.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 76daf9619d perf(send): poll the clipboard for the paste hint only when it helps
The send tab read the OS clipboard every frame the paste button was
hovered, to preview a pasted address. SDL's clipboard read is a
synchronous round-trip that can stall, and the preview can only ever
latch when the recipient field is empty, so most of those reads were
wasted work.

- The clipboard is now read only while the button is hovered and the
  recipient field is empty, polled once on hover entry and then at most
  twice a second.
- Trimming the clipboard text moves to a shared helper that also bounds
  the copy so a pathological multi-megabyte clipboard is ignored.

Tests: the trim helper handles null, surrounding whitespace, an
all-whitespace string, and the length bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 3f82af99c4 refactor(market): resolve exchange/pair selection through one helper
The chart fetch and the Market tab both resolved the saved exchange and
pair names into the registry, with their own near-identical scans and
slightly different fallbacks. Keeping two copies risked the chart and
the chips disagreeing about which pair was selected.

- A single resolver maps the saved names to a registry index, matching
  the exchange name (any exchange when none is saved) then the pair
  display name, and falling back to the registry's first pair, the UI
  default. The chart fetch and the Market tab both call it.
- The Market tab now derives its selection indices from the saved names
  against the current registry every frame, so the indices stay correct
  when the live exchange list replaces the fallback; a steady-state fast
  path skips the scan once the indices already name the saved selection.

Tests: the resolver returns the right index for a matched exchange/pair,
an empty-exchange match, and the first-pair fallback on misses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 ce00621a4a perf(market): precompute the address-picker selection flag once
The portfolio address picker's sort comparator looked up each address in
the selected-address set on every comparison, two hash lookups per call,
and the per-row draw repeated the lookup again. On a long address list
that is a lot of redundant hashing every frame.

- Each filtered candidate now carries its selected flag, looked up once
  when the list is built. The comparator reads the stored flag, so the
  sort produces the same ordering with no lookups of its own.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 a631e4421a refactor(contacts): share case-insensitive compare, truncation helpers
The contacts list carried its own lowercase-and-compare sort, its own
lowercase substring search, and an inline linear middle-truncation loop
for fitting an address to a column width. These duplicated logic that
belongs in the shared text helpers.

- A new allocation-free case-insensitive compare replaces the per-sort
  lowercasing; it lowercases ASCII per byte and leaves bytes >= 0x80
  alone, so UTF-8 labels sort exactly as before.
- Search now uses the shared case-insensitive contains helper.
- Middle-truncation to a pixel width moves to a shared template that
  binary-searches the fit (about log2(n) width measurements instead of
  up to n) while producing the same result as the old linear shrink.

Tests: the compare matches lowercase-then-compare across ASCII, UTF-8
bytes and length ties, and the fit helper matches the old linear loop
across lengths and widths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 32ea4d0524 fix(mining): keep the thread benchmark running while the tab is hidden
The thread benchmark state machine was advanced from the Mining tab's
render path, so it only progressed while that tab was visible and the
window was on screen. Hiding the tab or minimizing the window froze the
benchmark mid-sweep.

- The benchmark now advances from the main update loop, ungated by
  connectivity, so it keeps running when the tab is hidden or the window
  is minimized.
- Its clock is driven from a steady wall-clock reading rather than the
  render delta, and each step is clamped like the other logic timers so
  a suspend/resume cannot leap a benchmark phase.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 fae5ef05b9 fix(mining): restore the miner after a benchmark only if it was running
Cancelling a thread benchmark always stopped and restarted the pool
miner at its pre-benchmark thread count, even when the user had not been
mining before the benchmark began. That silently started the miner on a
plain cancel or a switch to solo mode.

- A cancel now always drops the candidate-thread miner, and restarts the
  pool miner only when the user was mining before the benchmark, read
  from a snapshot taken at the start rather than the live running state
  (the benchmark sweep transiently stops the miner between candidates,
  so a live check would drop the restore).
- Callers that must not restore the miner (leaving solo mode) pass the
  new restore flag as false.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 16:00:10 -05:00
DanSandClaude Opus 4.8 13989cf268 fix(notifications): prune expired toasts and skip invisible draws
The status-bar notifications only expired the front of the queue, so an
expired newer toast could sit in front of a still-live older one and
hide it from view. The liveness check that drives continuous repaints
also disagreed with what render() actually showed.

- Expired toasts are now pruned wherever they sit in the queue, not just
  at the front, so the newest remaining toast is always a live one.
- The visible toast is the newest live entry, skipping an expired newer
  one. The liveness check reports active while a toast is visible plus
  one trailing frame, so the final faded-out state is painted once and
  then repaints stop; it self-expires if render() is skipped.

Tests: pruning removes expired entries from anywhere in the queue, the
visible toast skips an expired newest entry, and liveness tracks it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 c5b325a752 perf(charts): fill chart areas in one linear pass
The filled sparklines and the market price chart built their area fill
with PathFillConcave, which runs a per-scanline pass over the whole
filled region. For a simple area under an x-monotone curve that work is
unnecessary.

- A new helper fills the area between an x-monotone polyline and a
  horizontal baseline directly as a triangle strip, in one pass over the
  points. The market sparklines, the market price chart and the mining
  hashrate sparkline all use it. The fill is flat and non-anti-aliased;
  the anti-aliased curve drawn on top hides the stepped top edge, and
  the faint side and bottom edges are imperceptible at these alphas.

Tests: the strip's vertex and index counts, index bounds, and that its
triangle area matches the trapezoid area under the polyline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 969718e03a perf(theme): drop the redundant acrylic grain under glass cards
Glass cards draw their own grain layer over the surface overlay, but the
acrylic primitive beneath them was also drawing its grain. That lower
layer sits under the overlay at well below one grey level, so it is
invisible and only costs an extra noise pass per card.

- The card now asks the acrylic primitive for zero grain and keeps its
  own visible grain layer, removing the redundant pass with no visible
  change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 7a9b29f358 fix(effects): stop retrying scroll-fade shader init after it fails
If the scroll-edge fade shader failed to compile or link, the init path
left its ready flag clear and tried again on the very next frame. On a
driver that rejects the shader that meant a fresh compile attempt every
frame, for the life of the program.

- A failed init is now latched and treated as final until destroy(), so
  the compile is attempted once, the failure is logged once, and the
  fades stay disabled without further retries. Destroying the shader
  clears the latch so a later re-init can try again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 968d6d3875 fix(effects): capture the acrylic backdrop at framebuffer size on HiDPI
The acrylic effect blits its backdrop from the default framebuffer using
the logical viewport size as the source rectangle. On a HiDPI display
the framebuffer is larger than the logical size, so the blur sampled
only the lower-left corner of the screen and the glass showed the wrong
region stretched across each panel.

- The capture blit source now uses the real framebuffer pixel size
  (logical size times ImGui's DisplayFramebufferScale, truncated the
  same way the GL backend does), while the capture and blur targets stay
  at logical size. The framebuffer size is tracked separately from the
  viewport size so quality or buffer-lifetime resets cannot lose it, and
  moving the window between a Retina and a non-Retina display redoes the
  capture. DX11 is unaffected; its scale is one.
- The debug screenshot sweep captures at framebuffer size too, so HiDPI
  captures are no longer cropped.

Tests: the framebuffer-extent helper matches the GL backend's
truncation and falls back to the logical size when the scale is unknown.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 fdd95aa457 refactor(effects): drop the unused composite acrylic shader
The acrylic material carried a full composite shader — vertex and
fragment source, compile and link helpers, the program handle and seven
uniform locations — that nothing ever invoked. The acrylic is composited
on the ImGui draw list, not through this program.

- Remove the dead shader source, its compile/link helpers, the program
  and uniform fields, and the compile/link/cleanup steps in init and
  shutdown. No behaviour change; init no longer builds a program that is
  never bound.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00
DanSandClaude Opus 4.8 75e0c5048e fix(theme): give the menu acrylic the card blur radius
Every panel class samples one shared blur texture, so a menu asking for
a different blur radius than cards could not actually get it. All it did
was force a second full-screen blur pass each frame as the radius
flipped between the card and menu values, and let whichever class drew
last decide the radius for everything.

- Setting the acrylic theme now copies the card blur radius onto the
  menu class, so menus and cards share one radius and the blur runs
  once per frame. The first-use default path goes through the same
  setter so it is covered too.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 13:30:20 -05:00