feat(overlay): blocking progress overlay + SPV-sync hard-block#863
Conversation
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>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (2)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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>
…ng-progress-overlay
|
✅ Review complete (commit 8fefb7e) |
There was a problem hiding this comment.
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
ProgressOverlayas a reusable UI component (globalctx.datapath +Componentinstance path), including input-claiming at frame start and keyed action delivery viaOverlayHandle::take_actions. - Integrates the overlay into
AppState::updateand adds an SPV-sync-specific blocking policy (armed episode, disarm on terminal, escape hatch). - Adds extensive kittest coverage plus supporting test seams (
testingfeature) 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.
thepastaclaw
left a comment
There was a problem hiding this comment.
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.
…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
left a comment
There was a problem hiding this comment.
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.
… 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
left a comment
There was a problem hiding this comment.
Code Review
Both prior findings (one-frame interactive gap; watchdog false-stall on slow phases) are FIXED at 20723d2 — update_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.
| // 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. |
There was a problem hiding this comment.
🟡 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']
There was a problem hiding this comment.
Resolved in f50cbca — Pointer 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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 1f45f519 — ctx.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
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>
…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>
…esign' into feat/blocking-progress-overlay
thepastaclaw
left a comment
There was a problem hiding this comment.
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.
| let escape_action = top.keyboard_escape_action.clone(); | ||
| let key = top.key; |
There was a problem hiding this comment.
🟡 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']
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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>
Fix: SPV escape button was unclickable with the mouse (
|
| let token = progress.as_ref().and_then(spv_progress_token); | ||
| let description = if step.is_some() { | ||
| SPV_SYNCING_DESCRIPTION | ||
| } else { | ||
| SPV_CONNECTING_DESCRIPTION | ||
| }; |
| let escape_action = top.keyboard_escape_action.clone(); | ||
| let key = top.key; | ||
| let mut activate_escape = false; |
thepastaclaw
left a comment
There was a problem hiding this comment.
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.
…-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
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
0d301d0
into
docs/platform-wallet-migration-design
Why this PR exists
docs/platform-wallet-migration-designbranch and targets it; merge after feat: platform-wallet backend rewrite (spec + implementation) #860.What was done
1.
ProgressOverlaycomponent (src/ui/components/progress_overlay.rs)Componenttrait alongside a globalctx.datarender path, mirroringMessageBanner.AppState::updatelevel (claim_inputat frame start +render_globalafter panels) atOrder::Foreground, so it covers all panels; the secret-prompt modal stays above and fully typeable (keyboard gated off while a prompt is active).with_button/with_secondary_button; the caller owns semantics. Clicks are keyed per overlay entry and delivered to the owner viaOverlayHandle::take_actions; the app loop only sweeps orphan actions.2. First adopter — SPV-sync hard-block (
src/app.rs)spv_block_armedgate scopes it to that episode and disarms onSynced/Error/network-switch — so ambient mid-session reconnects and per-block catch-up never re-block a working user.Testing
cargo test --lib --all-features→ 919 passed, 0 failed, 1 ignored.cargo test --test kittest --all-features→ 135 passed, 0 failed, 0 ignored.cargo +nightly fmt --all -- --checkandcargo clippy --all-features --all-targets -- -D warnings→ clean.Breaking changes
None.
Checklist
fmt+clippy -D warningscleandocs/user-stories.mdupdated (UX-001 + new UX-002 SPV-block story)Attribution
🤖 Co-authored by Claudius the Magnificent AI Agent