mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-28 05:41:38 +00:00
## Summary Phase 2 of #979 — overlay per-hop relay SNR onto the Traces page path graph for TRACE-type packets. When the viewed packet is a firmware TRACE and `decoded.snrValues` is non-empty, each hop edge in the existing path graph gets a small `<text class="hop-snr">` label at its midpoint with the corresponding numeric SNR value (Tufte: numeric overlay only — edge color encodes observer attribution, thickness encodes count; per triage, do **not** double-encode). Non-TRACE packets render unchanged. Observer-level SNR in the timeline is unaffected (different concept: observer receive SNR vs relay hop SNR). ## TDD - **Red commit:** `8d441aa51e4b38dec962c7a32d31e9f7080f2786` — adds 4 assertions in `test-traces.js` against the (not-yet-emitted) `<text class="hop-snr">` element. CI run: see Actions on this PR. - **Green commit:** implements the SNR-label emission in `renderPathGraph` (`public/traces.js`). ## Test `test-traces.js` asserts: - TRACE + non-empty `snrValues` → `<text class="hop-snr">` labels render with the numeric values - non-TRACE → labels absent (regression gate for AC2) - TRACE + empty `snrValues` → labels absent - `decoded` omitted → labels absent (back-compat) Fixes #1004 --------- Co-authored-by: corescope-bot <bot@corescope.local> Co-authored-by: clawbot <bot@openclaw.local>
This commit is contained in:
@@ -138,6 +138,7 @@ jobs:
|
||||
node test-issue-1619-feed-detail-card-draggable.js
|
||||
node test-xss-escape-sinks.js
|
||||
node test-preflight-xss-gate.js
|
||||
node test-traces.js
|
||||
|
||||
- name: 🛡️ Preflight XSS gate — actual --diff check (PR only)
|
||||
# The fixture self-test above (test-preflight-xss-gate.js) only
|
||||
|
||||
+26
-3
@@ -147,12 +147,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${allPaths.length > 0 ? renderPathGraph(allPaths, allPathsRaw) : ''}
|
||||
${allPaths.length > 0 ? renderPathGraph(allPaths, allPathsRaw, decoded) : ''}
|
||||
${traceData.length > 0 ? renderTimeline(t0, spreadMs) : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderPathGraph(allPaths, allPathsRaw) {
|
||||
function renderPathGraph(allPaths, allPathsRaw, decoded) {
|
||||
// Collect unique nodes and edges across all observed paths
|
||||
const nodeSet = new Set();
|
||||
const edgeMap = new Map(); // "from→to" => Set of observer labels
|
||||
@@ -261,6 +261,29 @@
|
||||
}
|
||||
|
||||
let nodesSvg = '';
|
||||
// Per-hop SNR overlay: TRACE packets only, numeric labels at hop midpoints
|
||||
// (Tufte: no double-encoding via color/thickness — number is the signal).
|
||||
const snrValues = (decoded && decoded.type === 'TRACE' && Array.isArray(decoded.snrValues))
|
||||
? decoded.snrValues : null;
|
||||
if (snrValues && snrValues.length > 0) {
|
||||
// Build the hop chain from the first observed full path (Origin → hops → Dest).
|
||||
const firstPath = allPaths[0];
|
||||
if (firstPath && Array.isArray(firstPath.hops)) {
|
||||
const chain = ['Origin', ...firstPath.hops, 'Dest'];
|
||||
const hopCount = Math.min(snrValues.length, chain.length - 1);
|
||||
for (let i = 0; i < hopCount; i++) {
|
||||
const p1 = nodePos.get(chain[i]);
|
||||
const p2 = nodePos.get(chain[i + 1]);
|
||||
if (!p1 || !p2) continue;
|
||||
const v = snrValues[i];
|
||||
if (v == null || !Number.isFinite(Number(v))) continue;
|
||||
const mx = (p1.x + p2.x) / 2;
|
||||
const my = (p1.y + p2.y) / 2 - 6;
|
||||
const label = Number(v).toFixed(1);
|
||||
nodesSvg += `<text class="hop-snr" x="${mx}" y="${my}" text-anchor="middle" font-size="10" font-weight="600" fill="var(--text, #111827)">${label}</text>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [node, pos] of nodePos) {
|
||||
const isEndpoint = node === 'Origin' || node === 'Dest';
|
||||
const r = isEndpoint ? 18 : 16;
|
||||
@@ -335,7 +358,7 @@
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.TracesHelpers = { dedupePrefixPaths };
|
||||
window.TracesHelpers = { dedupePrefixPaths, renderPathGraph };
|
||||
}
|
||||
|
||||
registerPage('traces', { init, destroy });
|
||||
|
||||
@@ -119,6 +119,45 @@ console.log('\n=== traces.js: dedupePrefixPaths ===');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== renderPathGraph: per-hop SNR overlay (#1004 Phase 2 of #979) =====
|
||||
console.log('\n=== traces.js: renderPathGraph hop-SNR overlay ===');
|
||||
{
|
||||
const ctx = makeSandbox();
|
||||
loadTracesJs(ctx);
|
||||
const { renderPathGraph } = ctx.TracesHelpers;
|
||||
assert.strictEqual(typeof renderPathGraph, 'function', 'renderPathGraph must be exported');
|
||||
|
||||
const paths = [{ hops: ['R1', 'R2'], observer: 'OBS' }];
|
||||
const decodedTrace = { type: 'TRACE', snrValues: [-3.5, -7.0, -12.25] };
|
||||
const decodedNonTrace = { type: 'CHAN', snrValues: [-3.5, -7.0] };
|
||||
|
||||
test('TRACE with snrValues emits <text class="hop-snr"> labels with values', () => {
|
||||
const html = renderPathGraph(paths, paths, decodedTrace);
|
||||
assert.ok(/class="hop-snr"/.test(html), 'expected <text class="hop-snr"> in output');
|
||||
// Each numeric value should appear in a hop-snr label.
|
||||
const labels = html.match(/<text[^>]*class="hop-snr"[^>]*>([^<]+)<\/text>/g) || [];
|
||||
assert.ok(labels.length >= 1, 'expected at least one hop-snr label');
|
||||
const joined = labels.join(' ');
|
||||
assert.ok(/-3\.5/.test(joined), 'expected -3.5 in hop-snr label, got: ' + joined);
|
||||
assert.ok(/-7(\.0)?/.test(joined), 'expected -7 in hop-snr label, got: ' + joined);
|
||||
});
|
||||
|
||||
test('non-TRACE packet: hop-snr labels are ABSENT even when snrValues present', () => {
|
||||
const html = renderPathGraph(paths, paths, decodedNonTrace);
|
||||
assert.ok(!/class="hop-snr"/.test(html), 'hop-snr must not render for non-TRACE');
|
||||
});
|
||||
|
||||
test('TRACE with empty snrValues: no hop-snr labels', () => {
|
||||
const html = renderPathGraph(paths, paths, { type: 'TRACE', snrValues: [] });
|
||||
assert.ok(!/class="hop-snr"/.test(html), 'hop-snr must not render when snrValues empty');
|
||||
});
|
||||
|
||||
test('decoded omitted: no hop-snr labels (back-compat)', () => {
|
||||
const html = renderPathGraph(paths, paths);
|
||||
assert.ok(!/class="hop-snr"/.test(html), 'hop-snr must not render when decoded omitted');
|
||||
});
|
||||
}
|
||||
|
||||
// ===== SUMMARY =====
|
||||
console.log(`\n${'═'.repeat(40)}`);
|
||||
console.log(` traces.js: ${passed} passed, ${failed} failed`);
|
||||
|
||||
Reference in New Issue
Block a user