Skip to content

feat(overlay): blocking progress overlay + SPV-sync hard-block#863

Merged
lklimek merged 25 commits into
docs/platform-wallet-migration-designfrom
feat/blocking-progress-overlay
Jun 18, 2026
Merged

feat(overlay): blocking progress overlay + SPV-sync hard-block#863
lklimek merged 25 commits into
docs/platform-wallet-migration-designfrom
feat/blocking-progress-overlay

Conversation

@lklimek

@lklimek lklimek commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Why this PR exists

  • Problem: Long, unsafe operations (signing, broadcasting a state transition, identity registration, key import, migration) and the initial SPV chain sync run with the whole UI live. There was no component to block interaction behind a clear "please wait" plane, and nothing wired it to the get-connected sync.
  • What breaks without it: A user clicks Broadcast, sees no block, clicks again → a second conflicting state transition (double broadcast). Or, on startup/Connect, they start using the app mid-sync and act on stale/empty state. These are the hazards this PR fences off.
  • Blocking relationship: Stacked atop feat: platform-wallet backend rewrite (spec + implementation) #860 (platform-wallet migration). Built on the docs/platform-wallet-migration-design branch and targets it; merge after feat: platform-wallet backend rewrite (spec + implementation) #860.

Imagine you launch the app or press Connect. The screen dims, a spinner with "Syncing with the Dash network." and a "Step 2 of 5" indicator appears, and you can't act on half-synced data — but a "Continue in the background" button is always there if you'd rather not wait.

What was done

1. ProgressOverlay component (src/ui/components/progress_overlay.rs)

  • Full-window blocking modal — indeterminate spinner (no fake ETA), optional "Step N of M" counter, scrollable description, optional primary/secondary buttons. Implements the project Component trait alongside a global ctx.data render path, mirroring MessageBanner.
  • Renders at AppState::update level (claim_input at frame start + render_global after panels) at Order::Foreground, so it covers all panels; the secret-prompt modal stays above and fully typeable (keyboard gated off while a prompt is active).
  • Generic button facility, no built-in Cancel: with_button / with_secondary_button; the caller owns semantics. Clicks are keyed per overlay entry and delivered to the owner via OverlayHandle::take_actions; the app loop only sweeps orphan actions.
  • Safety model: no fake cancellation; a caller contract (clear on every terminal path; button-less blocks cover only bounded ops) + a 30 s reassurance line and 120 s no-progress watchdog. Total input block (text + Space/Enter/Tab/arrows, focus released from widgets beneath).

2. First adopter — SPV-sync hard-block (src/app.rs)

  • Hard-blocks the UI during the user-initiated get-connected sync only: startup auto-start and the Connect button. An spv_block_armed gate scopes it to that episode and disarms on Synced/Error/network-switch — so ambient mid-session reconnects and per-block catch-up never re-block a working user.
  • Plain-language copy ("Connecting to the Dash network." / "Syncing with the Dash network.") + a jargon-free "Step N of 5" phase counter. Always-visible "Continue in the background" escape — SPV sync is unbounded (offline ⇒ never completes), so the escape guarantees the user is never trapped.
  • Suppresses the redundant Connecting/Syncing text banner while blocked; keeps the ambient top-panel indicator.

Testing

  • cargo test --lib --all-features919 passed, 0 failed, 1 ignored.
  • cargo test --test kittest --all-features135 passed, 0 failed, 0 ignored.
  • cargo +nightly fmt --all -- --check and cargo clippy --all-features --all-targets -- -D warnings → clean.
  • Coverage incl. teeth-y regression guards: keyboard/text blocked beneath a button-less overlay; passphrase prompt typeable over an overlay (real AppState gate); backdrop/Esc/Space do not dismiss/activate; keyed action FIFO + orphan sweep; 30 s/120 s watchdog via clock seam; and the SPV arming-gate test proving ambient reconnect does NOT block (fails if the gate is removed).

Breaking changes

None.

Checklist

  • fmt + clippy -D warnings clean
  • Tests green (919 lib + 135 kittest)
  • Design docs + docs/user-stories.md updated (UX-001 + new UX-002 SPV-block story)
  • Wired the overlay to a first real operation (SPV sync) under the caller contract

Attribution

🤖 Co-authored by Claudius the Magnificent AI Agent

