Skip to content

Fix matrix strategy for ARM and AMD Builds - #2

Closed
vivekgsharma wants to merge 2 commits into
masterfrom
fix_matrix
Closed

Fix matrix strategy for ARM and AMD Builds#2
vivekgsharma wants to merge 2 commits into
masterfrom
fix_matrix

Conversation

@vivekgsharma

Copy link
Copy Markdown
Collaborator

No description provided.

@QuantumExplorer

Copy link
Copy Markdown
Member

Latte used this. Thanks Vivek.

@pauldelucia
pauldelucia deleted the fix_matrix branch October 21, 2025 11:15
lklimek added a commit that referenced this pull request Feb 24, 2026
- Replace assume_checked() with require_network() for address
  validation (CodeRabbit #2)
- Use styled Frame-with-dismiss error display matching Send dialog
  pattern (CodeRabbit #3)
- Don't open dialog when no wallet selected; show MessageBanner
  instead (CodeRabbit #4)
- Extract shared load_bip44_external_addresses() helper to eliminate
  near-duplicate code between mine and receive dialogs (CodeRabbit #5)
- Add backend-side network guard (Regtest/Devnet) for defense-in-depth
  (CodeRabbit #6)
- Rename shadowed ctx binding to refresh_ctx for clarity (CodeRabbit #7)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
lklimek added a commit that referenced this pull request Feb 24, 2026
… mode (#638)

* feat(wallet): add Mine Blocks dialog for Regtest/Devnet dev mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: add manual test scenarios for mine blocks dialog

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wallet): close Mine Blocks dialog after Cancel/Mine click

The dialog stayed open (in a broken state) after clicking Mine or Cancel
because the local `open` variable was written back to `is_open` after the
dialog state had already been reset. Pass `is_open` directly to egui's
`.open()` and use a separate `close` flag for button-triggered dismissal.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wallet): address audit findings for Mine Blocks dialog

- Wrap `generate_to_address` in `spawn_blocking` to avoid blocking
  the async runtime thread (HIGH)
- Replace `.expect()` on core client lock with `.map_err()?` for
  graceful error handling instead of panic (HIGH)
- Cap block count at 1000 to prevent resource exhaustion on the
  Core node (HIGH)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wallet): filter non-numeric input in Mine Blocks block count field

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wallet): address review comments on Mine Blocks dialog

- Replace assume_checked() with require_network() for address
  validation (CodeRabbit #2)
- Use styled Frame-with-dismiss error display matching Send dialog
  pattern (CodeRabbit #3)
- Don't open dialog when no wallet selected; show MessageBanner
  instead (CodeRabbit #4)
- Extract shared load_bip44_external_addresses() helper to eliminate
  near-duplicate code between mine and receive dialogs (CodeRabbit #5)
- Add backend-side network guard (Regtest/Devnet) for defense-in-depth
  (CodeRabbit #6)
- Rename shadowed ctx binding to refresh_ctx for clarity (CodeRabbit #7)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(wallet): address remaining review comments on Mine Blocks dialog

- Change MineBlocksSuccess(usize) to MineBlocksSuccess(u64) for
  type consistency with block_count parameter (Claudius #5)
- Align dialog close pattern with Send/Receive: use local `open`
  variable for egui X button, reset state inside closure for
  Cancel/Mine buttons (Claudius #6)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
shumkov added a commit that referenced this pull request Apr 12, 2026
Addresses all critical and important findings from the M2 code review.

Critical #1: write_core SPV height
  Changed `WHERE seed_hash = ?2` to `WHERE wallet_id = ?2` in the
  wallet.last_terminal_block UPDATE. The persister passes wallet_id
  bytes; the old SQL matched against the seed_hash column which holds
  different bytes — 0 rows matched, sync height was silently lost on
  every restart.

Critical #2: handle_wallet_unlocked shielded init
  After register_with_platform_wallet_manager (which may re-key the
  map), use wallet_id from the Wallet struct for subsequent lookups
  (initialize_shielded_wallet, queue_shielded_sync) instead of the
  stale seed_hash variable.

Critical #3: WalletDerivationPath stores wrong key
  Changed qualified_identity_public_key.rs to populate
  wallet_seed_hash with wallet.wallet_id() instead of
  wallet.seed_hash(). Post-v40, determine_wallet_info() returns
  wallet_id bytes, matching the map key.

Important #4/#5: wallet selection + UI validation
  wallets_screen uses wallet_id for persist_selected_wallet_hash
  and the arc-matches validation check.

Finding #6: shielded_wallet_meta in v40 DELETE sweep
  Added to the cache nuke table list.

Wallet.wallet_id is now non-optional (WalletId, not Option<WalletId>).
The wallet migration screen (to be implemented) ensures every wallet
has wallet_id before the main UI loads. WalletArcRef.seed_hash
renamed to wallet_id. No more map_key() fallback — wallet_id is
always the canonical key.

get_wallets() uses [0u8; 32] as sentinel for NULL wallet_id rows
(password wallets pre-migration). The migration screen detects this
sentinel and prompts for unlock.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
shumkov added a commit that referenced this pull request Apr 12, 2026
… stale docs

Review finding #1 (Critical): Silent data loss on proof encoding
  write_asset_locks used unwrap_or_default() on bincode encode failure,
  silently writing an empty blob. Changed to propagate the error via
  SqlitePersistError::Encode so the flush fails visibly instead of
  losing the proof.

Review finding #4 (Important): Dead code cleanup
  Deleted store_asset_lock_transaction and
  update_asset_lock_chain_locked_height from Database — all callers
  were removed in Item 8.1d. Removed unused imports (Hash, serialize).

Review finding #5 (Important): Stale doc comment
  Updated platform_wallet_bridge.rs module docs to reflect the
  current state: WalletId = SHA256(root_pub_key || chain_code),
  both AppContext and PlatformWalletManager keyed consistently.

Review finding #2 (FK mismatch) acknowledged as pre-existing:
  asset_lock_transaction.wallet FK references wallet(seed_hash) but
  stores wallet_id bytes. FKs are off at runtime. Proper fix deferred
  to the wallet table PK migration.

Review finding #3 (no round-trip test) acknowledged: adding a test
  for write_asset_locks + load_asset_locks is a follow-up.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
shumkov added a commit that referenced this pull request Apr 12, 2026
Critical #2 from the 7-agent review: dashpay_payments had no
wallet_id column and no filters in the load query — all payments
from all wallets were loaded into every wallet's changeset.

Changes:
- dashpay_payments schema: add `wallet_id BLOB` column
- write_identity_dashpay_subset: now takes wallet_id parameter,
  writes it in the INSERT
- load() payments section: filters by `WHERE wallet_id = ?1`

No migration needed — v40 already deleted all rows from this table.
The fresh-install schema now includes the column.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
shumkov added a commit that referenced this pull request Apr 13, 2026
Add wallet_id BLOB column to dashpay_profiles, dashpay_contacts,
and dashpay_contact_requests schemas. Write paths store wallet_id.
Load paths filter by wallet_id.

Same pattern as the dashpay_payments fix (Critical #2). No migration
needed — v40 already nuked all data in these tables. The fresh-
install schema now includes the column.

This prevents cross-wallet data contamination: loading wallet A no
longer sees profiles, contacts, or contact requests from wallet B.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
lklimek added a commit that referenced this pull request Jun 18, 2026
* docs(overlay): requirements + UX spec for blocking progress overlay

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): test case specification

49 TCs covering FR-1..FR-10, NFR-1..NFR-6, and R-7 kittest checklist.
Items depending on the FR-10 concurrent-overlay architecture decision
(stack vs. replace vs. reject) and the stuck-overlay threshold (R-4)
are marked [depends on 1d] for Nagatha to resolve.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(overlay): development plan and architecture decisions

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(overlay): generic button facility + Component trait conformance

Folds in two user-mandated redesigns of the blocking progress overlay
that the prior session did not land:

Redirect 1 — generic button facility (no first-class Cancel). The overlay
knows nothing about cancellation. `OVERLAY_CANCEL_ACTION_ID`, `with_cancel`,
`CANCEL_LABEL`, and the Esc->Cancel routing are gone. A caller attaches a
generic button via `OverlayConfig::with_button(id, label)` /
`OverlayHandle::with_button(id, label)`, choosing its own opaque action id
and label. A click enqueues the id; the owning screen drains it via
`take_actions` and runs whatever logic it wants — including its own
cancellation. Esc/Tab/Enter are swallowed so a hard block is never
keyboard-dismissable.

Redirect 2 — `Component` trait conformance (placement legitimacy for
`src/ui/components/`). `ProgressOverlay` is now a struct holding
`state: Option<OverlayState>`; `Component::show` renders that instance's
card and returns `ProgressOverlayResponse` (`DomainType = String`, the
clicked action id), with `current_value()` reporting the last clicked id.
The global `render_global` path is preserved as the production entry point;
the instance `show()` is additive, mirroring `MessageBanner`.

Also: clamp the card to the window so it never runs off-screen in a narrow
window (FR-6); settle the centered card in the kittest click/focus cases
before interacting (anchored CENTER_CENTER needs a few frames to cache its
size). Docs: dev-plan gains a post-outage note superseding D-5/FR-7;
test-spec reframes the Cancel-specific cases to the generic-button model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): align D-5 and risk notes to the generic-button redesign

Rewrites the D-5 decision body and §8 risk #3 in place to drop the stale
`with_cancel`/`OVERLAY_CANCEL_ACTION_ID` framing and describe the generic
`with_button(id, label)` facility instead — consistent with the post-outage
note added at the top of the plan. Documentation only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(overlay): add ignored probe proving button-less keyboard-block gap (QA-001)

TC-OVL-029 only exercises a with-button overlay, where the first button steals
focus on raise, so typing is blocked incidentally rather than by the overlay's
input handling. This probe raises a button-less hard block over an
already-focused field (the J-2 broadcast / J-4 migration case) and asserts
FR-8 AC-8.2: typed input must not reach the field beneath.

The probe currently FAILS — render_global filters Tab/Enter/Esc only after the
beneath widgets have consumed input that frame, and a button-less overlay has
no first button to steal focus, so keystrokes leak into the focused field
beneath. Marked #[ignore] so the suite stays green; un-ignore once the overlay
claims keyboard focus / consumes text while active.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): frame-start input claim (QA-001) + clear action queue on switch (SEC-007)

Implements two QA-wave findings from the design addendum (§1 A-2, §2 A-4):

- QA-001 (HIGH) — button-less keyboard/text leak. `render_global`'s key filter
  runs at end-of-frame, one frame too late: a button-less hard block raised over
  an already-focused field let typed characters reach the field beneath (the
  J-2 broadcast / J-4 migration case). New `ProgressOverlay::claim_input(ctx)`,
  called near the top of `AppState::update` (before the panels) and gated on no
  active secret prompt, releases beneath text-edit focus and strips `Event::Text`
  plus the navigation/confirm keys (Tab, Enter, Escape, Space, arrows). The
  `#[ignore]`d probe `qa_buttonless_overlay_blocks_typing_into_focused_field_beneath`
  is un-ignored and now passes.

- SEC-007 — `clear_all_global` (network switch) now also drains the action queue,
  so a click queued just before the switch cannot survive into the new context
  and be mis-dispatched.

Adds inline unit tests: `claim_input` strips text + nav/confirm keys while a
block is up and is a no-op when idle; `clear_all_global` clears the queue.

Scope note: this is a partial pass on the QA list. The end-of-frame filter in
`render_global` is kept as belt-and-suspenders and is NOT yet gated on a secret
prompt (marked TODO at the call site — blocker #2's full fix removes it and
routes the keyboard tests through `claim_input`). Still outstanding from the
addendum / task: A-1 no-progress watchdog, A-3 keyed `OverlayHandle::take_actions`
+ `sweep_orphan_actions`, instance `Component::show` focus-trap separation,
secondary-button styling, 30s clock seam, Foreground layering, and doc sync.
Also adds Nagatha's `04-design-addendum.md` (the authoritative spec).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): QA-wave hardening — watchdog, keyed dispatch, secondary buttons, Foreground, focus separation

Implements the design addendum (§1/§2) plus the rest of the QA fix list and the
three cross-finding reconciliations. All on top of the earlier claim_input/SEC-007 pass.

Addendum §1 (safety-valve / A-1):
- 120 s no-progress watchdog: STUCK_OVERLAY_WATCHDOG_THRESHOLD, OverlayState
  { last_progress_at, watchdog_logged }, watchdog_tripped() clock seam, escalated
  STUCK_WATCHDOG_REASSURANCE (replaces the soft line, never stacks), one-shot
  tracing::error! (no flaky time-based panic). last_progress_at is bumped on a real
  content change, reusing log_overlay_state's change detection, so a progressing
  multi-step flow never trips it.

Addendum §2 (action-dispatch / A-3, SEC-007/A-4):
- Actions are keyed: OverlayAction { key, action_id }. OverlayHandle::take_actions()
  drains only its own ids (FIFO); clear() purges its key's pending ids; the static
  take_actions is demoted to sweep_orphan_actions() (dead-owner ids only). app's
  drain logs orphans. clear_all_global already clears the queue (SEC-007).

Reconciliations (lead brief):
1. SEC-004/F-1 — claim_input is gated on no active secret prompt at the app site,
   and render_global no longer strips keyboard at all (the gated claim_input is the
   sole keyboard block); release-beneath-focus is button-less only (stop_text_input
   clears ANY focus, which would steal a button's focus otherwise).
2. QA-002 — claim_input strips Space (and render_global's removal means the kittest
   keyboard path runs through claim_input). TC-OVL-044 now also presses Space.
3. QA-003 — render_card/render_buttons take trap_focus; the instance Component::show
   passes false so it never seizes the host screen's focus or installs the lock.

Rest of the list:
- SEC-002: overlay dim/sink/card raised to Order::Foreground (above ComboBox /
  autocomplete / SelectionDialog popups); passphrase modal also raised to Foreground
  so it stays above the overlay (R-1, TC-OVL-048).
- F-3/4/7: ButtonStyle { Primary, Secondary }, with_secondary_button on
  OverlayConfig/OverlayHandle/instance, ConfirmationDialog-style right_to_left layout
  (primary right, secondary left).
- SEC-005: corrected the Send+Sync note to the real invariant (UI-thread-only ops).
- F-6: Elapsed uses a named placeholder. SEC-006: log-content doc note on show_global.
- QA-007: instance clear() makes the empty-response path reachable.
- QA-008: TC-OVL-013b asserts elapsed >= 2s; TC-OVL-021 also bounds vertically.

Tests: un-ignored qa_buttonless probe; new inline tests (watchdog threshold/clock-reset/
one-shot, keyed FIFO/isolation/orphan-sweep, QA-007); new kittest reconciliations
(render_global keeps keyboard for the prompt; instance show leaves host focus navigable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): sync requirements/dev-plan to the shipped design + add UX user story

- 01-requirements-ux.md: add a supersession callout flagging the Cancel-era
  items now overtaken by the generic-button + watchdog + claim_input redesign
  (FR-7, AC-7.3/7.4, NFR-3 AC-3b, AC-8.4, AC-10.5, J-1/J-2/J-3, §6.3-6.5), pointing
  at the dev-plan post-outage note, the addendum, and the code as source of truth.
- 03-dev-plan.md: drop OVERLAY_CANCEL_ACTION_ID from the §2 re-export row; mark the
  §3 API block superseded (real surface is with_button/with_secondary_button, keyed
  take_actions/sweep_orphan_actions, OptionOverlayExt::raise, the watchdog); fix the
  §4.1 drain comment; update the §9 D-4/D-5 rows.
- user-stories.md: add UX-001 (blocking please-wait overlay; cannot fire a
  conflicting second action), tagged across personas, [Implemented].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(overlay): close re-QA coverage residuals RQ-1/RQ-2/RQ-3

RQ-1 (security) — the app.rs secret-prompt gate had no test; deleting
`if self.active_secret_prompt.is_none()` left every test green. Extracted the
gate into `AppState::claim_overlay_input` (called from `update`) and added a
`#[cfg(feature = "testing")]` seam (`AppState::test_set_secret_prompt_active`,
`ActivePrompt::test_stub`). New AppState-level kittest
`rq1_appstate_secret_prompt_gate_keeps_prompt_typeable_over_overlay` drives the
REAL `update()` loop with a prompt active over a button-less overlay and asserts
the prompt input keeps focus AND accepts typed text (types a passphrase + Enter,
the prompt submits and closes). Deleting the gate makes `claim_input`
(button-less → `stop_text_input`) steal focus and strip the keys, failing both
assertions. Extended `tc_ovl_048` to assert prompt interactivity (submit button
renders + input holds focus), not just visibility.

RQ-2 — added a `#[cfg(feature = "testing")]` clock seam `OverlayHandle::backdate`
(shifts `created_at` + `last_progress_at` into the past). New kittest
`tc_ovl_047b_threshold_reveals_via_clock_seam` renders past 30 s and 120 s and
asserts: the soft "This is taking longer than usual." line + Elapsed
force-reveal, then `STUCK_WATCHDOG_REASSURANCE` REPLACING the soft line (never
both) — the addendum §1 obligation that was previously only flag-checked.

RQ-3 — reframed the `tc_ovl_047` doc comment (the escape-hatch button is a
deliberate v1 non-feature per addendum §1, not a deferred T7 TODO); added a
"(superseded)" note to 01-requirements-ux.md's "what to reuse" list where it
still cited `with_cancel`/`with_action`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): close QA residuals — README catalog entry, requirements Cancel reconciliation, T7 TODO

Post-gate cleanup on the blocking progress overlay (gate green):

- README: add a ProgressOverlay row to the Feedback Components table,
  covering show_global/render_global, with_button(id, label), the 120s
  watchdog, and companions OverlayConfig/OverlayHandle/OptionOverlayExt/
  ProgressOverlayResponse.
- 01-requirements-ux.md: reconcile the remaining literal-Cancel acceptance
  criteria (intro line, AC-7.3, AC-8.4, the §6.5 "Visible, cancelable" row,
  R-3) to the shipped generic-button model, matching the top supersession
  callout — Esc/Tab/Enter/Space are swallowed and there is no built-in Cancel.
- app.rs: mark drain_overlay_actions with a TODO(T7) recording that an overlay
  button can only stop waiting (not abort) until the BackendTask system gains
  cooperative cancellation; until then the 120s watchdog (see
  progress_overlay.rs) bounds every block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(overlay): hard-block the UI during startup/Connect SPV sync

Raises the blocking ProgressOverlay while a startup- or Connect-initiated SPV
sync runs, and lowers it when the chain becomes usable (Synced) or fails (Error).

Honors the overlay's C1/C2 caller contract. SPV sync is UNBOUNDED — it can wait
indefinitely for peers — so a button-less block would trap the user. The block
therefore carries a "Continue in the background" escape
(`SYNC_CONTINUE_BACKGROUND_ACTION`); clicking it lowers the block while sync
proceeds safely in the background (read-only — nothing is stranded). C1: the
block also always lowers on its own at a terminal state.

- `AppState`: `sync_overlay`/`sync_block_active`/`sync_overlay_dismissed` fields;
  armed on boot auto-start and on the manual `StartSpv` (Connect); reset on
  network switch so the handle never goes stale.
- New per-frame `update_sync_overlay` driver (called beside
  `update_connection_banner`) applies a pure, unit-tested policy `sync_block_step`
  (Block / Release / Idle) and drains the escape click.
- Pure decision + descriptions are i18n-clean single sentences.

Tests: 6 inline unit tests of `sync_block_step` (inactive→Idle; active+not-usable
→Block; terminal→Release for both dismissed states; dismissed→Idle; stable action
id; sentence descriptions). New `#[cfg(feature = "testing")]` integration kittest
`task9_sync_overlay_blocks_lowers_on_synced_and_on_escape` drives the real
`update_sync_overlay` against a forced connection state: asserts the block raises
while connecting, lowers on Synced (C1), and lowers on the escape click (C2 — user
never trapped). Adds `ConnectionStatus::set_overall_state` + AppState
`test_activate_sync_block`/`test_drive_sync_overlay` test seams.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(overlay): align SPV-sync block to the approved spec

Reworks the SPV-sync overlay wiring (introduced in the previous commit) to the
user-approved design. Net behaviour: while the active context is Connecting or
Syncing the overlay hard-blocks the UI, lowering when the chain becomes usable
(Synced), fails (Error), or drops (Disconnected).

Changes vs the first cut:
- Keyed purely to the live connection state + a per-episode dismissal flag — drops
  the separate "armed" flag, so any sync episode (startup, Connect, or reconnect)
  blocks. Pure policy renamed `sync_block_step` -> `spv_block_step`
  (Block/Release/Stand); Disconnected now Releases + re-arms.
- Escape is now an always-visible SECONDARY button "Continue in the background"
  (id renamed `spv:sync:continue_background`); fields renamed to
  `spv_overlay`/`spv_overlay_dismissed`; method renamed `update_spv_overlay` and
  driven BEFORE `update_connection_banner`.
- Live content: description = `spv_phase_summary(progress)` (else a generic
  connecting line), plus a "Step N of 5" counter via new
  `connection_status::spv_phase_step` (Headers=1 … Blocks=5). Raises once per
  episode, then updates in place.
- Suppresses the redundant Connecting/Syncing connection-banner text while the
  overlay is up (don't double-shout); keeps Error/Disconnected banners.

C1/C2 contract preserved: SPV sync is UNBOUNDED, so the escape (lower while sync
continues safely in the background — read-only, nothing stranded) guarantees the
user is never trapped; episode-ending states always release.

Tests updated: 4 inline `spv_block_step` unit tests; the integration kittest
`task9_spv_overlay_blocks_lowers_on_synced_and_on_escape` now also asserts the
secondary escape button, re-raise for a fresh episode, no re-raise within a
dismissed episode, and re-raise after the episode ends. Test seams renamed to
`AppState::test_drive_spv_overlay` (+ `ConnectionStatus::set_overall_state`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): reconcile SPV-sync block decision (F-SPV-1) + phase-step test (F-SPV-2)

F-SPV-1 — the user-authorized SPV-sync hard-block + always-visible "Continue in
the background" escape contradicted three docs written for the standalone
overlay. Reconcile the docs to the decision (the feature is correct; the docs
were stale) so a future dev does not "correctly" remove the button per old docs:

- docs/user-stories.md: carve out the SPV-sync exception in UX-001's "no
  background/dismiss button" guarantee, and add UX-002 — the blocking SPV-sync
  overlay with the always-on "Continue in the background" escape (tagged across
  personas, [Implemented]).
- 01-requirements-ux.md §5: supersession note — the user chose to block the
  startup/Connect get-connected sync; the power-user concern is mitigated by the
  escape (sync is read-only and safe to background); this is the overlay's first
  adopter.
- 04-design-addendum.md A-1: record that A-1's "ship NO dismiss/background button
  in v1" was scoped to unsafe-to-interrupt ops whose safety rests on boundedness;
  for the unbounded-but-read-only SPV-sync adopter the C2 "never trap the user"
  guarantee is met by the always-on escape, which must NOT be removed.

F-SPV-2 — the granular phase progress (spv_phase_summary description +
"Step N of 5" via spv_phase_step) was already wired in the previous commit; adds
a unit test locking the active-phase → step mapping and the summary text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): scope SPV block to user-initiated sync + de-jargon copy (F-SPV-A/B/E)

F-SPV-A (sev-2/1 regression, introduced by the prior refactor) — the SPV block
fired on ANY Connecting/Syncing, so an ambient mid-session reconnect, or the SPV
engine flipping Synced→Syncing as it processes each new block (event_bridge
on_progress maps !is_synced() → Syncing), would hard-block a working user.
Re-introduce a startup/Connect-SCOPED arming gate:
- `spv_block_armed` flag, armed only on boot auto-start and the Connect button
  (AppAction::StartSpv); reset on network switch.
- `spv_block_step(armed, dismissed, state)`: !armed → Idle (never block); armed +
  Synced/Error → Disarm (lower + clear armed); armed + Connecting/Syncing/
  Disconnected → Block (or Stand if dismissed). Once disarmed, ambient sync never
  re-blocks until the next user-initiated episode.

F-SPV-B (sev-2) — the block description showed blockchain jargon ("Headers:
12345 / 27000 (45%)") to the Everyday User. Replace with plain complete
sentences ("Connecting to the Dash network." / "Syncing with the Dash network.");
keep the jargon-free "Step N of 5" counter (via spv_phase_step) as the
determinate granularity. spv_phase_summary stays (still used by wallets_screen);
it is just no longer the overlay description. UX-002 acceptance criterion updated
to stop enshrining the jargon.

F-SPV-E (sev-4) — AppAction::StartSpv set an orphaned Info banner whose handle was
dropped (could not be cleared by the overlay's banner suppression). Dropped it;
the block conveys "connecting" and the error path still surfaces via replace_global.

Tests: spv_block_step unit tests rewritten around the arming gate —
`unarmed_never_blocks` is the regression guard (ambient sync never blocks);
`armed_terminal_state_disarms`; jargon-free-description test. The integration
kittest is rewritten to `task9_spv_overlay_armed_scope_disarm_and_escape`: an
un-armed Connecting does NOT block, an armed one does, Synced disarms, ambient
sync afterward does NOT re-block, the escape lowers without re-raising, and only a
fresh armed episode re-blocks. New `AppState::test_arm_spv_block` seam.

is_synced() finding: `EventBridge::on_progress` (event_bridge.rs) does map
`!is_synced()` → `SpvStatus::Syncing`, so overall_state CAN flip Synced→Syncing on
per-block catch-up — the arming gate makes that harmless (disarmed after the
initial episode).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): address review findings — deterministic elapsed test, SPV phase-count constant, input-claim hardening, doc drift

- Replace the 2.1s wall-clock sleep in tc_ovl_013b with the deterministic
  `backdate` clock seam (gated behind `testing`), mirroring tc_ovl_047b — zero
  wall-clock waiting; asserts the elapsed readout counts up to a concrete 2s.
- Add `SPV_SYNC_PHASE_COUNT` next to `spv_phase_step` as the single source of
  truth for the "Step N of 5" total; reference it at both app.rs call sites and
  guard the max step with a `debug_assert!` so it cannot silently drift.
- Delete the misplaced orphan-sweeper paragraph from `claim_overlay_input`'s doc
  (it belongs to `drain_overlay_actions`, which already carries it).
- Reconcile the `Order::Middle` → `Order::Foreground` doc drift: supersession
  callouts in the dev plan §4.2/§4.3 and the kittest module doc, citing SEC-002.
- Drop the dead `CONNECTING_MSG`/`replace_global` swap in the StartSpv failure
  path (the "Connecting…" banner was removed in F-SPV-E) for a plain
  `set_global(...).with_details(e)`; fix the now-stale comment.
- Extend `claim_input`'s per-frame strip to also drop Backspace, Delete, Home,
  End, PageUp, PageDown and the Copy/Cut/Paste clipboard events; add a kittest
  locking the new classes via event survival + the field-beneath contract.
- Strengthen the SEC-001 lifecycle rustdoc on `show_global` /
  `show_global_spinner_only` (button-less blocks need a frame-driven reconcile
  owner or an escape; the watchdog only logs).
- Nits: UX-001 "developer warning" → "developer error"; "while a armed" →
  "while an armed". Add deferred TODOs (SEC-002-pointer, SEC-001, RUST-006).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): close one-frame SPV block gap, fix slow-phase watchdog, align API to MessageBanner

Three changes to the blocking progress overlay + SPV-sync hard-block:

A — Close the one-frame interactive gap. `update_spv_overlay` now runs at the
top of `AppState::update`, BEFORE `claim_overlay_input`, the visible screen
`ui()`, and `render_global`. A freshly-armed episode therefore raises, claims
input, AND paints on the same frame; previously the block was raised only after
`render_global`, leaving the frame right after Connect/arming fully interactive
(effective at frame N+2). The connection banner still reads the block state
afterwards, so its Connecting/Syncing suppression is unchanged.

B — Stop the 120s no-progress watchdog from falsely escalating on slow phases.
A single SPV phase running >120s (e.g. Headers on a slow link) wrote a constant
(description, step), so `log_overlay_state` never reset `last_progress_at` and
the watchdog tripped — swapping to the STUCK copy and firing the one-shot
dev-error, the exact false signal the SPV escape was meant to avoid. A hidden,
monotonic `progress_token` (step in the high 32 bits, advancing height in the
low 32) is threaded from `ConnectionStatus` into the overlay; an advancing token
resets the watchdog even when the shown (description, step) is unchanged. The
token is NEVER rendered — copy is byte-for-byte unchanged and the jargon-free
test stays green. Distinct from TODO(SEC-001), which is left in place.

C — Align the overlay public API toward MessageBanner so migrating from the
banner is a name-for-name swap. One-way (overlay → banner), no capability loss:
  with_button(id, label)            -> with_action(label, action_id)
  with_secondary_button(id, label)  -> with_secondary_action(label, action_id)
  show_global(...)                  -> set_global(...)  (return type kept)
  show_global_spinner_only(...)     -> set_global_spinner_only(...)
`OptionOverlayExt::raise` keeps its name: renaming to `replace` (the banner
analogue) would be shadowed by the inherent `Option::replace`, so every
`slot.replace(ctx, desc, config)` call would fail with E0061 (verified). A doc
note records why. `render_global`, `claim_input`, the watchdog, `OverlayConfig`,
and all handle progress methods are untouched. Rustdoc, the README catalog row,
and the design-doc API references are updated to the new names; the banner's own
`MessageBanner::show_global(ui)` render path is left alone.

Tests: new real-AppState kittest for the one-frame gap (same-frame paint), new
backdate kittest + unit tests for the token-driven watchdog reset, and a
`spv_progress_token` monotonicity unit test. fmt + clippy clean; kittest 138
passed; lib 926 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(overlay): keyboard-reachable escape for the SPV hard block (QA-002 refinement)

Resolves the TODO(RUST-006) marker: the SPV-sync hard block's "Continue in
the background" escape was mouse-only, stranding keyboard-only / assistive-tech
users behind the UNBOUNDED block. Hard blocks strip Enter/Space every frame
(the deliberate QA-002 rule, guarded by TC-OVL-044), so the escape could not be
activated by keyboard.

Add a per-block opt-in — `OverlayConfig::with_keyboard_escape(action_id)` and
`OverlayHandle::with_keyboard_escape(action_id)` — that designates ONE action as
the single keyboard-reachable escape. The general rule is unchanged: a block
with no designated escape stays fully keyboard-blocked.

- claim_input: when the active block designates an escape AND that escape button
  is *confirmed* to hold focus (its egui id was recorded by last frame's
  render_buttons and still matches the focused widget), Enter/Space pass through;
  every other key, and the raise frame (focus not yet confirmed), stays stripped.
  So the passthrough can never reach a widget beneath.
- render_buttons: for an opt-in block, pin focus to the designated escape (match
  by action id) — re-requested every frame and locked — and record its id for the
  claim_input gate.
- SPV adopter (update_spv_overlay): mark "Continue in the background" as the
  keyboard escape; it remains unconditionally present whenever the block is up.

Tests (egui_kittest — the reliable check for input/focus):
- TC-OVL-051/052: Enter / Space activate the focus-pinned escape.
- TC-OVL-053: a TextEdit beneath never receives Enter; Tab and a backdrop click
  cannot move focus off the escape.
- task9_spv_escape_is_keyboard_activatable: the REAL SPV block lowers on Enter.
- TC-OVL-044 and the keyboard-block tests stay green (general rule intact).
- Unit tests for the opt-in API + the claim_input safety gate.

Docs: QA-002 design note + NFR-3 accessibility ACs, test-spec, user story UX-002,
and the public rustdoc updated to state the refined rule.

cargo +nightly fmt: clean. clippy --all-features --all-targets -D warnings: 0.
kittest --all-features: 142 passed. lib --all-features: 928 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): activate keyboard escape at frame start (SEC-001, SEC-002)

The opt-in keyboard escape used to "keep" Enter/Space in `i.events` only
while the escape button was confirmed-focused, and `render_buttons`
re-requested that focus every frame. Two bugs fell out of it:

- SEC-001: `render_global` runs before `render_secret_prompt`, so the
  per-frame focus re-request stole focus from a passphrase modal raised
  above the block — the field went un-typeable and Enter fired the escape
  instead of submitting. Realistic on a cold-start migration prompt over
  the startup SPV auto-sync block.
- SEC-002: the kept Enter/Space reached the beneath screen's `ui()` (which
  runs before `render_global`), so a focus-independent global key handler
  beneath (info_popup / selection_dialog / address_input) observed the key
  — a single Enter/Space leaked through the "hard" block.

Unified fix: move escape activation to frame start in `claim_input`. When
a block designates a `keyboard_escape_action` and Enter/Space is pressed,
enqueue that action directly (the same queue a click feeds) and strip the
key with all the others. Activation no longer needs the button focused
(SEC-001) and the key never survives to a widget beneath (SEC-002). Focus
on the escape is now purely visual and is suppressed while a secret prompt
is active — `render_global` takes a `secret_prompt_active` flag mirroring
the `claim_overlay_input` gate. A non-opted block still strips Enter/Space
and activates nothing; Esc still never dismisses.

Drops the now-dead `escape_focus_id` field and confirmed-focus logic.

Also in this rework:
- SEC-003 residual: TODO documenting the narrow constant-height >120s
  watchdog false-alarm (benign log + accurate copy, no abort) pending a
  coarser SDK liveness signal.
- RUST-001: `keyboard_escape_action.clone()` -> `as_deref()` in
  render_buttons (no per-frame String alloc).
- RUST-002: corrected the stale `log_overlay_state` call comment to note
  the watchdog also resets on a hidden progress_token advance.
- PROJ-001: render_global rustdoc now cross-references
  `MessageBanner::show_global` for the set_global/render_global vs
  set_global/show_global asymmetry.

Tests (egui_kittest, the authority for input/focus):
- sec001_* drives the real AppState loop with an escape block beneath an
  active secret prompt: the prompt keeps focus, Enter submits it, the
  escape action is never enqueued.
- sec002_* a focus-independent `key_pressed(Enter)` sentinel beneath an
  escape block never fires; the Enter is stripped and routed to the escape.
- Replaced the obsolete confirmed-focus unit test with one asserting the
  frame-start enqueue + strip. TC-OVL-044/048/051/052/053, rq1, and
  task9 escape tests stay green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): document hidden progress_token watchdog reset; pin cross-phase token invariant

- RUST-003: strengthen `spv_progress_token_advances_with_height_and_is_monotonic`
  with a cross-phase assertion — a later phase (masternodes, step 2) at
  height 0 must out-rank an earlier phase (headers, step 1) near the u32
  ceiling, pinning the high-bits-dominate invariant the test name claims.
- DOC-001: design-addendum §1 now documents the hidden progress_token
  watchdog reset — `last_progress_at` resets on a shown (description, step)
  change OR a token advance; the token is never rendered and its reset is
  decoupled from the once-per-content-change log (NFR-5). Corrected the
  now-wrong "only when content changes" instructions and the test note, and
  the superseded confirmed-focus escape description.
- DOC-002: dev-plan §3 superseded block — dropped `with_action` from the
  "there is no ..." list (it is the real shipped builder), resolving the
  self-contradiction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): keep escape button mouse-clickable after a backdrop press

The blocking ProgressOverlay rendered its dim/pointer sink and its content
card as peer Order::Foreground areas. egui auto-raises any interactable Area
to the top of its Order on a pointer press (area.rs bring-to-front), so a
single click on the dim backdrop floated the full-window sink above the card
and permanently buried its buttons beneath the click-absorbing sink. For the
unbounded SPV-sync block that meant the "Continue in the background" escape
became unclickable with the mouse — force-quit was the only exit.

Pin the card as a sublayer of the sink (ctx.set_sublayer): egui places a
sublayer directly above its parent after the per-frame order sort, so the
card-above-sink z-order now holds by construction, immune to the bring-to-
front race. The sink still blocks every widget beneath, and the secret-prompt
window still wins above the overlay.

Add TC-OVL-054: press the backdrop, then click the escape at its own position
and assert the action enqueues. It fails before this change (the sink eats the
click) and passes after. Existing button-click tests never press the backdrop
first, so they missed this path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): arm the SPV-sync block on the post-onboarding auto-start path

The blocking SPV-sync overlay only shows for an *armed* episode
(spv_block_step returns Idle when !armed). Two production paths armed it
— boot auto-start (via the constructor: spv_block_armed = boot_auto_start_spv)
and the Connect button (AppAction::StartSpv) — but the third did not:
AppAction::OnboardingComplete calls try_auto_start_spv(), which spawned
ensure_wallet_backend_and_start_spv WITHOUT setting spv_block_armed.

So a fresh user who enabled auto-start and then finished onboarding
(onboarding_completed was false at boot, so boot_auto_start_spv was false
and the flag stayed false) would sync with no blocking overlay at all —
exactly the journey the overlay exists to cover.

Fix: arm the block inside try_auto_start_spv when the start actually
fires (spv_block_armed = true; spv_overlay_dismissed = false), mirroring
AppAction::StartSpv. This is the single correct arming point for that
caller — the method takes &mut self now, and the active context is cloned
up front so the mutation does not alias the borrow. Boot auto-start is
untouched (it arms via the constructor and inlines its own start).

Test: fspv_a_onboarding_auto_start_arms_spv_block drives the REAL
try_auto_start_spv via a testing-only seam and asserts both the armed
flag flips and that an armed Connecting sync then raises the overlay.
Verified the test fails when the arm is removed and passes with it.

Verified: cargo clippy --all-features --all-targets -D warnings (0
warnings), cargo test --test kittest (146 ok).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lklimek added a commit that referenced this pull request Jun 22, 2026
…(Bucket A) (#866)

* docs(overlay): requirements + UX spec for blocking progress overlay

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): test case specification

49 TCs covering FR-1..FR-10, NFR-1..NFR-6, and R-7 kittest checklist.
Items depending on the FR-10 concurrent-overlay architecture decision
(stack vs. replace vs. reject) and the stuck-overlay threshold (R-4)
are marked [depends on 1d] for Nagatha to resolve.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(overlay): development plan and architecture decisions

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(overlay): generic button facility + Component trait conformance

Folds in two user-mandated redesigns of the blocking progress overlay
that the prior session did not land:

Redirect 1 — generic button facility (no first-class Cancel). The overlay
knows nothing about cancellation. `OVERLAY_CANCEL_ACTION_ID`, `with_cancel`,
`CANCEL_LABEL`, and the Esc->Cancel routing are gone. A caller attaches a
generic button via `OverlayConfig::with_button(id, label)` /
`OverlayHandle::with_button(id, label)`, choosing its own opaque action id
and label. A click enqueues the id; the owning screen drains it via
`take_actions` and runs whatever logic it wants — including its own
cancellation. Esc/Tab/Enter are swallowed so a hard block is never
keyboard-dismissable.

Redirect 2 — `Component` trait conformance (placement legitimacy for
`src/ui/components/`). `ProgressOverlay` is now a struct holding
`state: Option<OverlayState>`; `Component::show` renders that instance's
card and returns `ProgressOverlayResponse` (`DomainType = String`, the
clicked action id), with `current_value()` reporting the last clicked id.
The global `render_global` path is preserved as the production entry point;
the instance `show()` is additive, mirroring `MessageBanner`.

Also: clamp the card to the window so it never runs off-screen in a narrow
window (FR-6); settle the centered card in the kittest click/focus cases
before interacting (anchored CENTER_CENTER needs a few frames to cache its
size). Docs: dev-plan gains a post-outage note superseding D-5/FR-7;
test-spec reframes the Cancel-specific cases to the generic-button model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): align D-5 and risk notes to the generic-button redesign

Rewrites the D-5 decision body and §8 risk #3 in place to drop the stale
`with_cancel`/`OVERLAY_CANCEL_ACTION_ID` framing and describe the generic
`with_button(id, label)` facility instead — consistent with the post-outage
note added at the top of the plan. Documentation only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(overlay): add ignored probe proving button-less keyboard-block gap (QA-001)

TC-OVL-029 only exercises a with-button overlay, where the first button steals
focus on raise, so typing is blocked incidentally rather than by the overlay's
input handling. This probe raises a button-less hard block over an
already-focused field (the J-2 broadcast / J-4 migration case) and asserts
FR-8 AC-8.2: typed input must not reach the field beneath.

The probe currently FAILS — render_global filters Tab/Enter/Esc only after the
beneath widgets have consumed input that frame, and a button-less overlay has
no first button to steal focus, so keystrokes leak into the focused field
beneath. Marked #[ignore] so the suite stays green; un-ignore once the overlay
claims keyboard focus / consumes text while active.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): frame-start input claim (QA-001) + clear action queue on switch (SEC-007)

Implements two QA-wave findings from the design addendum (§1 A-2, §2 A-4):

- QA-001 (HIGH) — button-less keyboard/text leak. `render_global`'s key filter
  runs at end-of-frame, one frame too late: a button-less hard block raised over
  an already-focused field let typed characters reach the field beneath (the
  J-2 broadcast / J-4 migration case). New `ProgressOverlay::claim_input(ctx)`,
  called near the top of `AppState::update` (before the panels) and gated on no
  active secret prompt, releases beneath text-edit focus and strips `Event::Text`
  plus the navigation/confirm keys (Tab, Enter, Escape, Space, arrows). The
  `#[ignore]`d probe `qa_buttonless_overlay_blocks_typing_into_focused_field_beneath`
  is un-ignored and now passes.

- SEC-007 — `clear_all_global` (network switch) now also drains the action queue,
  so a click queued just before the switch cannot survive into the new context
  and be mis-dispatched.

Adds inline unit tests: `claim_input` strips text + nav/confirm keys while a
block is up and is a no-op when idle; `clear_all_global` clears the queue.

Scope note: this is a partial pass on the QA list. The end-of-frame filter in
`render_global` is kept as belt-and-suspenders and is NOT yet gated on a secret
prompt (marked TODO at the call site — blocker #2's full fix removes it and
routes the keyboard tests through `claim_input`). Still outstanding from the
addendum / task: A-1 no-progress watchdog, A-3 keyed `OverlayHandle::take_actions`
+ `sweep_orphan_actions`, instance `Component::show` focus-trap separation,
secondary-button styling, 30s clock seam, Foreground layering, and doc sync.
Also adds Nagatha's `04-design-addendum.md` (the authoritative spec).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): QA-wave hardening — watchdog, keyed dispatch, secondary buttons, Foreground, focus separation

Implements the design addendum (§1/§2) plus the rest of the QA fix list and the
three cross-finding reconciliations. All on top of the earlier claim_input/SEC-007 pass.

Addendum §1 (safety-valve / A-1):
- 120 s no-progress watchdog: STUCK_OVERLAY_WATCHDOG_THRESHOLD, OverlayState
  { last_progress_at, watchdog_logged }, watchdog_tripped() clock seam, escalated
  STUCK_WATCHDOG_REASSURANCE (replaces the soft line, never stacks), one-shot
  tracing::error! (no flaky time-based panic). last_progress_at is bumped on a real
  content change, reusing log_overlay_state's change detection, so a progressing
  multi-step flow never trips it.

Addendum §2 (action-dispatch / A-3, SEC-007/A-4):
- Actions are keyed: OverlayAction { key, action_id }. OverlayHandle::take_actions()
  drains only its own ids (FIFO); clear() purges its key's pending ids; the static
  take_actions is demoted to sweep_orphan_actions() (dead-owner ids only). app's
  drain logs orphans. clear_all_global already clears the queue (SEC-007).

Reconciliations (lead brief):
1. SEC-004/F-1 — claim_input is gated on no active secret prompt at the app site,
   and render_global no longer strips keyboard at all (the gated claim_input is the
   sole keyboard block); release-beneath-focus is button-less only (stop_text_input
   clears ANY focus, which would steal a button's focus otherwise).
2. QA-002 — claim_input strips Space (and render_global's removal means the kittest
   keyboard path runs through claim_input). TC-OVL-044 now also presses Space.
3. QA-003 — render_card/render_buttons take trap_focus; the instance Component::show
   passes false so it never seizes the host screen's focus or installs the lock.

Rest of the list:
- SEC-002: overlay dim/sink/card raised to Order::Foreground (above ComboBox /
  autocomplete / SelectionDialog popups); passphrase modal also raised to Foreground
  so it stays above the overlay (R-1, TC-OVL-048).
- F-3/4/7: ButtonStyle { Primary, Secondary }, with_secondary_button on
  OverlayConfig/OverlayHandle/instance, ConfirmationDialog-style right_to_left layout
  (primary right, secondary left).
- SEC-005: corrected the Send+Sync note to the real invariant (UI-thread-only ops).
- F-6: Elapsed uses a named placeholder. SEC-006: log-content doc note on show_global.
- QA-007: instance clear() makes the empty-response path reachable.
- QA-008: TC-OVL-013b asserts elapsed >= 2s; TC-OVL-021 also bounds vertically.

Tests: un-ignored qa_buttonless probe; new inline tests (watchdog threshold/clock-reset/
one-shot, keyed FIFO/isolation/orphan-sweep, QA-007); new kittest reconciliations
(render_global keeps keyboard for the prompt; instance show leaves host focus navigable).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): sync requirements/dev-plan to the shipped design + add UX user story

- 01-requirements-ux.md: add a supersession callout flagging the Cancel-era
  items now overtaken by the generic-button + watchdog + claim_input redesign
  (FR-7, AC-7.3/7.4, NFR-3 AC-3b, AC-8.4, AC-10.5, J-1/J-2/J-3, §6.3-6.5), pointing
  at the dev-plan post-outage note, the addendum, and the code as source of truth.
- 03-dev-plan.md: drop OVERLAY_CANCEL_ACTION_ID from the §2 re-export row; mark the
  §3 API block superseded (real surface is with_button/with_secondary_button, keyed
  take_actions/sweep_orphan_actions, OptionOverlayExt::raise, the watchdog); fix the
  §4.1 drain comment; update the §9 D-4/D-5 rows.
- user-stories.md: add UX-001 (blocking please-wait overlay; cannot fire a
  conflicting second action), tagged across personas, [Implemented].

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(overlay): close re-QA coverage residuals RQ-1/RQ-2/RQ-3

RQ-1 (security) — the app.rs secret-prompt gate had no test; deleting
`if self.active_secret_prompt.is_none()` left every test green. Extracted the
gate into `AppState::claim_overlay_input` (called from `update`) and added a
`#[cfg(feature = "testing")]` seam (`AppState::test_set_secret_prompt_active`,
`ActivePrompt::test_stub`). New AppState-level kittest
`rq1_appstate_secret_prompt_gate_keeps_prompt_typeable_over_overlay` drives the
REAL `update()` loop with a prompt active over a button-less overlay and asserts
the prompt input keeps focus AND accepts typed text (types a passphrase + Enter,
the prompt submits and closes). Deleting the gate makes `claim_input`
(button-less → `stop_text_input`) steal focus and strip the keys, failing both
assertions. Extended `tc_ovl_048` to assert prompt interactivity (submit button
renders + input holds focus), not just visibility.

RQ-2 — added a `#[cfg(feature = "testing")]` clock seam `OverlayHandle::backdate`
(shifts `created_at` + `last_progress_at` into the past). New kittest
`tc_ovl_047b_threshold_reveals_via_clock_seam` renders past 30 s and 120 s and
asserts: the soft "This is taking longer than usual." line + Elapsed
force-reveal, then `STUCK_WATCHDOG_REASSURANCE` REPLACING the soft line (never
both) — the addendum §1 obligation that was previously only flag-checked.

RQ-3 — reframed the `tc_ovl_047` doc comment (the escape-hatch button is a
deliberate v1 non-feature per addendum §1, not a deferred T7 TODO); added a
"(superseded)" note to 01-requirements-ux.md's "what to reuse" list where it
still cited `with_cancel`/`with_action`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): close QA residuals — README catalog entry, requirements Cancel reconciliation, T7 TODO

Post-gate cleanup on the blocking progress overlay (gate green):

- README: add a ProgressOverlay row to the Feedback Components table,
  covering show_global/render_global, with_button(id, label), the 120s
  watchdog, and companions OverlayConfig/OverlayHandle/OptionOverlayExt/
  ProgressOverlayResponse.
- 01-requirements-ux.md: reconcile the remaining literal-Cancel acceptance
  criteria (intro line, AC-7.3, AC-8.4, the §6.5 "Visible, cancelable" row,
  R-3) to the shipped generic-button model, matching the top supersession
  callout — Esc/Tab/Enter/Space are swallowed and there is no built-in Cancel.
- app.rs: mark drain_overlay_actions with a TODO(T7) recording that an overlay
  button can only stop waiting (not abort) until the BackendTask system gains
  cooperative cancellation; until then the 120s watchdog (see
  progress_overlay.rs) bounds every block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(overlay): hard-block the UI during startup/Connect SPV sync

Raises the blocking ProgressOverlay while a startup- or Connect-initiated SPV
sync runs, and lowers it when the chain becomes usable (Synced) or fails (Error).

Honors the overlay's C1/C2 caller contract. SPV sync is UNBOUNDED — it can wait
indefinitely for peers — so a button-less block would trap the user. The block
therefore carries a "Continue in the background" escape
(`SYNC_CONTINUE_BACKGROUND_ACTION`); clicking it lowers the block while sync
proceeds safely in the background (read-only — nothing is stranded). C1: the
block also always lowers on its own at a terminal state.

- `AppState`: `sync_overlay`/`sync_block_active`/`sync_overlay_dismissed` fields;
  armed on boot auto-start and on the manual `StartSpv` (Connect); reset on
  network switch so the handle never goes stale.
- New per-frame `update_sync_overlay` driver (called beside
  `update_connection_banner`) applies a pure, unit-tested policy `sync_block_step`
  (Block / Release / Idle) and drains the escape click.
- Pure decision + descriptions are i18n-clean single sentences.

Tests: 6 inline unit tests of `sync_block_step` (inactive→Idle; active+not-usable
→Block; terminal→Release for both dismissed states; dismissed→Idle; stable action
id; sentence descriptions). New `#[cfg(feature = "testing")]` integration kittest
`task9_sync_overlay_blocks_lowers_on_synced_and_on_escape` drives the real
`update_sync_overlay` against a forced connection state: asserts the block raises
while connecting, lowers on Synced (C1), and lowers on the escape click (C2 — user
never trapped). Adds `ConnectionStatus::set_overall_state` + AppState
`test_activate_sync_block`/`test_drive_sync_overlay` test seams.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(overlay): align SPV-sync block to the approved spec

Reworks the SPV-sync overlay wiring (introduced in the previous commit) to the
user-approved design. Net behaviour: while the active context is Connecting or
Syncing the overlay hard-blocks the UI, lowering when the chain becomes usable
(Synced), fails (Error), or drops (Disconnected).

Changes vs the first cut:
- Keyed purely to the live connection state + a per-episode dismissal flag — drops
  the separate "armed" flag, so any sync episode (startup, Connect, or reconnect)
  blocks. Pure policy renamed `sync_block_step` -> `spv_block_step`
  (Block/Release/Stand); Disconnected now Releases + re-arms.
- Escape is now an always-visible SECONDARY button "Continue in the background"
  (id renamed `spv:sync:continue_background`); fields renamed to
  `spv_overlay`/`spv_overlay_dismissed`; method renamed `update_spv_overlay` and
  driven BEFORE `update_connection_banner`.
- Live content: description = `spv_phase_summary(progress)` (else a generic
  connecting line), plus a "Step N of 5" counter via new
  `connection_status::spv_phase_step` (Headers=1 … Blocks=5). Raises once per
  episode, then updates in place.
- Suppresses the redundant Connecting/Syncing connection-banner text while the
  overlay is up (don't double-shout); keeps Error/Disconnected banners.

C1/C2 contract preserved: SPV sync is UNBOUNDED, so the escape (lower while sync
continues safely in the background — read-only, nothing stranded) guarantees the
user is never trapped; episode-ending states always release.

Tests updated: 4 inline `spv_block_step` unit tests; the integration kittest
`task9_spv_overlay_blocks_lowers_on_synced_and_on_escape` now also asserts the
secondary escape button, re-raise for a fresh episode, no re-raise within a
dismissed episode, and re-raise after the episode ends. Test seams renamed to
`AppState::test_drive_spv_overlay` (+ `ConnectionStatus::set_overall_state`).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(overlay): reconcile SPV-sync block decision (F-SPV-1) + phase-step test (F-SPV-2)

F-SPV-1 — the user-authorized SPV-sync hard-block + always-visible "Continue in
the background" escape contradicted three docs written for the standalone
overlay. Reconcile the docs to the decision (the feature is correct; the docs
were stale) so a future dev does not "correctly" remove the button per old docs:

- docs/user-stories.md: carve out the SPV-sync exception in UX-001's "no
  background/dismiss button" guarantee, and add UX-002 — the blocking SPV-sync
  overlay with the always-on "Continue in the background" escape (tagged across
  personas, [Implemented]).
- 01-requirements-ux.md §5: supersession note — the user chose to block the
  startup/Connect get-connected sync; the power-user concern is mitigated by the
  escape (sync is read-only and safe to background); this is the overlay's first
  adopter.
- 04-design-addendum.md A-1: record that A-1's "ship NO dismiss/background button
  in v1" was scoped to unsafe-to-interrupt ops whose safety rests on boundedness;
  for the unbounded-but-read-only SPV-sync adopter the C2 "never trap the user"
  guarantee is met by the always-on escape, which must NOT be removed.

F-SPV-2 — the granular phase progress (spv_phase_summary description +
"Step N of 5" via spv_phase_step) was already wired in the previous commit; adds
a unit test locking the active-phase → step mapping and the summary text.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): scope SPV block to user-initiated sync + de-jargon copy (F-SPV-A/B/E)

F-SPV-A (sev-2/1 regression, introduced by the prior refactor) — the SPV block
fired on ANY Connecting/Syncing, so an ambient mid-session reconnect, or the SPV
engine flipping Synced→Syncing as it processes each new block (event_bridge
on_progress maps !is_synced() → Syncing), would hard-block a working user.
Re-introduce a startup/Connect-SCOPED arming gate:
- `spv_block_armed` flag, armed only on boot auto-start and the Connect button
  (AppAction::StartSpv); reset on network switch.
- `spv_block_step(armed, dismissed, state)`: !armed → Idle (never block); armed +
  Synced/Error → Disarm (lower + clear armed); armed + Connecting/Syncing/
  Disconnected → Block (or Stand if dismissed). Once disarmed, ambient sync never
  re-blocks until the next user-initiated episode.

F-SPV-B (sev-2) — the block description showed blockchain jargon ("Headers:
12345 / 27000 (45%)") to the Everyday User. Replace with plain complete
sentences ("Connecting to the Dash network." / "Syncing with the Dash network.");
keep the jargon-free "Step N of 5" counter (via spv_phase_step) as the
determinate granularity. spv_phase_summary stays (still used by wallets_screen);
it is just no longer the overlay description. UX-002 acceptance criterion updated
to stop enshrining the jargon.

F-SPV-E (sev-4) — AppAction::StartSpv set an orphaned Info banner whose handle was
dropped (could not be cleared by the overlay's banner suppression). Dropped it;
the block conveys "connecting" and the error path still surfaces via replace_global.

Tests: spv_block_step unit tests rewritten around the arming gate —
`unarmed_never_blocks` is the regression guard (ambient sync never blocks);
`armed_terminal_state_disarms`; jargon-free-description test. The integration
kittest is rewritten to `task9_spv_overlay_armed_scope_disarm_and_escape`: an
un-armed Connecting does NOT block, an armed one does, Synced disarms, ambient
sync afterward does NOT re-block, the escape lowers without re-raising, and only a
fresh armed episode re-blocks. New `AppState::test_arm_spv_block` seam.

is_synced() finding: `EventBridge::on_progress` (event_bridge.rs) does map
`!is_synced()` → `SpvStatus::Syncing`, so overall_state CAN flip Synced→Syncing on
per-block catch-up — the arming gate makes that harmless (disarmed after the
initial episode).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): address review findings — deterministic elapsed test, SPV phase-count constant, input-claim hardening, doc drift

- Replace the 2.1s wall-clock sleep in tc_ovl_013b with the deterministic
  `backdate` clock seam (gated behind `testing`), mirroring tc_ovl_047b — zero
  wall-clock waiting; asserts the elapsed readout counts up to a concrete 2s.
- Add `SPV_SYNC_PHASE_COUNT` next to `spv_phase_step` as the single source of
  truth for the "Step N of 5" total; reference it at both app.rs call sites and
  guard the max step with a `debug_assert!` so it cannot silently drift.
- Delete the misplaced orphan-sweeper paragraph from `claim_overlay_input`'s doc
  (it belongs to `drain_overlay_actions`, which already carries it).
- Reconcile the `Order::Middle` → `Order::Foreground` doc drift: supersession
  callouts in the dev plan §4.2/§4.3 and the kittest module doc, citing SEC-002.
- Drop the dead `CONNECTING_MSG`/`replace_global` swap in the StartSpv failure
  path (the "Connecting…" banner was removed in F-SPV-E) for a plain
  `set_global(...).with_details(e)`; fix the now-stale comment.
- Extend `claim_input`'s per-frame strip to also drop Backspace, Delete, Home,
  End, PageUp, PageDown and the Copy/Cut/Paste clipboard events; add a kittest
  locking the new classes via event survival + the field-beneath contract.
- Strengthen the SEC-001 lifecycle rustdoc on `show_global` /
  `show_global_spinner_only` (button-less blocks need a frame-driven reconcile
  owner or an escape; the watchdog only logs).
- Nits: UX-001 "developer warning" → "developer error"; "while a armed" →
  "while an armed". Add deferred TODOs (SEC-002-pointer, SEC-001, RUST-006).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): close one-frame SPV block gap, fix slow-phase watchdog, align API to MessageBanner

Three changes to the blocking progress overlay + SPV-sync hard-block:

A — Close the one-frame interactive gap. `update_spv_overlay` now runs at the
top of `AppState::update`, BEFORE `claim_overlay_input`, the visible screen
`ui()`, and `render_global`. A freshly-armed episode therefore raises, claims
input, AND paints on the same frame; previously the block was raised only after
`render_global`, leaving the frame right after Connect/arming fully interactive
(effective at frame N+2). The connection banner still reads the block state
afterwards, so its Connecting/Syncing suppression is unchanged.

B — Stop the 120s no-progress watchdog from falsely escalating on slow phases.
A single SPV phase running >120s (e.g. Headers on a slow link) wrote a constant
(description, step), so `log_overlay_state` never reset `last_progress_at` and
the watchdog tripped — swapping to the STUCK copy and firing the one-shot
dev-error, the exact false signal the SPV escape was meant to avoid. A hidden,
monotonic `progress_token` (step in the high 32 bits, advancing height in the
low 32) is threaded from `ConnectionStatus` into the overlay; an advancing token
resets the watchdog even when the shown (description, step) is unchanged. The
token is NEVER rendered — copy is byte-for-byte unchanged and the jargon-free
test stays green. Distinct from TODO(SEC-001), which is left in place.

C — Align the overlay public API toward MessageBanner so migrating from the
banner is a name-for-name swap. One-way (overlay → banner), no capability loss:
  with_button(id, label)            -> with_action(label, action_id)
  with_secondary_button(id, label)  -> with_secondary_action(label, action_id)
  show_global(...)                  -> set_global(...)  (return type kept)
  show_global_spinner_only(...)     -> set_global_spinner_only(...)
`OptionOverlayExt::raise` keeps its name: renaming to `replace` (the banner
analogue) would be shadowed by the inherent `Option::replace`, so every
`slot.replace(ctx, desc, config)` call would fail with E0061 (verified). A doc
note records why. `render_global`, `claim_input`, the watchdog, `OverlayConfig`,
and all handle progress methods are untouched. Rustdoc, the README catalog row,
and the design-doc API references are updated to the new names; the banner's own
`MessageBanner::show_global(ui)` render path is left alone.

Tests: new real-AppState kittest for the one-frame gap (same-frame paint), new
backdate kittest + unit tests for the token-driven watchdog reset, and a
`spv_progress_token` monotonicity unit test. fmt + clippy clean; kittest 138
passed; lib 926 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(overlay): keyboard-reachable escape for the SPV hard block (QA-002 refinement)

Resolves the TODO(RUST-006) marker: the SPV-sync hard block's "Continue in
the background" escape was mouse-only, stranding keyboard-only / assistive-tech
users behind the UNBOUNDED block. Hard blocks strip Enter/Space every frame
(the deliberate QA-002 rule, guarded by TC-OVL-044), so the escape could not be
activated by keyboard.

Add a per-block opt-in — `OverlayConfig::with_keyboard_escape(action_id)` and
`OverlayHandle::with_keyboard_escape(action_id)` — that designates ONE action as
the single keyboard-reachable escape. The general rule is unchanged: a block
with no designated escape stays fully keyboard-blocked.

- claim_input: when the active block designates an escape AND that escape button
  is *confirmed* to hold focus (its egui id was recorded by last frame's
  render_buttons and still matches the focused widget), Enter/Space pass through;
  every other key, and the raise frame (focus not yet confirmed), stays stripped.
  So the passthrough can never reach a widget beneath.
- render_buttons: for an opt-in block, pin focus to the designated escape (match
  by action id) — re-requested every frame and locked — and record its id for the
  claim_input gate.
- SPV adopter (update_spv_overlay): mark "Continue in the background" as the
  keyboard escape; it remains unconditionally present whenever the block is up.

Tests (egui_kittest — the reliable check for input/focus):
- TC-OVL-051/052: Enter / Space activate the focus-pinned escape.
- TC-OVL-053: a TextEdit beneath never receives Enter; Tab and a backdrop click
  cannot move focus off the escape.
- task9_spv_escape_is_keyboard_activatable: the REAL SPV block lowers on Enter.
- TC-OVL-044 and the keyboard-block tests stay green (general rule intact).
- Unit tests for the opt-in API + the claim_input safety gate.

Docs: QA-002 design note + NFR-3 accessibility ACs, test-spec, user story UX-002,
and the public rustdoc updated to state the refined rule.

cargo +nightly fmt: clean. clippy --all-features --all-targets -D warnings: 0.
kittest --all-features: 142 passed. lib --all-features: 928 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(overlay): adopt blocking overlay for DPNS registration (Bucket A exemplar)

Establish the canonical Bucket A overlay-adoption pattern on the DPNS
username registration screen, the template for the remaining transaction
screens.

The screen used a progress banner as its in-progress indicator and did
not block re-entry while WaitingForResult, leaving a double-submit hole
(duplicate name registration). Replace the banner with a button-less
full-window ProgressOverlay raised at dispatch and torn down on every
terminal result, which both signals progress and closes the double-submit
hole.

- Add `op_overlay: Option<OverlayHandle>`; raise it in `begin_registration`
  only when a real BackendTask is produced, so a no-op click never strands
  a block.
- Tear the overlay down on both terminal paths (SEC-001): the success arm
  of `display_task_result` and the error/warning branch of `display_message`,
  mirroring the prior `refresh_banner` lifecycle.
- Remove the now-redundant progress banner; the full-window block makes a
  WaitingForResult button-disable unnecessary.
- Add a `raise_progress_overlay_for_test` seam and kittests proving the
  raise + guaranteed teardown on success and error.
- Make `ui::identities` `pub` (the lone non-pub sibling) so the screen is
  reachable from the kittest crate, matching `wallets`/`dpns`/`tokens`.
- Note the blocking overlay + double-submit prevention on DPN-001.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(overlay): restore SEC-003 watchdog TODO comment dropped by the base-merge

Comment-only restore (matches the base #863 app.rs); no logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lklimek added a commit that referenced this pull request Jul 10, 2026
…et rewrite (#876)

* fix(withdraw): pre-select only a locally-signable withdrawal key

The Withdraw screen constructor pre-selected a key via the on-chain
lookup `identity.get_first_public_key_matching(TRANSFER, ...)`, which is
unfiltered by local private-key presence. On loaded masternode/evonode
identities where only the Owner key was supplied, this picked a
"ghost" Transfer key with no local private material, so the withdrawal
failed at signing with a raw, unhelpful protocol error.

- model: add `QualifiedIdentity::default_withdrawal_key()` — sourced from
  `available_withdrawal_keys()` (private-key-backed only), Transfer
  preferred with Owner fallback, `None` when nothing is signable.
- ui: constructor now pre-selects via `default_withdrawal_key()`; the
  developer-mode on-chain escape hatch is preserved. When no usable key
  exists the existing empty-state guides the user to add one.
- error: add `TaskError::NoWithdrawalSigningKey` (typed `#[source]`,
  plain-language actionable Display) mapping the SDK
  `DesiredKeyWithTypePurposeSecurityLevelMissing` protocol error as a
  defense-in-depth backstop instead of leaking a raw string.
- tests: 4 model cases (ghost key rejected, private-backed selected,
  owner fallback, transfer preferred) + 2 error-mapping/Display cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(claude): correct secret-storage note on identity-key encryption

Identity keys (imported/loaded, including masternode voting/owner/payout)
are no longer categorically in the deferred keyless tier: they enter
unprotected at load time but can be sealed to Tier-2 per-identity via
IdentityTask::ProtectIdentityKeys (Key Info screen "Add password
protection"). Clarify that the keyless residual is only no-password
secrets and keys the user has not opted to protect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(masternodes): add page-nav model with two-scope selection (A1)

Introduce ui/state/global_nav.rs: PageNavSpec (page-aware segment-1 +
per-page pill composition) and IdentityPillScope (AppGlobalUser vs
PageScopedObject). The PageScopedObject variant carries its own selection
and never writes AppContext::selected_identity_id — the structural FR-6
boundary the global switcher (A2) and the Masternodes page (B7) build on.

Pure state, renders nothing (module-placement discriminator -> ui/state).

Satisfies TC-NAV-13, TC-NAV-14, TC-NAV-15; foundation for TC-NAV-12,
TC-FR6-07.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): generalize breadcrumb into page-aware global switcher (A2)

Add ui/components/global_nav_switcher.rs: a page-aware switcher driven by
PageNavSpec, rendering segment-1 (page label + link) plus composable
wallet / identity-or-object pills. GlobalNavEffect generalizes the hub's
BreadcrumbEffect and adds SelectPageObject for the page-scoped pill — kept
distinct from SelectIdentity so a page-scoped selection never writes the
app-global identity (FR-6 boundary at the effect level).

Reuses BreadcrumbPill / IdentityPill / BreadcrumbPillMode verbatim (no new
pill widget). breadcrumb_switcher.rs becomes a thin hub-facing shim that
builds the hub spec, delegates to the generalized render, and maps the
effect back (self-nav to the hub root -> OpenPicker), keeping hub behavior
unchanged — verified by the existing identity_hub_switcher kittests.

Satisfies TC-NAV-01, TC-NAV-02, TC-NAV-03, TC-NAV-13, TC-NAV-16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): render global switcher on root screens + shared applier (A3)

Add the shell glue in top_panel.rs: apply_global_nav_effect (the shared
successor to the hub's apply_breadcrumb_effect — silent app-scoped
wallet/identity writes, no forced navigation) and add_top_panel_with_global_nav
(one-call render-plus-apply), with subdued_everyday_spec / subdued_wallet_only_spec
Phase-A rollout helpers.

Wire the switcher onto four non-Hub root screens with Subdued (unwired)
specs + TODO markers: Identities, DPNS, DashPay (everyday: wallet + identity
pills) and Wallets (wallet-only composition, TC-NAV-15). The Hub keeps its
existing interactive pills via the breadcrumb shim (regression — full
kittest suite green).

Document (comment + test, PROJ-010) that set_selected_hd_wallet reconciles
the app-global identity as a side effect on non-Hub pages, and that combined
with B1's resolution-layer filter it must never reconcile onto an MN/Evonode.

Deferred (documented): tokens/tools screens carry in-header sub-navigation
that needs the FR-GLOBAL-NAV-5 content-panel back-row migration before their
plain breadcrumb can be swapped for the global switcher — a follow-up, not a
mechanical swap.

Satisfies TC-NAV-06, TC-NAV-15, TC-NAV-16; enables TC-NAV-12/17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): load-time key encryption plumbing (B0)

FR-8: add IdentityInputToLoad.encryption_password: Option<Secret>. When
Some, load_identity validates the password up front (fast fail) then, after
insert migrates the keyless keys into the vault, seals them Tier-2 through
the existing per-identity protect envelope (protect_identity_keys →
put_secret_protected via the secret_seam chokepoint) — no new crypto, no
second persistence path. When None, the keyless Tier-1 path is unchanged.

Relocate validate_protection_password from protect_identity_keys.rs into
model/identity_key_protection.rs (PROJ-006, DET validation-placement rule);
the seal path and load path both call the model validator.

MCP masternode_identity_load passes encryption_password: None (PROJ-007 —
GUI-only this iteration, requirements §2.3) with a TODO for headless
password parity.

Add typed TaskError variants DuplicateProTxHash { identity_id } and
MalformedProTxHash { input } for later duplicate/malformed rejection (B1/B4),
avoiding string parsing.

Tests: model validator (relocated); an offline-wired-AppContext test proving
a load-time password seals a masternode's voting (V-target), owner and
identity (M-target) keys Tier-2 and round-trips under the password — the
exact call load_identity makes (TC-FR8-01/02/10). Load-form UI is B4;
end-to-end load routing is covered by the network backend-e2e suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(masternodes): drop ephemeral review ID from A3 reconciliation comment

Self-review: replace a transient review-finding ID in the apply_global_nav_effect
reconciliation note with the durable FR reference. Comment-only; no behavior
change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): FR-6 resolution-layer boundary + masternode accessor (B1)

