mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-16 19:01:49 +00:00
Compare commits
2
Commits
master
...
fix/issue-1849
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
115368d02d | ||
|
|
dd7a4e7850 |
+21
-8
@@ -2244,7 +2244,12 @@
|
||||
const groupTypeClass = payloadTypeColor(p.payload_type);
|
||||
const groupSize = p.raw_hex ? Math.floor(p.raw_hex.length / 2) : 0;
|
||||
const _grpPlOff = getPathLenOffset(p.route_type);
|
||||
const groupHashBytes = ((parseInt(p.raw_hex?.slice(_grpPlOff * 2, _grpPlOff * 2 + 2), 16) || 0) >> 6) + 1;
|
||||
// #1849: TRACE (payload_type=9) header path bytes are per-hop SNR readings,
|
||||
// not truncated hop hashes (see internal/packetpath/route.go PathBytesAreHops).
|
||||
// The (>>6)+1 derivation is meaningless for TRACE — render — with a tooltip.
|
||||
const _grpIsTrace = p.payload_type === 9;
|
||||
const groupHashBytes = _grpIsTrace ? '—' : (((parseInt(p.raw_hex?.slice(_grpPlOff * 2, _grpPlOff * 2 + 2), 16) || 0) >> 6) + 1);
|
||||
const _grpHashSizeTitle = _grpIsTrace ? ' title="TRACE path bytes are SNR readings, not hash prefixes — see sidebar decoder for actual hop count"' : '';
|
||||
const isSingle = p.count <= 1;
|
||||
// Channel color highlighting (#271)
|
||||
const _grpDecoded = getParsedDecoded(p) || {};
|
||||
@@ -2257,7 +2262,7 @@
|
||||
<td class="col-time">${renderTimestampCell(p.latest)}</td>
|
||||
<td class="mono col-hash" data-filter-field="hash" data-filter-value="${escapeHtml(p.hash || '')}">${truncate(p.hash || '—', 8)}</td>
|
||||
<td class="col-size" data-filter-field="size" data-filter-value="${groupSize || ''}">${groupSize ? groupSize + 'B' : '—'}</td>
|
||||
<td class="col-hashsize mono">${groupHashBytes}</td>
|
||||
<td class="col-hashsize mono"${_grpHashSizeTitle}>${groupHashBytes}</td>
|
||||
<td class="col-type" data-filter-field="type" data-filter-value="${escapeHtml(groupTypeName || '')}">${p.payload_type != null ? `<span class="badge badge-${groupTypeClass}">${groupTypeName}</span>${transportBadge(p.route_type)}` : '—'}</td>
|
||||
<td class="col-observer" data-filter-field="observer" data-filter-value="${escapeHtml(obsNameOnly(headerObserverId) || '')}">${isSingle ? escapeHtml(truncate(obsNameOnly(headerObserverId), 16)) + obsIataBadge(p) : escapeHtml(truncate(obsNameOnly(headerObserverId), 10)) + groupedObserverIataBadgesHtml(p)}</td>
|
||||
<td class="col-path"><span class="path-hops">${groupPathStr}</span></td>
|
||||
@@ -2275,9 +2280,14 @@
|
||||
const size = c.raw_hex ? Math.floor(c.raw_hex.length / 2) : 0;
|
||||
const childPath = getParsedPath(c);
|
||||
const _cPlOff = getPathLenOffset(p.route_type);
|
||||
const childHashBytes = c.raw_hex
|
||||
? (((parseInt(c.raw_hex.slice(_cPlOff * 2, _cPlOff * 2 + 2), 16) || 0) >> 6) + 1)
|
||||
: (childPath.length > 0 ? childPath[0].length / 2 : 0);
|
||||
// #1849: TRACE header path bytes are SNR readings, not hop hashes.
|
||||
const _cIsTrace = c.payload_type === 9;
|
||||
const childHashBytes = _cIsTrace
|
||||
? '—'
|
||||
: (c.raw_hex
|
||||
? (((parseInt(c.raw_hex.slice(_cPlOff * 2, _cPlOff * 2 + 2), 16) || 0) >> 6) + 1)
|
||||
: (childPath.length > 0 ? childPath[0].length / 2 : 0));
|
||||
const _cHashSizeTitle = _cIsTrace ? ' title="TRACE path bytes are SNR readings, not hash prefixes — see sidebar decoder for actual hop count"' : '';
|
||||
const childRegion = c.observer_id ? (observerMap.get(c.observer_id)?.iata || '') : '';
|
||||
const childPathStr = renderPath(childPath, c.observer_id);
|
||||
const _childHashStripe = _hashStripeStyle(c.hash || p.hash);
|
||||
@@ -2286,7 +2296,7 @@
|
||||
<td class="col-time">${renderTimestampCell(c.timestamp)}</td>
|
||||
<td class="mono col-hash" data-filter-field="hash" data-filter-value="${escapeHtml(c.hash || '')}">${truncate(c.hash || '', 8)}</td>
|
||||
<td class="col-size" data-filter-field="size" data-filter-value="${size || ''}">${size}B</td>
|
||||
<td class="col-hashsize mono">${childHashBytes}</td>
|
||||
<td class="col-hashsize mono"${_cHashSizeTitle}>${childHashBytes}</td>
|
||||
<td class="col-type" data-filter-field="type" data-filter-value="${escapeHtml(typeName || '')}"><span class="badge badge-${typeClass}">${typeName}</span>${transportBadge(c.route_type)}</td>
|
||||
<td class="col-observer" data-filter-field="observer" data-filter-value="${escapeHtml(obsNameOnly(c.observer_id) || '')}">${escapeHtml(truncate(obsNameOnly(c.observer_id), 16))}${obsIataBadge(c)}</td>
|
||||
<td class="col-path"><span class="path-hops">${childPathStr}</span></td>
|
||||
@@ -2309,7 +2319,10 @@
|
||||
const _chanStyle = window.ChannelColors ? window.ChannelColors.getRowStyle(decoded.type || typeName, decoded.channel) : '';
|
||||
const size = p.raw_hex ? Math.floor(p.raw_hex.length / 2) : 0;
|
||||
const _flatPlOff = getPathLenOffset(p.route_type);
|
||||
const hashBytes = ((parseInt(p.raw_hex?.slice(_flatPlOff * 2, _flatPlOff * 2 + 2), 16) || 0) >> 6) + 1;
|
||||
// #1849: TRACE path bytes = SNR readings, not hop hashes.
|
||||
const _flatIsTrace = p.payload_type === 9;
|
||||
const hashBytes = _flatIsTrace ? '—' : (((parseInt(p.raw_hex?.slice(_flatPlOff * 2, _flatPlOff * 2 + 2), 16) || 0) >> 6) + 1);
|
||||
const _flatHashSizeTitle = _flatIsTrace ? ' title="TRACE path bytes are SNR readings, not hash prefixes — see sidebar decoder for actual hop count"' : '';
|
||||
const pathStr = renderPath(pathHops, p.observer_id);
|
||||
const detail = getDetailPreview(decoded);
|
||||
const _flatHashStripe = _hashStripeStyle(p.hash);
|
||||
@@ -2319,7 +2332,7 @@
|
||||
<td class="col-time">${renderTimestampCell(p.timestamp)}</td>
|
||||
<td class="mono col-hash" data-filter-field="hash" data-filter-value="${escapeHtml(p.hash || '')}">${truncate(p.hash || String(p.id), 8)}</td>
|
||||
<td class="col-size" data-filter-field="size" data-filter-value="${size || ''}">${size}B</td>
|
||||
<td class="col-hashsize mono">${hashBytes}</td>
|
||||
<td class="col-hashsize mono"${_flatHashSizeTitle}>${hashBytes}</td>
|
||||
<td class="col-type" data-filter-field="type" data-filter-value="${escapeHtml(typeName || '')}"><span class="badge badge-${typeClass}">${typeName}</span>${transportBadge(p.route_type)}</td>
|
||||
<td class="col-observer" data-filter-field="observer" data-filter-value="${escapeHtml(obsNameOnly(p.observer_id) || '')}">${escapeHtml(truncate(obsNameOnly(p.observer_id), 16))}${obsIataBadge(p)}</td>
|
||||
<td class="col-path"><span class="path-hops">${pathStr}</span></td>
|
||||
|
||||
@@ -65,6 +65,7 @@ node test-naive-banner-tone.js
|
||||
node test-issue-1473-reserved-prefixes.js
|
||||
node test-issue-1473-prefix-generator.js
|
||||
node test-issue-1770-mobile-row-clamp.js
|
||||
node test-issue-1849-trace-hashbytes.js
|
||||
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════"
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
/**
|
||||
* #1849 — "Hop Bytes" (col-hashsize) column shows misleading `1` for TRACE packets.
|
||||
*
|
||||
* TRACE packets (payload_type=9) use header path bytes as per-hop SNR readings, not
|
||||
* truncated hop hashes (see internal/packetpath/route.go PathBytesAreHops(TRACE)=false).
|
||||
* The high-2-bit "hash_size" derivation is meaningless for TRACE.
|
||||
*
|
||||
* Fix: at the 3 render sites in public/packets.js (buildGroupRowHtml header, its child
|
||||
* rows, buildFlatRowHtml), when payload_type === 9 render `—` in col-hashsize with an
|
||||
* explanatory title tooltip. Non-TRACE behavior unchanged.
|
||||
*/
|
||||
'use strict';
|
||||
const vm = require('vm');
|
||||
const fs = require('fs');
|
||||
const assert = require('assert');
|
||||
|
||||
let passed = 0, failed = 0;
|
||||
function test(name, fn) {
|
||||
try { fn(); passed++; console.log(' ✅ ' + name); }
|
||||
catch (e) { failed++; console.log(' ❌ ' + name + ': ' + e.message); }
|
||||
}
|
||||
|
||||
// Reuse the sandbox construction from test-packets.js (kept in sync intentionally).
|
||||
function makeSandbox() {
|
||||
const registeredPages = {};
|
||||
const ctx = {
|
||||
window: {
|
||||
addEventListener: () => {}, removeEventListener: () => {}, dispatchEvent: () => {},
|
||||
innerWidth: 1200, PacketFilter: null,
|
||||
},
|
||||
document: {
|
||||
readyState: 'complete',
|
||||
createElement: () => ({ id: '', textContent: '', innerHTML: '', className: '', style: {},
|
||||
appendChild: () => {}, setAttribute: () => {}, addEventListener: () => {},
|
||||
querySelectorAll: () => [], querySelector: () => null,
|
||||
classList: { add: () => {}, remove: () => {}, contains: () => false } }),
|
||||
head: { appendChild: () => {} }, getElementById: () => null,
|
||||
addEventListener: () => {}, removeEventListener: () => {},
|
||||
querySelectorAll: () => [], querySelector: () => null, body: { appendChild: () => {} },
|
||||
},
|
||||
console, Date, Infinity, Math, Array, Object, String, Number, JSON, RegExp,
|
||||
Error, TypeError, RangeError, parseInt, parseFloat, isNaN, isFinite,
|
||||
encodeURIComponent, decodeURIComponent,
|
||||
setTimeout: () => {}, clearTimeout: () => {}, setInterval: () => {}, clearInterval: () => {},
|
||||
fetch: () => Promise.resolve({ ok: true, json: () => Promise.resolve({}) }),
|
||||
performance: { now: () => Date.now() },
|
||||
localStorage: (() => { const s = {}; return {
|
||||
getItem: k => s[k] || null, setItem: (k, v) => { s[k] = String(v); }, removeItem: k => { delete s[k]; },
|
||||
}; })(),
|
||||
location: { hash: '' }, history: { replaceState: () => {} },
|
||||
CustomEvent: class CustomEvent {}, Map, Set, Promise, URLSearchParams,
|
||||
addEventListener: () => {}, removeEventListener: () => {}, dispatchEvent: () => {},
|
||||
requestAnimationFrame: (cb) => setTimeout(cb, 0),
|
||||
registerPage: (name, handler) => { registeredPages[name] = handler; },
|
||||
};
|
||||
vm.createContext(ctx);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
function loadInCtx(ctx, file) {
|
||||
vm.runInContext(fs.readFileSync(file, 'utf8'), ctx, { filename: file });
|
||||
for (const k of Object.keys(ctx.window)) { ctx[k] = ctx.window[k]; }
|
||||
}
|
||||
|
||||
function loadPacketsSandbox() {
|
||||
const ctx = makeSandbox();
|
||||
loadInCtx(ctx, 'public/payload-labels.js');
|
||||
loadInCtx(ctx, 'public/roles.js');
|
||||
loadInCtx(ctx, 'public/app.js');
|
||||
loadInCtx(ctx, 'public/packet-helpers.js');
|
||||
vm.runInContext(`
|
||||
window.HopDisplay = {
|
||||
renderHop: function(h, entry, opts) { return '<span>' + h + '</span>'; },
|
||||
_showFromBtn: function() {}
|
||||
};
|
||||
`, ctx);
|
||||
loadInCtx(ctx, 'public/packets.js');
|
||||
return ctx;
|
||||
}
|
||||
|
||||
console.log('\n=== #1849: TRACE col-hashsize renders — not misleading integer ===');
|
||||
{
|
||||
const ctx = loadPacketsSandbox();
|
||||
const api = ctx._packetsTestAPI;
|
||||
assert(api, '_packetsTestAPI must be exposed');
|
||||
|
||||
test('buildFlatRowHtml: TRACE (payload_type=9) renders — in col-hashsize with title tooltip', () => {
|
||||
const p = {
|
||||
id: 100, hash: 'trace1', timestamp: '', observer_id: null,
|
||||
raw_hex: '00090000000000000000000102030405', payload_type: 9,
|
||||
route_type: 0, decoded_json: '{}', path_json: '[]'
|
||||
};
|
||||
const html = api.buildFlatRowHtml(p);
|
||||
const m = html.match(/<td class="col-hashsize[^"]*"([^>]*)>([^<]*)<\/td>/);
|
||||
assert(m, 'col-hashsize cell must be present. Got: ' + html.slice(0, 500));
|
||||
assert.strictEqual(m[2].trim(), '—',
|
||||
'TRACE col-hashsize must be — (em-dash), got: ' + JSON.stringify(m[2]));
|
||||
assert(/title="[^"]*TRACE[^"]*"/.test(m[1]),
|
||||
'col-hashsize <td> must carry a title attribute mentioning TRACE. Got attrs: ' + m[1]);
|
||||
});
|
||||
|
||||
test('buildFlatRowHtml: non-TRACE (payload_type=0) still renders numeric hashBytes', () => {
|
||||
const p = {
|
||||
id: 101, hash: 'adv1', timestamp: '', observer_id: null,
|
||||
raw_hex: '00000000000000000000C100', payload_type: 0,
|
||||
route_type: 0, decoded_json: '{}', path_json: '[]'
|
||||
};
|
||||
const html = api.buildFlatRowHtml(p);
|
||||
const m = html.match(/<td class="col-hashsize[^"]*"[^>]*>([^<]*)<\/td>/);
|
||||
assert(m, 'col-hashsize cell must be present');
|
||||
assert(/^\d+$/.test(m[1].trim()),
|
||||
'non-TRACE col-hashsize must be numeric, got: ' + JSON.stringify(m[1]));
|
||||
});
|
||||
|
||||
test('buildGroupRowHtml: TRACE header row (payload_type=9) renders — in col-hashsize with title', () => {
|
||||
const p = {
|
||||
hash: 'trg1', count: 1, latest: '',
|
||||
observer_id: null, raw_hex: '00090000000000000000000102030405',
|
||||
payload_type: 9, route_type: 0, decoded_json: '{}', path_json: '[]',
|
||||
observation_count: 1, observer_count: 1,
|
||||
};
|
||||
const html = api.buildGroupRowHtml(p);
|
||||
const m = html.match(/<td class="col-hashsize[^"]*"([^>]*)>([^<]*)<\/td>/);
|
||||
assert(m, 'col-hashsize cell must be present');
|
||||
assert.strictEqual(m[2].trim(), '—',
|
||||
'TRACE group header col-hashsize must be —, got: ' + JSON.stringify(m[2]));
|
||||
assert(/title="[^"]*TRACE[^"]*"/.test(m[1]),
|
||||
'col-hashsize <td> must carry title mentioning TRACE');
|
||||
});
|
||||
|
||||
test('buildGroupRowHtml: non-TRACE header row still numeric', () => {
|
||||
const p = {
|
||||
hash: 'grp1', count: 1, latest: '',
|
||||
observer_id: null, raw_hex: '00000000000000000000C100',
|
||||
payload_type: 0, route_type: 0, decoded_json: '{}', path_json: '[]',
|
||||
observation_count: 1, observer_count: 1,
|
||||
};
|
||||
const html = api.buildGroupRowHtml(p);
|
||||
const m = html.match(/<td class="col-hashsize[^"]*"[^>]*>([^<]*)<\/td>/);
|
||||
assert(m, 'col-hashsize cell must be present');
|
||||
assert(/^\d+$/.test(m[1].trim()),
|
||||
'non-TRACE group col-hashsize must be numeric, got: ' + JSON.stringify(m[1]));
|
||||
});
|
||||
}
|
||||
|
||||
console.log('');
|
||||
if (failed > 0) {
|
||||
console.error(`❌ ${failed} test(s) failed, ${passed} passed`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✅ All ${passed} tests passed`);
|
||||
Reference in New Issue
Block a user