Skip to content

feat(gui): search the connections table by column, block an address from a row - #103

Merged
donislawdev merged 4 commits into
masterfrom
feat/connections-tab-improvements
Aug 2, 2026
Merged

feat(gui): search the connections table by column, block an address from a row#103
donislawdev merged 4 commits into
masterfrom
feat/connections-tab-improvements

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Three improvements to the Connections tab, plus the guard that should have been there first.

The search box can name a column

Plain text works exactly as before. A term may now name its column instead:

port:443            ip:10.0.0.0/8       pid:>4000
port:53,8080        ip:!192.168.*       scoped:yes
port:8000-8100      proc:chrome         dropped:>0

Several terms narrow together (proc:chrome port:443 dropped:>0), and a new "?" button next to the box opens a cheat sheet listing every column name with examples.

Why: the search matched one substring against a blob of process, protocol, direction, addresses and ports - 6 of the table's 17 columns. A PID was on screen and could not be searched for, and there was no way to ask for "only the rows this session impaired" or "only the rows that dropped something", which are the questions a tester has when the table holds a hundred thousand flows.

Values are parsed by matchers.py, so this is the same mini-language as the form fields rather than a second syntax to learn and to maintain (convention 10). The query compiles once into a list of predicates - parsing per row would put the expression parser on the path of every row on every keystroke. Half-typed queries match nothing instead of raising, and an unknown field falls back to plain text, because http://x is a URL someone pasted and not a field name.

🔴 The first version silently returned zero rows for proto:tcp. A process matcher judges (pid, name), so passing a text column positionally handed it to pid, where a name cannot be evaluated - and an unevaluable term matches nothing without complaining. The search looked like it worked. Now pinned by a test and a mutation entry.

Two new right-click actions

"Block this IP address" adds the row's address to the blocking field. "Leave this process alone" excludes that process from impairment by adding !name to the target.

Both append. They are used one row at a time, so replacing would discard the address blocked a moment ago and make the second click look broken. A repeat is dropped rather than doubled. With an empty target, !name is not a narrowing but a flip: "impair everything" becomes "impair everything except this", which is the case that entry exists for.

The appending lives in matchers.add_term, not in the GUI, because splitting on commas is a question about the filter syntax. 🔴 It shipped the comma-escape bug for about ten minutes - re-joining without re-escaping emits a regex's literal comma as a separator - which is the same failure a property test once found in Matcher.describe, reintroduced in a new function on the day the old one was cited as solved. Pinned by a test.

The no-auto-apply rule finally has a guard

The question that started this was whether the row actions apply themselves. They do not - measured on a live engine: target_active and dst_active stay False after both actions and only flip after "Apply changes".

But the only test covering that area was named "feeds the targeting FIELDS" and checked only the form. Pushing a row action straight into a running engine would have kept the whole suite green. The new guard asserts both directions - untouched engine before Apply, changed after - because only the pair separates "did not apply" from "did not work at all". Mutation-checked.

One design decision made by a guard

The row actions were written as App methods first and pushed gui/app.py from 1299 to 1319 logic lines, turning the size ratchet red. Its answer is to put code where it belongs rather than to raise the number, so they moved to the Connections page, where they belong. That is the first time that guard has changed a design decision rather than just reported one.

Testing

python -m pytest tests: 929 passed, exit 0. python smoke_gui.py: OK. Mutation registry: 11 caught, 0 survived, canary BROKEN.

Every behaviour added here is mutation-checked: the search's text-column handling, the row actions reaching the engine, and the comma escape.

Not in this PR

The column chooser is still to come. It needs a decision first: a menu of checkbuttons would need add_checkbutton added to the tkinter double (which today knows only add_command and add_separator, so the feature would ship untested), or it becomes a small dialog instead.

🤖 Generated with Claude Code

donislawdev and others added 4 commits August 2, 2026 19:55
…tion row

Two right-click actions on the Connections table. "Block this IP address" adds
the row's address to the blocking field; "Leave this process alone" excludes
that process from impairment by adding !name to the target.

Both APPEND. They are used one row at a time, so replacing would discard the
address blocked a moment ago and make the second click look broken. A repeat is
dropped rather than doubled. With an empty target, !name is not a narrowing but
a flip: "impair everything" becomes "impair everything except this", which is
the case that entry exists for.

The appending lives in matchers.add_term, not in the GUI, because splitting on
commas is a question about the filter syntax (convention 10). It shipped the
comma-escape bug for about ten minutes - re-joining without re-escaping emits a
regex's literal comma as a separator - which is the same failure a property test
once found in Matcher.describe. Pinned by a test now.

Convention 15 holds and is finally guarded: the existing test was named "feeds
the targeting FIELDS" and checked only the form, so a row action pushed straight
into a running engine would have kept the suite green. The new guard asserts the
engine is untouched before Apply and changed after it, and is mutation-checked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Plain text works exactly as before. A term may now name its column instead:
port:443, ip:10.0.0.0/8, pid:>4000, scoped:yes, dropped:>0. Several terms
narrow together, and a "?" next to the box opens the cheat sheet.

The search used to match one substring against a blob of process, protocol,
direction, addresses and ports - 6 of the table's 17 columns. A PID was on
screen and could not be searched for, and there was no way to ask for "only the
rows this session impaired" or "only the rows that dropped something", which
are the questions a tester has when the table holds a hundred thousand flows.

Values are parsed by matchers.py, so this is the same mini-language as the form
fields rather than a second syntax to learn and maintain (convention 10). The
query compiles ONCE into a list of predicates: parsing per row would put the
expression parser on the path of every row on every keystroke. Half-typed
queries match nothing instead of raising, and an unknown field falls back to
plain text, because http://x is a URL someone pasted and not a field name.

The first version silently returned zero rows for proto:tcp. A process matcher
judges (pid, name), so passing a text column positionally handed it to pid,
where a name cannot be evaluated - and an unevaluable term matches nothing
without complaining. Now pinned by a test and a mutation entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two new row actions and their shared helper were written as App methods and
pushed gui/app.py from 1299 to 1319 logic lines, which turned the size ratchet
red. The ratchet's answer is to put code where it belongs rather than to raise
the number, and a Connections row action belongs to the Connections page.

app.py is back to its previous size and conns.py, at less than half the ceiling,
absorbs three short functions that already reach into app the way the rest of
that page does.

This also fixes a commit that should not have been made: the previous one landed
with test_code_shape red, because the verification pipeline piped pytest into
tail and therefore reported tail's exit code rather than pytest's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The docstring pinning the comma-escape regression contained a literal backslash
comma in a non-raw string, which Python 3.14 reports as an invalid escape
sequence. The suite ran green with two SyntaxWarnings, and a warning nobody
fixes is a warning nobody reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit b865f48 into master Aug 2, 2026
6 checks passed
@donislawdev
donislawdev deleted the feat/connections-tab-improvements branch August 2, 2026 18:29
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