FR-6 (R1, release-blocking): keep masternode/evonode identities out of every
everyday-user surface by filtering at the resolution layer, not the display
call sites.

- resolve_selected_identity(): candidate set filtered to IdentityType::User
  before resolving, so neither keep-if-loaded nor the first-loaded fallback
  can ever resolve a masternode — even when a masternode is the only/first
  loaded identity (TC-NAV-12b).
- set_selected_hd_wallet(): the wallet-switch identity reconciliation resolves
  only over the wallet's User identities.
- restore_selected_identity_from_kv(): one-time sanitization — a masternode
  persisted as selected_identity_id in a prior session is cleared on load;
  a User selection is kept (TC-NAV-12c). In-memory only (non-destructive).
- Display sources switched to the established User-only accessor
  load_local_user_identities(): the global switcher's identity pill + dropdown
  and the Identity Hub landing/picker now list User identities only, so the
  wallet-less "no wallet on this device" group can no longer surface an
  MN/Evonode (TC-NAV-17). Masternodes stay in the legacy Identities table
  (unfiltered accessor untouched — locked decision #2).

New context accessor load_local_masternode_identities() (hydrated MN/Evonode)
— the Masternodes-page card list + page-scoped pill source (B3/B7).

Tests: offline-wired-AppContext unit tests (accessors, resolution filter incl.
lone-masternode fallback, stale-MN sanitization) + a Hub kittest asserting a
seeded Masternode+Evonode never appear on the hub while remaining in the
masternode accessor. TC-FR6-01…06, TC-NAV-12b/12c, TC-NAV-17.

Deferred to consuming tasks (documented): FR-7 refresh (TC-FR7-02/03) composes
existing RefreshIdentity + contested-names refresh at the card Refresh button
(B3); the per-node open-contest card read accessor lands in B3 where the card
consumes it and it is testable against the rendered status line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): register Expert-gated Masternodes root tab (B2)

