From 5dcce3bada8bdd09dbdf9b1eb059fe9c1c24f263 Mon Sep 17 00:00:00 2001 From: efiten Date: Sat, 5 Sep 2026 23:54:11 +0200 Subject: [PATCH] feat(scope-audit): one colour-coded Scopes column instead of three The audit table printed DECLARED, NOT OBSERVED and UNDECLARED OBSERVED side by side. The first two were never independent: notObserved is a strict subset of declaredRegions, so the page showed the same set twice, once whole and once filtered, and left the reader to diff them. Measured on a live 197-row response before changing anything: notObserved is a subset of declaredRegions : 197 of 197 rows, no exceptions all declared regions observed : 7 rows none observed : 78 rows mixed : 44 rows no declared regions at all : 68 rows The 44 mixed rows are the ones that cost the reader time. They declare 8.3 regions on average of which 6.5 are unobserved, so usually one or two are green. BE-TUR-REP1_ON3FNZ declares 20 regions and lists 19 under NOT OBSERVED; the only difference is that "be" is missing from the second list. Finding that today means comparing two lists of twenty. Now it is one green chip among nineteen red. UNDECLARED OBSERVED is dropped. A repeater does not forward a scope it has not been configured with, so the column cannot fill except transiently when an owner REMOVES scopes while older messages are still inside the window, which is an artefact of the window rather than a fault worth a column. Confirmed empty on all 197 rows at both 24h and 7d. Nothing is lost: the STATUS column already renders an "N undeclared" badge for that case, and undeclaredObserved still feeds statusScore and the search index, both untouched. Sorting keys off notObserved, which is what the page ranks by anyway. Declared order is preserved rather than regrouped by colour, so a region stays in the position an operator is used to scanning. Verification: 8 new cases in test-frontend-helpers.js asserting the RENDERED markup through a new window.__meshcoreScopeAuditInternals handle, following map.js (#1356/#1933) rather than grepping source. Suite 662 to 670 passed, 0 failed. Then every one of the 197 live rows was pushed through the real function: 7 all-green, 78 all-red, 44 mixed, 68 empty, reproducing the independently computed figures exactly, and 793 chips against 793 declared regions so no chip is invented or dropped. Colours use existing variables (--status-green, --status-green-text) alongside the --status-red the missing chip already used, so the two read as one scale. Dead code removed: scopeChips and undeclaredChips had no remaining callers. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01PQS3XLoPD98yu9pxdRujqg --- public/scope-audit.css | 5 +++ public/scope-audit.js | 53 ++++++++++++++++++++---------- test-frontend-helpers.js | 71 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 18 deletions(-) diff --git a/public/scope-audit.css b/public/scope-audit.css index 6aaee15a8..59e570579 100644 --- a/public/scope-audit.css +++ b/public/scope-audit.css @@ -52,3 +52,8 @@ @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-missing above, which keeps the red. Same colour-mix construction so + the two read as one scale rather than two unrelated styles. */ +.sa-chip-observed { background: color-mix(in srgb, var(--status-green) 16%, transparent); color: var(--status-green-text); } diff --git a/public/scope-audit.js b/public/scope-audit.js index 89159dfbf..44e7ad6c2 100644 --- a/public/scope-audit.js +++ b/public/scope-audit.js @@ -57,17 +57,32 @@ return '' + escapeHtml(age) + ''; } - function scopeChips(names, cls) { - if (!names.length) return ''; - return names.map(function (n) { return '' + escapeHtml(n) + ''; }).join(' '); - } - - function undeclaredChips(rows) { - if (!rows.length) return ''; - return rows.map(function (o) { - return '' + escapeHtml(o.scope) + ''; - }).join(' '); + // mergedScopeChips renders ONE chip per declared region, coloured by whether + // that region was actually observed forwarding in the window. + // + // This replaces the old DECLARED and NOT OBSERVED pair. They were never + // independent: notObserved is a strict subset of declaredRegions, checked + // against a live 197-row response where it held on 197 of 197 rows. The two + // columns printed the same set twice, once whole and once filtered, and left + // the reader to diff them. On a typical mixed row that meant comparing two + // lists of eight to find the one or two entries that differ. Here the + // observed ones are simply the green ones. + // + // No new claim is made about the data: a region present in declaredRegions + // and absent from notObserved is exactly what the server already means by + // "observed forwarding in this window". + function mergedScopeChips(row) { + var missing = Object.create(null); + row.notObserved.forEach(function (n) { missing[n] = true; }); + var chips = row.declaredRegions.map(function (n) { + var observed = !missing[n]; + return '' + escapeHtml(n) + ''; + }); + if (!chips.length) return ''; + return chips.join(' '); } // nameHtml renders the repeater identity cell. row.name == null means this @@ -171,15 +186,12 @@ // parsed back into a date. var declaredAtMs = row.declaredAt ? new Date(row.declaredAt).getTime() : NaN; var nameSortValue = row.name != null ? row.name : row.publicKey; - var declaredCount = row.declaredRegions.length + (row.declaredWildcard ? 1 : 0); return '' + '' + nameHtml(row) + (row.role != null && row.role !== '' ? ' ' + escapeHtml(row.role) + '' : '') + '' + '' + issuesHtml + '' + '' + configStateHtml(row) + '' + - '' + scopeChips(row.declaredRegions, 'sa-chip-declared') + (row.declaredWildcard ? ' *' : '') + '' + - '' + scopeChips(row.notObserved, 'sa-chip-missing') + ambiguousCaveat(row) + '' + - '' + undeclaredChips(row.undeclaredObserved) + '' + + '' + mergedScopeChips(row) + (row.declaredWildcard ? ' *' : '') + ambiguousCaveat(row) + '' + '' + ageHtml(row) + (row.truncated ? ' truncated' : '') + '' + ''; } @@ -247,9 +259,7 @@ 'Repeater' + 'Status' + 'Config' + - 'Declared' + - 'Not observed' + - 'Undeclared observed' + + 'Scopes' + 'Declared age' + '' + d.repeaters.map(rowHtml).join('') + @@ -327,5 +337,12 @@ sortCtl = null; } + if (typeof window !== 'undefined') { + // Exposed so the helper tests can assert what the Scopes column RENDERS + // rather than grepping this file, the same reason map.js exposes its label + // builder (#1356/#1933). + window.__meshcoreScopeAuditInternals = { mergedScopeChips: mergedScopeChips }; + } + registerPage('scope-audit', { init: init, destroy: destroy }); })(); diff --git a/test-frontend-helpers.js b/test-frontend-helpers.js index b2cf51d1d..bf688f562 100644 --- a/test-frontend-helpers.js +++ b/test-frontend-helpers.js @@ -6966,3 +6966,74 @@ console.log('\n=== observers.js: healthStatus (configurable thresholds) ==='); assert.strictEqual(r.label, 'Unknown'); }); } + +// ===== scope-audit.js: mergedScopeChips ===== +// DECLARED and NOT OBSERVED were merged into one colour-coded Scopes column. +// They were never independent: notObserved is a strict subset of +// declaredRegions, so the page printed the same set twice and made the reader +// diff it. These assert the rendered markup, not the source. +console.log('\n=== scope-audit.js: mergedScopeChips ==='); +{ + const ctx = makeSandbox(); + ctx.registerPage = () => {}; + loadInCtx(ctx, 'public/app.js'); + loadInCtx(ctx, 'public/scope-audit.js'); + const chips = ctx.__meshcoreScopeAuditInternals.mergedScopeChips; + const row = (declared, notObserved) => ({ declaredRegions: declared, notObserved: notObserved }); + + test('a declared region absent from notObserved renders as observed', () => { + const h = chips(row(['be'], [])); + assert.ok(h.includes('sa-chip-observed'), 'should carry the observed class'); + assert.ok(!h.includes('sa-chip-missing'), 'and not the missing one'); + }); + + test('a declared region present in notObserved renders as missing', () => { + const h = chips(row(['be'], ['be'])); + assert.ok(h.includes('sa-chip-missing')); + assert.ok(!h.includes('sa-chip-observed')); + }); + + test('a mixed row renders both colours, one chip per declared region', () => { + // The case the merge exists for: on live data a typical mixed row declares + // 8 regions of which 6 are unobserved, so the observed ones are the needle. + const h = chips(row(['be', 'eu', 'nl'], ['eu', 'nl'])); + assert.strictEqual((h.match(/sa-chip-observed/g) || []).length, 1); + assert.strictEqual((h.match(/sa-chip-missing/g) || []).length, 2); + assert.strictEqual((h.match(/ { + // Regrouping would break the operator habit of scanning for a known + // region in the position it always sits. + const h = chips(row(['be', 'eu', 'nl'], ['eu'])); + assert.ok(h.indexOf('>be<') < h.indexOf('>eu<'), 'be before eu'); + assert.ok(h.indexOf('>eu<') < h.indexOf('>nl<'), 'eu before nl'); + }); + + test('no declared regions renders an em dash, not an empty cell', () => { + // 68 of 197 rows on live data declare nothing at all; an empty cell reads + // as a rendering fault rather than as an answer. + assert.ok(chips(row([], [])).includes('—')); + }); + + test('every chip explains its own colour in a title', () => { + const h = chips(row(['be', 'eu'], ['eu'])); + assert.ok(h.includes('observed forwarding in this window')); + assert.ok(h.includes('declared, but no forwarding observed in this window')); + }); + + test('region names are HTML-escaped', () => { + const h = chips(row([''], [])); + assert.ok(!h.includes(' { + // Defensive: the server guarantees notObserved is a subset (197 of 197 + // rows checked), but the column must not grow a phantom chip if that ever + // stops holding. + const h = chips(row(['be'], ['be', 'ghost'])); + assert.strictEqual((h.match(/