/* === CoreScope — compare.js === */ /* Observer packet comparison — Fixes #129 */ 'use strict'; /** * Compare two sets of packet hashes using Set operations. * Returns { onlyA, onlyB, both } as arrays of hashes. * O(n) via Set lookups — no nested loops. */ function comparePacketSets(hashesA, hashesB) { var setA = hashesA instanceof Set ? hashesA : new Set(hashesA || []); var setB = hashesB instanceof Set ? hashesB : new Set(hashesB || []); var onlyA = []; var onlyB = []; var both = []; setA.forEach(function (h) { if (setB.has(h)) both.push(h); else onlyA.push(h); }); setB.forEach(function (h) { if (!setA.has(h)) onlyB.push(h); }); return { onlyA: onlyA, onlyB: onlyB, both: both }; } /** * Filter packets by route type. * mode: 'all' | 'flood' | 'direct' * Flood = route_type 0 (TransportFlood) or 1 (Flood) * Direct = route_type 2 (Direct) or 3 (TransportDirect) */ function filterPacketsByRoute(packets, mode) { if (!packets || mode === 'all') return packets || []; if (mode === 'flood') { return packets.filter(function (p) { return p.route_type === 0 || p.route_type === 1; }); } if (mode === 'direct') { return packets.filter(function (p) { return p.route_type === 2 || p.route_type === 3; }); } return packets; } /** * Compute asymmetric overlap statistics between two observer packet sets. * Given a comparePacketSets() result, returns: * - totalA / totalB: unique packet count for each observer * - shared: packets seen by both * - onlyA / onlyB: exclusive packet counts * - aSeesOfB: percentage of B's packets that A also saw (rounded to 0.1%) * - bSeesOfA: percentage of A's packets that B also saw (rounded to 0.1%) * Returns 0% (not NaN) when a denominator is zero. */ function computeOverlapStats(cmp) { var onlyA = (cmp && cmp.onlyA && cmp.onlyA.length) || 0; var onlyB = (cmp && cmp.onlyB && cmp.onlyB.length) || 0; var shared = (cmp && cmp.both && cmp.both.length) || 0; var totalA = onlyA + shared; var totalB = onlyB + shared; var aSeesOfB = totalB > 0 ? Math.round((shared / totalB) * 1000) / 10 : 0; var bSeesOfA = totalA > 0 ? Math.round((shared / totalA) * 1000) / 10 : 0; return { totalA: totalA, totalB: totalB, shared: shared, onlyA: onlyA, onlyB: onlyB, aSeesOfB: aSeesOfB, bSeesOfA: bSeesOfA, }; } // Expose for testing if (typeof window !== 'undefined') { window.comparePacketSets = comparePacketSets; window.filterPacketsByRoute = filterPacketsByRoute; window.computeOverlapStats = computeOverlapStats; } (function () { var PAYLOAD_LABELS = { 0: 'Request', 1: 'Response', 2: 'Direct Msg', 3: 'ACK', 4: 'Advert', 5: 'Channel Msg', 7: 'Anon Req', 8: 'Path', 9: 'Trace', 11: 'Control' }; var MAX_PACKETS = 10000; var observers = []; var selA = null; var selB = null; var comparisonResult = null; var packetsA = []; var packetsB = []; var currentView = 'summary'; var routeFilter = 'all'; function init(app, routeParam) { // Parse preselected observers from URL: #/compare?a=ID1&b=ID2 var hashParams = location.hash.split('?')[1] || ''; var params = new URLSearchParams(hashParams); selA = params.get('a') || null; selB = params.get('b') || null; comparisonResult = null; packetsA = []; packetsB = []; currentView = 'summary'; routeFilter = 'all'; app.innerHTML = '
In the last 24 hours, ' + nameA + ' saw ' + (r.onlyA.length + r.both.length).toLocaleString() + ' unique packets ' + 'and ' + nameB + ' saw ' + (r.onlyB.length + r.both.length).toLocaleString() + ' unique packets. ' + '' + r.both.length.toLocaleString() + ' (' + overlap + '%) were seen by both observers.
' + (r.both.length === 0 && total > 0 ? '\u26A0\uFE0F These observers share no packets \u2014 they may be on different frequencies or too far apart.
' : '') + (r.onlyA.length === 0 && r.onlyB.length === 0 && r.both.length > 0 ? '\u2705 Perfect overlap \u2014 both observers see the same packets.
' : '') + '| Hash | Time | Type | Observer | ' + '
|---|---|---|---|
| ' + escapeHtml(h.substring(0, 12)) + ' | ' + '' + timeAgo(p.timestamp || p.first_seen) + ' | ' + '' + escapeHtml(typeName) + ' | ' + '' + obsLabel + ' | ' + '