Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions public/scope-audit.css
Original file line number Diff line number Diff line change
Expand Up @@ -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); }
53 changes: 35 additions & 18 deletions public/scope-audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,32 @@
return '<span title="Declared regions answer captured ' + escapeHtml(row.declaredAt) + '">' + escapeHtml(age) + '</span>';
}

function scopeChips(names, cls) {
if (!names.length) return '<span class="text-muted">—</span>';
return names.map(function (n) { return '<span class="sa-chip ' + cls + '">' + escapeHtml(n) + '</span>'; }).join(' ');
}

function undeclaredChips(rows) {
if (!rows.length) return '<span class="text-muted">—</span>';
return rows.map(function (o) {
return '<span class="sa-chip sa-chip-undeclared" title="' + o.packets + ' packet' + (o.packets === 1 ? '' : 's') +
', last seen ' + escapeHtml(timeAgo(o.lastSeen)) + '">' + escapeHtml(o.scope) + '</span>';
}).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 '<span class="sa-chip ' + (observed ? 'sa-chip-observed' : 'sa-chip-missing') +
'" title="' + escapeHtml(n) +
(observed ? ': observed forwarding in this window' : ': declared, but no forwarding observed in this window') +
'">' + escapeHtml(n) + '</span>';
});
if (!chips.length) return '<span class="text-muted">—</span>';
return chips.join(' ');
}

// nameHtml renders the repeater identity cell. row.name == null means this
Expand Down Expand Up @@ -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 '<tr data-pubkey="' + escapeHtml(row.publicKey) + '">' +
'<td class="sa-name" data-value="' + escapeHtml(nameSortValue) + '">' + nameHtml(row) + (row.role != null && row.role !== '' ? '<span class="text-muted sa-role"> ' + escapeHtml(row.role) + '</span>' : '') + '</td>' +
'<td data-value="' + statusScore(row) + '">' + issuesHtml + '</td>' +
'<td data-value="' + escapeHtml(CONFIG_STATES[row.configState].label) + '">' + configStateHtml(row) + '</td>' +
'<td data-value="' + declaredCount + '">' + scopeChips(row.declaredRegions, 'sa-chip-declared') + (row.declaredWildcard ? ' <span class="sa-chip sa-chip-wildcard" title="Declares the \'*\' wildcard — allows plain unscoped floods.">*</span>' : '') + '</td>' +
'<td data-value="' + row.notObserved.length + '">' + scopeChips(row.notObserved, 'sa-chip-missing') + ambiguousCaveat(row) + '</td>' +
'<td data-value="' + row.undeclaredObserved.length + '">' + undeclaredChips(row.undeclaredObserved) + '</td>' +
'<td data-value="' + row.notObserved.length + '">' + mergedScopeChips(row) + (row.declaredWildcard ? ' <span class="sa-chip sa-chip-wildcard" title="Declares the \'*\' wildcard — allows plain unscoped floods.">*</span>' : '') + ambiguousCaveat(row) + '</td>' +
'<td data-value="' + (isNaN(declaredAtMs) ? '' : declaredAtMs) + '">' + ageHtml(row) + (row.truncated ? ' <span class="ns-truncated" title="Declared list was truncated by the repeater — a missing region here is not necessarily a real absence.">truncated</span>' : '') + '</td>' +
'</tr>';
}
Expand Down Expand Up @@ -247,9 +259,7 @@
'<th data-sort-key="name">Repeater</th>' +
'<th data-sort-key="status" data-type="numeric">Status</th>' +
'<th data-sort-key="config">Config</th>' +
'<th data-sort-key="declared" data-type="numeric">Declared</th>' +
'<th data-sort-key="notObserved" data-type="numeric">Not observed</th>' +
'<th data-sort-key="undeclared" data-type="numeric">Undeclared observed</th>' +
'<th data-sort-key="notObserved" data-type="numeric" title="Declared regions, coloured by whether forwarding was observed in this window. Green = observed, red = declared but not observed.">Scopes</th>' +
'<th data-sort-key="declaredAt" data-type="numeric">Declared age</th>' +
'</tr></thead><tbody>' +
d.repeaters.map(rowHtml).join('') +
Expand Down Expand Up @@ -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 });
})();
71 changes: 71 additions & 0 deletions test-frontend-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(/<span/g) || []).length, 3, 'one chip per declared region, no more');
});

test('declared order is preserved, not regrouped by colour', () => {
// 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(['<img src=x onerror=alert(1)>'], []));
assert.ok(!h.includes('<img'), 'must not emit raw markup from server-supplied names');
assert.ok(h.includes('&lt;img'));
});

test('a notObserved entry that is not declared cannot invent a chip', () => {
// 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(/<span/g) || []).length, 1);
assert.ok(!h.includes('ghost'));
});
}
Loading