mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-15 18:50:17 +00:00
## Summary Builds on the transport view from #2167 and #2169 (both merged); rebased onto `master` (`352a0ccd`), so the diff is this PR's six commits only. The Scope Audit was the one analysis page without the region selector. This adds the shared region filter to it, for both the declared and the transport view. `?region=SFO,SJC` means what it means on every other endpoint: count only forwarding **heard by observers in those regions**, not repeaters located there. - **API** (`GET /api/scope-audit`, both modes): - The region codes are resolved to their observers first (a handful of rows), then the audit's hop scan gets `AND o.observer_idx IN (…)` on v3, or `o.observer_id IN (…)` on v2. The 3.5M-row hop scan never joins `observers`. - `All` means no filter (shared `normalizeRegionCodes`). Codes with no observer match nothing, instead of silently widening to every observer. - Responses echo the normalised codes as `region`. The declared and transport caches and their singleflight keys now include the region, so each region set is cached separately with the existing TTLs. - Declared-region verification (`regionEvidence`) is deliberately left unfiltered: it corroborates what a repeater declares from its own traffic, which doesn't depend on where a hop was heard. - **Page:** the shared `RegionFilter` sits with the view and window controls; a change reloads the view, and the selection persists like every other page. The transport view's text box is relabelled **Scope** (it already sends `?scope=` since the rename in #2167), so the page no longer has two controls called Region. - **Docs:** `docs/api-spec.md` (parameter, both response shapes) and the OpenAPI parameters for the route (`mode`, `scope` and `region` were all missing). Partial fix for #2142 (its region filter for rollout tracking), together with #2167 and #2169. ## Performance The unfiltered path must not get slower: it's the same query with an empty clause. `BenchmarkComputeScopeTransport` (from #2167, 1,200 repeaters), the PR it stacks on vs this branch, `-count 8`: | | before | after | | |---|---|---|---| | sec/op | 220.3 ms | 218.0 ms | ~ (p=0.065) | | B/op | 43.77 MiB | 43.70 MiB | ~ | | allocs/op | 833.5k | 833.5k | ~ | **Filtered requests, measured on staging** (a copy of a production database): the first version drove `o.observer_idx IN (…)` from `idx_observations_observer_idx`, reading every observation the region's observers ever made. Fixed in `80414242` with a unary `+` (the same treatment as #2166), so the scan starts from the `first_seen` window and filters per row. Red test `67e063b1` asserts the query plan against a fixture with the real indexes. | scan, one region over 24h (13 observers) | plan | time | hop rows | |---|---|---|---| | `o.observer_idx IN (…)` | observer index | 13.44 s | 102,114 | | `+o.observer_idx IN (…)` | `first_seen` window | **0.25 s** | 102,114 | | unfiltered | `first_seen` window | 0.35 s | 225,983 | Cold end-to-end requests on staging after the fix: 24h declared, unfiltered 0.84 s · one region 0.48 s (was 16.7 s) · two regions 0.22 s · transport, one region 0.42 s · transport, six regions 0.75 s · 7d, one region 3.46 s. 24h transport, live data: 383 repeaters unfiltered, 204 heard by one country's observers. ## Tests - Red `629e4f36` (Go): extends the audit fixture with an `observers` table and `observations.observer_idx`. Covers the declared view ignoring a scope heard only by another region's observer, the transport view listing only repeaters heard in the region, `region=All`, per-region caching, a region with no observers, and the echoed codes. Five fail on assertions; `region=All` passes because the stub ignores the filter, which is the behaviour it pins. - Green `6f1b1e1c`: the implementation, plus a v2-schema case (`observer_id`), so both branches of the clause are covered. - Red `b4933d68` / green `08ecc172` (frontend): `apiPath` carries the selection in both views and adds nothing when none is selected. The region control on the page is a net-new UI surface. - `go test` (cmd/server) passes. Frontend suites: 193 of 194. The one failure, `test-issue-1956-release-routing.js`, needs CI's `GITHUB_REF_NAME` and fails the same way on `master` locally. ## Screenshot The same transport view with the shared region filter set to **SJC**: only forwarding heard by San Jose observers counts, so 7 repeaters are listed instead of 44, with their carried scopes counted from those observers alone. Taken against `test-fixtures/e2e-fixture.db` with fictional scopes.  ## Browser validation The branch's server was run locally with 14 configured regions (dropdown layout), checked in headless Chromium at 1280, 900, 700 and 390 px (touch): | check | 1280 | 900 | 700 | 390 touch | |---|---|---|---|---| | ticking SJC re-requests with `®ion=SJC` | yes | yes | yes | yes | | menu fully on screen | yes | yes | yes | yes | | page wider than viewport | no | no | no | no | | page errors | none | none | none | none | Found and fixed this way: the filter is the right-most control on desktop, and the menu (which opens rightwards) ran 66 px off the page. It now opens leftwards above 640 px. On a phone the controls wrap, the filter starts a line, and the default applies. ## SonarLint SonarQube Community rules (the SonarLint engine) were run on the files this PR touches, and only issues on lines it adds are listed. - **S3776 on `ScopeAuditForwardingHeardIn`:** complexity 55. The function it generalises, `ScopeAuditForwarding`, is 54 on `master`; the region clause adds one point. - **S3776 on `computeScopeAudit`:** complexity 65, unchanged from `master`. - Both pre-date this PR. Splitting them is better as a refactor PR of its own than mixed into a feature. - The page code's findings were fixed in the transport-view PR it stacks on. ## Checked against the repo rules - Rebased onto `master` (`673442cc`). Each red commit was re-run and still fails on its assertions; each green commit passes (TDD.md). - `make test`: all 16 modules pass. Frontend suites: all pass except `test-issue-1956-release-routing.js`, which needs CI's `GITHUB_REF_NAME` and fails the same way on `master` locally (#2160 addresses it). - No private data: test fixtures and examples use the codes the existing tests use (SJC/SFO/LAX, `#be`-style scopes). - No new `map[string]interface{}`, no hard-coded colours. `git add` was explicit, and there is one logical change per commit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KaR4xu8gVykQBkH8yG8m6H --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
7.8 KiB
CSS
127 lines
7.8 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); }
|
|
/* Same outline, same reason as .ns-decl-unknown: --section-bg is dark-theme
|
|
only, so in light this falls back to --card-bg, which is white on a
|
|
near-white page ground and leaves the wildcard chip unpainted. */
|
|
.sa-chip-wildcard { background: var(--section-bg, var(--card-bg)); box-shadow: inset 0 0 0 1px var(--border); 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; }
|
|
|
|
/* A 640px rule hiding the table's sixth column used to live here. The table
|
|
has five (scope-audit.js), so it hid nothing, and the wrapper already scrolls
|
|
horizontally. Removed rather than repointed: hiding a column of this table is
|
|
what made the Config badge disappear on phones when node-scopes.css landed. */
|
|
|
|
/* 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. */
|
|
/* #1996: the chip's own tint darkens whatever is behind it, so
|
|
--status-green-text (green-700, #15803d) landed at 4.04:1 on --surface-0 and
|
|
4.38:1 on white — both under the 4.5:1 the #1719 gate requires. Measured, and
|
|
it reproduces the ratio in the report exactly.
|
|
Fixed on the TEXT rather than by thinning the tint: 12% tint would reach
|
|
4.52:1, two hundredths above the line, which is the kind of margin that fails
|
|
again on the next surface change. green-800 gives 6.23:1 on white and 5.74:1
|
|
on --surface-0, and keeps the tint that makes the chip read as a chip.
|
|
Dark is untouched: --status-green-text is the bright #22c55e there and
|
|
already passes at 6.48:1 / 5.73:1. */
|
|
.sa-chip-observed { background: color-mix(in srgb, var(--status-green) 16%, transparent); color: var(--sa-chip-observed-fg); }
|
|
|
|
/* 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; }
|
|
|
|
/* #2142 transport view: view toggle, region filter, carried-scope chips. */
|
|
.sa-controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
/* The region filter is the last control in the row, at the right edge: open
|
|
its menu leftwards so it stays on screen. On a phone the controls wrap and
|
|
the filter starts its own line at the left, where the default is right. */
|
|
@media (min-width: 641px) {
|
|
.sa-controls .region-dropdown-menu { left: auto; right: 0; }
|
|
}
|
|
.sa-region-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0 0 10px; font-size: 12px; }
|
|
.sa-region-bar[hidden] { display: none; } /* display:flex above would otherwise override the hidden attribute */
|
|
.sa-region-bar label { font-weight: 600; }
|
|
/* .nodes-search sets width:100%; the region box only needs room for one code. */
|
|
.sa-region-bar .sa-region { width: 160px; max-width: 100%; flex: none; }
|
|
.sa-chip-transported { background: var(--section-bg, var(--card-bg)); color: var(--text); border: 1px solid var(--border); }
|
|
.sa-chip-transported .sa-chip-count { color: var(--text-muted); }
|
|
/* The region being filtered on: same chip, heavier outline in the link colour,
|
|
so it stands out without adding a third status colour. */
|
|
.sa-chip-match { border-color: var(--link-color); box-shadow: inset 0 0 0 1px var(--link-color); }
|
|
.sa-not-asked { color: var(--text-muted); font-style: italic; font-size: 12px; }
|
|
.sa-counts { font-size: 11px; white-space: nowrap; }
|