Add RootScreenType::RootScreenMasternodes (stable int 28, round-trip test),
ScreenType::Masternodes, Screen::MasternodesScreen, create_screen and all
ScreenLike dispatch arms; register the always-present root screen in app.rs
(gated at runtime by Expert Mode, not a Cargo feature, so the screen exists
to switch into when the gate is on).

Nav: a "Masternodes" left-panel entry gated FeatureGate::DeveloperMode,
positioned directly below the identity cluster (locked decision #3),
independent of the identity-hub feature. Distinct glyph voting.png (TODO:
dedicated node/server icon). The existing per-entry gate skip hides the nav
item and route when Expert Mode is off.

Live de-gating (§10.11): active_root_screen_mut falls the active tab back to
Identities (always registered) if Expert Mode flips off while Masternodes is
selected, so the gated screen is never shown without its gate.

MasternodesScreen is a scaffold (global-nav header + left rail + island
placeholder); the empty state + card grid land in B3, the page-scoped
masternode pill in B7. Network-switch already calls change_context on
main_screens; the sub-screen reset (§10.10) applies once B4/B5 push
sub-screens (noted for B8).

Tests: RootScreenType round-trip (int 28 stable); kittest — nav absent
Expert-off / present Expert-on, and de-gating falls back to Identities.
TC-FR1-01…07, TC-EDGE-05/06.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): empty state + card grid + card body (B3)

