Files
meshcore-analyzer/public/scope-audit.css
T
efiten 2c8c1161b5 feat(#1975): network-wide Scope Audit page, fed by confirmed scopes (#1976)
One row per repeater whose configured region list is known, answering a question
no other view answers: you declare these regions, but were you seen forwarding
them? default_scope says what a node's adverts were observed under and
transported_scopes (#1751) says what it carried, but nothing lined the declared
list up against observed forwarding.

The declared side merges every confirmed-scope source the database carries,
newest answer per node wins, rather than naming one. On a stock install only
nodes.configured_scope (#1865/#1971) exists and it degrades to the one-source
case; deployments that collect the same fact another way keep working. Reading a
single hard-coded source would have rendered an empty page on the very instance
the evidence came from.

Declared and observed are compared through normScope, so a leading "#" and a
bare region name are one region. Unobserved regions render neutral, not red:
absence over a short window is weak evidence, which the page header already
states in words.

Ported from a long-running fork deployment with its 17 server tests, rewired to
the upstream data source, plus 14 frontend cases asserting rendered markup.
2026-09-06 23:04:42 +02:00

82 lines
4.5 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; }