mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-12 22:39:52 +00:00
b812a98a71
Red commit: 537fbbc6b0 (CI run:
https://github.com/Kpa-clawbot/CoreScope/actions?query=branch%3Afix%2F1648-m3-details-badges)
Partial fix for #1648 (M3 of 6). Do NOT close the tracking issue.
M3 covers detail panes, status pills, role/payload-type badges per the
tracking-issue M3 checklist. Builds on M1 sprite + M2 chrome.
## Per-file swap counts
| file | swaps (sprite refs) |
| --- | --- |
| public/home.js | 26 |
| public/channels.js | 27 |
| public/route-view-utils.js | 11 |
| public/route-view.js | 4 |
| public/app.js | 4 |
| public/hop-display.js | 3 |
| public/path-inspector.js | 1 |
| **total** | **76** |
Plus 6 new Phosphor SVG symbols vendored into
`public/icons/phosphor-sprite.svg` (regular weight, alphabetical):
`ph-bluetooth`, `ph-camera`, `ph-hexagon`, `ph-paper-plane-tilt`,
`ph-plant`, `ph-rocket`.
## Status-token integration (design decision 3)
`.status-{ok,warn,err,muted}` rules added in `public/style.css` (lines
22-35). Each threads a `--status-*` color via `color:` and Phosphor
sprites inside inherit via `currentColor` — no fill colors baked into
sprite refs. Verified by an emoji-scan assertion
(`test-issue-1648-m3-emoji-scan.js` `assertStatusTokenCss()`) and an E2E
computed-style probe.
## TDD evidence
- Red commit `537fbbc6` adds the failing scan
(`test-issue-1648-m3-emoji-scan.js`) alone — see branch CI history.
- Green commit `4a0cd89a` implements the swaps.
- Anti-tautology: reverting one sprite swap in `path-inspector.js`
reproduces the assertion failure; restored.
## E2E assertions added
`test-issue-1648-m3-icons-e2e.js:1-188` — Playwright behavioral checks
for `/home` (welcome cards), `/channels` (sidebar + modal),
`/nodes/<pk>` (detail pane), `/analytics`, `/live`, plus `.notdef`
resolver and `.status-ok` computed-color probe. Registered in
`.github/workflows/deploy.yml`.
## Test updates (drift)
`test-frontend-helpers.js` updated 6 assertions to match sprite-rendered
HTML: hop-display unreliable-badge (`#ph-warning`), `#1504`
`PATH_SYMBOLS_LEGEND` (`glyph` → `glyphHtml`), `#781`/`#811`
channel-lock affordance (`#ph-lock`). Pre-existing 2 `favStar` failures
from M2 baseline remain unchanged (out-of-scope here).
## Out of scope (next milestones)
- `public/customize.js` / `public/customize-v2.js` operator-customizable
emoji config → **M5**
- `cmd/server/routes.go` server-rendered onboarding config → **M6**
- `public/route-view-v2.js` route-overlay glyph logic → **M4** (this PR
touches only `route-view-utils.js` payload taxonomy + `route-view.js`
sidebar, not the overlay)
206 lines
8.2 KiB
JavaScript
206 lines
8.2 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 status-warn" title="Low evidence; may be wrong"><svg class="ph-icon" aria-hidden="true"><use href="/icons/phosphor-sprite.svg#ph-warning"/></svg></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, '>').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 });
|
|
})();
|