Render the Masternodes root screen content on top of the B2 scaffold:

- Empty state (FR-2): canonical §7 copy — heading, body, "Load a
  masternode" primary CTA, and the ProTxHash reassurance line.
- Card grid (FR-3): responsive minmax(260,1fr) grid reusing the identity
  picker's visual language via a new `MasternodeCard` (monogram +
  `draw_type_badge`, both now `pub(crate)`). Body adds masternode rows the
  picker lacks: voter readiness, compact `V O P` key status (glyph, not
  colour-only — NFR-6), DPNS status line, and the IdentityStatus dot+label.
- DPNS status precedence (§10.1): open-contest count first, then a pending
  scheduled vote, then "No open contests", via a display-layer
  `AppContext::masternode_contest_summary` read (no new backend concept).
- Key presence: `QualifiedIdentity::masternode_key_presence` maps
  Voting/Owner/Payout to voter-identity / OWNER / TRANSFER keys.
- Top-right Refresh toolbar button (FR-7) reloads the cached node list.
- Whole card is a single accessible click target (`WidgetInfo::labeled`,
  NFR-6); selection/load intents are captured for B4/B5a/B7 wiring.

Traceability: TC-FR2-01…07, TC-FR3-01…15, TC-FR7-01, TC-NFR6-01/03.
Unit tests cover heading/sub-line, DPNS precedence, key tokens, badge, and
the 8 V/O/P combinations; kittest covers empty-state copy and the grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): dedicated load form + ProTxHash validator (B4)

Add the MN/Evonode-only load flow (FR-4), carved out of the generic
add-existing-identity path:

