mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-08-28 09:25:16 +00:00
## Summary Closes #1504. Adds a tiny, dismissible "Path symbols" legend next to the Path column header on the Packets page (and reused on the Nodes page's "Paths Through This Node" card), explaining the three otherwise-undiscoverable path glyphs: - `⚠N` — regional conflict count (multiple candidates for the hop's prefix in this region) - `⚠️` — unreliable name resolution (best-guess pubkey couldn't be confirmed) - dashed underline — ambiguous / global-fallback resolution ## Rationale (from triage) - **Tufte**: integrate words and graphics. A hidden per-row tooltip violates "don't make the viewer cross-reference." A small, persistent inline key next to the column header is dense, on-data, and dismissible. - **Avoid a modal** — chartjunk for a 3-glyph vocabulary. - **Munger** rejected the reporter's option #2 (hover overlay that pauses live updates): a power-user table must not stall from accidental hovers. - Single shared constant on `HopDisplay` so the Nodes page reuses the same vocabulary without drift. ## Files - `public/hop-display.js` — export `PATH_SYMBOLS_LEGEND` constant + `renderPathSymbolsLegend()` helper (no changes to existing badge rendering logic) - `public/packets.js` — wire renderer into the Path `<th>` header - `public/nodes.js` — reuse renderer on `#fullPathsSection` h4 - `public/style.css` — minimal styling (subtle dotted-underline trigger + floating disclosure panel, all via theme vars) - `test-frontend-helpers.js` — 5 new assertions (TDD red→green) ## TDD red → green - RED commit `46741267` — adds 5 assertion-shaped tests; all fail on the assertion (not on import/build). - GREEN commit `fab27ec5` — implements the constant, renderer, wiring, and CSS; all 607 frontend-helper tests pass. ## Tested via - DOM-grep assertions on the rendered `<details>` markup (`<summary>Path symbols</summary>`, all three glyphs present, dashed-underline description). - Static grep that `packets.js` invokes the shared renderer adjacent to the Path column. - Full `test-frontend-helpers.js`, `test-packet-filter.js`, `test-aging.js` pass. ## Hard rules honored - No modal, no pause-on-hover, no changes to `hop-display.js`'s badge rendering logic. - No `<img>`/SVG additions, no new CSS vars (uses existing theme vars), no Go changes. - PII grep clean on every commit and on this body. Browser verified: manual smoke pending — disclosure is closed-by-default and uses standard `<details>` semantics; renders inline with column header. E2E assertion added: `test-frontend-helpers.js` — `#1504: renderPathSymbolsLegend returns <details> disclosure with "Path symbols" summary + all glyphs` (and 4 sibling assertions). --------- Co-authored-by: Kpa-clawbot <bot@meshcore-analyzer> Co-authored-by: clawbot <bot@openclaw.local>
This commit is contained in:
co-authored by
Kpa-clawbot
clawbot
parent
0af968811f
commit
5fd8900cfc
+22
-1
@@ -121,5 +121,26 @@ window.HopDisplay = (function() {
|
||||
} catch (e) { console.error('Conflict popover error:', e); }
|
||||
}
|
||||
|
||||
return { renderHop, renderPath, _showFromBtn };
|
||||
// #1504 — Path symbols legend (shared by Packets + Nodes pages).
|
||||
// Tufte: integrate words and graphics — small, on-data, dismissible.
|
||||
// Glyph strings here MUST match exactly what hop-display.js emits in renderHop()
|
||||
// (the yellow ⚠N button + the bare ⚠ unreliable button + dashed-underline class).
|
||||
const PATH_SYMBOLS_LEGEND = [
|
||||
{ glyph: '⚠N',
|
||||
description: 'Yellow button next to a hop — N regional candidates share this hop\u2019s prefix. Click for the candidate list.' },
|
||||
{ glyph: '⚠',
|
||||
description: 'Warning icon alone (no number) — unreliable name resolution: the best-guess pubkey couldn\u2019t be confirmed against surrounding path hops.' },
|
||||
{ glyph: 'dashed underline',
|
||||
description: 'Ambiguous or global-fallback resolution — the name matched outside the current region.' },
|
||||
];
|
||||
|
||||
function renderPathSymbolsLegend() {
|
||||
const items = PATH_SYMBOLS_LEGEND.map(function(e) {
|
||||
return '<li><span class="path-legend-glyph">' + escapeHtml(e.glyph) + '</span> — ' + escapeHtml(e.description) + '</li>';
|
||||
}).join('');
|
||||
return '<details class="path-symbols-legend"><summary>Path symbols</summary>' +
|
||||
'<ul class="path-legend-list">' + items + '</ul></details>';
|
||||
}
|
||||
|
||||
return { renderHop, renderPath, _showFromBtn, PATH_SYMBOLS_LEGEND, renderPathSymbolsLegend };
|
||||
})();
|
||||
|
||||
@@ -689,6 +689,7 @@
|
||||
|
||||
<div class="node-full-card" id="fullPathsSection">
|
||||
<h4>Paths Through This Node</h4>
|
||||
<div class="path-symbols-legend-wrapper">${(window.HopDisplay && HopDisplay.renderPathSymbolsLegend) ? HopDisplay.renderPathSymbolsLegend() : ''}</div>
|
||||
<div id="fullPathsContent"><div class="text-muted" style="padding:8px"><span class="spinner"></span> Loading paths…</div></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1404,6 +1404,7 @@
|
||||
<button class="btn btn-icon${showHexHashes ? ' active' : ''}" id="hexHashToggle" title="Show raw hex hash prefixes instead of resolved node names in the path column">Hex Paths</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="path-symbols-legend-wrapper">${(window.HopDisplay && HopDisplay.renderPathSymbolsLegend) ? HopDisplay.renderPathSymbolsLegend() : ''}</div>
|
||||
<div class="table-fluid-wrap"><table class="data-table" id="pktTable">
|
||||
<thead><tr>
|
||||
<th scope="col" class="col-expand" data-priority="1"></th><th scope="col" class="col-region" data-sort-key="region" data-priority="3">Region</th><th scope="col" class="col-time" data-sort-key="time" data-type="date" data-priority="1">Time</th><th scope="col" class="col-hash" data-sort-key="hash" data-priority="3">Hash</th><th scope="col" class="col-size" data-sort-key="size" data-type="numeric" data-priority="4">Size</th>
|
||||
|
||||
@@ -4719,3 +4719,50 @@ body.embed #app.app-fixed {
|
||||
height: 100vh !important;
|
||||
height: 100dvh !important;
|
||||
}
|
||||
|
||||
/* #1504 — Path symbols legend disclosure (subtle inline key, shared by Packets + Nodes). */
|
||||
.path-symbols-legend-wrapper {
|
||||
margin: 4px 0 6px;
|
||||
text-align: right;
|
||||
}
|
||||
.path-symbols-legend {
|
||||
position: relative; /* anchor for absolutely-positioned .path-legend-list */
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
font-weight: normal;
|
||||
font-size: 11px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.path-symbols-legend > summary {
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted, #888);
|
||||
list-style: none;
|
||||
border-bottom: 1px dotted currentColor;
|
||||
user-select: none;
|
||||
}
|
||||
.path-symbols-legend > summary::-webkit-details-marker { display: none; }
|
||||
.path-symbols-legend > summary::marker { content: ''; }
|
||||
.path-symbols-legend > summary::before { content: 'ⓘ '; opacity: 0.7; }
|
||||
.path-symbols-legend[open] > summary { color: var(--text, inherit); }
|
||||
.path-symbols-legend .path-legend-list {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
margin: 4px 0 0;
|
||||
padding: 8px 12px;
|
||||
list-style: none;
|
||||
background: var(--bg-card, var(--bg, #fff));
|
||||
border: 1px solid var(--border, #ddd);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
max-width: 360px;
|
||||
color: var(--text, inherit);
|
||||
}
|
||||
.path-symbols-legend .path-legend-list li { padding: 2px 0; }
|
||||
.path-symbols-legend .path-legend-glyph {
|
||||
display: inline-block;
|
||||
min-width: 1.6em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -6406,6 +6406,139 @@ console.log('\n=== roles.js: Map Tile Config Parsing ===');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== #1504 — Path symbols legend disclosure =====
|
||||
{
|
||||
console.log('\n--- #1504: Path symbols legend disclosure ---');
|
||||
const sb = {
|
||||
window: { addEventListener: () => {}, dispatchEvent: () => {} },
|
||||
document: {
|
||||
readyState: 'complete',
|
||||
createElement: () => ({ id: '', textContent: '', innerHTML: '' }),
|
||||
head: { appendChild: () => {} },
|
||||
getElementById: () => null,
|
||||
addEventListener: () => {},
|
||||
querySelectorAll: () => [],
|
||||
querySelector: () => null,
|
||||
},
|
||||
console, Date, Math, Array, Object, String, Number, JSON, RegExp, Map, Set,
|
||||
encodeURIComponent, parseInt, parseFloat, isNaN, Infinity, NaN, undefined,
|
||||
setTimeout: () => {}, setInterval: () => {}, clearTimeout: () => {}, clearInterval: () => {},
|
||||
};
|
||||
sb.window.document = sb.document; sb.self = sb.window; sb.globalThis = sb.window;
|
||||
const ctx1504 = vm.createContext(sb);
|
||||
vm.runInContext(fs.readFileSync(__dirname + '/public/hop-display.js', 'utf8'), ctx1504);
|
||||
const HD = ctx1504.window.HopDisplay;
|
||||
|
||||
test('#1504: HopDisplay.PATH_SYMBOLS_LEGEND is defined and non-empty array', () => {
|
||||
assert.ok(Array.isArray(HD.PATH_SYMBOLS_LEGEND), 'PATH_SYMBOLS_LEGEND must be an array');
|
||||
assert.ok(HD.PATH_SYMBOLS_LEGEND.length >= 3, 'must have at least 3 entries (⚠N, ⚠️, dashed underline)');
|
||||
});
|
||||
|
||||
test('#1504: each legend entry has glyph + description', () => {
|
||||
HD.PATH_SYMBOLS_LEGEND.forEach((e, i) => {
|
||||
assert.ok(e && typeof e.glyph === 'string' && e.glyph.length > 0, 'entry ' + i + ' needs non-empty glyph');
|
||||
assert.ok(typeof e.description === 'string' && e.description.length > 0, 'entry ' + i + ' needs non-empty description');
|
||||
});
|
||||
});
|
||||
|
||||
test('#1504: legend constant + renderer exposed on window.HopDisplay namespace', () => {
|
||||
assert.ok(ctx1504.window.HopDisplay.PATH_SYMBOLS_LEGEND, 'PATH_SYMBOLS_LEGEND exported on namespace');
|
||||
assert.strictEqual(typeof ctx1504.window.HopDisplay.renderPathSymbolsLegend, 'function', 'renderPathSymbolsLegend exported');
|
||||
});
|
||||
|
||||
test('#1504: renderPathSymbolsLegend returns <details> disclosure with "Path symbols" summary + all glyphs', () => {
|
||||
const html = HD.renderPathSymbolsLegend();
|
||||
assert.ok(html.includes('<details'), 'must render a <details> element');
|
||||
assert.ok(html.includes('<summary>Path symbols</summary>'), 'must have summary text "Path symbols"');
|
||||
assert.ok(html.includes('⚠'), 'must contain warning glyph');
|
||||
assert.ok(/dashed/i.test(html), 'must describe the dashed underline convention');
|
||||
});
|
||||
|
||||
test('#1504: packets.js places legend in a sibling wrapper (NOT inside any <th> with data-sort-key)', () => {
|
||||
const src = fs.readFileSync(__dirname + '/public/packets.js', 'utf8');
|
||||
assert.ok(src.includes('renderPathSymbolsLegend'),
|
||||
'packets.js must invoke HopDisplay.renderPathSymbolsLegend()');
|
||||
assert.ok(src.includes('path-symbols-legend-wrapper'),
|
||||
'packets.js must wrap the legend in .path-symbols-legend-wrapper (sibling, not inside <th>)');
|
||||
// The legend invocation must NOT be inside a <th>...</th> with data-sort-key.
|
||||
// Simple structural check: in any line that contains renderPathSymbolsLegend,
|
||||
// we must NOT see "data-sort-key" on that same line.
|
||||
src.split('\n').forEach((line, i) => {
|
||||
if (line.includes('renderPathSymbolsLegend') && line.includes('data-sort-key')) {
|
||||
throw new Error('packets.js line ' + (i+1) + ' places legend inside a sortable <th> — will clobber the sort handler: ' + line.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('#1504: nodes.js places legend in a sibling wrapper (NOT inside <h4>)', () => {
|
||||
const src = fs.readFileSync(__dirname + '/public/nodes.js', 'utf8');
|
||||
assert.ok(src.includes('path-symbols-legend-wrapper'),
|
||||
'nodes.js must wrap the legend in .path-symbols-legend-wrapper (sibling, not inside <h4>)');
|
||||
src.split('\n').forEach((line, i) => {
|
||||
// line must not contain BOTH <h4 and renderPathSymbolsLegend
|
||||
if (line.includes('renderPathSymbolsLegend') && /<h4[\s>]/.test(line)) {
|
||||
throw new Error('nodes.js line ' + (i+1) + ' still embeds legend inside <h4>: ' + line.trim());
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('#1504: style.css gives .path-symbols-legend position:relative so absolutely-positioned panel anchors correctly', () => {
|
||||
const css = fs.readFileSync(__dirname + '/public/style.css', 'utf8');
|
||||
// Find the rule block for .path-symbols-legend (NOT .path-symbols-legend-wrapper)
|
||||
const m = css.match(/\.path-symbols-legend\s*\{[^}]*\}/);
|
||||
assert.ok(m, '.path-symbols-legend rule must exist');
|
||||
assert.ok(/position\s*:\s*relative/.test(m[0]),
|
||||
'.path-symbols-legend must declare position:relative so .path-legend-list (position:absolute) anchors to it, not a random ancestor. Block was: ' + m[0]);
|
||||
// And the panel must still be position:absolute
|
||||
const panel = css.match(/\.path-symbols-legend\s+\.path-legend-list\s*\{[^}]*\}/);
|
||||
assert.ok(panel && /position\s*:\s*absolute/.test(panel[0]),
|
||||
'.path-symbols-legend .path-legend-list must remain position:absolute');
|
||||
});
|
||||
|
||||
test('#1504: legend glyphs match what hop-display.js actually renders (no documented-but-missing glyphs)', () => {
|
||||
const hopSrc = fs.readFileSync(__dirname + '/public/hop-display.js', 'utf8');
|
||||
HD.PATH_SYMBOLS_LEGEND.forEach(entry => {
|
||||
const g = entry.glyph;
|
||||
if (g === 'dashed underline') {
|
||||
// documented as a CSS convention; class hop-ambiguous uses border-bottom: dashed
|
||||
assert.ok(/hop-ambiguous|hop-global-fallback/.test(hopSrc),
|
||||
'legend mentions "dashed underline" but hop-display.js has no ambiguous/global-fallback class');
|
||||
return;
|
||||
}
|
||||
if (g === '⚠N') {
|
||||
// Real template literal in hop-display.js: ⚠${badgeCount}
|
||||
assert.ok(hopSrc.includes('⚠${badgeCount}') || hopSrc.includes('\u26a0${badgeCount}'),
|
||||
'legend documents ⚠N but hop-display.js does not emit ⚠${badgeCount}');
|
||||
return;
|
||||
}
|
||||
// Otherwise the literal glyph must appear in the file
|
||||
assert.ok(hopSrc.includes(g),
|
||||
'legend glyph ' + JSON.stringify(g) + ' (codepoints ' +
|
||||
[...g].map(c => 'U+' + c.codePointAt(0).toString(16).toUpperCase()).join(',') +
|
||||
') not found in hop-display.js');
|
||||
});
|
||||
});
|
||||
|
||||
test('#1504 regression: clicking <summary> in legend does NOT trigger column-sort handler', () => {
|
||||
// Simulate the structural guarantee from the wrapper move: legend must not be a descendant of a sortable <th>.
|
||||
// table-sort.js binds click on th[data-sort-key]; with the legend in a sibling div, no click on summary
|
||||
// can bubble to a sortable th.
|
||||
const pktSrc = fs.readFileSync(__dirname + '/public/packets.js', 'utf8');
|
||||
// Extract the snippet around the table head and confirm the legend is OUTSIDE <thead>...</thead>
|
||||
const theadIdx = pktSrc.indexOf('<thead>');
|
||||
const theadEnd = pktSrc.indexOf('</thead>', theadIdx);
|
||||
const tbodyEnd = pktSrc.indexOf('</table>', theadEnd);
|
||||
assert.ok(theadIdx > 0 && theadEnd > theadIdx, 'must find thead boundaries');
|
||||
const insideThead = pktSrc.slice(theadIdx, theadEnd);
|
||||
assert.ok(!insideThead.includes('renderPathSymbolsLegend'),
|
||||
'renderPathSymbolsLegend must NOT appear inside <thead> — would clobber sort handler');
|
||||
// Also: legend must be outside the entire <table> (sibling)
|
||||
const insideTable = pktSrc.slice(theadIdx, tbodyEnd);
|
||||
assert.ok(!insideTable.includes('renderPathSymbolsLegend'),
|
||||
'legend must be sibling of <table>, not a child of any <th>/<thead>/<tr>');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== SUMMARY =====
|
||||
Promise.allSettled(pendingTests).then(() => {
|
||||
console.log(`\n${'═'.repeat(40)}`);
|
||||
|
||||
Reference in New Issue
Block a user