lklimek and others added 10 commits June 17, 2026 15:47
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
…ap (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>
…n 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>
… 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>
… 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>
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>
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • master
  • v1.0-dev

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8865e2ba-d9a7-4dee-86ee-85d910cdd46b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/blocking-progress-overlay

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

lklimek and others added 5 commits June 17, 2026 21:30
…s 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>
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>
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>
…ep 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>
… (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>
@lklimek lklimek changed the title feat(overlay): blocking progress overlay component feat(overlay): blocking progress overlay + SPV-sync hard-block Jun 17, 2026
@lklimek lklimek marked this pull request as ready for review June 17, 2026 21:07
@thepastaclaw

thepastaclaw commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

✅ Review complete (commit 8fefb7e)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new global, full-window blocking progress overlay component and wires it into the app frame loop, with a first concrete adopter that hard-blocks UI interaction during user-initiated SPV “get connected” sync (startup/Connect) while still providing a safe “Continue in the background” escape.

Changes:

  • Introduces ProgressOverlay as a reusable UI component (global ctx.data path + Component instance path), including input-claiming at frame start and keyed action delivery via OverlayHandle::take_actions.
  • Integrates the overlay into AppState::update and adds an SPV-sync-specific blocking policy (armed episode, disarm on terminal, escape hatch).
  • Adds extensive kittest coverage plus supporting test seams (testing feature) and updates documentation/catalogs.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/ui/components/progress_overlay.rs Implements the blocking overlay (rendering, input claiming, action queue semantics, watchdog/escalation).
src/app.rs Integrates overlay into the frame loop and adds SPV-sync blocking overlay policy + test seams.
src/context/connection_status.rs Adds test seam to force overall connection state and a helper mapping SPV phases to “Step N of 5”.
src/ui/components/passphrase_modal.rs Ensures secret prompt window renders above the overlay via Order::Foreground.
src/ui/components/secret_prompt_host.rs Adds a testing-only stub active prompt for AppState-level overlay/prompt gate tests.
src/ui/components/mod.rs Exposes progress_overlay module and re-exports overlay types.
src/ui/components/README.md Documents ProgressOverlay in the reusable component catalog.
tests/kittest/progress_overlay.rs Adds comprehensive kittest suite covering overlay behavior, input blocking, z-order, and SPV overlay wiring.
tests/kittest/main.rs Registers the new progress overlay kittests module.
docs/user-stories.md Adds UX stories for the blocking overlay and SPV-sync blocking adopter.
docs/ai-design/2026-06-17-blocking-progress-overlay/01-requirements-ux.md Requirements/UX spec for the overlay (with supersession notes).
docs/ai-design/2026-06-17-blocking-progress-overlay/02-test-spec.md Detailed acceptance test specification for the overlay.
docs/ai-design/2026-06-17-blocking-progress-overlay/03-dev-plan.md Development plan and architecture decisions (incl. post-outage redesign notes).
docs/ai-design/2026-06-17-blocking-progress-overlay/04-design-addendum.md QA-wave design addendum (watchdog, input-claim, keyed action dispatch, SPV exception).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tests/kittest/progress_overlay.rs

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Generally well-factored overlay + SPV hard-block wiring with solid test coverage. Verified concerns: (1) frame ordering leaves one frame of interactive UI after Connect because update_spv_overlay runs after claim_overlay_input and the screen's ui(); (2) the 120s no-progress watchdog will spuriously fire on legitimately slow SPV phases because (description, step) doesn't change while heights advance; (3) the doc comment on claim_overlay_input is a copy/paste residue describing the orphan-sweeper; (4) a kittest header still cites Order::Middle though the renderer moved to Order::Foreground for SEC-002. No in-scope blocking defects.

🟡 2 suggestion(s) | 💬 2 nitpick(s)

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/app.rs`:
- [SUGGESTION] src/app.rs:1791-1830: Connect leaves one frame of interactive UI before the SPV block is raised
  `update_spv_overlay` runs at line 1824, after `claim_overlay_input` (1794), the visible screen's `ui()` (1803), and `ProgressOverlay::render_global` (1810). `StartSpv` is then processed at line 1882 in the action loop, setting `spv_block_armed = true`. On the next frame, `claim_overlay_input` and the screen's `ui()` execute before `update_spv_overlay` raises the overlay, so a full frame passes with no overlay in `ctx.data`, no input claim, and a fully interactive screen. The overlay only becomes visible and effective on frame N+2. For a feature explicitly framed as a hard-block on user-initiated sync (PR title + F-SPV-A in the doc comment), the one-frame interactive gap contradicts the safety goal. Fix is local: drive `update_spv_overlay` before `claim_overlay_input` so a newly armed episode raises the overlay in time for the same frame's input claim and global render.
- [SUGGESTION] src/app.rs:1158-1189: 120s no-progress watchdog will spuriously escalate during slow SPV phases
  `update_spv_overlay` writes the same `description` (`SPV_SYNCING_DESCRIPTION` or `SPV_CONNECTING_DESCRIPTION`) and the same step number (1–5 from `spv_phase_step`) for the entire duration of a phase — the underlying `SpvSyncProgress` heights are not threaded into overlay content. `log_overlay_state` (progress_overlay.rs:819) only resets `last_progress_at` when `(description, step)` actually changes, so any phase that runs past 120s (typical for Headers on slower links) trips `watchdog_tripped`, swaps the copy to `STUCK_WATCHDOG_REASSURANCE` ("This is taking much longer than expected…") AND fires a `tracing::error!` (progress_overlay.rs:677-684) claiming a leaked handle or un-bounded operation. That dev-error is exactly the signal the PR's C2 escape was designed to suppress for SPV. Thread a monotonic progress signal into `set_description` (e.g. include a height/percent fragment for the active phase) so content ticks across frames while real sync work happens; or special-case the SPV adopter to skip the escalated copy when the underlying sync state is still advancing.

Comment thread src/app.rs
Comment thread src/app.rs
Comment thread src/app.rs Outdated
Comment thread tests/kittest/progress_overlay.rs Outdated
…PV 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>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Incremental reconciliation: prior findings #1 (Connect one-frame interactive leak) and #2 (no-progress watchdog spurious escalation during slow SPV phases) remain STILL VALID at b726871 and are carried forward; prior nitpicks #3 (claim_overlay_input doc) and #4 (Order::Middle kittest comment) are FIXED in the latest delta. The latest delta (phase-count constant, claim_input keyboard hardening, deterministic elapsed test, doc cleanup) is sound and introduces no new regressions. One new agent finding (SPV unbounded block lacks keyboard-accessible escape) is already acknowledged in-code via TODO(RUST-006) pending product decision and is therefore intentionally deferred.

🟡 1 suggestion(s)

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/app.rs`:
- [SUGGESTION] src/app.rs:1160-1195: No-progress watchdog still misfires during slow but healthy SPV phases
  Carried-forward (STILL VALID at current head). `update_spv_overlay` still writes only `(description, step)` to the overlay — one of two fixed `&'static str` descriptions (`SPV_CONNECTING_DESCRIPTION` / `SPV_SYNCING_DESCRIPTION`) plus the phase step from `spv_phase_step`, which is constant for the entire duration of a single phase (Headers, Masternodes, Filter Headers, Filters, Blocks). `log_overlay_state` in `src/ui/components/progress_overlay.rs:849-869` only bumps `last_progress_at` when `(description, step)` changes, so after `STUCK_OVERLAY_WATCHDOG_THRESHOLD` (120s) the watchdog at `progress_overlay.rs:702-714` will log a one-shot `tracing::error!("… likely a leaked handle or an un-bounded operation")` and the overlay flips to the “taking much longer than expected” copy — even when the underlying `SpvSyncProgress` heights are visibly advancing on a slow Filters or Blocks phase. The new `TODO(SEC-001)` comment acknowledges the watchdog isn't actionable but doesn't address the false-positive itself. Either (a) thread a monotonic progress signal (e.g. current/target height per phase, or even a phase-tick counter) into the overlay state so the content tuple changes as the chain advances, or (b) extend the watchdog with a caller-supplied progress monotone (`bump_progress()` on `OverlayHandle`) that the SPV adopter calls whenever any phase's `current_height` advances. As is, the watchdog log will fire on healthy testnet/mainnet syncs.

Comment thread src/app.rs
lklimek and others added 2 commits June 18, 2026 10:38
… 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>
…02 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>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Both prior findings (one-frame interactive gap; watchdog false-stall on slow phases) are FIXED at 20723d2update_spv_overlay now runs before input claim and screen UI, and a hidden monotonic spv_progress_token resets the watchdog on intra-phase height progress. New delta findings: (1) try_auto_start_spv on post-onboarding auto-start launches SPV without arming spv_block_armed, so the first sync after onboarding is not hard-blocked; (2) claim_input still TODOs pointer claiming, so a mouse click queued at the start of an overlay's raise frame can reach widgets beneath before the foreground sink paints. Both are suggestion-grade — the boot-time and Connect-button arming paths are correctly covered.

🟡 3 suggestion(s)

1 additional finding(s) omitted (not in diff).

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/app.rs`:
- [SUGGESTION] src/app.rs:1469-1483: Post-onboarding auto-start launches SPV without arming the hard block
  `try_auto_start_spv` (called from `AppAction::OnboardingComplete` at app.rs:1960) spawns `ensure_wallet_backend_and_start_spv` but never sets `spv_block_armed = true` or clears `spv_overlay_dismissed`. The only arming sites are the boot constructor (app.rs:760, gated on `boot_auto_start_spv`) and the manual `StartSpv` action (app.rs:1916). When a user completes onboarding with auto-start enabled, the initial SPV sync episode kicks off but `update_spv_overlay` observes `armed == false` and returns `Idle`, leaving the UI fully interactive against an unsynced chain — the exact scenario the hard-block PR exists to prevent. The fix is symmetrical with the boot path: set `spv_block_armed = true; spv_overlay_dismissed = false;` before spawning. The method needs to take `&mut self` for that.

In `src/ui/components/progress_overlay.rs`:
- [SUGGESTION] src/ui/components/progress_overlay.rs:791-793: Pointer events can still click through on the frame an overlay is raised
  `claim_input` is now the frame-start ownership boundary for blocking overlays, but it only strips keyboard/text events — the in-code TODO acknowledges pointer claiming is still missing. In `AppState::update`, `update_spv_overlay` can raise the overlay, `claim_overlay_input` runs, then the visible screen's `ui(ctx)` runs, then `ProgressOverlay::render_global` finally installs the full-window `Sense::click_and_drag` foreground sink. On the first frame an overlay is newly raised, no sink existed last frame, so a mouse press queued at the start of this frame is evaluated against widgets beneath before the sink exists. For SPV-sync today this is mitigated because arming usually happens via `AppAction::StartSpv` in the prior frame (not synchronous with a mouse-down), but it leaves the same class of double-click hazard the PR closes for keyboard input. Mirror the QA-001 frame-start key claim for `PointerButton::Primary` press/release/click/drag events and add a kittest with a synthetic pointer event over a button under the overlay.
- [SUGGESTION] src/ui/components/progress_overlay.rs:737-767: Opt-in keyboard escape lets Enter/Space remain on `ctx.input` for the screen ui pass
  When `escape_confirmed`, `claim_input` retains Enter/Space key-press events on `ctx.input` so the focused overlay button can activate. The focus check (`escape_focus_id` recorded by last frame + `ctx.memory().focused()` match) correctly denies the raise frame and prevents focused widgets beneath from consuming the event — but `AppState::update` still runs `visible_screen_mut().ui(ctx)` after `claim_overlay_input` and before `ProgressOverlay::render_global`. Any screen or component that reads raw `ctx.input(|i| i.key_pressed(Key::Enter))` (global shortcuts, list-row activation by Enter, etc.) will observe the same event before the overlay button processes it on the later render pass. A safer pattern is to consume Enter/Space at frame start and enqueue the escape action directly for the overlay entry, rather than retaining the event on the shared input stream. Worth auditing existing screens for raw key-press reads before treating this as benign.

Comment on lines +791 to +793
// TODO(SEC-002-pointer): claim pointer press/click/drag at frame start
// (analogue of the keyboard QA-001 frame-start claim) to close the
// one-frame click-through on the raising frame.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Pointer events can still click through on the frame an overlay is raised

claim_input is now the frame-start ownership boundary for blocking overlays, but it only strips keyboard/text events — the in-code TODO acknowledges pointer claiming is still missing. In AppState::update, update_spv_overlay can raise the overlay, claim_overlay_input runs, then the visible screen's ui(ctx) runs, then ProgressOverlay::render_global finally installs the full-window Sense::click_and_drag foreground sink. On the first frame an overlay is newly raised, no sink existed last frame, so a mouse press queued at the start of this frame is evaluated against widgets beneath before the sink exists. For SPV-sync today this is mitigated because arming usually happens via AppAction::StartSpv in the prior frame (not synchronous with a mouse-down), but it leaves the same class of double-click hazard the PR closes for keyboard input. Mirror the QA-001 frame-start key claim for PointerButton::Primary press/release/click/drag events and add a kittest with a synthetic pointer event over a button under the overlay.

source: ['claude', 'codex']

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in f50cbcaPointer events can still click through on the frame an overlay is raised no longer present.

Auto-resolved by the review system based on the latest commit diff. If you believe this was closed in error, reopen the thread.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: the reconciliation reply above was posted in error. Re-validating the current head 4ad29505, the pointer-event click-through issue is still valid and was carried forward in review #863 (review).

Please ignore the resolved note above; this thread should remain open until pointer events are claimed on the overlay-raising frame.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed still open and intentional. Frame-start pointer-event claiming is deferred follow-up, tracked in-source as TODO(SEC-002-pointer) (progress_overlay.rs) and in our backlog alongside its keyboard sibling.

For the originally-reported symptom (escape button unclickable during SPV sync), the shipped fix at 1f45f519ctx.set_sublayer(sink_layer, card_layer) — pins the content card above the dim sink, so the button receives clicks reliably even after a backdrop press. Full frame-start pointer claiming (defense-in-depth against the one-frame raise gap) remains a distinct hardening task. Leaving this thread open to track it.

🤖 Co-authored by Claudius the Magnificent AI Agent

Comment thread src/ui/components/progress_overlay.rs Outdated
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>
lklimek and others added 2 commits June 18, 2026 12:14
…ss-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>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Incremental review against 4ad2950. Carried-forward findings from 20723d2: the post-onboarding auto-start path still does not arm the SPV hard block (try_auto_start_spv at app.rs:1480 takes &self and never sets spv_block_armed), and pointer events still click through on the overlay's raising frame (claim_input strips only keyboard/text — TODO(SEC-002-pointer) acknowledged at progress_overlay.rs:789). The prior Enter/Space passthrough finding is FIXED by f50cbca — claim_input now enqueues the designated escape action at frame start and strips the key unconditionally, with a dedicated test. New in this delta: the focus-independent escape activation removes the implicit "button-id must match" check that previously enforced the documented no-op contract for unmatched escape ids (with_keyboard_escape doc at line 290-291 vs. claim_input at line 736).

🟡 2 suggestion(s)

1 additional finding(s) omitted (not in diff).

1 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/app.rs`:
- [SUGGESTION] src/app.rs:1480-1494: Post-onboarding auto-start launches SPV without arming the hard block
  `try_auto_start_spv` (called from `AppAction::OnboardingComplete` at app.rs:1973) calls `ensure_wallet_backend_and_start_spv` but never sets `spv_block_armed = true` or clears `spv_overlay_dismissed`. The two other initial-sync entrypoints arm the block symmetrically with the spawn: the boot constructor at app.rs:760 (`spv_block_armed: boot_auto_start_spv`) and the manual `StartSpv` action at app.rs:1929. When a user finishes onboarding with `auto_start_spv` enabled, the SPV episode kicks off but `update_spv_overlay` observes `armed == false` and resolves to Idle, leaving the UI fully interactive against an unsynced chain — the exact double-broadcast hazard this PR exists to prevent. The cheapest fix is to change the signature to `&mut self` and set `self.spv_block_armed = true; self.spv_overlay_dismissed = false;` before spawning, mirroring the boot/manual paths. A kittest driving `AppAction::OnboardingComplete` with `auto_start_spv = true` would lock in the invariant.

In `src/ui/components/progress_overlay.rs`:
- [SUGGESTION] src/ui/components/progress_overlay.rs:736-737: Keyboard escape can enqueue an action with no rendered owner button
  The new frame-start escape path at line 736 clones `top.keyboard_escape_action` and enqueues it on Enter/Space without checking whether any `OverlayButton` in `top.buttons` carries that action id. That breaks the documented contract on `with_keyboard_escape` at line 290-291 ("Designating an action id that no button carries is a no-op") which was previously enforced implicitly: under the prior focus-based activation, focus could only be granted to a button whose id matched the escape, so an unmatched id could never activate via Enter. The SEC-001/SEC-002 refactor moved activation to frame-start and dropped that implicit check. Consequence: a caller that typos the escape id wires a live, invisible keyboard action the user cannot see or click — and a screen drain that's not expecting it. Filter `escape_action` against `top.buttons` (`top.buttons.iter().any(|b| b.action_id == *id)`) or update the doc to reflect the new "always-on" semantics. The codex-suggested filter at line 736 is the minimal fix.

Comment on lines +736 to +737
let escape_action = top.keyboard_escape_action.clone();
let key = top.key;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: Keyboard escape can enqueue an action with no rendered owner button

The new frame-start escape path at line 736 clones top.keyboard_escape_action and enqueues it on Enter/Space without checking whether any OverlayButton in top.buttons carries that action id. That breaks the documented contract on with_keyboard_escape at line 290-291 ("Designating an action id that no button carries is a no-op") which was previously enforced implicitly: under the prior focus-based activation, focus could only be granted to a button whose id matched the escape, so an unmatched id could never activate via Enter. The SEC-001/SEC-002 refactor moved activation to frame-start and dropped that implicit check. Consequence: a caller that typos the escape id wires a live, invisible keyboard action the user cannot see or click — and a screen drain that's not expecting it. Filter escape_action against top.buttons (top.buttons.iter().any(|b| b.action_id == *id)) or update the doc to reflect the new "always-on" semantics. The codex-suggested filter at line 736 is the minimal fix.

source: ['codex-rust-quality']

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid observation — with_keyboard_escape documents that an action id no button carries is a no-op, but claim_input enqueues the escape action regardless of whether a matching button exists. Low severity in practice: the only production opt-in (the SPV Continue in the background escape) always carries a matching secondary-action button, so no live path violates the contract.

We're tracking this and will reconcile doc and behavior — either guard the enqueue against state.buttons or change the doc to document always-on semantics. Leaving open until that lands.

🤖 Co-authored by Claudius the Magnificent AI Agent

@thepastaclaw thepastaclaw Jun 18, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction: this auto-resolve reply was produced by the reconciler in error. The finding is still valid at 8fefb7e4: claim_input still enqueues keyboard_escape_action on Enter/Space without first checking that a rendered OverlayButton owns that action id. The current review summary intentionally carries this forward as unresolved.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Incremental review against d057027. The latest delta is a merge of origin/docs/platform-wallet-migration-design (mnlist-inspector removal, watch-only identity funding fix, SPV shutdown fix) that does not touch the overlay/SPV-blocking wiring. No new in-scope defects introduced by the delta. Carried-forward from the prior review at 4ad2950: all three findings (post-onboarding auto-start does not arm the SPV hard block; pointer events click through on the overlay's raising frame; keyboard escape can enqueue an action no rendered button carries) verified STILL VALID at head and re-emitted as suggestions.

🟡 1 suggestion(s)

1 additional finding(s) omitted (not in diff).

2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/app.rs`:
- [SUGGESTION] src/app.rs:1473-1487: Post-onboarding auto-start launches SPV without arming the hard block
  Carried forward — verified still valid at d0570277. `try_auto_start_spv` (app.rs:1473) is called from `AppAction::OnboardingComplete` (app.rs:1966) but only spawns `ensure_wallet_backend_and_start_spv` — it never sets `spv_block_armed = true` or clears `spv_overlay_dismissed`. The other two initial-sync entrypoints arm the block symmetrically: the boot constructor at app.rs:753 (`spv_block_armed: boot_auto_start_spv`) and the manual `StartSpv` action at app.rs:1922 (`self.spv_block_armed = true; self.spv_overlay_dismissed = false;` before spawning). On the onboarding path `update_spv_overlay` observes `armed == false` and resolves to Idle, leaving the UI fully interactive during exactly the initial-sync episode this PR is meant to hard-block. Change the signature to `&mut self` and set `self.spv_block_armed = true; self.spv_overlay_dismissed = false;` before `spawn_sync`, mirroring the boot/manual paths. A kittest driving `AppAction::OnboardingComplete` with `auto_start_spv = true` would lock the invariant in.

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>
@lklimek

lklimek commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Fix: SPV escape button was unclickable with the mouse (1f45f519)

Symptom: while the SPV-sync hard block was up, clicking "Continue in the background" did nothing — force-quit was the only way out.

Root cause: a pre-existing egui z-order trap (not the keyboard-escape rework in f50cbcad). The overlay paints its full-window dim/pointer sink and its content card as two 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 click-absorbing sink above the card and permanently buried the button beneath it.

Fix (one line of logic): ctx.set_sublayer(sink_layer, card_layer) — egui places a sublayer directly above its parent after the per-frame order sort, so card-above-sink now holds by construction, immune to the bring-to-front race. The sink still blocks every widget beneath; the passphrase prompt still wins above the overlay.

Why review/tests missed it: existing button-click tests click straight on the button and never press the backdrop first. Added TC-OVL-054 (tc_ovl_054_escape_clickable_after_backdrop_press): presses the backdrop, then clicks the escape and asserts the action enqueues — fails pre-fix, passes post-fix.

Gates: fmt clean · clippy --all-features --all-targets -- -D warnings 0 warnings · kittest 145 passed (incl. new TC-OVL-054, and regression guards TC-OVL-028 sink-still-blocks / TC-OVL-048 secret-prompt gate) · lib progress_overlay 26 passed.

🤖 Co-authored by Claudius the Magnificent AI Agent

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Comment thread src/app.rs
Comment on lines +1179 to +1184
let token = progress.as_ref().and_then(spv_progress_token);
let description = if step.is_some() {
SPV_SYNCING_DESCRIPTION
} else {
SPV_CONNECTING_DESCRIPTION
};
Comment on lines +736 to +738
let escape_action = top.keyboard_escape_action.clone();
let key = top.key;
let mut activate_escape = false;

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Incremental review at 1f45f51. The delta itself is a focused, well-tested fix: pinning the card layer above the sink via ctx.set_sublayer to defeat egui's bring-to-front on backdrop press, with a clean kittest regression (TC-OVL-054). No new defects in the delta. All three prior findings (post-onboarding hard-block arming, frame-start pointer claim, keyboard-escape unvalidated id) remain STILL VALID at head and are carried forward — none are blocking, but they materially weaken the overlay's input-blocking and SPV hard-block contracts that this PR exists to enforce.

🟡 1 suggestion(s)

1 additional finding(s) omitted (not in diff).

2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

In `src/app.rs`:
- [SUGGESTION] src/app.rs:1473-1487: Carried-forward: post-onboarding SPV auto-start does not arm the hard block
  Verified at 1f45f519. `try_auto_start_spv` (app.rs:1473, signature is `&self`) is the third initial-sync entrypoint, alongside the boot constructor at app.rs:753 (`spv_block_armed: boot_auto_start_spv`) and `AppAction::StartSpv` at app.rs:1922-1923 (`self.spv_block_armed = true; self.spv_overlay_dismissed = false;`). Unlike those two, it spawns `ensure_wallet_backend_and_start_spv` without setting `spv_block_armed` or clearing `spv_overlay_dismissed`. Its sole caller is `AppAction::OnboardingComplete` at app.rs:1966 — exactly the initial-sync episode the hard block was designed to cover. With `armed == false`, `spv_block_step` resolves to Idle and the UI stays fully interactive during the freshly-onboarded user's first sync, when stale/empty state is most dangerous. The current `&self` signature also has to change to support the fix. The asymmetry across the three entrypoints is itself a maintainability hazard — the arming side-effect should live next to `spawn_sync` to prevent future entrypoints from reintroducing the same gap.

lklimek and others added 2 commits June 18, 2026 14:24
…-progress-overlay

Brings #860's wallet-backend migration groundwork onto the blocking
progress overlay branch (#863) ahead of further overlay work:

- mcp/resolve.rs: ensure_spv_synced now waits on a tokio::watch SPV-status
  event instead of a 1s poll loop (a98186a).
- context/wallet_lifecycle.rs: stop_spv adds a bounded persister-release
  barrier (await_persister_released) so the next Connect can reopen the
  platform-wallet persister without a file-lock race (976ad0d).
- context/connection_status.rs: watch-based SPV-status notification
  (subscribe_spv_status / send_if_modified in set_spv_status) backing the
  event wait above.

Conflict resolved: src/context/connection_status.rs test-module `use`
imports. Took the union — #863's
`{BlockHeadersProgress, MasternodesProgress}` SDK import plus #860's
`std::sync::Arc`. All three symbols are exercised by the merged test set
(MasternodesProgress by #863's monotonic-token test, Arc by #860's
watch-channel tests). No production code conflicted; the overlay's SPV
phase/step/token logic and #860's watch wiring coexist untouched.

Verified: cargo build --all-features, cargo clippy --all-features
--all-targets -D warnings (0 warnings), cargo test --test kittest
(145 ok), cargo test --lib (932 ok).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t 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>

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Incremental review at 327b603. The PR-scoped files src/app.rs and src/ui/components/progress_overlay.rs are byte-identical to 1f45f51, so all three prior findings are carried forward as STILL VALID. The latest delta (merge of platform-wallet-migration-design) adds a tokio::sync::watch SPV-status mirror, a persister-release barrier in stop_spv, and backend-e2e tests; verification surfaced one new split-state concern: begin_spv_stop bypasses the watch sender, leaving subscribers stale during disconnect.

Reviewed commit: 327b6033 (PR head advanced after this queue item was claimed, so this body-only review is pinned to the assigned SHA.)

4 🟡 suggestion

Carried-forward prior findings

suggestion: Carried-forward: post-onboarding SPV auto-start does not arm the hard block

src/app.rs (lines 1473-1487)

Verified STILL VALID at 327b603 — src/app.rs:1473 still has fn try_auto_start_spv(&self) and spawns ensure_wallet_backend_and_start_spv without setting spv_block_armed = true or clearing spv_overlay_dismissed. The other two initial-sync entrypoints arm the block symmetrically: the boot constructor sets spv_block_armed: boot_auto_start_spv, and AppAction::StartSpv at src/app.rs:1922-1923 sets both fields before spawning. The sole caller of try_auto_start_spv is AppAction::OnboardingComplete at src/app.rs:1966 — exactly the initial-sync episode the hard block was designed to cover. With armed == false, update_spv_overlay resolves to Idle and the UI stays fully interactive during a freshly-onboarded user's first sync, when stale/empty state is most dangerous. The asymmetry across the three entrypoints is itself a maintainability hazard; the arming side-effect belongs next to spawn_sync so future entrypoints cannot reintroduce the same gap. Fix is mechanical: change to &mut self and set the two fields before spawning. A kittest driving AppAction::OnboardingComplete with auto_start_spv = true would lock the invariant in.

    fn try_auto_start_spv(&mut self) {
        let ctx = self.current_app_context().clone();
        let auto_start = ctx.get_app_settings().auto_start_spv;
        if auto_start && FeatureGate::SpvBackend.is_available(&ctx) {
            self.spv_block_armed = true;
            self.spv_overlay_dismissed = false;
            let sender = self.task_result_sender.clone();
            self.subtasks.spawn_sync("spv_auto_start", async move {
                if let Err(e) = ctx.ensure_wallet_backend_and_start_spv(sender).await {
                    tracing::warn!("Failed to auto-start SPV sync: {e}");
                } else {
                    tracing::info!("SPV sync started automatically for {:?}", ctx.network);
                }
            });
        }
    }
suggestion: Carried-forward: pointer events can click through on the overlay raise frame

src/ui/components/progress_overlay.rs (lines 789-791)

Verified STILL VALID at 327b603 — the in-source TODO(SEC-002-pointer) at progress_overlay.rs:789-791 still acknowledges the gap. claim_input is the frame-start ownership boundary that strips keyboard/text/clipboard events (lines 739-783) and frame-start-enqueues the designated keyboard escape (lines 786-788), but egui::Event::PointerButton press/release/click/drag events are never claimed. In AppState::update the sequence is: update_spv_overlay raises an overlay → claim_overlay_input (keyboard-only) → the visible screen's ui(ctx) consumes input → ProgressOverlay::render_global installs the full-window Sense::click_and_drag foreground sink. On the frame an overlay is newly raised, no sink existed last frame, so a queued primary pointer press is hit-tested against widgets beneath before the sink exists — the pointer analogue of the keyboard hazard SEC-001/002 already closed (distinct from the recent TC-OVL-054 backdrop z-order fix). For SPV this leaves a fast second Connect click possible; for the future broadcast/signing adopters this overlay is being built to fence off, it reopens the original double-broadcast hazard. Mirror the frame-start key claim for Event::PointerButton { button: Primary, pressed: true, .. } (and matching release/drag stripping) inside claim_input, and add a kittest with a synthetic pointer press over a button beneath a just-raised overlay.

suggestion: Carried-forward: keyboard escape enqueues an action with no rendered owner button

src/ui/components/progress_overlay.rs (lines 736-788)

Verified STILL VALID at 327b603. The docstring on with_keyboard_escape (progress_overlay.rs:290-291) explicitly states: "Designating an action id that no button carries is a no-op." The claim_input path at line 736 clones top.keyboard_escape_action and enqueues it via push_overlay_action on Enter/Space (lines 758-760, 786-787) based only on escape_action.is_some() and !*repeat, without checking whether any OverlayButton in top.buttons carries that action id. Under the prior focus-based activation this contract was enforced implicitly — focus could only be granted to a rendered button whose id matched the escape. The SEC-001/002 refactor moved activation to frame-start and dropped that implicit guard. Consequence: a caller that typos the escape id, removes the matching button while leaving the designation, or builds the entry conditionally silently dispatches an invisible keyboard-only action that no button advertises — and an owner screen drains an action it never offered. Either filter escape_action against the rendered button set at the same point it is cloned, or update the docstring to reflect the new "always-on" semantics. The filter is a one-line, behaviour-preserving change.

        let escape_action = top
            .keyboard_escape_action
            .as_ref()
            .filter(|action_id| {
                top.buttons
                    .iter()
                    .any(|button| button.action_id.as_str() == action_id.as_str())
            })
            .cloned();
        let key = top.key;
        let mut activate_escape = false;

New finding in latest delta

suggestion: begin_spv_stop bypasses the SPV status watch channel

src/context/connection_status.rs (lines 268-284)

Verified at 327b603. The latest delta introduces spv_status_tx: watch::Sender<SpvStatus> (connection_status.rs:60) as an event-driven mirror of the atomic, consumed by mcp::resolve::ensure_spv_synced (mcp/resolve.rs:147-163) via subscribe_spv_status().borrow_and_update(). set_spv_status (lines 223-235) keeps the atomic and the watch coherent via send_if_modified, but begin_spv_stop (lines 268-284) writes the atomic directly with compare_exchange and returns without updating the watch. There is a window where spv_status() returns Stopping but a freshly-subscribed receiver's first borrow_and_update() still sees the previous Running value. The next set_spv_status call (e.g. from a teardown event handler) closes the gap, but a wallet-facing MCP call that subscribes during that window passes the sync gate while teardown has already been claimed. The two state-stores need a single coherent mutation path: mirror the transition into the watch the same way set_spv_status does.

    pub fn begin_spv_stop(&self) -> bool {
        for current in [SpvStatus::Starting, SpvStatus::Syncing, SpvStatus::Running] {
            if self
                .spv_status
                .compare_exchange(
                    current as u8,
                    SpvStatus::Stopping as u8,
                    Ordering::AcqRel,
                    Ordering::Relaxed,
                )
                .is_ok()
            {
                let _ = self.spv_status_tx.send_if_modified(|cur| {
                    if *cur != SpvStatus::Stopping {
                        *cur = SpvStatus::Stopping;
                        true
                    } else {
                        false
                    }
                });
                return true;
            }
        }
        false
    }
🤖 Prompt for all review comments with AI agents
These findings are from an automated code review. Verify each finding against the current code and only fix it if needed.

- [SUGGESTION] In `src/app.rs`:1473-1487: Carried-forward: post-onboarding SPV auto-start does not arm the hard block
  Verified STILL VALID at 327b6033 — src/app.rs:1473 still has `fn try_auto_start_spv(&self)` and spawns `ensure_wallet_backend_and_start_spv` without setting `spv_block_armed = true` or clearing `spv_overlay_dismissed`. The other two initial-sync entrypoints arm the block symmetrically: the boot constructor sets `spv_block_armed: boot_auto_start_spv`, and `AppAction::StartSpv` at src/app.rs:1922-1923 sets both fields before spawning. The sole caller of `try_auto_start_spv` is `AppAction::OnboardingComplete` at src/app.rs:1966 — exactly the initial-sync episode the hard block was designed to cover. With `armed == false`, `update_spv_overlay` resolves to Idle and the UI stays fully interactive during a freshly-onboarded user's first sync, when stale/empty state is most dangerous. The asymmetry across the three entrypoints is itself a maintainability hazard; the arming side-effect belongs next to `spawn_sync` so future entrypoints cannot reintroduce the same gap. Fix is mechanical: change to `&mut self` and set the two fields before spawning. A kittest driving `AppAction::OnboardingComplete` with `auto_start_spv = true` would lock the invariant in.
- [SUGGESTION] In `src/ui/components/progress_overlay.rs`:789-791: Carried-forward: pointer events can click through on the overlay raise frame
  Verified STILL VALID at 327b6033 — the in-source `TODO(SEC-002-pointer)` at progress_overlay.rs:789-791 still acknowledges the gap. `claim_input` is the frame-start ownership boundary that strips keyboard/text/clipboard events (lines 739-783) and frame-start-enqueues the designated keyboard escape (lines 786-788), but `egui::Event::PointerButton` press/release/click/drag events are never claimed. In `AppState::update` the sequence is: `update_spv_overlay` raises an overlay → `claim_overlay_input` (keyboard-only) → the visible screen's `ui(ctx)` consumes input → `ProgressOverlay::render_global` installs the full-window `Sense::click_and_drag` foreground sink. On the frame an overlay is newly raised, no sink existed last frame, so a queued primary pointer press is hit-tested against widgets beneath before the sink exists — the pointer analogue of the keyboard hazard SEC-001/002 already closed (distinct from the recent TC-OVL-054 backdrop z-order fix). For SPV this leaves a fast second Connect click possible; for the future broadcast/signing adopters this overlay is being built to fence off, it reopens the original double-broadcast hazard. Mirror the frame-start key claim for `Event::PointerButton { button: Primary, pressed: true, .. }` (and matching release/drag stripping) inside `claim_input`, and add a kittest with a synthetic pointer press over a button beneath a just-raised overlay.
- [SUGGESTION] In `src/ui/components/progress_overlay.rs`:736-788: Carried-forward: keyboard escape enqueues an action with no rendered owner button
  Verified STILL VALID at 327b6033. The docstring on `with_keyboard_escape` (progress_overlay.rs:290-291) explicitly states: "Designating an action id that no button carries is a no-op." The `claim_input` path at line 736 clones `top.keyboard_escape_action` and enqueues it via `push_overlay_action` on Enter/Space (lines 758-760, 786-787) based only on `escape_action.is_some()` and `!*repeat`, without checking whether any `OverlayButton` in `top.buttons` carries that action id. Under the prior focus-based activation this contract was enforced implicitly — focus could only be granted to a rendered button whose id matched the escape. The SEC-001/002 refactor moved activation to frame-start and dropped that implicit guard. Consequence: a caller that typos the escape id, removes the matching button while leaving the designation, or builds the entry conditionally silently dispatches an invisible keyboard-only action that no button advertises — and an owner screen drains an action it never offered. Either filter `escape_action` against the rendered button set at the same point it is cloned, or update the docstring to reflect the new "always-on" semantics. The filter is a one-line, behaviour-preserving change.
- [SUGGESTION] In `src/context/connection_status.rs`:268-284: begin_spv_stop bypasses the SPV status watch channel
  Verified at 327b6033. The latest delta introduces `spv_status_tx: watch::Sender<SpvStatus>` (connection_status.rs:60) as an event-driven mirror of the atomic, consumed by `mcp::resolve::ensure_spv_synced` (mcp/resolve.rs:147-163) via `subscribe_spv_status().borrow_and_update()`. `set_spv_status` (lines 223-235) keeps the atomic and the watch coherent via `send_if_modified`, but `begin_spv_stop` (lines 268-284) writes the atomic directly with `compare_exchange` and returns without updating the watch. There is a window where `spv_status()` returns `Stopping` but a freshly-subscribed receiver's first `borrow_and_update()` still sees the previous `Running` value. The next `set_spv_status` call (e.g. from a teardown event handler) closes the gap, but a wallet-facing MCP call that subscribes during that window passes the sync gate while teardown has already been claimed. The two state-stores need a single coherent mutation path: mirror the transition into the watch the same way `set_spv_status` does.

Inline posting was rejected by GitHub with HTTP 422 after the PR advanced, so I posted the same verified findings as a body-only review for the assigned commit.

@thepastaclaw thepastaclaw left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Latest head 8fefb7e fixes the prior post-onboarding SPV auto-start arming gap: try_auto_start_spv now takes &mut self and sets spv_block_armed = true / spv_overlay_dismissed = false before spawning, with a dedicated kittest (fspv_a_onboarding_auto_start_arms_spv_block) locking the real path. Two prior overlay findings remain carried-forward and unaddressed in source: the TODO(SEC-002-pointer) at progress_overlay.rs:789-791 confirms pointer events are still not claimed at frame start (the set_sublayer change addresses z-order, not raise-frame pointer stripping), and claim_input still enqueues keyboard_escape_action on Enter/Space without validating against top.buttons, violating the documented no-op contract for the generic API. The new persister-release barrier and watch-channel ensure_spv_synced waiter in the latest delta look correct and adequately tested. One low-priority latest-delta nitpick remains: the persister-release barrier still hardcodes platform-wallet.sqlite separately from WalletBackend::new, so the comment's “no hardcoded path, no drift” claim is not quite true; a single helper for the full persister path would avoid future silent drift.

💬 1 nitpick(s)

1 additional finding(s) omitted (not in diff).

2 carried-forward finding(s) already raised on this PR; not re-posting as new inline comments.

@lklimek lklimek merged commit 0d301d0 into docs/platform-wallet-migration-design Jun 18, 2026
5 checks passed
@lklimek lklimek deleted the feat/blocking-progress-overlay branch June 18, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants