Skip to content

Move blocking work off tokio workers and fix GUI interaction defects (B-10, B-11, B-36, M-1/2/3/5/7, C-12/16/32) - #175

Merged
fstubner merged 2 commits into
mainfrom
fix/tui-async-and-gui-polish
Aug 15, 2026
Merged

fstubner merged 2 commits into
mainfrom
fix/tui-async-and-gui-polish

Conversation

@fstubner

Copy link
Copy Markdown
Owner

B-10 — five sync-in-async call sites

Site Cost
event::poll(tick_rate) Parked a tokio worker 100 ms every loop iteration, key or no key. Poll and read move together so the read can't race another poller.
NetworkManager::find_mac Shells out to arp on Windows/macOS — a whole subprocess inline.
pcap_check_support Blocking device enumeration, twice per /pcap. Uses block_in_place because ops is a borrow, not 'static.
get_stats Holds a mutex across a Networks::refresh syscall — from the draw path. Drawing is synchronous so it can't yield; the sample moves to the event loop and drawing reads cached numbers.
detect_default_ipv4_addr Delayed every startup path behind it.

B-11 — a file rewrite per keypress

/config did a synchronous fs::write + fs::rename on every arrow keypress, on the event-loop thread — so key repeat meant one full file rewrite per repeat tick. Cycling now marks settings dirty; the single write happens in exit_config, which every close path (Esc, Done, toggle) already funnels through. Reset still writes immediately, being discrete and destructive.

B-36 — spinner driven by frame rate

It advanced an index from the draw path, coupling its speed to how often the screen redrew and making rendering mutate state. Now derived from wall time, so it spins at a constant rate and takes &self.

GUI

  • M-7 — the table and detail pane disagreed: a closed port read '' in one and refused in the other, and both were used inside the detail pane. One function now.
  • M-2 — keyboard column resize read column.width, the static definition rather than the live width, so holding an arrow moved the border one step and stopped.
  • M-3 — NumberField clamped every keystroke: with min: 10, the "5" of "50" became "10" before the "0" arrived, and the field couldn't be cleared.
  • M-1 / C-12 — duplicate React keys (repeated history commands; two identical TXT records).
  • M-5 — menu strip had no menubar semantics.
  • C-16 — progress line counted 1-1024 as one port and said "1 ports".
  • C-32 — duplicate App.css import; the root assertion now throws a real message.

Verification

94 GUI tests (up from 88), 134 Rust tests, clippy clean with and without --features pcap.

Checked live in the running GUI: menubar exposes 7 menuitems; the number field keeps partial input and stays clearable.

One honest note: I could not verify NumberField's blur path through the browser — a focus trap in the app kept focus in the field, and my synthetic blur events never fired focusout. Rather than claim it worked, I covered all three commit paths (blur, Enter, steppers) with component tests, which is more durable anyway.

One existing test asserted the old M-7 divergence; it's updated with a note explaining why the expected value changed.

B-10, five sync-in-async call sites:
  - `event::poll(tick_rate)` parked a tokio worker for 100ms every loop
    iteration whether or not a key arrived. Poll and read move together to
    a blocking thread so the read cannot race another poller.
  - `NetworkManager::find_mac` shells out to `arp` on Windows and macOS.
  - `pcap_check_support` enumerates devices via a blocking syscall, twice
    per /pcap. Uses block_in_place because `ops` is a borrow, not 'static.
  - `get_stats` holds a mutex across a `Networks::refresh` syscall and was
    called from the draw path. Drawing is synchronous so it cannot yield;
    the sample moves to the event loop and the draw path reads cached
    numbers.
  - `detect_default_ipv4_addr` at startup delayed every path behind it.

B-11: /config wrote settings on every arrow keypress -- a synchronous
fs::write + fs::rename per key event, on the event-loop thread, so key
repeat meant one full file rewrite per repeat tick. Cycling now marks the
settings dirty and the single write happens in exit_config, which every
close path (Esc, Done, toggle) already funnels through. Reset still writes
immediately, being discrete and destructive.

B-36: the spinner advanced an index from the draw path, coupling its speed
to the frame rate and making rendering mutate state. It is now derived from
wall time, so it spins at a constant rate and takes &self.

GUI:
  M-1: History menu items keyed on their label, so two runs of the same
    command collided and React dropped one.
  M-2: keyboard column resize read `column.width` -- the static column
    definition, not the live width -- so every press recomputed from the
    same base and holding an arrow moved the border one step and stopped.
  M-3: NumberField clamped on every keystroke, so in a field with min 10
    the "5" of "50" became "10" before the "0" arrived, and the field could
    not be cleared. Clamping moves to blur/Enter/steppers.
  M-5: the menu strip had no menubar semantics.
  M-7: the table and detail pane disagreed on latency -- a closed port read
    '' in one and 'refused' in the other, and both were used inside the
    detail pane. One function now.
  C-12: DetailList keys collided for two identical TXT records.
  C-16: the progress line counted `1-1024` as one port and said "1 ports".
  C-32: duplicate App.css import; the non-null root assertion now throws a
    real message.

94 GUI tests (up from 88) and 134 Rust tests. clippy clean with and
without --features pcap.

Verified in the running GUI: menubar exposes 7 menuitems, and the number
field keeps partial input and stays clearable. NumberField's commit paths
are covered by component tests rather than the browser, because a focus
trap in the app prevented a real blur through the automation harness.
B-28: exerciseDns resolved netscli.com against real public DNS with a 25s
budget, while every other scenario is hermetic against the local probe
server -- so the suite failed on an air-gapped runner or a slow resolver
for reasons that had nothing to do with the app. It now uses `localhost`,
which resolves through the system resolver without leaving the host and
has both A and AAAA records, so the record-type assertions still exercise
what they were written for. NETSCLI_E2E_DNS_HOST overrides it.

M-13: teardown called child.kill(), which signals only the direct child.
The processes here are supervisors -- tauri-driver spawns the platform
WebDriver, which spawns the browser -- so grandchildren survived, kept
holding their ports, and the next run failed to bind or attached to a
stale session; on CI the job hung until its timeout. Now kills the tree:
taskkill /T on Windows, and the process group elsewhere, with tauri-driver
spawned detached so it leads one.

M-14: roughly a third of assertions pinned pixel measurements, some to
within 3px. Those catch real layout regressions but at that precision they
also fail for reasons that are not bugs -- font rendering, fractional DPI,
platform scrollbar width, a Chrome subpixel-rounding change. A suite that
cries wolf gets muted, and a muted suite catches nothing. Alignment checks
now pass within tolerance, warn and record beyond it, and fail only on
gross misalignment (4x tolerance, i.e. visibly broken). Drift is summarised
at the end of the run so the warnings do not scroll past.

Deliberately kept strict: a non-numeric or non-finite delta still fails
hard, since that means the measurement itself broke -- exactly the
"check that cannot fail" shape this is meant to avoid.

M-16: assertOperationToastReturnsToTab did `inactiveTab?.click()`. With no
inactive tab the optional chain silently did nothing, and the final
assertion then passed trivially because the expected tab had never been
left. It now asserts a second tab exists first.

Checked shell.mjs's `if (!mark) return null` -- that one is already caught
by an assert.ok on the result, so it is not a silent skip.
@fstubner
fstubner merged commit 3f27aaa into main Aug 15, 2026
15 of 16 checks passed
@fstubner
fstubner deleted the fix/tui-async-and-gui-polish branch August 15, 2026 02:57
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.

1 participant