- `ui/masternodes/load_form.rs`: ProTxHash (required), Masternode/Evonode
  segmented toggle (default Masternode, no User option), optional alias,
  V/O/P key inputs (reused hold-to-reveal `PasswordInput`), optional at-load
  encryption password (drives B0's seal), always-visible Warning-tone
  key-storage note, and a Load button gated on a non-empty ProTxHash with the
  §7 disabled tooltip. Switching node type clears all fields (§10.6). No
  auto-derive affordance — masternode keys are never wallet-derived
  (US-6 retired, §Locked-#4).
- `model/masternode_input.rs`: `is_valid_pro_tx_hash` shape validator (hex or
  Base58) for inline on-blur validation; the backend load task remains the
  authoritative existence/duplicate check.
- Masternodes screen gains a List/Load view enum; the empty-state CTA and a
  `+ Load` toolbar button open the form; submit dispatches
  `IdentityTask::LoadIdentity`; cancel/submit return to the list with a fresh
  form on reopen.
- `add_existing_identity_screen`: remove Masternode/Evonode from the
  Advanced-Options Identity-Type dropdown (User-only remains) — no competing
  entry point (§10.2 / TC-FR4-22, FR-6).

Traceability: TC-FR4-01…22 (render/logic; live-network accept/duplicate/
error-banner paths land in B8), TC-EDGE-01/02.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): detail view — header, actions, keys, remove (B5a)

Add the node detail view (FR-5), reusing existing screens rather than
reimplementing:

- Fixed section order Header → Actions → Keys → DPNS → Remove (TC-FR5-01,
  the human-requested Actions-above-Keys correction), pinned by a unit test.
- Header: conditional alias, shortened ProTxHash + copy-full-value, type
  badge (shared `draw_type_badge`), IdentityStatus dot + label.
- Actions row (FR-9): Withdraw / Top up / Transfer push the existing
  WithdrawalScreen / TopUpIdentity / TransferScreen scoped to the node's
  QualifiedIdentity (both node types). Evonode-only `Claim token rewards ›`
  cross-link (FR-11), absent for a plain masternode.
- Keys section (FR-10): V/O/P presence, copyable voter-identity id, at-rest
  protection tier (vault-scheme probe), Add-protection offered only Tier-1,
  `Manage keys ›` into the existing key screen.
- DPNS section: collapsible, open-contest count in the header (voting table
  lands in B5b).
- Remove: danger ConfirmationDialog; deletes the node and its voter identity.
- `‹ All masternodes` back row + detail Refresh (FR-7/TC-FR7-04); card click
  opens the detail via a List/Load/Detail view enum.

Deviations (documented): the Evonode claim cross-link routes to the Tokens
area — precise ClaimTokensScreen token-scoping is deferred to B8 where the
evonode reward-token context is resolvable. Add-protection routes into the
reused key screen (which hosts the password-entry seal flow) rather than
duplicating the form.

Traceability: TC-FR5-01…05/07, TC-FR7-04, TC-FR9-01/02, TC-FR11-01/02.
Live-network credit/claim routing and TC-FR8-07 land in B8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): Testnet Fill-Random on the load form (B6)

Add the FR-12 dev convenience to the masternode load form:

- New `testnet_fixture` module owns the `.testnet_nodes.yml` structs + loader.
  The loader returns None for BOTH a missing and a malformed file — a malformed
  file is logged at debug and treated as absent (TC-FR12-04, a deliberate
  divergence from the legacy screen which banners the parse error).
- Fill-Random button + hint render only when Expert Mode is on, the network is
  Testnet, and the fixture is present — never shown-but-disabled (TC-FR12-01…06).
  The `dev_mode` gate is a defense-in-depth re-check at the call site
  (TC-FR12-09 decision: added on future-proofing grounds — a plaintext-key dev
  tool stays inside the Expert-Mode envelope).
- Button label follows the node-type toggle (TC-FR12-01/02).
- Autofill pulls from the correct list per type (TC-FR12-07/08): Masternode →
  `masternodes` (Voting + Owner only; the fixture has no payout key, PROJ-003),
  Evonode → `hp_masternodes` (all three keys). The node-type toggle still clears
  autofilled fields (§10.6).
- The fixture loads once when the form opens (Testnet only), not per frame.

Traceability: TC-FR12-01…09.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): inline DPNS voting + missing-voter prompt (B5b)

Populate the detail view's collapsible DPNS section (FR-5):

- Collapsed by default; header shows the open-contest count
  (`DPNS name contests to vote on (N)`, TC-DPNS-01/02).
- Voter present + open contests: per-contest Abstain / Lock / Vote-for-candidate
  choices with the candidate list scoped to that contest's contestants; a
  `Cast votes` button dispatches the existing
  `ContestedResourceTask::VoteOnDPNSNames` backend inline (locked decision #1 —
  not a deep-link). TC-DPNS-03/04/05.
- Voter present, zero open contests: exact §7 empty copy (TC-DPNS-08).
- Missing voter identity: the actionable §7 message (never the raw
  NoVotingIdentity error) plus an `Add voting key` action that opens a scoped,
  in-place voter-key prompt with the node context pre-bound — distinct from
  FR-4's load form, no ProTxHash re-entry (TC-DPNS-09/10/11, §10.8). Save
  re-loads this node with just the voting key to update its voter identity.
- Detail Refresh now re-reads both the contest summary and the open-contest
  list.

Active/open contests only — scheduled/past history stays on the DPNS Scheduled
Votes screen (§10.7).

Traceability: TC-DPNS-01…11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): page-scoped nav pill — FR-6 boundary in code (B7)

Wire the Masternodes page into the global-nav switcher with a page-scoped
masternode pill whose selection lives on the page and is NEVER written to
`AppContext::selected_identity_id` — the structural FR-6 boundary in code,
complementing B1's resolution-layer filter.

- New `ui/state/masternodes_view.rs` builds the page's `PageNavSpec`: page-aware
  `Masternodes` segment-1, an interactive wallet pill (funds Top up — FR-9), and
  a page-scoped identity pill via `IdentityPillScope::PageScopedObject`. Empty →
  subdued `(no masternode yet)` placeholder; ≥1 node → interactive dropdown with
  `Choose a masternode` placeholder; the pill reflects the node in detail and
  resets to the placeholder on `‹ All masternodes` (§10.4).
- New `top_panel::add_top_panel_with_global_nav_capturing` surfaces the
  `SelectPageObject` pick to the caller (applying all other effects as usual)
  without ever routing it into the app-global identity selection.
- The Masternodes screen builds the spec each frame from its node list + current
  view and opens the picked node's detail — two-way with the card grid.

TC-NAV-12 / TC-FR6-07 (release-blocking): a masternode selected on the page
never becomes, or resolves as, the app-global identity — verified across
Identities and the Identity Hub with no User identity loaded.

Traceability: TC-NAV-01/03/04/05/07, TC-NAV-12, TC-FR5-06, TC-FR6-07.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(masternodes): cross-cutting integration coverage (B8)

Add the Remove-flow integration kittest (TC-US4-01/02/06/07): the detail danger
button opens a confirmation carrying the `Remove masternode` verb, and
confirming deletes only the target node — its card disappears while other nodes
survive (isolation). Also sets the confirmation's confirm verb to
`Remove masternode` (§7 / TC-US4-02), the one small production touch the test
surfaced.

Deferred to the network/backend-e2e pass (out of kittest reach without live
DAPI or heavy vault fixtures, and flagged as such in the plan): TC-FR8-07
(detail reflecting a load-time Tier-2-sealed node — needs the real
password-at-load seal path), TC-US4-05 voter-row deletion assertion (needs a
seeded voter-identity row), and the live-network credit/vote/claim dispatch
paths behind FR-9/FR-11/DPNS Cast-votes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(user-stories): catalog the Masternodes tab, retire the legacy load story

Adds MN-001..MN-009 (load, card grid, vote, remove, Hub filter, at-load
encryption, credit actions, key management, evonode token-reward cross-link)
and UX-003 (global wallet/identity switcher) per the completed Masternodes
feature. Flips IDN-003 to superseded — its generic-screen masternode load
path was removed when the dedicated tab shipped.

* docs(masternodes): commit final design docs (DOC-002)

Lands the human-accepted requirements, UX spec, test-case spec, and dev
plan under docs/ai-design/, following the existing 2026-04-23-identity-hub-impl
numbered-file convention. Resolves the ~100+ FR-/TC-/US-/§-ID references
already scattered through the feature's code comments and tests, which
pointed at an uncommitted /data/artifacts scratch copy. Internal
cross-file references (requirements.md, ux-spec.md, etc.) are updated to
the new numbered filenames.

* docs(masternodes): trim oversized module docs, catalog global-nav switcher

