fix(overview): the attack map never re-measured the card it was drawn into - #1937
Merged
Merged
Conversation
…e to write The lab's whole premise is that a theme is reviewed against real captured data rather than fixtures -- both previous design refreshes were run that way and that is what shipped. The harness that made it possible went with the Go dashboard in cb77cdf, so the pattern branding/design-lab/ documents has not been executable since. The Go version got its safety from constructing the server with nil write-services. frontend-next has no such handle: it reaches data over HTTP through two bases, and one of them is write-capable by definition. So the guarantee is made at that seam instead -- BACKEND_URL a gate forwarding GET/HEAD, refusing the rest with 405 BACKEND_MOUNTED_URL nothing at all; every request 503s -- which is stronger than what it replaces, because it is enforced per request rather than by remembering to pass nil. That is not a formality. The Rust tier really exposes stores.rs's generic_delete, preferences.rs's put, the honeyfs implant writer and the canarytoken minter, so a reviewer clicking through a variant would otherwise delete real documents and mint real tokens against live infrastructure. lab.test.mjs drives the actual harness against a recording stand-in backend and asserts a write never arrives; it runs in CI, because the guarantee is the point of the tool and a silent regression would be indistinguishable from it working. Variant stylesheets are symlinked into the dev server's public/ tree, so a variant layers on the vendored theme.css with no rebuild and no change to the shipped vite config -- and saving the source file is enough to see it. Ports are the ones the lab has always used, so the review log's links keep resolving. Verified end to end against the homeserver's real backend: the page renders cowrie, dionaea, wordpot, galah and suricata with live states, carries both stylesheets in the right order, and the gate refuses a delete without it reaching Elasticsearch. Booting it also surfaced that routeShape.test.ts was being scanned as a route file and warned on every dev-server start; renamed to the generator's ignore prefix, which vitest's own include pattern is unaffected by. Closes #1828
…t have Chasing #1566's "near-duplicate rows" on /ml-anomalies turned up why they looked the way they did: the list was not in any order at all. Store pages sort with `unmapped_type` set, so a store whose index does not exist yet returns an empty list instead of an error. The same setting means a field the documents do not have sorts every hit as null rather than failing -- and nothing anywhere says so. Three stores shipped that way: ml-anomalies asked for `timestamp` where the worker writes `@timestamp`, auth-events asked for `last_seen` where Keycloak writes `@timestamp`, and static-analysis asked for `Analysis.GeneratedUTC`, which its documents do not carry in any spelling -- they hold only Analysis and Fingerprint, and no time field whatsoever. Measured on the live index, the first page of /ml-anomalies read 20:58, 20:58, 20:59, 20:59, 20:59, 20:57, 20:57, 20:58. The newest document was third by luck. Worse than the disorder: an all-null sort is one enormous tie, and `from`/`size` over a tie leaves the order inside it undefined. Paging could hand back the same document twice and never show another. So every store sort now carries `_doc` as a tiebreak -- which the numeric sorts needed anyway, since campaigns-by-score and attackers-by-events tie constantly -- and each declares its field's real type, because an `unmapped_type` that disagrees with the mapping is the same silent no-op for keyword sorts. CI cannot see Elasticsearch, so scripts/check-store-sorts.py is the guard: it checks all 23 declared sort fields against a live mapping dump. Run against the homeserver it passes, and re-introducing either bug class fails it with the reason. With the order fixed, the duplicates #1566 actually reported are adjacent for the first time, so they can be folded. 66% of the 7,305 live anomalies sit in an (address, same second) bucket with siblings; the worst single burst is 48 rows for one IP inside one second, every one scoring exactly 0.8000. Consecutive rows from one address in one second scoring within 0.01 now collapse to one row badged with what it stands for. Acknowledging a folded row acknowledges all of it -- otherwise it would return as open on the next refresh and the button would look broken while behaving exactly as written -- and the status badge reports "3 of 48 open" rather than speaking only for whichever row represents the run. Folding is deliberately page-local and says so in the badge: doing it in the Rust tier would change the shape of the shared /api/v1/store endpoint for every other consumer, and a burst longer than one page still splits at the boundary. Also verified while here, and already correct: page-title casing is uniformly sentence case across all 27 headers, and /campaigns renders 7 columns rather than 15 -- the rest are behind `detail: true`. Closes #1566
… into Leaflet sizes its tile grid once, against whatever the container measured at construction time, and the map is built before the card has settled at its final width. Measured at an 1854px viewport: six 256px tile columns laid out from the wrong origin for a 1442px container, leaving a 92px strip of bare card down the right-hand edge. It reads as a rendering fault rather than as ocean, which is how #1565 reported it. invalidateSize() re-measures and recomputes the grid. A ResizeObserver repeats that whenever the card changes width, because a fixed zoom covers a fixed pixel width and the card's width is not fixed -- a window resize puts the strip straight back otherwise. Verified on a rendered page against live data, at two widths. Before, at 1442px: tiles ended 92px short on the right. After: every edge overflows the container (-75 left, -19 right, -179 top, -69 bottom) and the whole world is still in frame, Australia and southern Africa included. Narrowed to a 488px card the observer holds it (-552/-496/-249/-139). Fitting the world to *cover* the card was the other candidate and is worse: it zooms to 3 and crops the southern hemisphere, trading data for coverage. This keeps zoom 2 and simply stops lying to leaflet about how big the card is. Closes #1565
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
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.
Closes out #1565 — the residue that was explicitly waiting on a rendered page. #1828 landed the design lab, so these could finally be looked at rather than reasoned about.
The one real bug: map gutters
Leaflet sizes its tile grid once, against whatever the container measured at construction time, and the map is built before the card has settled at its final width.
invalidateSize()was never called.Measured at 1854px viewport, 1442px card — six 256px tile columns from the wrong origin:
A
ResizeObserverrepeats the re-measure whenever the card changes width — a fixed zoom covers a fixed pixel width, and the card's width is not fixed, so a window resize otherwise puts the strip straight back. Verified narrowed to a 488px card: −552 / −496 / −249 / −139.Rejected alternative: fitting the world to cover the card (
getBoundsZoom(WORLD, true)). It works, but zooms to 3 and crops the southern hemisphere — Australia, NZ and southern Africa all had markers. That trades data for coverage. Keeping zoom 2 and simply not lying to leaflet about the card size gives full coverage and the whole world.Everything else was already fixed — verified, not assumed
Activity heatmap "one global colour scale" — it is already per-row normalised, and has been. The
maxindashboard.rsis computed inside the per-sensor map. Confirmed against the live payload: all 30 sensor rows reachpct: 100, includingwordpotwhose busiest hour holds 2 events andelasticpotat 4, alongsidesuricataat 1,095,300.The previous audit declined to make this change because per-row normalisation "changes what a cell's colour means". That is already the shipped behaviour — so there was no design decision to make, only a measurement to take.
Campaigns
scorecolumn is 100 for every row — previously unverifiable because the index was empty. It now holds 50 documents: 13 distinct scores spanning 62–75. The column carries signal."Network/provider classes" (4 rows) beside a packed "Top autonomous systems" — they are not beside each other. Every card on the threat-landscape tab is full-width and stacked (all 1492px at an 1854px viewport). The imbalance the issue describes cannot occur in the current layout.
Header copy "Every column, every network →" — no longer present anywhere in
src/.Attribution / "World" control colliding with the card edge — attribution sits 33px inside the card edge; there is no "World" reset control any more.
Checks
48 frontend tests,
tsc --noEmitclean. Map behaviour verified on a rendered page against live Elasticsearch data at two viewport widths.Closes #1565