Fix matrix strategy for ARM and AMD Builds - #2
Closed
vivekgsharma wants to merge 2 commits into
Closed
Conversation
Member
|
Latte used this. Thanks Vivek. |
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>
11 tasks
2 tasks
5 tasks
3 tasks
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>
7 tasks
3 tasks
4 tasks
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>
6 tasks
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>
5 tasks
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>
5 tasks
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.