Enhance path handling in BotDataViewer and ModernContactsManager

- Added support for bytes per hop in the BotDataViewer, allowing for better path data representation.
- Updated the contacts template to display bytes per hop and adjusted path formatting based on this value.
- Improved the decode path functionality to utilize the correct bytes per hop for decoding paths, enhancing overall path handling and user experience.
This commit is contained in:
agessaman
2026-03-06 10:02:45 -08:00
parent 7d76ed443b
commit a4d1f678cf
2 changed files with 43 additions and 18 deletions

View File

@@ -3569,6 +3569,7 @@ class BotDataViewer:
MAX(c.last_advert_timestamp) as last_message,
GROUP_CONCAT(op.path_hex, '|||') as all_paths_hex,
GROUP_CONCAT(op.path_length, '|||') as all_paths_length,
GROUP_CONCAT(COALESCE(op.bytes_per_hop, 1), '|||') as all_paths_bytes_per_hop,
GROUP_CONCAT(op.observation_count, '|||') as all_paths_observations,
GROUP_CONCAT(op.last_seen, '|||') as all_paths_last_seen
FROM complete_contact_tracking c
@@ -3606,14 +3607,24 @@ class BotDataViewer:
if row['all_paths_hex']:
paths_hex = row['all_paths_hex'].split('|||')
paths_length = row['all_paths_length'].split('|||') if row['all_paths_length'] else []
paths_bph = row['all_paths_bytes_per_hop'].split('|||') if row.get('all_paths_bytes_per_hop') else []
paths_observations = row['all_paths_observations'].split('|||') if row['all_paths_observations'] else []
paths_last_seen = row['all_paths_last_seen'].split('|||') if row['all_paths_last_seen'] else []
for i, path_hex in enumerate(paths_hex):
if path_hex: # Skip empty strings
bph = None
if i < len(paths_bph) and paths_bph[i]:
try:
bph = int(paths_bph[i])
if bph not in (1, 2, 3):
bph = 1
except (TypeError, ValueError):
bph = 1
all_paths.append({
'path_hex': path_hex,
'path_length': int(paths_length[i]) if i < len(paths_length) and paths_length[i] else 0,
'bytes_per_hop': bph,
'observation_count': int(paths_observations[i]) if i < len(paths_observations) and paths_observations[i] else 1,
'last_seen': paths_last_seen[i] if i < len(paths_last_seen) and paths_last_seen[i] else None
})

View File

