mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-12 07:54:43 +00:00
994544604f
## Bug Path Inspector "Show on Map" only rendered the first node of a candidate path. Multi-hop candidates appeared as a single dot instead of a polyline. ## Root cause `public/path-inspector.js:186-188` and `public/map.js:574` (cross-page handler) called: ```js drawPacketRoute(candidate.path.slice(1), candidate.path[0]); ``` But `drawPacketRoute(hopKeys, origin)` (`public/map.js:390`) expects `origin` to be an **object** with `pubkey`/`lat`/`lon`/`name` properties — not a bare string. The code at lines 451-460 does `origin.lat` / `origin.pubkey` lookups; with a string, both branches fail, `originPos` stays null, and the originating node never gets prepended to `positions`. Combined with `slice(1)` stripping the head, the resulting polyline was missing the first hop AND the origin marker — and short paths could collapse to a single resolved node. ## Fix Pass the full path as `hopKeys` and `null` as origin. `drawPacketRoute` already iterates `hopKeys`, resolves each against `nodes[]`, and draws a marker for every resolved hop. The "origin" arg was meant for cases where the originator is a separate object (e.g., from packet detail with sender metadata), not for paths where the origin IS the first hop. ```js drawPacketRoute(candidate.path, null); ``` Two call sites fixed: in-page direct call (`path-inspector.js:188`) and cross-page handler (`map.js:574`). ## Verification **Code reading only.** I did NOT manually load the page or visually verify the polyline renders. Reviewer should: 1. Open Path Inspector, query a multi-prefix path with ≥3 known hops 2. Click "Show on Map" 3. Confirm polyline draws through every resolved node, not just the first `drawPacketRoute` is hard to unit-test without a real Leaflet map, so no automated test added. --------- Co-authored-by: Kpa-clawbot <bot@example.invalid> Co-authored-by: you <you@example.com>
206 lines
8.1 KiB
JavaScript
206 lines
8.1 KiB
JavaScript
// Path Inspector — prefix candidate scoring with map overlay (issue #944).
|
|
// IIFE; exports window.PathInspector for testability.
|
|
(function () {
|
|
'use strict';
|
|
|
|
var container = null;
|
|
var currentResults = null;
|
|
|
|
function init(app) {
|
|
container = app;
|
|
var params = new URLSearchParams(location.hash.split('?')[1] || '');
|
|
var prefixParam = params.get('prefixes') || '';
|
|
|
|
container.innerHTML =
|
|
'<div class="path-inspector-page">' +
|
|
'<h2>Path Inspector</h2>' +
|
|
'<p class="help-text">Enter comma or space-separated hex prefixes (1-3 bytes each, e.g. <code>2C,A1,F4</code> or <code>2C A1 F4</code>).</p>' +
|
|
'<div class="path-inspector-input-row">' +
|
|
'<input type="text" id="path-inspector-input" class="input" placeholder="2C,A1,F4 or 2C A1 F4" value="' + escapeAttr(prefixParam) + '">' +
|
|
'<button id="path-inspector-submit" class="btn btn-primary">Inspect</button>' +
|
|
'</div>' +
|
|
'<div id="path-inspector-error" class="path-inspector-error"></div>' +
|
|
'<div id="path-inspector-results"></div>' +
|
|
'</div>';
|
|
|
|
var input = document.getElementById('path-inspector-input');
|
|
var btn = document.getElementById('path-inspector-submit');
|
|
btn.addEventListener('click', function () { submit(input.value); });
|
|
input.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Enter') submit(input.value);
|
|
});
|
|
|
|
// Auto-run if prefixes in URL.
|
|
if (prefixParam) submit(prefixParam);
|
|
}
|
|
|
|
function destroy() {
|
|
container = null;
|
|
currentResults = null;
|
|
}
|
|
|
|
function parsePrefixes(raw) {
|
|
// Accept comma or space separated.
|
|
var parts = raw.trim().split(/[\s,]+/).filter(function (s) { return s.length > 0; });
|
|
return parts.map(function (p) { return p.toLowerCase(); });
|
|
}
|
|
|
|
function validatePrefixes(prefixes) {
|
|
if (prefixes.length === 0) return 'Enter at least one prefix.';
|
|
if (prefixes.length > 64) return 'Too many prefixes (max 64).';
|
|
var hexRe = /^[0-9a-f]+$/;
|
|
var byteLen = -1;
|
|
for (var i = 0; i < prefixes.length; i++) {
|
|
var p = prefixes[i];
|
|
if (!hexRe.test(p)) return 'Invalid hex: ' + p;
|
|
if (p.length % 2 !== 0) return 'Odd-length prefix: ' + p;
|
|
var bl = p.length / 2;
|
|
if (bl > 3) return 'Prefix too long (max 3 bytes): ' + p;
|
|
if (byteLen === -1) byteLen = bl;
|
|
else if (bl !== byteLen) return 'Mixed prefix lengths not allowed.';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function submit(raw) {
|
|
var errDiv = document.getElementById('path-inspector-error');
|
|
var resultsDiv = document.getElementById('path-inspector-results');
|
|
errDiv.textContent = '';
|
|
resultsDiv.innerHTML = '';
|
|
|
|
var prefixes = parsePrefixes(raw);
|
|
var err = validatePrefixes(prefixes);
|
|
if (err) {
|
|
errDiv.textContent = err;
|
|
return;
|
|
}
|
|
|
|
// Update URL.
|
|
var base = '#/tools/path-inspector';
|
|
if (location.hash.indexOf(base) === 0) {
|
|
history.replaceState(null, '', base + '?prefixes=' + prefixes.join(','));
|
|
}
|
|
|
|
resultsDiv.innerHTML = '<p>Loading...</p>';
|
|
fetch('/api/paths/inspect', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ prefixes: prefixes })
|
|
})
|
|
.then(function (r) {
|
|
if (r.status === 503) return r.json().then(function (d) { throw new Error('Service warming up, retry in a few seconds.'); });
|
|
if (!r.ok) return r.json().then(function (d) { throw new Error(d.error || 'Request failed'); });
|
|
return r.json();
|
|
})
|
|
.then(function (data) {
|
|
currentResults = data;
|
|
renderResults(data, resultsDiv);
|
|
})
|
|
.catch(function (e) {
|
|
resultsDiv.innerHTML = '';
|
|
errDiv.textContent = e.message;
|
|
});
|
|
}
|
|
|
|
function renderResults(data, div) {
|
|
if (!data.candidates || data.candidates.length === 0) {
|
|
div.innerHTML = '<p class="no-results">No candidates found. The prefixes may not match any known path-eligible nodes.</p>';
|
|
return;
|
|
}
|
|
|
|
var html = '<table class="path-inspector-table"><thead><tr>' +
|
|
'<th>#</th><th>Score</th><th>Path</th><th>Action</th>' +
|
|
'</tr></thead><tbody>';
|
|
|
|
for (var i = 0; i < data.candidates.length; i++) {
|
|
var c = data.candidates[i];
|
|
var rowClass = c.speculative ? 'speculative-row' : '';
|
|
html += '<tr class="' + rowClass + '">';
|
|
html += '<td>' + (i + 1) + '</td>';
|
|
html += '<td class="' + (c.speculative ? 'speculative-warning' : '') + '">' +
|
|
c.score.toFixed(3) +
|
|
(c.speculative ? ' <span class="speculative-badge" title="Low evidence; may be wrong">⚠</span>' : '') +
|
|
'</td>';
|
|
html += '<td>' + escapeHtml(c.names.join(' → ')) + '</td>';
|
|
html += '<td><button class="btn btn-sm" data-idx="' + i + '">Show on Map</button></td>';
|
|
html += '</tr>';
|
|
|
|
// Per-hop evidence (collapsed).
|
|
html += '<tr class="evidence-row collapsed" data-evidence="' + i + '"><td colspan="4"><div class="evidence-detail">';
|
|
for (var j = 0; j < c.evidence.perHop.length; j++) {
|
|
var h = c.evidence.perHop[j];
|
|
html += '<div class="hop-evidence">Hop ' + (j + 1) + ': prefix=' + h.prefix +
|
|
', candidates=' + h.candidatesConsidered +
|
|
', edge=' + h.edgeWeight.toFixed(3);
|
|
if (h.alternatives && h.alternatives.length > 0) {
|
|
html += '<div class="hop-alternatives" style="margin-left:12px;font-size:12px;color:var(--text-muted);">';
|
|
for (var k = 0; k < h.alternatives.length; k++) {
|
|
var alt = h.alternatives[k];
|
|
html += '<div>↳ ' + escapeHtml(alt.name || alt.publicKey.substring(0, 8)) + ' (score=' + alt.score.toFixed(3) + ')</div>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
html += '</div>';
|
|
}
|
|
html += '</div></td></tr>';
|
|
}
|
|
|
|
html += '</tbody></table>';
|
|
html += '<div class="path-inspector-stats">Beam width: ' + data.stats.beamWidth +
|
|
' | Expansions: ' + data.stats.expansionsRun +
|
|
' | Elapsed: ' + data.stats.elapsedMs + 'ms</div>';
|
|
|
|
div.innerHTML = html;
|
|
|
|
// Wire up Show on Map buttons.
|
|
div.querySelectorAll('button[data-idx]').forEach(function (btn) {
|
|
btn.addEventListener('click', function () {
|
|
var idx = parseInt(btn.dataset.idx);
|
|
showOnMap(data.candidates[idx]);
|
|
});
|
|
});
|
|
|
|
// Wire up row expand for evidence.
|
|
div.querySelectorAll('.path-inspector-table tbody tr:not(.evidence-row)').forEach(function (row) {
|
|
row.style.cursor = 'pointer';
|
|
row.addEventListener('click', function (e) {
|
|
if (e.target.tagName === 'BUTTON') return;
|
|
var idx = row.querySelector('button[data-idx]');
|
|
if (!idx) return;
|
|
var evidenceRow = div.querySelector('tr[data-evidence="' + idx.dataset.idx + '"]');
|
|
if (evidenceRow) evidenceRow.classList.toggle('collapsed');
|
|
});
|
|
});
|
|
}
|
|
|
|
function showOnMap(candidate) {
|
|
// Store pending route for map init to pick up.
|
|
window._pendingPathInspectorRoute = candidate;
|
|
// Switch to map page if not there; map init will draw the route.
|
|
if (location.hash.indexOf('#/map') !== 0) {
|
|
location.hash = '#/map';
|
|
} else {
|
|
// Already on map — draw directly.
|
|
delete window._pendingPathInspectorRoute;
|
|
if (window.routeLayer) window.routeLayer.clearLayers();
|
|
// Pass FULL path as hopKeys (not slice(1)) — drawPacketRoute resolves
|
|
// each entry against nodes[] for plotting. The 2nd arg is the origin
|
|
// OBJECT (with pubkey/lat/lon/name); pass null since the origin is
|
|
// already the first hop in the path itself, and drawPacketRoute draws
|
|
// a marker for every resolved hop.
|
|
if (window.drawPacketRoute) window.drawPacketRoute(candidate.path, null);
|
|
}
|
|
}
|
|
|
|
function escapeAttr(s) {
|
|
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<');
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
window.PathInspector = { init: init, destroy: destroy, parsePrefixes: parsePrefixes, validatePrefixes: validatePrefixes };
|
|
if (typeof registerPage === 'function') registerPage('path-inspector', { init: init, destroy: destroy });
|
|
})();
|