mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-06 04:17:16 +00:00
Follow-up to #1987, and the point of counting that traffic in the first place. A region this instance holds no `hashRegions` key for is **unnameable, not absent**. #1987 says so with a caveat chip. This settles it wherever the evidence allows: derive `SHA256("#region")[:16]` from the repeater's own declaration and HMAC that repeater's own unmatched packets with it. Same computation the ingestor performs at ingest, with the candidate set narrowed from every configured key to this repeater's handful of declarations. Where it fires, a grey "declared but not observed" chip becomes a green one and the caveat count shrinks by the packets it explained. ## Two packets, not one `code1` is two bytes, so an unrelated name matches a given packet with probability 1/65536. Across ~400 unmatched packets and ~124 declared names, chance alone produces roughly one false match per refresh. Two matches on the same region for the same repeater is (1/65536)², about one in four billion. Lowering the threshold to one would not make this noisy, it would make it **unsound**, so the constant carries that arithmetic and a test rather than a comment. A region with exactly one hit stays grey and reports its single hit, so the page can say why it is still shown as not observed instead of leaving the reader to wonder. ## What it deliberately does not do **It writes nothing.** Read-time only. A wrong answer expires with the window instead of sitting in `transmissions.scope_name` until someone runs a repair, and `cmd/server` stays read-only per the invariant in AGENTS.md. **`notObserved` remains the single source of chip colour.** `regionEvidence` says only HOW a region was established. Two fields that can disagree about the same fact is how this column got confusing in the first place. ## Rule 0, including the part that was wrong at first The naive shape is `targets × names × packets` HMACs: 205 × 124 × 400 ≈ 10M. Caching per `(region, transmission)` pair cuts the HMACs to ~50k. **That measured 501ms**, because the HMACs had become a rounding error while the *iteration* stayed cubic at 10.2M map lookups. Re-keyed per region, holding the set of matching transmissions, it is **36ms** at the same worst-case shape: a region is HMACed over every packet once, and a target then asks one question per declared region instead of one per (region, packet). Most declared regions match nothing, so the common case is a single map lookup and no packet loop at all. `hmacCount` exists so a test can assert the first mistake cannot come back; the benchmark exists because only it caught the second. ## Both axes are bounded, because neither is bounded by the data The "~400 packets in a 7 day window" this was sized against is a property of one instance's configuration, not of the feature: the ingestor stores the unnameable state for every transport-scoped packet no configured key names, so an instance with few or no `hashRegions` entries — the stock state, and the one this helps most — has **every** scoped packet in that set. - the window query takes the 4096 most recent candidates and reports truncation, which the handler logs, so a grey chip on a sampled refresh is not read as "not forwarded" - the declared list is capped at 32 names per repeater: it arrives from a collector that validates each entry's shape but never how many entries there are - measured at the cap: **306ms** for 205 targets over 124 names, against 29ms for the shape a real network produces Because both caps make the evidence a sample, the response carries `observedUnmatchedSampled`. Without it a client subtracts a capped numerator from an uncapped total and overstates the unexplained traffic with no way to know it is doing so. The chip subtracts only evidence for regions **absent** from `notObserved` — a single-hit region the server refused to accept is not called explained either — and says "at most N" when the count was sampled. ## Verified on live data Six repeaters clear the threshold in a 7d window on a real instance. One of them: `nl-nb` green with 3 corroborating packets and the tooltip stating the count, `belml` still grey on 1, and the caveat chip reading 31 of 34 packets unexplained rather than 30. ## Tests `scope_verify_test.go` covers the HMAC-input walk against a real transport-flood packet captured from a live instance (a hand-built fixture would only prove the parser agrees with itself), that `regionCode` does not fold case, the threshold in both directions, the memo's HMAC count, both bounds with their truncation flag, and the benchmark at cap size. Handler-level tests cover a region verified into green, a single hit left grey with its count reported, and the sample-size field. `cd cmd/server && go test ./...` passes (77s), frontend 723 assertions pass, `go vet` and `gofmt -l` clean.
92 lines
5.2 KiB
CSS
92 lines
5.2 KiB
CSS
/* Network-wide scope audit page (#/scope-audit). Reuses .ns-decl / .ns-table /
|
|
.ns-empty / .ns-truncated from node-scopes.css (same badge vocabulary, so
|
|
the per-node and network-wide views agree visually) plus .analytics-time-range
|
|
for the window buttons. No hex/rgb literals — --text-muted / --border /
|
|
--status-* / --link-color are defined globally. */
|
|
|
|
.sa-page { max-width: 1200px; margin: 0 auto; padding: 12px 16px; }
|
|
|
|
.sa-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 8px; margin-bottom: 4px; }
|
|
.sa-head h2 { margin: 0; font-size: 18px; }
|
|
|
|
.sa-intro { color: var(--text-muted); font-size: 12px; margin: 4px 0 10px; }
|
|
.sa-intro a { color: var(--link-color); }
|
|
|
|
/* .sa-search reuses .nodes-search (style.css) for the input's colours/border
|
|
so this page doesn't invent a second search-box style — only the sizing
|
|
below is page-specific. */
|
|
.sa-search-bar { margin: 0 0 10px; }
|
|
.sa-search { width: 100%; max-width: 360px; }
|
|
|
|
.sa-window-note {
|
|
font-size: 12px; color: var(--text-muted); margin: 4px 0 10px;
|
|
border-left: 3px solid var(--border); padding: 4px 10px;
|
|
}
|
|
|
|
.sa-table-wrap { overflow-x: auto; }
|
|
.sa-table { min-width: 720px; }
|
|
.sa-name a { color: var(--link-color); text-decoration: none; }
|
|
.sa-name a:hover { text-decoration: underline; }
|
|
.sa-name-unknown { color: var(--text-muted); font-style: italic; }
|
|
.sa-role { font-size: 10px; text-transform: uppercase; letter-spacing: .3px; }
|
|
|
|
.sa-chip {
|
|
display: inline-block; padding: 1px 6px; margin: 1px 2px 1px 0; border-radius: 4px;
|
|
font-size: 11px; font-family: var(--mono); white-space: nowrap;
|
|
}
|
|
.sa-chip-declared { background: var(--section-bg, var(--card-bg)); color: var(--text); border: 1px solid var(--border); }
|
|
.sa-chip-undeclared { background: color-mix(in srgb, var(--status-yellow) 18%, transparent); color: var(--status-amber-text); }
|
|
.sa-chip-wildcard { background: var(--section-bg, var(--card-bg)); color: var(--text-muted); font-weight: 700; }
|
|
.sa-chip-ambiguous { background: var(--section-bg, var(--card-bg)); color: var(--text-muted); border: 1px dashed var(--border); font-family: inherit; font-style: italic; }
|
|
|
|
.sa-count { font-size: 11px; margin-top: 6px; }
|
|
|
|
/* sa-summary is the per-configState count line at the top of the page —
|
|
"13 no scopes configured" etc — reusing .ns-decl (node-scopes.css) for the
|
|
count pill so it shares the exact colour vocabulary the row-level Config
|
|
badge (also .ns-decl) uses, rather than a separate summary palette. */
|
|
.sa-summary { display: flex; flex-wrap: wrap; gap: 10px 16px; font-size: 12px; color: var(--text-muted); margin: 2px 0 10px; }
|
|
.sa-summary-item { display: inline-flex; align-items: center; gap: 5px; }
|
|
|
|
@media (max-width: 640px) {
|
|
.sa-table th:nth-child(6), .sa-table td:nth-child(6) { display: none; }
|
|
}
|
|
|
|
/* Observed forwarding, the green half of the merged Scopes column. Pairs with
|
|
.sa-chip-unobserved below, which stays neutral. Only the observed side is
|
|
coloured: red on every unobserved region would read as an alarm on rows that
|
|
are often just a quiet region over a short window. */
|
|
.sa-chip-observed { background: color-mix(in srgb, var(--status-green) 16%, transparent); color: var(--status-green-text); }
|
|
|
|
/* Declared but not observed in this window. Deliberately NOT red: absence over
|
|
a short window is weak evidence, which the page header says in words, so it
|
|
should not shout in colour. */
|
|
.sa-chip-unobserved { background: var(--section-bg, var(--card-bg)); color: var(--text-muted); border: 1px solid var(--border); }
|
|
|
|
/* Provenance note under the intro. Same muted treatment as .sa-intro; it is
|
|
context, not a finding, and must not compete with the table. */
|
|
.sa-sources { color: var(--text-muted); font-size: 12px; margin: 0 0 10px; line-height: 1.5; }
|
|
.sa-sources a { color: var(--link-color); }
|
|
.sa-sources code { font-family: var(--mono); font-size: 11px; }
|
|
|
|
/* Provenance note under the intro, and the empty state that has to carry the
|
|
same explanation on its own: on a stock install the table IS empty, so that
|
|
is where an operator actually reads it. Muted like .sa-intro; this is
|
|
context, not a finding, and must not compete with the table. */
|
|
.sa-sources { color: var(--text-muted); font-size: 12px; margin: 0 0 10px; line-height: 1.5; }
|
|
.sa-sources a, .sa-empty a { color: var(--link-color); }
|
|
.sa-sources code, .sa-empty code { font-family: var(--mono); font-size: 11px; }
|
|
.sa-empty { line-height: 1.6; max-width: 70ch; }
|
|
.sa-empty ul { margin: 8px 0; padding-left: 20px; }
|
|
.sa-empty li { margin: 4px 0; }
|
|
|
|
/* Same muted, dashed treatment as .sa-chip-ambiguous on purpose: both are
|
|
caveats on the row's finding, not findings themselves, and neither may
|
|
compete visually with the red/green scope chips beside them. */
|
|
.sa-chip-unmatched { background: var(--section-bg, var(--card-bg)); color: var(--text-muted); border: 1px dashed var(--border); font-family: inherit; font-style: italic; }
|
|
|
|
/* Verified-by-declaration: the same green as any observed chip, because the
|
|
region IS observed. The dotted underline says how that was established
|
|
without introducing a third colour into a column that already carries two. */
|
|
.sa-chip-verified { text-decoration: underline dotted; text-underline-offset: 2px; }
|