fix(ui): pinned critical as a block, and a group that names its attack type - #90
Merged
Conversation
…ttack type Three complaints from reviewing the shipped grouped alerts view. The pinned latest-critical row was distinguished only by a red left border, and by an accidental red BOTTOM border: `border-severity-critical-foreground/60` sets the colour on every edge, and TableRow already draws a bottom border, so the row picked up a red underline nobody asked for. The accent is now scoped to the left edge, the row carries a faint critical tint, and a section strip sits above it carrying the label. The label moved out of the transaction-hash cell for the reason it looked misplaced there: being pinned is a property of the row's PLACEMENT, and a badge beside the hash reads as an attribute of that transaction. A spacer row separates the block from the sorted list below. The group row's file icon was generic and said nothing, so it is gone. The contract address gains a copy button instead: the cell shows a registry label or a truncated address, so the full bech32 value could not be selected off the screen. It stops propagation, or copying would also expand the group. The Attack Type column held the alert count on a group row, promising a type and delivering a number. It now names the class of the group's WORST alert, which is the same alert the severity badge on that line describes, plus "+N more" when the group holds other kinds, tooltipped with their names. The count moves under the contract name, where it describes the group. Two aggregates back this: `argMax(max_class, max_score) AS worst_class` and `groupUniqArray(max_class)`, both aliased away from the source column name because on ClickHouse 26.x an aggregate aliased to a column a sibling aggregate reads returns Code 184. The contract_anomaly reconciliation keeps both in step: a verdict that takes over a group renames its attack type as well as its band, and its synthetic class is unioned into the class set that SQL cannot see. The union never subtracts, since proving no sibling row still carries the stored class would need per-class counts rather than a set; over-stating by one is the safe direction for a "+N other kinds" hint, and it is documented at the merge. Gates: 1283 backend (up 7), recall gate 554 unchanged, 136 frontend (up 5), ruff and mypy clean on both trees, eslint and build clean. The five new Tailwind utilities were checked against the built CSS, because a class Tailwind cannot generate fails silently rather than erroring.
…ate live
Seven review findings on the previous commit. All seven were real.
The band and the class came from two independent argMax calls sharing an
ordering, and the docstring claimed they agreed "by construction". They did not:
ClickHouse breaks a tie on equal max_score arbitrarily, and two aggregates can
resolve it on different rows, which is precisely the invariant the change set
out to establish. One argMax over the tuple (risk_band, max_class) now returns
one row's pair. Live evidence that ties really are arbitrary: two contracts
holding the same two scores resolved to different winners in the same query,
('Critical','circular') and ('High','phishing'), each pair internally coherent.
groupUniqArray has no stable order, so the "+N more" tooltip could reshuffle
between identical refreshes. arraySort fixes the SQL path, and the API boundary
re-sorts because the contract_anomaly merge APPENDS its synthetic class after
SQL has sorted.
The aggregate had never run on a real server: the test asserted query text
against a fake client, and tests/live_db/ had no grouping coverage. That is the
tier which exists because mocked clients let two 26.x regressions ship, and this
query reads max_class from two aggregates at once, the exact shape that returns
Code 184 on an alias collision. It is now covered there and verified against
ClickHouse 26.1.3.52, the production version.
Honest limit of that live test: with two independent argMax calls it still
passes, because on a small dataset ClickHouse happens to pick the same row for
both. It documents the invariant; what deterministically catches the old form is
the query-text test, and reverting proves it fails.
The pinned row lost its tint on hover. TableRow ships hover:bg-muted/40 and
tailwind-merge does not drop it for a plain bg-*, so the row turned grey while
the strip above stayed red: the block came apart at the moment the analyst
pointed at it. hover:bg-severity-critical/30 restated, and checked in the built
CSS.
The un-clusterable marker sat behind an early return on the attack type. It is
unreachable today, since such a group has worst_class = contract_anomaly by
construction, but "do not trust this model" is the signal that must survive
longest and it does not depend on the class. It now renders outside that guard,
with a test that pins the pairing the current data cannot produce.
The "+N more" tooltip spoke in the absolute present while classes comes from the
same filtered GROUP BY as every other number on the row, so it now says "matching
the current filter".
Formatting: the four test lines added by hand were not prettier-clean. Fixed for
those files only; the 19 other files that fail prettier --check are pre-existing
drift on main and are left alone.
Gates: 1282 backend (one test fewer, two merged into one), recall gate 554
unchanged, 9 live-DB ClickHouse tests, 137 frontend, ruff and mypy clean, eslint
and build clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Follow-up review of the grouped alerts view shipped in #87 / #89, now live on mainnet. Three things did not hold up in use, and fixing the third needed the API to say something it was not saying.
The pinned latest-critical row read as an accident. It was distinguished only by a red left border plus a red BOTTOM border nobody intended:
border-severity-critical-foreground/60sets the colour on every edge, andTableRowalready draws a bottom border, so the row inherited a red underline as a side effect. The strong accent is now scoped to the left edge, the row carries a faint critical tint closed by a deliberate edge, and a section strip sits above it.The "latest critical" label moved out of the transaction-hash cell. Sitting beside the hash it read as an attribute of that transaction, when being pinned is a property of the row's PLACEMENT. It is now a strip above the row, saying what the pin means: kept in view regardless of the sort below. A spacer row separates the block from the sorted list.
A group row's generic file icon is gone, and the contract address gained a copy button instead. The cell shows a registry label or a truncated address, so the full bech32 value could not be selected off the screen. It stops propagation, or copying would also expand the group.
The Attack Type column held the alert count, promising a type and delivering a number. It now names the class of the group's WORST alert, the same alert the severity badge on that line describes, plus "+N more" when the group holds other kinds. The count moved under the contract name, where it describes the group.
That last one needed two new aggregates, and both carry a hazard this repo has been bitten by:
One argMax over a TUPLE, not two argMax calls sharing an ordering: ClickHouse breaks a tie on equal
max_scorearbitrarily, and two aggregates can resolve it on different rows, which would put a severity badge and an attack type belonging to two different alerts on one line. That is not theoretical. In one live query, two contracts holding the same two scores resolved to different winners,('Critical','circular')and('High','phishing'), each pair internally coherent.arraySortbecausegroupUniqArrayhas no stable order and the tooltip would otherwise reshuffle between identical refreshes. Both aliased away from the source column names, because on 26.x an aggregate aliased to a column a sibling aggregate reads returns Code 184.The
contract_anomalyreconciliation keeps pace: a verdict that takes over a group renames its attack type as well as its band, and its synthetic class is unioned into the set SQL cannot see. The union never subtracts, since proving no sibling row still carries the stored class would need per-class counts rather than a set; over-stating by one is the safe direction for a "+N other kinds" hint, and it is documented at the merge. The API re-sorts the set at the boundary because that merge appends after SQL has sorted.Verified on a real server, not just against a fake client
The grouped aggregate now has coverage in
backend/tests/live_db/, the tier that exists because mocked clients let two ClickHouse 26.x regressions ship with green tests. Run against 26.1.3.52, the production version: the aggregate executes (no Code 184 from the two aggregates readingmax_class), a group counts and names its worst alert, the class list comes back sorted, and a tie returns a coherent pair.One honest limit: that tie test still passes with the OLD two-argMax form, because on a small dataset ClickHouse happens to pick the same row for both aggregates. It documents the invariant. What deterministically catches the old form is the query-text test, and reverting the SQL proves it fails.
Checklist
pytest tests/green (1282), plus 9 live-DB ClickHouse tests against 26.1.3.52pnpm lint && pnpm buildgreen,pnpm test137 passeduv run pytest -qgreen (sidecar untouched)The five new Tailwind utilities were checked against the built CSS, because a class Tailwind cannot generate fails silently rather than erroring. That is also how the hover fix was confirmed:
TableRowshipshover:bg-muted/40and tailwind-merge does not drop it for a plainbg-*, so without restating the hover tint the pinned block came apart at the moment the analyst pointed at it.