@@ -982,10 +982,12 @@ class ModernContactsManager {
// Create a unique ID for this tooltip
const tooltipId = `hops-tooltip-${contact.user_id.substring(0, 16).replace(/[^a-zA-Z0-9]/g, '_')}`;
const pathCountText = hasMultiplePaths ? ` <small>(${allPaths.length} paths)</small>` : '';
const bytesPerHop = contact.out_bytes_per_hop != null && [1, 2, 3].includes(Number(contact.out_bytes_per_hop)) ? Number(contact.out_bytes_per_hop) : '';
return `<span class="badge bg-primary path-badge"
id="${tooltipId}"
data-path-hex="${escapedPath}"
data-path-len="${outPathLen}"
data-bytes-per-hop="${bytesPerHop}"
data-user-id="${escapedUserId}"
onclick="contactsManager.showPathsModal('${escapedUserId}')"
style="cursor: pointer; position: relative;"
@@ -1073,16 +1075,18 @@ class ModernContactsManager {
try {
badge.dataset.loading = 'true';
const body = { path_hex: pathHex };
const bytesPerHop = badge.dataset.bytesPerHop;
if (bytesPerHop && ['1', '2', '3'].includes(bytesPerHop)) {
body.bytes_per_hop = parseInt(bytesPerHop, 10);
}
// Fetch decoded path from API
const response = await fetch('/api/decode-path', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
path_hex: pathHex
})
body: JSON.stringify(body)
});
const data = await response.json();
@@ -1139,11 +1143,15 @@ class ModernContactsManager {
});
}
formatPathHex(pathHex) {
// Break hex string into two-character chunks with spaces for readability
formatPathHex(pathHex, bytesPerHop) {
// Break hex string into chunks for readability. bytesPerHop 1,2,3 → 2,4,6 hex chars per node.
if (!pathHex) return '';
// Match every 1-2 characters (handles odd-length strings gracefully)
return pathHex.match(/.{1,2}/g)?.join(' ') || pathHex;
const n = (bytesPerHop && [1, 2, 3].includes(Number(bytesPerHop))) ? (Number(bytesPerHop) * 2) : 2;
const chunks = [];
for (let i = 0; i < pathHex.length; i += n) {
chunks.push(pathHex.slice(i, i + n));
}
return chunks.join(' ') || pathHex;
}
sortPaths(paths, sortOrder) {
@@ -1197,20 +1205,23 @@ class ModernContactsManager {
const path = paths[i];
const isPrimary = path.path_hex === primaryPath;
const primaryBadge = isPrimary ? '<span class="badge bg-success ms-2">Primary</span>' : '';
const formattedPathHex = this.formatPathHex(path.path_hex);
const bph = path.bytes_per_hop != null && [1, 2, 3].includes(Number(path.bytes_per_hop)) ? Number(path.bytes_per_hop) : null;
const formattedPathHex = this.formatPathHex(path.path_hex, bph);
const hopLabel = bph && path.path_length > 0 ? `${Math.floor(path.path_length / bph)} hops, ` : '';
const bytesPerHopAttr = bph != null ? ` data-path-bph="${bph}"` : '';
pathsHtml += `
<div class="list-group-item path-list-item ${isPrimary ? 'border-success' : ''}" id="path-item-${i}">
<div class="list-group-item path-list-item ${isPrimary ? 'border-success' : ''}" id="path-item-${i}"${bytesPerHopAttr}>
<div class="d-flex justify-content-between align-items-start">
<div class="flex-grow-1">
<h6 class="mb-1">
Path ${i + 1} ${primaryBadge}
<small class="text-muted">(${path.path_length} bytes, ${path.observation_count} observations)</small>
<small class="text-muted">(${hopLabel}${path.path_length} bytes, ${path.observation_count} observations)</small>
</h6>
<p class="mb-1"><code class="path-hex-code">${this.escapeHtml(formattedPathHex)}</code></p>
<small class="text-muted">Last seen: ${this.formatTimeAgo(path.last_seen)}</small>
</div>
<button class="btn btn-sm btn-outline-primary" onclick="contactsManager.decodePathInModal('${path.path_hex}', ${i})">
<button class="btn btn-sm btn-outline-primary" onclick="contactsManager.decodePathInModal('${String(path.path_hex).replace(/\\/g, '\\\\').replace(/'/g, "\\'")}', ${i}, ${bph != null ? bph : 'null'})">
<i class="fas fa-code"></i> Decode
</button>
</div>
@@ -1321,9 +1332,10 @@ class ModernContactsManager {
});
}
async decodePathInModal(pathHex, pathIndex) {
async decodePathInModal(pathHex, pathIndex, pathBytesPerHop) {
const decodedDiv = document.getElementById(`decoded-path-${pathIndex}`);
const button = document.querySelector(`#path-item-${pathIndex} button`);
const pathItem = document.getElementById(`path-item-${pathIndex}`);
const button = pathItem ? pathItem.querySelector('button') : null;
if (!decodedDiv || !button) return;
@@ -1335,11 +1347,13 @@ class ModernContactsManager {
decodedDiv.innerHTML = '<div class="text-muted"><i class="fas fa-spinner fa-spin"></i> Decoding path...</div>';
try {
// Use contact's out_bytes_per_hop when path came from a packet with multi-byte hops
const bytesPerHop = this.currentModalContact?.out_bytes_per_hop || null;
// Prefer this path's bytes_per_hop; fall back to contact's out_bytes_per_hop
const bytesPerHop = (pathBytesPerHop != null && [1, 2, 3].includes(Number(pathBytesPerHop)))
? Number(pathBytesPerHop)
: (this.currentModalContact?.out_bytes_per_hop != null && [1, 2, 3].includes(Number(this.currentModalContact.out_bytes_per_hop)) ? Number(this.currentModalContact.out_bytes_per_hop) : null);
const body = { path_hex: pathHex };
if (bytesPerHop != null && [1, 2, 3].includes(Number(bytesPerHop))) {
body.bytes_per_hop = Number(bytesPerHop);
if (bytesPerHop != null) {
body.bytes_per_hop = bytesPerHop;
}
const response = await fetch('/api/decode-path', {
method: 'POST',