Shortens the four ui/masternodes/*.rs module doc comments to the
internal-tier length cap (DOC-003) — they weren't published API, so the
5-10 line public-rustdoc relaxation didn't apply. Adds GlobalNavSwitcher
and its top_panel entry point to ui/components/README.md's catalog
(DOC-004), so the next screen needing a page-aware switcher finds it
instead of reimplementing one.

* fix(masternodes): guard identity load against silent overwrite (QA-005/006)

Root-cause storage fix. insert_local_qualified_identity is INSERT OR
REPLACE, so a load with no guard silently clobbers an already-stored
identity and its keys. Thread an IdentityLoadMode through
IdentityInputToLoad so each entry point declares intent:

- RejectIfExists: the masternode load form rejects a duplicate ProTxHash
  with TaskError::DuplicateProTxHash before any network fetch (QA-006).
- MergeIntoExisting: the scoped Add-voting-key prompt merges the new key
  into the stored identity, preserving Owner/Payout it did not resupply
  (QA-005), via merge_existing_keys_into.
- Overwrite: legacy User re-load and headless flows unchanged (default).

Adds get_local_qualified_identity accessor backing the existence check
and merge read. Failing-first TDD: a unit test proving Owner/Payout keys
survive a voting-key-only merge, and an offline test proving a duplicate
ProTxHash is rejected and the first node is left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): key routing, network-switch reset, live refresh

QA-007: the detail Keys section pushed the static read-only KeysScreen.
Render a per-key 'Manage keys' list and route the Add-protection CTA to
KeyInfoScreen (interactive view/sign/seal per key), mirroring
identities_screen.

QA-001: MasternodesScreen had no change_context override, so a network
switch left an open load form or cross-network detail view actionable.
Add an explicit change_context arm that resets to the List view and
reloads from the now-active network.

QA-003: both Refresh buttons only re-read the local cache. Wire them to
dispatch IdentityTask::RefreshIdentity (per loaded node on the list, the
open node on detail) plus a QueryDPNSContests re-query, alongside the
optimistic local re-read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): SHOULD-FIX pass + QA-token cleanup + offline tests

- QA-002: remove the legacy Fill-Random HPMN/Masternode bypass from the
  now User-only add-existing-identity screen (it set identity_type to
  Evonode/Masternode directly, defeating the User-only restriction).
- QA-004: the evonode 'Claim token rewards' CTA pushes a real scoped
  ClaimTokensScreen when the node holds exactly one token, falling back to
  My Tokens when the target is ambiguous — no more bare SetMainScreen.
- QA-008: refresh the open detail view after its own backend task, not
  just the card list.
- Diziet-F3: render the missing-voter 'Add voting key' CTA above, outside
  the collapsed DPNS section, so it is visible without expanding.
- QA-009: surface a MessageBanner when node removal fails instead of a
  silent tracing::warn.
- SEC-001: log the testnet-fixture parse error by position only, never
  its Display text (which echoes a private key).
- SEC-002: parse fixture key fields as Secret (redacted/zeroized).
- Strip stale PROJ-003/PROJ-004 markers and all ephemeral QA-xxx review
  IDs from source comments (kept only in commit messages).
- TODOs for the deferred mixed-protection-tier CTA and the
  is_valid_pro_tx_hash/decode_identity_id duplication.
- Offline tests: TC-FR8-07 (protection_tier reflects a Tier-2 seal) and
  TC-US4-05 (Remove deletes the associated voter identity).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dashpay): close FR-6 boundary bypass via DashPay identity selectors (SEC-005)

DashPay screens (send_payment, contacts_list, profile_screen,
contact_requests, qr_scanner, qr_code_generator, add_contact_screen,
profile_search) built their IdentitySelector and constructor seed from
the unfiltered load_local_qualified_identities() chained with
.syncing_global(...). IdentitySelector::sync_to_global() writes the
picked id straight to AppContext::selected_identity_id — a separate path
from B1's resolve_selected_identity()/restore filters — so a user could
select a masternode/evonode as the app-global operate-as identity from
inside DashPay, bypassing the FR-6/R1 boundary B1 established.

DashPay operates on User identities only, so every identity list in these
screens is sourced from load_local_user_identities() (the same swap B1
made for the global-nav switcher and Identity Hub). This filters the
masternode out of both the selector write-path and the constructor seed.

Adds a kittest (masternode_never_selectable_in_dashpay_screens) exercising
the FR-6 boundary through five DashPay screens — the existing FR-6 kittest
only covered Identities/Identity Hub, which is how this slipped through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(masternodes): Marvin punch-list — in-flight guard + execution tests

- QA-012: gate re-submission while a node-load is in flight. Add a
  load_in_flight flag on MasternodesScreen, set on Submit dispatch and
  cleared on the task result or a new display_task_error override; the
  '+ Load' toolbar button and empty-state CTA show a spinner + disabled
  'Loading…' while set, so a rapid double-submit of a brand-new ProTxHash
  cannot race two loads past the pre-fetch existence check.
- Extend masternode_never_selectable_in_dashpay_screens to QRScanner,
  QRCodeGenerator (both seed selected_identity in new()) and assert
  ProfileSearchScreen's User-filtered data source excludes the masternode
  — FR-6 coverage now spans all 8 DashPay screens.
- Add manage_keys_button_opens_key_info_screen: clicks a per-key
  'Voting key ›' button and asserts a KeyInfoScreen is pushed with its
  'Key Information' heading (execution-level proof of the QA-007 fix).
- Add refresh_from_network unit test: one RefreshIdentity per loaded node
  plus a trailing QueryDPNSContests, None when empty (QA-003).
- Fix two doc-comment lines mangled by the earlier review-ID strip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(changelog): add Masternodes tab and global nav switcher (DOC-005)

Covers the user-facing outcomes of the completed Masternodes feature:
the new Expert-Mode-gated tab (card list, detail view, load-time key
encryption, inline DPNS voting, credit actions, Evonode token-reward
claiming) replacing the old generic load path for masternode/evonode
identities, the resulting Identity Hub / Identities picker filter, and
the wallet/identity switcher now present on every root screen instead
of just the Identity Hub.

* fix(masternodes): default GUI build broken — masternode_input feature-gated

The whole model::masternode_input module was gated behind
load form (ui/masternodes/load_form.rs) imports is_valid_pro_tx_hash from
it unconditionally. So a plain 'cargo build --bin dash-evo-tool' (default
features only — no mcp/cli, the documented quick-start build) failed with
E0432 unresolved import. Every gate this feature ran used --all-features,
which always pulls mcp+cli and masked it.

The module can't be blanket-ungated: its parse/decode helpers return
McpToolError (from the feature-gated mcp module). Fix ungates the module
and the pure is_valid_pro_tx_hash validator (the only thing the GUI needs),
and gates precisely the McpToolError-coupled items — KeyMode, parse_node_type,
parse_key_mode, require_at_least_one_signing_key, decode_identity_id, their
imports, and their tests — behind mcp/cli. The pure validator's tests move
to an always-compiled module so they run in the default build too.

Verified: 'cargo build --bin dash-evo-tool' (no flags) compiles; default-
feature clippy clean; both default and --all-features test paths pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(masternodes): correct global-nav coverage claim (F-003)

CHANGELOG and the components README claimed the global wallet/identity
switcher was on "every screen". It ships Phase-A: rendered on Identities,
DashPay, DPNS, Wallets, Identity Hub, and Masternodes, interactive only
on the Hub and Masternodes (the other four render subdued, read-only
pills), and absent from every other root screen (Contracts, Tokens,
Tools, Network Chooser, Withdraws, ...). Names the actual screens and
notes the rest as a tracked follow-up instead of implying full rollout.

* fix(masternodes): Fable final round — Tier-2 merge seal, load gate, ProTxHash error

F-001 (MUST-FIX): "Add voting key" on a password-protected (Tier-2) node no
longer trips the insert's fail-closed guard. load_identity now verifies the
node's object password UP FRONT (before the network fetch, mirroring
add_key_to_identity's verify-before-broadcast order) and seals the merged
plaintext keys Tier-2 via seal_merged_plaintext_keys just before the at-rest
insert. Two regression tests: a scripted-prompt success path proving the new
key flips InVault and reads back Protected, and a headless NullSecretPrompt
path proving the merge fails closed with SecretPromptUnavailable before fetch.

F-002: the list screen's load_in_flight gate is cleared only on the load's own
LoadedIdentity result variant (not any routed result), with a refresh_on_arrival
backstop so a tab switch mid-load can never strand "+ Load" at "Loading…".

F-005: a malformed identity-id input now surfaces MalformedProTxHash for
masternode/evonode loads (where the field IS a ProTxHash) and keeps
IdentifierParsingError for User loads. Regression test added.

F-006: masternodes/evonodes legitimately have no HD wallet, so the
"saving identity without wallet" warning is gated to User identities; nodes log
at debug instead.

F-004: correct the MCP masternode_identity_load comment — Overwrite is a
destructive full-replace of stored keys, not a merge/refresh; TODO for a future
load-mode param.

F-007: strip ephemeral review-ID prefixes (SEC-*, Diziet-*, Smythe) from
masternode-scope source comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): add load-form back link + remove object pill from breadcrumb

Live-walkthrough fixes on real testnet data.

Fix 1 — load form back link: the load form now renders the same
`‹ All masternodes` back link as the detail view (wireframe C shows it on
both), at the top of the form, returning to the card list. New kittest
`load_form_back_link_returns_to_list` covers it; the existing
`load_form_opens_from_cta_and_cancels` gets a taller headless window so the
bottom Cancel button stays reachable now that the back row is present.

Fix 2 — remove the masternode object/identity pill from the Masternodes
breadcrumb. Masternode/evonode identities are never wallet-linked (wallet_info
is always None — locked decision #4), so pairing a wallet pill with an object
pill implied a wallet↔masternode relationship that does not exist. The
breadcrumb now carries only segment-1 + the interactive wallet pill; node
selection is driven entirely by card-click → detail and the back link. The
Masternodes page switches to add_top_panel_with_global_nav (non-capturing),
matching every other non-object page. The masternodes_page_nav_spec builder
drops its items/selected params.

This does NOT touch the FR-6 boundary, which is enforced structurally at the
resolution layer (B1) independent of any pill. The release-blocking FR-6
boundary tests (TC-NAV-12 / TC-FR6-07) remain unchanged and green. The
PageScopedObject / SelectPageObject / add_top_panel_with_global_nav_capturing
machinery is retained as the documented, tested boundary pattern for future
page-scoped-object features (the global_nav_switcher tests still exercise it);
only the Masternodes page's use of it is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): reject load when selected node type mismatches on-chain

Loading a masternode/evonode by ProTxHash trusted the load-form Masternode/
Evonode toggle as ground truth with no cross-check. A regular masternode loaded
with "Evonode" selected was silently accepted, mis-badged "Evonode", and shown
the Evonode-only "Claim token rewards" action.

The load task (authoritative layer) now cross-checks the selected type against
the node's actual on-chain registration. A masternode's Platform identity id is
its ProTxHash, so the node is looked up via Core RPC `protx info`; the `type`
field ("Regular"/"Evo") is classified and compared. On a confirmed mismatch the
load is rejected with a typed, actionable `TaskError::NodeTypeMismatch` naming
both the selected and actual types. When the on-chain type cannot be determined
(Core RPC unreachable — e.g. an SPV-only setup with no Core node) the load
proceeds unverified, so this adds no regression for those users.

Layering per CLAUDE.md: the pure classification (`classify_protx_node_type`)
and rejection decision (`node_type_conflict`) live in `model/masternode_input`
and are exhaustively unit-tested (the reported Evonode-on-regular case
included); the backend task owns the network lookup and enforcement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): surface a visible warning when node type is unverified

Follow-up to the node-type cross-check: when Core RPC is unreachable (the
common case for SPV-only users) the node type cannot be verified, and silently
proceeding with an unverified badge reproduced the original UX bug downgraded
from "wrong" to "unverified". The load task now distinguishes the two success
outcomes via a new `BackendTaskSuccessResult::LoadedIdentityTypeUnverified`
variant, and the Masternodes screen surfaces a visible warning banner (not just
a log line) telling the user the badge reflects their selection and to reload
later to confirm. The MCP masternode-load tool reports the same distinction via
a new `node_type_verified` output field.

Regression tests: the pure reject decision (`node_type_conflict`) and the
`NodeTypeMismatch` user message cover the hard-reject path; a kittest drives the
unverified-load result into the live screen and asserts the warning banner is
surfaced to the UI, not merely logged.

Confirmed with team-lead: hard-reject on a confirmed mismatch; a visible (not
log-only) warning on the unverified path. The upstream platform-wallet SPV
masternode-list passthrough (for verifying node type without Core RPC) is
tracked as a separate follow-up against the platform repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert(masternodes): drop Fix #3 node-type validation entirely

Reverts c5167787 and 755eee87. Product decision: trust the user's
Masternode/Evonode toggle as-is, with no on-chain node-type verification.

Rationale: the only non-type-gated actions (Withdraw/Top up/Transfer) are
unaffected by a mis-toggle; the sole type-gated action ("Claim token rewards")
degrades to a clean no-op/failed Platform state transition, not a fund-safety
issue — so the toggle working as the user set it is correct behavior, not a
defect. Dropping verification also removes the dependency on Core RPC (being
deleted in the platform-wallet migration) and on fetching the operator identity
(extra scope), leaving the load path simpler and migration-proof.

Removes: TaskError::NodeTypeMismatch, the onchain_node_type Core RPC cross-check,
the classify_protx_node_type/node_type_conflict model helpers, the
LoadedIdentityTypeUnverified result variant + UI warning banner, the MCP
node_type_verified output field, and all associated tests. Fixes #1 (load-form
back link) and #2 (breadcrumb pill removal) in 5db23f72 are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): adapt retargeted tab to platform-wallet rewrite APIs

Rebasing the Masternodes tab onto the platform-wallet backend rewrite
(PR #860) surfaced three call sites where the rewrite reshaped an API the
masternode-tab code depended on:

- `AppContext::identity_kv()` was renamed to `det_kv()`; the load-path
  existence check (`get_local_qualified_identity`) now calls the new name.
- `ContestState::state_is_votable()` was dead-code-removed by the rewrite,
  but `ContestedName::is_open_for_voter` (Masternodes card DPNS status)
  relies on it — restored as a live, un-gated method.
- The rewrite dropped the `identity-hub` Cargo feature and renders the
  Identity Hub nav entry unconditionally; the left-panel builder no longer
  gates that entry behind the removed `#[cfg(feature = "identity-hub")]`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(withdraw): screen-level kittest coverage for default_withdrawal_key fix

WithdrawalScreen::new() pre-selecting via default_withdrawal_key() was only
unit-tested at the QualifiedIdentity model layer. Add tests/kittest/withdraw_screen.rs
to verify the fix at the actual screen layer: ghost-key identities (on-chain-only
TRANSFER key) render the no-keys empty state instead of a form, private-key-backed
TRANSFER/OWNER keys are pre-selected AND rendered correctly in the key-selection
ComboBox (via accesskit value, not label). Also locks in a genuine regression the
fix newly exposes: WithdrawalScreen::new() leaks a raw "No key provided when
getting selected wallet" error banner for any identity with no locally-signable
withdrawal key.

* fix(withdraw): skip wallet resolution when no signable key exists

WithdrawalScreen::new() called get_selected_wallet with selected_key=None
after default_withdrawal_key() correctly began returning None for identities
with no locally-signable withdrawal key. With app_context=None that hits the
"No key provided" String Err path, which .or_show_error() posted verbatim
into a user-facing MessageBanner — violating the plain-language error policy.

Guard the call on selected_key being Some, so the raw-string Err branch is
structurally unreachable here instead of avoided by luck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(masternodes): add user story for network-switch reset behavior

Live QA of the Masternodes tab confirmed MasternodesScreen::reset_for_network_change()
cleanly resets the List view and clears stale banners/form data on a network switch,
with no existing story documenting this PR's fix. Add MN-010.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(withdraw): flip banner-leak test to a regression lock (2edbc18)

WithdrawalScreen::new() now guards get_selected_wallet on selected_key
being Some (2edbc18), so the raw "No key provided..." banner leak for
ghost-key-only identities is fixed. Rename
ghost_key_construction_leaks_raw_error_banner ->
ghost_key_construction_does_not_leak_raw_error_banner and invert the
assertion; also verify the no-keys empty state still renders correctly
for the same identity, so the fix didn't trade the leak for a broken
empty state.

* fix(mcp): stop det-cli double-prefixing the HTTP bearer token

rmcp 1.7's StreamableHttpClientTransportConfig::auth_header takes the
raw token and lets reqwest's bearer_auth prepend "Bearer " itself. The
det-cli client passed format!("Bearer {token}"), so the wire header
became "Authorization: Bearer Bearer <token>". The server middleware
strips one "Bearer " and compares the remaining "Bearer <token>" against
the configured key — never equal — so headless HTTP mode returned 401 on
every request. The auth path had no end-to-end coverage, which is why it
shipped broken.

Pass the raw token and add tests/mcp_http_auth.rs pinning the server's
wire contract: raw token accepted, a double-"Bearer" prefix rejected,
missing credentials rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): share Expert Mode flag app-wide; node-specific load error

Two live-QA bugs on the Masternodes tab (PR #876).

Bug 1 — Expert Mode didn't reveal the Masternodes nav without a restart.
`developer_mode` was an independent per-network `AppContext` AtomicBool, kept
in sync only by a best-effort loop in the Settings checkbox handler over the
contexts that happened to exist at click time. AppState keeps one context per
network (only the active one at startup; others created lazily on switch), so
the left-nav `FeatureGate::DeveloperMode` gate could read a stale value on a
different context than the toggle mutated — the nav entry stayed hidden until a
restart re-read the persisted flag into the single fresh context. Promote the
flag to one shared `Arc<AtomicBool>`, created once in `AppState::new` and
injected into every `AppContext::new` (startup + `SwitchNetwork` via
`developer_mode_handle()`), so all per-network contexts observe one flag. Drop
the fragile sync loop; request a repaint on toggle since enabling Expert Mode
disables animations (which stops continuous repaints).

Bug 2 — loading a masternode by a valid-but-unregistered ProTxHash showed the
generic "Identity not found — check the ID or name" copy, wrong for a ProTxHash
load form. Add a dedicated `TaskError::MasternodeNotFound` variant and return it
from the masternode/evonode load path instead of `IdentityNotFound`.

Regression tests: `developer_mode_is_shared_across_network_contexts` (RED before
the shared-flag fix), `masternode_not_found_message_is_node_specific`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
lklimek added a commit that referenced this pull request Jul 13, 2026
…lassification, and role-picker UI (#879)

* fix(withdraw): pre-select only a locally-signable withdrawal key

The Withdraw screen constructor pre-selected a key via the on-chain
lookup `identity.get_first_public_key_matching(TRANSFER, ...)`, which is
unfiltered by local private-key presence. On loaded masternode/evonode
identities where only the Owner key was supplied, this picked a
"ghost" Transfer key with no local private material, so the withdrawal
failed at signing with a raw, unhelpful protocol error.

- model: add `QualifiedIdentity::default_withdrawal_key()` — sourced from
  `available_withdrawal_keys()` (private-key-backed only), Transfer
  preferred with Owner fallback, `None` when nothing is signable.
- ui: constructor now pre-selects via `default_withdrawal_key()`; the
  developer-mode on-chain escape hatch is preserved. When no usable key
  exists the existing empty-state guides the user to add one.
- error: add `TaskError::NoWithdrawalSigningKey` (typed `#[source]`,
  plain-language actionable Display) mapping the SDK
  `DesiredKeyWithTypePurposeSecurityLevelMissing` protocol error as a
  defense-in-depth backstop instead of leaking a raw string.
- tests: 4 model cases (ghost key rejected, private-backed selected,
  owner fallback, transfer preferred) + 2 error-mapping/Display cases.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(claude): correct secret-storage note on identity-key encryption

Identity keys (imported/loaded, including masternode voting/owner/payout)
are no longer categorically in the deferred keyless tier: they enter
unprotected at load time but can be sealed to Tier-2 per-identity via
IdentityTask::ProtectIdentityKeys (Key Info screen "Add password
protection"). Clarify that the keyless residual is only no-password
secrets and keys the user has not opted to protect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* feat(masternodes): add page-nav model with two-scope selection (A1)

Introduce ui/state/global_nav.rs: PageNavSpec (page-aware segment-1 +
per-page pill composition) and IdentityPillScope (AppGlobalUser vs
PageScopedObject). The PageScopedObject variant carries its own selection
and never writes AppContext::selected_identity_id — the structural FR-6
boundary the global switcher (A2) and the Masternodes page (B7) build on.

Pure state, renders nothing (module-placement discriminator -> ui/state).

Satisfies TC-NAV-13, TC-NAV-14, TC-NAV-15; foundation for TC-NAV-12,
TC-FR6-07.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): generalize breadcrumb into page-aware global switcher (A2)

Add ui/components/global_nav_switcher.rs: a page-aware switcher driven by
PageNavSpec, rendering segment-1 (page label + link) plus composable
wallet / identity-or-object pills. GlobalNavEffect generalizes the hub's
BreadcrumbEffect and adds SelectPageObject for the page-scoped pill — kept
distinct from SelectIdentity so a page-scoped selection never writes the
app-global identity (FR-6 boundary at the effect level).

Reuses BreadcrumbPill / IdentityPill / BreadcrumbPillMode verbatim (no new
pill widget). breadcrumb_switcher.rs becomes a thin hub-facing shim that
builds the hub spec, delegates to the generalized render, and maps the
effect back (self-nav to the hub root -> OpenPicker), keeping hub behavior
unchanged — verified by the existing identity_hub_switcher kittests.

Satisfies TC-NAV-01, TC-NAV-02, TC-NAV-03, TC-NAV-13, TC-NAV-16.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): render global switcher on root screens + shared applier (A3)

Add the shell glue in top_panel.rs: apply_global_nav_effect (the shared
successor to the hub's apply_breadcrumb_effect — silent app-scoped
wallet/identity writes, no forced navigation) and add_top_panel_with_global_nav
(one-call render-plus-apply), with subdued_everyday_spec / subdued_wallet_only_spec
Phase-A rollout helpers.

Wire the switcher onto four non-Hub root screens with Subdued (unwired)
specs + TODO markers: Identities, DPNS, DashPay (everyday: wallet + identity
pills) and Wallets (wallet-only composition, TC-NAV-15). The Hub keeps its
existing interactive pills via the breadcrumb shim (regression — full
kittest suite green).

Document (comment + test, PROJ-010) that set_selected_hd_wallet reconciles
the app-global identity as a side effect on non-Hub pages, and that combined
with B1's resolution-layer filter it must never reconcile onto an MN/Evonode.

Deferred (documented): tokens/tools screens carry in-header sub-navigation
that needs the FR-GLOBAL-NAV-5 content-panel back-row migration before their
plain breadcrumb can be swapped for the global switcher — a follow-up, not a
mechanical swap.

Satisfies TC-NAV-06, TC-NAV-15, TC-NAV-16; enables TC-NAV-12/17.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): load-time key encryption plumbing (B0)

FR-8: add IdentityInputToLoad.encryption_password: Option<Secret>. When
Some, load_identity validates the password up front (fast fail) then, after
insert migrates the keyless keys into the vault, seals them Tier-2 through
the existing per-identity protect envelope (protect_identity_keys →
put_secret_protected via the secret_seam chokepoint) — no new crypto, no
second persistence path. When None, the keyless Tier-1 path is unchanged.

Relocate validate_protection_password from protect_identity_keys.rs into
model/identity_key_protection.rs (PROJ-006, DET validation-placement rule);
the seal path and load path both call the model validator.

MCP masternode_identity_load passes encryption_password: None (PROJ-007 —
GUI-only this iteration, requirements §2.3) with a TODO for headless
password parity.

Add typed TaskError variants DuplicateProTxHash { identity_id } and
MalformedProTxHash { input } for later duplicate/malformed rejection (B1/B4),
avoiding string parsing.

Tests: model validator (relocated); an offline-wired-AppContext test proving
a load-time password seals a masternode's voting (V-target), owner and
identity (M-target) keys Tier-2 and round-trips under the password — the
exact call load_identity makes (TC-FR8-01/02/10). Load-form UI is B4;
end-to-end load routing is covered by the network backend-e2e suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(masternodes): drop ephemeral review ID from A3 reconciliation comment

Self-review: replace a transient review-finding ID in the apply_global_nav_effect
reconciliation note with the durable FR reference. Comment-only; no behavior
change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): FR-6 resolution-layer boundary + masternode accessor (B1)

FR-6 (R1, release-blocking): keep masternode/evonode identities out of every
everyday-user surface by filtering at the resolution layer, not the display
call sites.

- resolve_selected_identity(): candidate set filtered to IdentityType::User
  before resolving, so neither keep-if-loaded nor the first-loaded fallback
  can ever resolve a masternode — even when a masternode is the only/first
  loaded identity (TC-NAV-12b).
- set_selected_hd_wallet(): the wallet-switch identity reconciliation resolves
  only over the wallet's User identities.
- restore_selected_identity_from_kv(): one-time sanitization — a masternode
  persisted as selected_identity_id in a prior session is cleared on load;
  a User selection is kept (TC-NAV-12c). In-memory only (non-destructive).
- Display sources switched to the established User-only accessor
  load_local_user_identities(): the global switcher's identity pill + dropdown
  and the Identity Hub landing/picker now list User identities only, so the
  wallet-less "no wallet on this device" group can no longer surface an
  MN/Evonode (TC-NAV-17). Masternodes stay in the legacy Identities table
  (unfiltered accessor untouched — locked decision #2).

New context accessor load_local_masternode_identities() (hydrated MN/Evonode)
— the Masternodes-page card list + page-scoped pill source (B3/B7).

Tests: offline-wired-AppContext unit tests (accessors, resolution filter incl.
lone-masternode fallback, stale-MN sanitization) + a Hub kittest asserting a
seeded Masternode+Evonode never appear on the hub while remaining in the
masternode accessor. TC-FR6-01…06, TC-NAV-12b/12c, TC-NAV-17.

Deferred to consuming tasks (documented): FR-7 refresh (TC-FR7-02/03) composes
existing RefreshIdentity + contested-names refresh at the card Refresh button
(B3); the per-node open-contest card read accessor lands in B3 where the card
consumes it and it is testable against the rendered status line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): register Expert-gated Masternodes root tab (B2)

Add RootScreenType::RootScreenMasternodes (stable int 28, round-trip test),
ScreenType::Masternodes, Screen::MasternodesScreen, create_screen and all
ScreenLike dispatch arms; register the always-present root screen in app.rs
(gated at runtime by Expert Mode, not a Cargo feature, so the screen exists
to switch into when the gate is on).

Nav: a "Masternodes" left-panel entry gated FeatureGate::DeveloperMode,
positioned directly below the identity cluster (locked decision #3),
independent of the identity-hub feature. Distinct glyph voting.png (TODO:
dedicated node/server icon). The existing per-entry gate skip hides the nav
item and route when Expert Mode is off.

Live de-gating (§10.11): active_root_screen_mut falls the active tab back to
Identities (always registered) if Expert Mode flips off while Masternodes is
selected, so the gated screen is never shown without its gate.

MasternodesScreen is a scaffold (global-nav header + left rail + island
placeholder); the empty state + card grid land in B3, the page-scoped
masternode pill in B7. Network-switch already calls change_context on
main_screens; the sub-screen reset (§10.10) applies once B4/B5 push
sub-screens (noted for B8).

Tests: RootScreenType round-trip (int 28 stable); kittest — nav absent
Expert-off / present Expert-on, and de-gating falls back to Identities.
TC-FR1-01…07, TC-EDGE-05/06.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): empty state + card grid + card body (B3)

Render the Masternodes root screen content on top of the B2 scaffold:

- Empty state (FR-2): canonical §7 copy — heading, body, "Load a
  masternode" primary CTA, and the ProTxHash reassurance line.
- Card grid (FR-3): responsive minmax(260,1fr) grid reusing the identity
  picker's visual language via a new `MasternodeCard` (monogram +
  `draw_type_badge`, both now `pub(crate)`). Body adds masternode rows the
  picker lacks: voter readiness, compact `V O P` key status (glyph, not
  colour-only — NFR-6), DPNS status line, and the IdentityStatus dot+label.
- DPNS status precedence (§10.1): open-contest count first, then a pending
  scheduled vote, then "No open contests", via a display-layer
  `AppContext::masternode_contest_summary` read (no new backend concept).
- Key presence: `QualifiedIdentity::masternode_key_presence` maps
  Voting/Owner/Payout to voter-identity / OWNER / TRANSFER keys.
- Top-right Refresh toolbar button (FR-7) reloads the cached node list.
- Whole card is a single accessible click target (`WidgetInfo::labeled`,
  NFR-6); selection/load intents are captured for B4/B5a/B7 wiring.

Traceability: TC-FR2-01…07, TC-FR3-01…15, TC-FR7-01, TC-NFR6-01/03.
Unit tests cover heading/sub-line, DPNS precedence, key tokens, badge, and
the 8 V/O/P combinations; kittest covers empty-state copy and the grid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): dedicated load form + ProTxHash validator (B4)

Add the MN/Evonode-only load flow (FR-4), carved out of the generic
add-existing-identity path:

- `ui/masternodes/load_form.rs`: ProTxHash (required), Masternode/Evonode
  segmented toggle (default Masternode, no User option), optional alias,
  V/O/P key inputs (reused hold-to-reveal `PasswordInput`), optional at-load
  encryption password (drives B0's seal), always-visible Warning-tone
  key-storage note, and a Load button gated on a non-empty ProTxHash with the
  §7 disabled tooltip. Switching node type clears all fields (§10.6). No
  auto-derive affordance — masternode keys are never wallet-derived
  (US-6 retired, §Locked-#4).
- `model/masternode_input.rs`: `is_valid_pro_tx_hash` shape validator (hex or
  Base58) for inline on-blur validation; the backend load task remains the
  authoritative existence/duplicate check.
- Masternodes screen gains a List/Load view enum; the empty-state CTA and a
  `+ Load` toolbar button open the form; submit dispatches
  `IdentityTask::LoadIdentity`; cancel/submit return to the list with a fresh
  form on reopen.
- `add_existing_identity_screen`: remove Masternode/Evonode from the
  Advanced-Options Identity-Type dropdown (User-only remains) — no competing
  entry point (§10.2 / TC-FR4-22, FR-6).

Traceability: TC-FR4-01…22 (render/logic; live-network accept/duplicate/
error-banner paths land in B8), TC-EDGE-01/02.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): detail view — header, actions, keys, remove (B5a)

Add the node detail view (FR-5), reusing existing screens rather than
reimplementing:

- Fixed section order Header → Actions → Keys → DPNS → Remove (TC-FR5-01,
  the human-requested Actions-above-Keys correction), pinned by a unit test.
- Header: conditional alias, shortened ProTxHash + copy-full-value, type
  badge (shared `draw_type_badge`), IdentityStatus dot + label.
- Actions row (FR-9): Withdraw / Top up / Transfer push the existing
  WithdrawalScreen / TopUpIdentity / TransferScreen scoped to the node's
  QualifiedIdentity (both node types). Evonode-only `Claim token rewards ›`
  cross-link (FR-11), absent for a plain masternode.
- Keys section (FR-10): V/O/P presence, copyable voter-identity id, at-rest
  protection tier (vault-scheme probe), Add-protection offered only Tier-1,
  `Manage keys ›` into the existing key screen.
- DPNS section: collapsible, open-contest count in the header (voting table
  lands in B5b).
- Remove: danger ConfirmationDialog; deletes the node and its voter identity.
- `‹ All masternodes` back row + detail Refresh (FR-7/TC-FR7-04); card click
  opens the detail via a List/Load/Detail view enum.

Deviations (documented): the Evonode claim cross-link routes to the Tokens
area — precise ClaimTokensScreen token-scoping is deferred to B8 where the
evonode reward-token context is resolvable. Add-protection routes into the
reused key screen (which hosts the password-entry seal flow) rather than
duplicating the form.

Traceability: TC-FR5-01…05/07, TC-FR7-04, TC-FR9-01/02, TC-FR11-01/02.
Live-network credit/claim routing and TC-FR8-07 land in B8.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): Testnet Fill-Random on the load form (B6)

Add the FR-12 dev convenience to the masternode load form:

- New `testnet_fixture` module owns the `.testnet_nodes.yml` structs + loader.
  The loader returns None for BOTH a missing and a malformed file — a malformed
  file is logged at debug and treated as absent (TC-FR12-04, a deliberate
  divergence from the legacy screen which banners the parse error).
- Fill-Random button + hint render only when Expert Mode is on, the network is
  Testnet, and the fixture is present — never shown-but-disabled (TC-FR12-01…06).
  The `dev_mode` gate is a defense-in-depth re-check at the call site
  (TC-FR12-09 decision: added on future-proofing grounds — a plaintext-key dev
  tool stays inside the Expert-Mode envelope).
- Button label follows the node-type toggle (TC-FR12-01/02).
- Autofill pulls from the correct list per type (TC-FR12-07/08): Masternode →
  `masternodes` (Voting + Owner only; the fixture has no payout key, PROJ-003),
  Evonode → `hp_masternodes` (all three keys). The node-type toggle still clears
  autofilled fields (§10.6).
- The fixture loads once when the form opens (Testnet only), not per frame.

Traceability: TC-FR12-01…09.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): inline DPNS voting + missing-voter prompt (B5b)

Populate the detail view's collapsible DPNS section (FR-5):

- Collapsed by default; header shows the open-contest count
  (`DPNS name contests to vote on (N)`, TC-DPNS-01/02).
- Voter present + open contests: per-contest Abstain / Lock / Vote-for-candidate
  choices with the candidate list scoped to that contest's contestants; a
  `Cast votes` button dispatches the existing
  `ContestedResourceTask::VoteOnDPNSNames` backend inline (locked decision #1 —
  not a deep-link). TC-DPNS-03/04/05.
- Voter present, zero open contests: exact §7 empty copy (TC-DPNS-08).
- Missing voter identity: the actionable §7 message (never the raw
  NoVotingIdentity error) plus an `Add voting key` action that opens a scoped,
  in-place voter-key prompt with the node context pre-bound — distinct from
  FR-4's load form, no ProTxHash re-entry (TC-DPNS-09/10/11, §10.8). Save
  re-loads this node with just the voting key to update its voter identity.
- Detail Refresh now re-reads both the contest summary and the open-contest
  list.

Active/open contests only — scheduled/past history stays on the DPNS Scheduled
Votes screen (§10.7).

Traceability: TC-DPNS-01…11.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(masternodes): page-scoped nav pill — FR-6 boundary in code (B7)

Wire the Masternodes page into the global-nav switcher with a page-scoped
masternode pill whose selection lives on the page and is NEVER written to
`AppContext::selected_identity_id` — the structural FR-6 boundary in code,
complementing B1's resolution-layer filter.

- New `ui/state/masternodes_view.rs` builds the page's `PageNavSpec`: page-aware
  `Masternodes` segment-1, an interactive wallet pill (funds Top up — FR-9), and
  a page-scoped identity pill via `IdentityPillScope::PageScopedObject`. Empty →
  subdued `(no masternode yet)` placeholder; ≥1 node → interactive dropdown with
  `Choose a masternode` placeholder; the pill reflects the node in detail and
  resets to the placeholder on `‹ All masternodes` (§10.4).
- New `top_panel::add_top_panel_with_global_nav_capturing` surfaces the
  `SelectPageObject` pick to the caller (applying all other effects as usual)
  without ever routing it into the app-global identity selection.
- The Masternodes screen builds the spec each frame from its node list + current
  view and opens the picked node's detail — two-way with the card grid.

TC-NAV-12 / TC-FR6-07 (release-blocking): a masternode selected on the page
never becomes, or resolves as, the app-global identity — verified across
Identities and the Identity Hub with no User identity loaded.

Traceability: TC-NAV-01/03/04/05/07, TC-NAV-12, TC-FR5-06, TC-FR6-07.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(masternodes): cross-cutting integration coverage (B8)

Add the Remove-flow integration kittest (TC-US4-01/02/06/07): the detail danger
button opens a confirmation carrying the `Remove masternode` verb, and
confirming deletes only the target node — its card disappears while other nodes
survive (isolation). Also sets the confirmation's confirm verb to
`Remove masternode` (§7 / TC-US4-02), the one small production touch the test
surfaced.

Deferred to the network/backend-e2e pass (out of kittest reach without live
DAPI or heavy vault fixtures, and flagged as such in the plan): TC-FR8-07
(detail reflecting a load-time Tier-2-sealed node — needs the real
password-at-load seal path), TC-US4-05 voter-row deletion assertion (needs a
seeded voter-identity row), and the live-network credit/vote/claim dispatch
paths behind FR-9/FR-11/DPNS Cast-votes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(user-stories): catalog the Masternodes tab, retire the legacy load story

Adds MN-001..MN-009 (load, card grid, vote, remove, Hub filter, at-load
encryption, credit actions, key management, evonode token-reward cross-link)
and UX-003 (global wallet/identity switcher) per the completed Masternodes
feature. Flips IDN-003 to superseded — its generic-screen masternode load
path was removed when the dedicated tab shipped.

* docs(masternodes): commit final design docs (DOC-002)

Lands the human-accepted requirements, UX spec, test-case spec, and dev
plan under docs/ai-design/, following the existing 2026-04-23-identity-hub-impl
numbered-file convention. Resolves the ~100+ FR-/TC-/US-/§-ID references
already scattered through the feature's code comments and tests, which
pointed at an uncommitted /data/artifacts scratch copy. Internal
cross-file references (requirements.md, ux-spec.md, etc.) are updated to
the new numbered filenames.

* docs(masternodes): trim oversized module docs, catalog global-nav switcher

Shortens the four ui/masternodes/*.rs module doc comments to the
internal-tier length cap (DOC-003) — they weren't published API, so the
5-10 line public-rustdoc relaxation didn't apply. Adds GlobalNavSwitcher
and its top_panel entry point to ui/components/README.md's catalog
(DOC-004), so the next screen needing a page-aware switcher finds it
instead of reimplementing one.

* fix(masternodes): guard identity load against silent overwrite (QA-005/006)

Root-cause storage fix. insert_local_qualified_identity is INSERT OR
REPLACE, so a load with no guard silently clobbers an already-stored
identity and its keys. Thread an IdentityLoadMode through
IdentityInputToLoad so each entry point declares intent:

- RejectIfExists: the masternode load form rejects a duplicate ProTxHash
  with TaskError::DuplicateProTxHash before any network fetch (QA-006).
- MergeIntoExisting: the scoped Add-voting-key prompt merges the new key
  into the stored identity, preserving Owner/Payout it did not resupply
  (QA-005), via merge_existing_keys_into.
- Overwrite: legacy User re-load and headless flows unchanged (default).

Adds get_local_qualified_identity accessor backing the existence check
and merge read. Failing-first TDD: a unit test proving Owner/Payout keys
survive a voting-key-only merge, and an offline test proving a duplicate
ProTxHash is rejected and the first node is left untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): key routing, network-switch reset, live refresh

QA-007: the detail Keys section pushed the static read-only KeysScreen.
Render a per-key 'Manage keys' list and route the Add-protection CTA to
KeyInfoScreen (interactive view/sign/seal per key), mirroring
identities_screen.

QA-001: MasternodesScreen had no change_context override, so a network
switch left an open load form or cross-network detail view actionable.
Add an explicit change_context arm that resets to the List view and
reloads from the now-active network.

QA-003: both Refresh buttons only re-read the local cache. Wire them to
dispatch IdentityTask::RefreshIdentity (per loaded node on the list, the
open node on detail) plus a QueryDPNSContests re-query, alongside the
optimistic local re-read.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): SHOULD-FIX pass + QA-token cleanup + offline tests

- QA-002: remove the legacy Fill-Random HPMN/Masternode bypass from the
  now User-only add-existing-identity screen (it set identity_type to
  Evonode/Masternode directly, defeating the User-only restriction).
- QA-004: the evonode 'Claim token rewards' CTA pushes a real scoped
  ClaimTokensScreen when the node holds exactly one token, falling back to
  My Tokens when the target is ambiguous — no more bare SetMainScreen.
- QA-008: refresh the open detail view after its own backend task, not
  just the card list.
- Diziet-F3: render the missing-voter 'Add voting key' CTA above, outside
  the collapsed DPNS section, so it is visible without expanding.
- QA-009: surface a MessageBanner when node removal fails instead of a
  silent tracing::warn.
- SEC-001: log the testnet-fixture parse error by position only, never
  its Display text (which echoes a private key).
- SEC-002: parse fixture key fields as Secret (redacted/zeroized).
- Strip stale PROJ-003/PROJ-004 markers and all ephemeral QA-xxx review
  IDs from source comments (kept only in commit messages).
- TODOs for the deferred mixed-protection-tier CTA and the
  is_valid_pro_tx_hash/decode_identity_id duplication.
- Offline tests: TC-FR8-07 (protection_tier reflects a Tier-2 seal) and
  TC-US4-05 (Remove deletes the associated voter identity).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(dashpay): close FR-6 boundary bypass via DashPay identity selectors (SEC-005)

DashPay screens (send_payment, contacts_list, profile_screen,
contact_requests, qr_scanner, qr_code_generator, add_contact_screen,
profile_search) built their IdentitySelector and constructor seed from
the unfiltered load_local_qualified_identities() chained with
.syncing_global(...). IdentitySelector::sync_to_global() writes the
picked id straight to AppContext::selected_identity_id — a separate path
from B1's resolve_selected_identity()/restore filters — so a user could
select a masternode/evonode as the app-global operate-as identity from
inside DashPay, bypassing the FR-6/R1 boundary B1 established.

DashPay operates on User identities only, so every identity list in these
screens is sourced from load_local_user_identities() (the same swap B1
made for the global-nav switcher and Identity Hub). This filters the
masternode out of both the selector write-path and the constructor seed.

Adds a kittest (masternode_never_selectable_in_dashpay_screens) exercising
the FR-6 boundary through five DashPay screens — the existing FR-6 kittest
only covered Identities/Identity Hub, which is how this slipped through.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(masternodes): Marvin punch-list — in-flight guard + execution tests

- QA-012: gate re-submission while a node-load is in flight. Add a
  load_in_flight flag on MasternodesScreen, set on Submit dispatch and
  cleared on the task result or a new display_task_error override; the
  '+ Load' toolbar button and empty-state CTA show a spinner + disabled
  'Loading…' while set, so a rapid double-submit of a brand-new ProTxHash
  cannot race two loads past the pre-fetch existence check.
- Extend masternode_never_selectable_in_dashpay_screens to QRScanner,
  QRCodeGenerator (both seed selected_identity in new()) and assert
  ProfileSearchScreen's User-filtered data source excludes the masternode
  — FR-6 coverage now spans all 8 DashPay screens.
- Add manage_keys_button_opens_key_info_screen: clicks a per-key
  'Voting key ›' button and asserts a KeyInfoScreen is pushed with its
  'Key Information' heading (execution-level proof of the QA-007 fix).
- Add refresh_from_network unit test: one RefreshIdentity per loaded node
  plus a trailing QueryDPNSContests, None when empty (QA-003).
- Fix two doc-comment lines mangled by the earlier review-ID strip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(changelog): add Masternodes tab and global nav switcher (DOC-005)

Covers the user-facing outcomes of the completed Masternodes feature:
the new Expert-Mode-gated tab (card list, detail view, load-time key
encryption, inline DPNS voting, credit actions, Evonode token-reward
claiming) replacing the old generic load path for masternode/evonode
identities, the resulting Identity Hub / Identities picker filter, and
the wallet/identity switcher now present on every root screen instead
of just the Identity Hub.

* fix(masternodes): default GUI build broken — masternode_input feature-gated

The whole model::masternode_input module was gated behind
load form (ui/masternodes/load_form.rs) imports is_valid_pro_tx_hash from
it unconditionally. So a plain 'cargo build --bin dash-evo-tool' (default
features only — no mcp/cli, the documented quick-start build) failed with
E0432 unresolved import. Every gate this feature ran used --all-features,
which always pulls mcp+cli and masked it.

The module can't be blanket-ungated: its parse/decode helpers return
McpToolError (from the feature-gated mcp module). Fix ungates the module
and the pure is_valid_pro_tx_hash validator (the only thing the GUI needs),
and gates precisely the McpToolError-coupled items — KeyMode, parse_node_type,
parse_key_mode, require_at_least_one_signing_key, decode_identity_id, their
imports, and their tests — behind mcp/cli. The pure validator's tests move
to an always-compiled module so they run in the default build too.

Verified: 'cargo build --bin dash-evo-tool' (no flags) compiles; default-
feature clippy clean; both default and --all-features test paths pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(masternodes): correct global-nav coverage claim (F-003)

CHANGELOG and the components README claimed the global wallet/identity
switcher was on "every screen". It ships Phase-A: rendered on Identities,
DashPay, DPNS, Wallets, Identity Hub, and Masternodes, interactive only
on the Hub and Masternodes (the other four render subdued, read-only
pills), and absent from every other root screen (Contracts, Tokens,
Tools, Network Chooser, Withdraws, ...). Names the actual screens and
notes the rest as a tracked follow-up instead of implying full rollout.

* fix(masternodes): Fable final round — Tier-2 merge seal, load gate, ProTxHash error

F-001 (MUST-FIX): "Add voting key" on a password-protected (Tier-2) node no
longer trips the insert's fail-closed guard. load_identity now verifies the
node's object password UP FRONT (before the network fetch, mirroring
add_key_to_identity's verify-before-broadcast order) and seals the merged
plaintext keys Tier-2 via seal_merged_plaintext_keys just before the at-rest
insert. Two regression tests: a scripted-prompt success path proving the new
key flips InVault and reads back Protected, and a headless NullSecretPrompt
path proving the merge fails closed with SecretPromptUnavailable before fetch.

F-002: the list screen's load_in_flight gate is cleared only on the load's own
LoadedIdentity result variant (not any routed result), with a refresh_on_arrival
backstop so a tab switch mid-load can never strand "+ Load" at "Loading…".

F-005: a malformed identity-id input now surfaces MalformedProTxHash for
masternode/evonode loads (where the field IS a ProTxHash) and keeps
IdentifierParsingError for User loads. Regression test added.

F-006: masternodes/evonodes legitimately have no HD wallet, so the
"saving identity without wallet" warning is gated to User identities; nodes log
at debug instead.

F-004: correct the MCP masternode_identity_load comment — Overwrite is a
destructive full-replace of stored keys, not a merge/refresh; TODO for a future
load-mode param.

F-007: strip ephemeral review-ID prefixes (SEC-*, Diziet-*, Smythe) from
masternode-scope source comments.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): add load-form back link + remove object pill from breadcrumb

Live-walkthrough fixes on real testnet data.

Fix 1 — load form back link: the load form now renders the same
`‹ All masternodes` back link as the detail view (wireframe C shows it on
both), at the top of the form, returning to the card list. New kittest
`load_form_back_link_returns_to_list` covers it; the existing
`load_form_opens_from_cta_and_cancels` gets a taller headless window so the
bottom Cancel button stays reachable now that the back row is present.

Fix 2 — remove the masternode object/identity pill from the Masternodes
breadcrumb. Masternode/evonode identities are never wallet-linked (wallet_info
is always None — locked decision #4), so pairing a wallet pill with an object
pill implied a wallet↔masternode relationship that does not exist. The
breadcrumb now carries only segment-1 + the interactive wallet pill; node
selection is driven entirely by card-click → detail and the back link. The
Masternodes page switches to add_top_panel_with_global_nav (non-capturing),
matching every other non-object page. The masternodes_page_nav_spec builder
drops its items/selected params.

This does NOT touch the FR-6 boundary, which is enforced structurally at the
resolution layer (B1) independent of any pill. The release-blocking FR-6
boundary tests (TC-NAV-12 / TC-FR6-07) remain unchanged and green. The
PageScopedObject / SelectPageObject / add_top_panel_with_global_nav_capturing
machinery is retained as the documented, tested boundary pattern for future
page-scoped-object features (the global_nav_switcher tests still exercise it);
only the Masternodes page's use of it is removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): reject load when selected node type mismatches on-chain

Loading a masternode/evonode by ProTxHash trusted the load-form Masternode/
Evonode toggle as ground truth with no cross-check. A regular masternode loaded
with "Evonode" selected was silently accepted, mis-badged "Evonode", and shown
the Evonode-only "Claim token rewards" action.

The load task (authoritative layer) now cross-checks the selected type against
the node's actual on-chain registration. A masternode's Platform identity id is
its ProTxHash, so the node is looked up via Core RPC `protx info`; the `type`
field ("Regular"/"Evo") is classified and compared. On a confirmed mismatch the
load is rejected with a typed, actionable `TaskError::NodeTypeMismatch` naming
both the selected and actual types. When the on-chain type cannot be determined
(Core RPC unreachable — e.g. an SPV-only setup with no Core node) the load
proceeds unverified, so this adds no regression for those users.

Layering per CLAUDE.md: the pure classification (`classify_protx_node_type`)
and rejection decision (`node_type_conflict`) live in `model/masternode_input`
and are exhaustively unit-tested (the reported Evonode-on-regular case
included); the backend task owns the network lookup and enforcement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): surface a visible warning when node type is unverified

Follow-up to the node-type cross-check: when Core RPC is unreachable (the
common case for SPV-only users) the node type cannot be verified, and silently
proceeding with an unverified badge reproduced the original UX bug downgraded
from "wrong" to "unverified". The load task now distinguishes the two success
outcomes via a new `BackendTaskSuccessResult::LoadedIdentityTypeUnverified`
variant, and the Masternodes screen surfaces a visible warning banner (not just
a log line) telling the user the badge reflects their selection and to reload
later to confirm. The MCP masternode-load tool reports the same distinction via
a new `node_type_verified` output field.

Regression tests: the pure reject decision (`node_type_conflict`) and the
`NodeTypeMismatch` user message cover the hard-reject path; a kittest drives the
unverified-load result into the live screen and asserts the warning banner is
surfaced to the UI, not merely logged.

Confirmed with team-lead: hard-reject on a confirmed mismatch; a visible (not
log-only) warning on the unverified path. The upstream platform-wallet SPV
masternode-list passthrough (for verifying node type without Core RPC) is
tracked as a separate follow-up against the platform repo.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* revert(masternodes): drop Fix #3 node-type validation entirely

Reverts c5167787 and 755eee87. Product decision: trust the user's
Masternode/Evonode toggle as-is, with no on-chain node-type verification.

Rationale: the only non-type-gated actions (Withdraw/Top up/Transfer) are
unaffected by a mis-toggle; the sole type-gated action ("Claim token rewards")
degrades to a clean no-op/failed Platform state transition, not a fund-safety
issue — so the toggle working as the user set it is correct behavior, not a
defect. Dropping verification also removes the dependency on Core RPC (being
deleted in the platform-wallet migration) and on fetching the operator identity
(extra scope), leaving the load path simpler and migration-proof.

Removes: TaskError::NodeTypeMismatch, the onchain_node_type Core RPC cross-check,
the classify_protx_node_type/node_type_conflict model helpers, the
LoadedIdentityTypeUnverified result variant + UI warning banner, the MCP
node_type_verified output field, and all associated tests. Fixes #1 (load-form
back link) and #2 (breadcrumb pill removal) in 5db23f72 are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): adapt retargeted tab to platform-wallet rewrite APIs

Rebasing the Masternodes tab onto the platform-wallet backend rewrite
(PR #860) surfaced three call sites where the rewrite reshaped an API the
masternode-tab code depended on:

- `AppContext::identity_kv()` was renamed to `det_kv()`; the load-path
  existence check (`get_local_qualified_identity`) now calls the new name.
- `ContestState::state_is_votable()` was dead-code-removed by the rewrite,
  but `ContestedName::is_open_for_voter` (Masternodes card DPNS status)
  relies on it — restored as a live, un-gated method.
- The rewrite dropped the `identity-hub` Cargo feature and renders the
  Identity Hub nav entry unconditionally; the left-panel builder no longer
  gates that entry behind the removed `#[cfg(feature = "identity-hub")]`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(withdraw): screen-level kittest coverage for default_withdrawal_key fix

WithdrawalScreen::new() pre-selecting via default_withdrawal_key() was only
unit-tested at the QualifiedIdentity model layer. Add tests/kittest/withdraw_screen.rs
to verify the fix at the actual screen layer: ghost-key identities (on-chain-only
TRANSFER key) render the no-keys empty state instead of a form, private-key-backed
TRANSFER/OWNER keys are pre-selected AND rendered correctly in the key-selection
ComboBox (via accesskit value, not label). Also locks in a genuine regression the
fix newly exposes: WithdrawalScreen::new() leaks a raw "No key provided when
getting selected wallet" error banner for any identity with no locally-signable
withdrawal key.

* fix(withdraw): skip wallet resolution when no signable key exists

WithdrawalScreen::new() called get_selected_wallet with selected_key=None
after default_withdrawal_key() correctly began returning None for identities
with no locally-signable withdrawal key. With app_context=None that hits the
"No key provided" String Err path, which .or_show_error() posted verbatim
into a user-facing MessageBanner — violating the plain-language error policy.

Guard the call on selected_key being Some, so the raw-string Err branch is
structurally unreachable here instead of avoided by luck.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(masternodes): add user story for network-switch reset behavior

Live QA of the Masternodes tab confirmed MasternodesScreen::reset_for_network_change()
cleanly resets the List view and clears stale banners/form data on a network switch,
with no existing story documenting this PR's fix. Add MN-010.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test(withdraw): flip banner-leak test to a regression lock (2edbc18e)

WithdrawalScreen::new() now guards get_selected_wallet on selected_key
being Some (2edbc18e), so the raw "No key provided..." banner leak for
ghost-key-only identities is fixed. Rename
ghost_key_construction_leaks_raw_error_banner ->
ghost_key_construction_does_not_leak_raw_error_banner and invert the
assertion; also verify the no-keys empty state still renders correctly
for the same identity, so the fix didn't trade the leak for a broken
empty state.

* fix(mcp): stop det-cli double-prefixing the HTTP bearer token

rmcp 1.7's StreamableHttpClientTransportConfig::auth_header takes the
raw token and lets reqwest's bearer_auth prepend "Bearer " itself. The
det-cli client passed format!("Bearer {token}"), so the wire header
became "Authorization: Bearer Bearer <token>". The server middleware
strips one "Bearer " and compares the remaining "Bearer <token>" against
the configured key — never equal — so headless HTTP mode returned 401 on
every request. The auth path had no end-to-end coverage, which is why it
shipped broken.

Pass the raw token and add tests/mcp_http_auth.rs pinning the server's
wire contract: raw token accepted, a double-"Bearer" prefix rejected,
missing credentials rejected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(masternodes): share Expert Mode flag app-wide; node-specific load error

Two live-QA bugs on the Masternodes tab (PR #876).

Bug 1 — Expert Mode didn't reveal the Masternodes nav without a restart.
`developer_mode` was an independent per-network `AppContext` AtomicBool, kept
in sync only by a best-effort loop in the Settings checkbox handler over the
contexts that happened to exist at click time. AppState keeps one context per
network (only the active one at startup; others created lazily on switch), so
the left-nav `FeatureGate::DeveloperMode` gate could read a stale value on a
different context than the toggle mutated — the nav entry stayed hidden until a
restart re-read the persisted flag into the single fresh context. Promote the
flag to one shared `Arc<AtomicBool>`, created once in `AppState::new` and
injected into every `AppContext::new` (startup + `SwitchNetwork` via
`developer_mode_handle()`), so all per-network contexts observe one flag. Drop
the fragile sync loop; request a repaint on toggle since enabling Expert Mode
disables animations (which stops continuous repaints).

Bug 2 — loading a masternode by a valid-but-unregistered ProTxHash showed the
generic "Identity not found — check the ID or name" copy, wrong for a ProTxHash
load form. Add a dedicated `TaskError::MasternodeNotFound` variant and return it
from the masternode/evonode load path instead of `IdentityNotFound`.

Regression tests: `developer_mode_is_shared_across_network_contexts` (RED before
the shared-flag fix), `masternode_not_found_message_is_node_specific`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(user-role): introduce UserRole + composable FeatureGate checks (Phase 1)

Introduce the typed persona axis and generalise the feature gate into a
conjunction of heterogeneous checks, with a zero-behaviour-change compat
shim over the retired binary Expert Mode flag. Phase 1 only — the ~43
is_developer_mode() callsites and the role-setting UI are untouched.

- model/user_role.rs: ordered UserRole { Everyday<Power<Developer }, pinned
  discriminants, as_str/from_persisted (sentinel-safe)/at_least/from_u8.
- context/feature_gate.rs: Capability (ShieldedProtocol predicate moved
  verbatim, per-network by construction), Check { MinRole, Capability,
  Experimental }, empty ExperimentalFeature, FeatureGate::checks() table +
  is_available = checks().all(). DeveloperMode stays mapped to >= Power.
- context/mod.rs: re-type the shared app-global atomic Arc<AtomicBool> ->
  Arc<AtomicU8> (UserRole discriminant); user_role()/set_user_role()/
  experimental_enabled()/user_role_handle(); is_developer_mode() and
  enable_developer_mode() kept as >= Power compat shims; animation gate
  re-pointed at >= Power.
- model/settings.rs: replace UserMode/user_mode with Option<UserRole>/
  user_role, reusing the length-prefixed user_mode wire slot (no offset
  shift). "Advanced"/"Beginner"/empty/unknown decode to None (a sentinel),
  never a role — mapping the universal legacy default to a role would
  silently promote every user.
- context/settings_db.rs: seed a role-less (None) blob once from .env
  DEVELOPER_MODE (true -> Power, else Everyday) at get_app_settings, mirroring
  the impure dash-qt autodetect fallback.
- app.rs / backend_task / mcp: seed and share the role atomic from .env.

Tests: UserRole ordering/round-trip/sentinel; canonical wire round-trip;
"Advanced" -> None; role-less blob seeds Power from .env; explicit role not
reseeded.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(ai-design): add persona/capability-gating design doc

Commit the design doc referenced by src/model/user_role.rs's doc comment
so the pointer resolves once this branch merges — it previously only
existed on the separate design/persona-capability-gating branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(user-role): reclassify dev-mode callsites onto role/capability gates (Phase 2/3) (#880)

* feat(user-role): reclassify dev-mode callsites onto role/capability gates (Phase 2)

Walk every is_developer_mode()/FeatureGate::DeveloperMode callsite and
reclassify each per the four-bucket rubric, then wire the Masternodes tab
to its own gate.

- Add FeatureGate::Masternodes (>= Power); repoint the masternodes nav
  entry and the app.rs live de-gating guard onto it.
- Bucket 1 (disclosure): reclassify to user_role().at_least(Power).
- Bucket 2/3 (signing override at state_transition_options + has_keys
  proceed-without-key bypasses): tighten to at_least(Developer). This is
  an intentional behavior change from today's single dev flag (== Power).
- Bucket 4 (experimental/stability: shielded send + tab, DashPay pay/
  subscreens): move to Check::Experimental via new ExperimentalFeature
  {Shielded, DashPay}; experimental_enabled() stays >= Power for now.
- Rename FeatureGate::DeveloperMode -> DeveloperTools (>= Developer) as
  the forward-looking Developer-tier gate; delete the is_developer_mode()
  and enable_developer_mode() compat shims (no callers remain).
- Remove dead AddressInput::with_developer_mode/set_developer_mode and the
  never-set developer_mode field.
- Update kittest role toggles and the shared-role regression test; flip
  user story WAL-022 (system accounts) from developer-mode to Power role.

PROJ-007 sites (button_text.contains("Test") i18n fragility) left as-is
per brief — only their dev-mode gate portion was reclassified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(user-role): QA follow-ups — IDH-005 wording, dead fallback, override test

- docs/user-stories.md IDH-005: retitle to "Bulk identity creation", persona
  Jordan -> Priya,Jordan, and reword the footer/dropdown criteria to the Power
  role, matching the Power reclassification of the test-identities footer.
- withdraw_screen: tighten the on-chain-only key pre-select fallback from
  at_least(Power) to at_least(Developer). Only Developer can actually sign with
  such a key (signing override + the Developer branch of the has_keys gate), so
  the Power-level pre-select was dead and its comment overclaimed. Comment fixed.
- context: add regression test for the state_transition_options signing override
  — Everyday/Power -> None, Developer -> Some with both allow_signing_with_any_*
  flags true.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(user-role): role-picker UI on Settings + Welcome, single-source persistence (Phase 3/3) (#881)

* feat(user-role): role-picker UI on Settings + Welcome, single-source persistence (Phase 3)

Make AppSettings.user_role the single source of truth for the runtime role
atomic and add both role-setting UI surfaces.

Persistence wiring:
- app.rs boot no longer reads .env DEVELOPER_MODE directly; the shared role
  atomic starts at the default and is seeded from get_app_settings() once the
  active context exists (the .env parser stays for the v34 migration).
- settings_db: get_app_settings now persists the one-time .env seed back to the
  DB, so the sentinel slot is consumed exactly once and later .env changes no
  longer move the role. New AppContext::set_and_persist_user_role centralizes
  "set runtime atomic + write canonical AppSettings string" for both surfaces.

UI:
- Network Settings: replace the binary Expert-mode checkbox with a three-way
  UserRole selector — Default view / Detailed view / Developer tools — with a
  per-mode description. Advanced (RPC/SPV) options stay Power-gated; the
  Developer-tools sub-panel now keys off the Developer role.
- Welcome screen: add an experience-level onboarding row (Everyday/Power/
  Developer) writing the same persisted role.

Tests: add env-seed-consumed-once regression; existing role/seed tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(user-role): CLI/MCP boot single-source + role-selector UX polish (Phase 3 QA)

- mcp/server.rs init_app_context (CLI/MCP standalone boot) seeded the role
  atomic straight from .env DEVELOPER_MODE, bypassing AppSettings.user_role — a
  role chosen in the GUI was ignored headless. Now seeds from
  get_app_settings().user_role, matching the GUI boot path (single source of
  truth).
- UserRole gains label()/description() as the shared selector vocabulary; both
  the Settings selector and the Welcome onboarding row now use them, so a role
  picked in one is findable by name in the other. The Everyday description is a
  complete sentence (i18n rule).
- Welcome row now shows the selected mode's description (parity with Settings).
- Settings "Interface mode" selector lifted above the force-collapsing Advanced
  Settings panel so it is always discoverable; its description uses the
  theme-aware text_secondary(dark_mode) getter.
- NetworkChooserScreen::refresh_on_arrival re-syncs selected_role from the
  app-global role so the radios never show a stale value.
- Tests: kittest coverage for both surfaces (set + persist role) and a
  UserRole label/description test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(user-role): document three-role interface-mode system, close #371

Add docs/user-roles.md covering the Default view/Detailed view/Developer
tools model: where to set it (Network Settings "Interface mode" card,
Welcome screen onboarding row), reversibility, and the one-time
DEVELOPER_MODE .env seed (true -> Detailed view, false/unset -> Default
view) that is never re-read once a role is chosen. Link it from README
and .env.example. Fix docs/user-stories.md entries (NET-005, NET-006,
MN-002, NET-015) still describing the retired Expert Mode toggle/Beginner-
Advanced mechanism instead of the shipped role selector.

Note: docs/expert-mode.md (added by aebae01b) never merged into v1.0-dev,
so this is a net-new doc rather than the planned git-mv rewrite.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(changelog): document Expert-mode replacement with interface levels

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* fix(user-role): stop settings loss on k/v read failure; gate shielded ops on capability

Bot-review fixes on the persona/capability-gating rollout.

Settings persistence (data loss):
- `load_app_settings_uncached` mapped every k/v read error to
  `AppSettings::default()`, then — seeing a role-less default — seeded a role
  and persisted the whole defaults blob. One transient read failure (poisoned
  lock, SQLite hiccup, schema mismatch) therefore overwrote the user's real
  settings: network, onboarding, SPV prefs, theme. It now returns `Result`, so
  an unreadable blob is never mistaken for "nothing stored". `update_app_settings`
  aborts instead of committing its mutation on top of defaults; `get_app_settings`
  keeps its in-memory defaults fallback for the frame loop but writes nothing back.
- `set_and_persist_user_role` published the role to the runtime atomic before
  persisting and swallowed a persist failure as a log warning, so the UI accepted
  a mode that silently reverted on restart. It now persists first, publishes only
  on success, and returns the error. Both callers surface it: the settings selector
  reverts its radio group and shows a banner; the onboarding row shows a banner
  (its role is re-read from the context each frame).

Feature gating:
- Shielded send sources and shielded destinations called `experimental_enabled`
  directly, bypassing `Capability::ShieldedProtocol`, so a Power/Developer user
  was offered shielded options on networks whose protocol version defines no
  shielded state transitions — while the shielded tab itself was correctly hidden.
  Adds `FeatureGate::ShieldedOperations` (capability AND experimental — the first
  multi-check gate) and routes send_screen + shielded_tab through it.
- Routes the four raw DashPay `experimental_enabled` callsites through a new
  `FeatureGate::DashPayOperations`, so every gating decision goes through the
  single composition point. `FeatureGate::DashPay` (nav entry) is unchanged.

Tests:
- `FailingKv` (kv_test_support): a store whose reads can be armed to fail, counting
  puts — proves a failed read writes nothing back and that the stored blob survives.
- `context::test_support`: shared `AppContext` fixture, lifted out of settings_db's
  test module so feature_gate can reuse it.
- feature_gate gains its first test module: per-role availability, the empty
  conjunction, and the AND semantics of the new multi-check gate. No protocol
  version upstream defines the shielded state transitions today, so the
  "capability met" half of the AND is not yet reachable; a tripwire test fails
  loudly when upstream ships them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(user-role): encapsulate the shared role atomic behind UserRoleCell

`AppContext.user_role` was a raw `Arc<AtomicU8>` threaded through the
constructor and hand-decoded at every read (`UserRole::from_u8(load(..))`)
and write (`store(role as u8, ..)`), with ~20 construction sites spelling
out the atomic encoding.

Introduce `UserRoleCell` in `model/user_role.rs`, next to the enum it
wraps: `get()` / `set()` plus `Clone` as the cheap shared handle that wires
sibling per-network contexts to one value. `UserRole::from_u8` drops to
private — the encoding is now the cell's business alone.

Behaviour-preserving; `user_role_handle()` becomes `user_role_cell()`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: drop the stale DEVELOPER_MODE interface-mode claims

`DEVELOPER_MODE` no longer seeds the interface mode at all — the role of an
account that never chose one is Power, resolved in memory with no `.env` read.
Two living docs still advertised the retired one-time-seed behaviour.

CHANGELOG's `[Unreleased]` entry now states what an account without a chosen
level actually gets (Detailed view, so nothing the old Expert mode showed is
hidden) and that `.env` has no say in it.

README's environment-variable table drops the `DEVELOPER_MODE` row outright:
the app reads no such variable for configuration, so a row in a table of
supported variables is a false claim rather than a stale one. The migration
detail it used to carry — that the key survives only as an input to the
one-shot v34 SPV database upgrade — already lives in docs/user-roles.md, which
the replacement note points at.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Lukasz Klimek <842586+lklimek@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants