Skip to content

feat(ui): nest a contract's transactions, and make the alerts past the expansion limit reachable - #94

Merged
elluff merged 3 commits into
mainfrom
feat/contract-scoped-alerts
Aug 20, 2026
Merged

feat(ui): nest a contract's transactions, and make the alerts past the expansion limit reachable#94
elluff merged 3 commits into
mainfrom
feat/contract-scoped-alerts

Conversation

@elluff

@elluff elluff commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Opening a contract group produced rows styled exactly like the top-level ones, and the alerts an expansion could not list had nowhere to be seen. UI only: no backend file is touched.

1. Nest an expanded contract's transactions under it (40553ad)

The cause was a no-op: indented applied pl-6 to the row's gutter cell, which is empty on a child row. Padding needs content to push, so the indent had never moved anything since the day it was written. It now sits on the cell holding the hash.

  • A rule runs down the gutter, from the parent's chevron through its children. Drawn as a pseudo-element spanning each cell's full height, so consecutive children abut into ONE continuous line instead of a dashed column of segments, positioned at the chevron's centre so the line descends from the control that opened the group.
  • Child dividers are inset, not removed. Removing them was the first attempt and the render showed why it fails: the region gained no bottom edge and the rule ended in mid-air above the next group. A child keeps its divider but starts it after the gutter, so rows still separate, the last one still closes the group, and the undrawn segment is exactly where the rule runs. A divider spanning the table's whole width is what says "top-level row", and now only rows that are top-level have one.
  • The open contract is tinted, neutrally rather than in a severity colour, which would compete with the badge on the same line. While open, that row is both the heading of the rows beneath it and the control that closes them.
  • The beyond-the-limit note moved inside the region it describes. Outside the rule, "134 older alerts are not listed here" read as a note from the table about the whole list.

2. Reach the alerts an expansion cannot list (a490377)

An expansion fetches one page of GROUP_EXPANSION_LIMIT rows and has no pager of its own, so a contract with 137 alerts showed 50 and stated that the rest were not listed. The statement was honest and led nowhere: no view in the UI could reach them. Narrowing the filters shifted which alerts fell inside the cap, but nothing could page past it.

The table now takes a contract scope from the URL. While it is set, grouping stops and the table lists that contract's alerts flat, through the pager and page-size control that already exist. The note that reports the surplus is the way in: it ends in "See all N for this contract".

Almost none of this is new machinery: the contract filter already existed end to end, documented as tri-state and already used by the expansion itself; URL-held filter state and the pager already existed. What was missing was a way in, a way out, and the mode switch.

Decisions rather than defaults:

  • An empty value is treated as absent. contract='' is meaningful to the API, where it selects exactly the alerts naming NO contract, but that is the grouped view's own business: reaching it from the URL would produce a view that can be neither described nor cleared.
  • The two queries are exclusive, each holding the other off through enabled, rather than fetching both and rendering one.
  • Entering the scope drops the page number, which belonged to the grouped list, and closes the expansion that led there.
  • The pinned critical row inherits the scope, for the same reason it already inherits the attack-type filter: a pin from outside the view contradicts the view.
  • A chip beside the heading names the scope and is the only way out. Without it the table reads as a short, unexplained list; the URL explains it and nobody reads the URL. Scoped rows do not repeat the contract, since the chip states it once.

3. Start the rule below the chevron, not through it (bc23427)

The rule began at top-1/2 of the parent's gutter cell. The chevron is 1rem tall and vertically centred there, so that is the glyph's own middle: the line always drew through its lower half, crossing the arrow. Systematic, not a rendering accident. It now starts 0.5rem lower, exactly half the icon.

Verification

  • 168 frontend tests (up 6 from main), and every new behaviour verified falsifiable by injecting the regression it guards: restoring the original no-op indent, dropping the rule, dropping the tint, keeping the page on scope entry, repeating the contract per row, and reintroducing the chevron overlap each fail exactly the tests that target them.
  • One test was inert when first written and was fixed. It checked that scoped rows do not repeat the contract by asserting on the registry label, but a scoped row is given no label, so dropping the suppression would have repeated the truncated address and the assertion would have passed with the bug present. It now asserts on the cell's whole content.
  • Design decided by rendering, not by reasoning: headless Chrome against the real component markup and the real built stylesheet, iterating through no-dividers, inset dividers, the region's closing edge, and with/without the rule.
  • Every new utility confirmed present in the built CSS, @property --tw-content included: without its initial-value: "" the pseudo-element would not paint and the rule would be invisible with no error anywhere.
  • eslint, tsc -b, vite build and prettier --check clean. Backend untouched, so nothing here can affect detection behaviour.

🤖 Generated with Claude Code

elluff and others added 3 commits August 20, 2026 15:00
Opening a contract produced rows styled exactly like the top-level ones, so its
transactions read as siblings of the contract holding them rather than as its
contents.

The cause was a no-op. `indented` applied `pl-6` to the row's GUTTER cell, which
is empty on a child row: padding needs content to push, so the indent had never
moved anything since the day it was written. It now sits on the cell that holds
the hash.

A rule runs down the gutter, from the parent's chevron through its children.
Drawn as a pseudo-element spanning each cell's full height, so consecutive
children abut into ONE continuous line instead of a dashed column of segments,
and positioned at the chevron's centre so the line visibly descends from the
control that opened the group. On the parent it starts at half height, leaving
the chevron rather than passing through it. Structure as the device, not
decoration over it.

Child dividers are inset, not removed. Removing them entirely was the first
attempt and the render showed why it fails: the region gained no bottom edge and
the rule ended in mid-air above the next group. A child now keeps its divider but
starts it after the gutter, so rows still separate, the last one still closes the
group, and the undrawn segment is exactly where the rule runs. A divider spanning
the table's whole width is what says "top-level row", and now only rows that are
top-level have one.

The open contract is tinted, neutrally rather than in a severity colour, which
would compete with the badge on the same line. While open, that row is both the
heading of the rows beneath it and the control that closes them, and the tint
says both: the region has a top, and the top is a control.

The beyond-the-limit note moved inside the region it describes. Outside the rule,
"134 older alerts are not listed here" read as a note from the table about the
whole list. It also closes the region when it ends it, with the same inset edge
the children use.

Chosen by rendering, not by reasoning: headless Chrome against the real component
markup and the real built stylesheet, iterating through no-dividers, inset
dividers, and the region's closing edge. Every new utility was confirmed present
in the bundle, `@property --tw-content` included: without its `initial-value: ""`
the pseudo-element would not paint and the rule would be invisible with no error
anywhere.

Gates: 161 frontend tests (up 5), and the fixes verified falsifiable by restoring
the original no-op indent, dropping the rule and dropping the tint, which fails
exactly the three tests that target them. eslint, `tsc -b`, build and prettier
clean. Backend untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e contract

Expanding a contract fetches one page of GROUP_EXPANSION_LIMIT rows and has no
pager of its own, so a contract with 137 alerts showed 50 and stated that 87
were not listed. The statement was honest and led nowhere: no view in the UI
could reach them. Narrowing the attack-type or severity filters shifted which
alerts fell inside the cap, but nothing could page past it, and for a single
class holding more than the limit the older ones were simply unreachable.

The table now takes a `contract` scope from the URL. While it is set, grouping
stops (grouping by contract is pointless once a contract is chosen) and the table
lists that contract's alerts flat, through the pager and the page-size control
that already exist. The note that reports the surplus is the way in: it ends in
"See all N for this contract".

Almost none of this is new machinery. The `contract` filter already existed
end to end, documented as tri-state on RiskAlertsParams and already used by the
expansion itself; URL-held filter state already existed; the pager already
existed. What was missing was a way in, a way out, and the mode switch.

Details that are decisions rather than defaults:

An empty value is treated as absent. `contract=''` is meaningful to the API,
where it selects exactly the alerts naming NO contract, but that is the grouped
view's own business: reaching it from the URL would produce a view that can be
neither described nor cleared.

The two queries are exclusive and each holds the other off through `enabled`,
rather than fetching both and rendering one.

Entering the scope drops the page number, which belonged to the grouped list, and
closes the expansion that led there, which has no meaning once the whole table is
that contract.

The pinned critical row inherits the scope, for the same reason it already
inherits the attack-type filter: a pin from outside the view contradicts the
view.

A chip beside the heading names the scope and is the only way out of it. Without
it the table reads as a short, unexplained list; the URL explains it and nobody
reads the URL. Scoped rows do not repeat the contract, since the chip states it
once and restating one fact on every row is the noise grouping was built to
remove.

The truncation used for a contract address is now exported rather than
duplicated, because the chip shortens the same addresses the group rows do.

Gates: 168 frontend tests (up 7), each new behaviour verified falsifiable. One of
those tests was inert when first written and was fixed: it asserted on the
registry label, but a scoped row is given no label, so dropping the suppression
would have repeated the truncated ADDRESS and the assertion would have passed
with the bug present. It now asserts on the cell's whole content. eslint,
`tsc -b`, build and prettier clean, and every new utility confirmed in the built
CSS. Backend untouched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The rule marking an expanded contract's children began at `top-1/2` of the
parent's gutter cell. The chevron is 1rem tall and vertically centred there, so
that is the glyph's own middle: the line always drew through its lower half,
crossing the arrow. Systematic, not a rendering accident, and visible as soon as
anyone looked at a real render.

It now starts 0.5rem lower, which is exactly half the icon, so it leaves the
chevron and clears it. A test pins the offset, asserted on the class because
jsdom computes no geometry for a pseudo-element: without it, reintroducing the
overlap passed all 168 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@elluff
elluff merged commit d425577 into main Aug 20, 2026
9 checks passed
@elluff
elluff deleted the feat/contract-scoped-alerts branch August 20, 2026 14:45
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