mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-10-25 02:52:26 +00:00
Geographic hop disambiguation for 1-byte path prefixes
When multiple nodes match a 1-byte hop prefix, use geographic context (observer's typical nodes + other resolved hops) to pick the closest match. Shows ⚠ indicator on ambiguous hops with tooltip listing all candidates. Hover to see which nodes collide on that prefix.
This commit is contained in:
1 parent
72f540db86
commit
17524fa3db
3 files changed
+92
-13
No files matched your search
+11
-6
@@ -64,11 +64,15 @@
|
||||
}
|
||||
|
||||
function renderHop(h) {
|
||||
const name = hopNameCache[h];
|
||||
const entry = hopNameCache[h];
|
||||
const name = entry ? (typeof entry === 'string' ? entry : entry.name) : null;
|
||||
const pubkey = entry?.pubkey || h;
|
||||
const ambiguous = entry?.ambiguous || false;
|
||||
const display = name ? escapeHtml(name) : h;
|
||||
const pubkey = name ? Object.entries(hopNameCache).find(([k,v]) => v === name)?.[0] || h : h;
|
||||
// Try to find full pubkey from nodeData
|
||||
return `<a class="hop hop-link ${name ? 'hop-named' : ''}" href="#/nodes/${encodeURIComponent(h)}" title="${h}" onclick="event.stopPropagation()">${display}</a>`;
|
||||
const title = ambiguous
|
||||
? `${h} — ⚠ ${entry.candidates.length} matches: ${entry.candidates.map(c => c.name).join(', ')}`
|
||||
: h;
|
||||
return `<a class="hop hop-link ${name ? 'hop-named' : ''} ${ambiguous ? 'hop-ambiguous' : ''}" href="#/nodes/${encodeURIComponent(pubkey)}" title="${title}" onclick="event.stopPropagation()">${display}${ambiguous ? '<span class="hop-warn">⚠</span>' : ''}</a>`;
|
||||
}
|
||||
|
||||
function renderPath(hops) {
|
||||
@@ -555,8 +559,9 @@
|
||||
const pathByte = parseInt(buf.slice(2, 4), 16);
|
||||
const hashSize = (pathByte >> 6) + 1;
|
||||
for (let i = 0; i < pathHops.length; i++) {
|
||||
const hopName = hopNameCache[pathHops[i]];
|
||||
const label = hopName ? `Hop ${i} — ${escapeHtml(hopName)}` : `Hop ${i}`;
|
||||
const hopEntry = hopNameCache[pathHops[i]];
|
||||
const hopName = hopEntry ? (typeof hopEntry === 'string' ? hopEntry : hopEntry.name) : null;
|
||||
const label = hopName ? `Hop ${i} — ${escapeHtml(hopName)}${hopEntry?.ambiguous ? ' ⚠' : ''}` : `Hop ${i}`;
|
||||
rows += fieldRow(off + i * hashSize, label, pathHops[i], hopName ? hopName : '');
|
||||
}
|
||||
off += hashSize * pathHops.length;
|
||||
|
||||
@@ -1142,3 +1142,7 @@ button.ch-item.ch-item-encrypted .ch-badge { filter: grayscale(0.6); }
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Ambiguous hop indicator */
|
||||
.hop-ambiguous { border-bottom: 1px dashed #f59e0b; }
|
||||
.hop-warn { font-size: 0.7em; margin-left: 2px; vertical-align: super; }
|
||||
@@ -566,11 +566,23 @@ app.get('/api/analytics/rf', (req, res) => {
|
||||
// --- Topology Analytics ---
|
||||
app.get('/api/analytics/topology', (req, res) => {
|
||||
const packets = db.db.prepare(`SELECT path_json, snr, decoded_json, observer_id FROM packets WHERE path_json IS NOT NULL AND path_json != '[]'`).all();
|
||||
const allNodes = db.db.prepare('SELECT public_key, name FROM nodes WHERE name IS NOT NULL').all();
|
||||
const resolveHop = hop => {
|
||||
const allNodes = db.db.prepare('SELECT public_key, name, lat, lon FROM nodes WHERE name IS NOT NULL').all();
|
||||
const resolveHop = (hop, contextPositions) => {
|
||||
const h = hop.toLowerCase();
|
||||
const m = allNodes.find(n => n.public_key.toLowerCase().startsWith(h));
|
||||
return m ? { name: m.name, pubkey: m.public_key } : null;
|
||||
const candidates = allNodes.filter(n => n.public_key.toLowerCase().startsWith(h));
|
||||
if (candidates.length === 0) return null;
|
||||
if (candidates.length === 1) return { name: candidates[0].name, pubkey: candidates[0].public_key };
|
||||
// Disambiguate by proximity to context positions
|
||||
if (contextPositions && contextPositions.length > 0) {
|
||||
const cLat = contextPositions.reduce((s, p) => s + p.lat, 0) / contextPositions.length;
|
||||
const cLon = contextPositions.reduce((s, p) => s + p.lon, 0) / contextPositions.length;
|
||||
const withLoc = candidates.filter(c => c.lat && c.lon && !(c.lat === 0 && c.lon === 0));
|
||||
if (withLoc.length) {
|
||||
withLoc.sort((a, b) => Math.hypot(a.lat - cLat, a.lon - cLon) - Math.hypot(b.lat - cLat, b.lon - cLon));
|
||||
return { name: withLoc[0].name, pubkey: withLoc[0].public_key };
|
||||
}
|
||||
}
|
||||
return { name: candidates[0].name, pubkey: candidates[0].public_key };
|
||||
};
|
||||
|
||||
// Hop distribution
|
||||
@@ -866,14 +878,72 @@ app.get('/api/analytics/hash-sizes', (req, res) => {
|
||||
// Resolve path hop hex prefixes to node names
|
||||
app.get('/api/resolve-hops', (req, res) => {
|
||||
const hops = (req.query.hops || '').split(',').filter(Boolean);
|
||||
const observerId = req.query.observer || null;
|
||||
if (!hops.length) return res.json({ resolved: {} });
|
||||
const allNodes = db.db.prepare('SELECT public_key, name FROM nodes WHERE name IS NOT NULL').all();
|
||||
|
||||
const allNodes = db.db.prepare('SELECT public_key, name, lat, lon FROM nodes WHERE name IS NOT NULL').all();
|
||||
|
||||
// Build observer geographic center from nodes it typically hears
|
||||
let observerLat = null, observerLon = null;
|
||||
if (observerId) {
|
||||
const obsNodes = db.db.prepare(`
|
||||
SELECT DISTINCT n.lat, n.lon FROM packets p
|
||||
JOIN nodes n ON p.decoded_json LIKE '%' || n.public_key || '%'
|
||||
WHERE p.observer_id = ? AND n.lat IS NOT NULL AND n.lat != 0 AND n.lon != 0
|
||||
LIMIT 50
|
||||
`).all(observerId);
|
||||
if (obsNodes.length) {
|
||||
observerLat = obsNodes.reduce((s, n) => s + n.lat, 0) / obsNodes.length;
|
||||
observerLon = obsNodes.reduce((s, n) => s + n.lon, 0) / obsNodes.length;
|
||||
}
|
||||
}
|
||||
|
||||
const resolved = {};
|
||||
// First pass: find all candidates for each hop
|
||||
for (const hop of hops) {
|
||||
const hopLower = hop.toLowerCase();
|
||||
const match = allNodes.find(n => n.public_key.toLowerCase().startsWith(hopLower));
|
||||
if (match) resolved[hop] = match.name;
|
||||
const candidates = allNodes.filter(n => n.public_key.toLowerCase().startsWith(hopLower));
|
||||
if (candidates.length === 0) {
|
||||
resolved[hop] = { name: null, candidates: [] };
|
||||
} else if (candidates.length === 1) {
|
||||
resolved[hop] = { name: candidates[0].name, pubkey: candidates[0].public_key, candidates: [{ name: candidates[0].name, pubkey: candidates[0].public_key }] };
|
||||
} else {
|
||||
resolved[hop] = { name: candidates[0].name, pubkey: candidates[0].public_key, ambiguous: true, candidates: candidates.map(c => ({ name: c.name, pubkey: c.public_key, lat: c.lat, lon: c.lon })) };
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: disambiguate using geographic context
|
||||
// Collect known positions from unambiguous hops
|
||||
const knownPositions = [];
|
||||
for (const hop of hops) {
|
||||
const r = resolved[hop];
|
||||
if (r && !r.ambiguous && r.pubkey) {
|
||||
const node = allNodes.find(n => n.public_key === r.pubkey);
|
||||
if (node && node.lat && node.lon && !(node.lat === 0 && node.lon === 0)) {
|
||||
knownPositions.push({ lat: node.lat, lon: node.lon });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (observerLat != null) knownPositions.push({ lat: observerLat, lon: observerLon });
|
||||
|
||||
if (knownPositions.length > 0) {
|
||||
const centerLat = knownPositions.reduce((s, p) => s + p.lat, 0) / knownPositions.length;
|
||||
const centerLon = knownPositions.reduce((s, p) => s + p.lon, 0) / knownPositions.length;
|
||||
const dist = (lat, lon) => Math.sqrt((lat - centerLat) ** 2 + (lon - centerLon) ** 2);
|
||||
|
||||
for (const hop of hops) {
|
||||
const r = resolved[hop];
|
||||
if (r && r.ambiguous) {
|
||||
const withLoc = r.candidates.filter(c => c.lat && c.lon && !(c.lat === 0 && c.lon === 0));
|
||||
if (withLoc.length > 0) {
|
||||
withLoc.sort((a, b) => dist(a.lat, a.lon) - dist(b.lat, b.lon));
|
||||
r.name = withLoc[0].name;
|
||||
r.pubkey = withLoc[0].pubkey;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ resolved });
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user