mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-14 17:55:08 +00:00
Fix region filtering in Route Patterns, Nodes, and Network Status tabs
- Add RegionFilter.regionQueryString() to all API calls in renderSubpaths and renderNodesTab - Add region filtering to /api/analytics/subpaths (filter packets by regional observer hashes) - Add region filtering to /api/nodes/bulk-health (filter nodes by regional presence) - Add region filtering to /api/nodes/network-status (filter node counts by region) - Add region param to nodes lookup in hash collision tab - Update cache keys to include region param for proper cache separation
This commit is contained in:
+10
-8
@@ -772,7 +772,7 @@
|
||||
</div>
|
||||
`;
|
||||
let allNodes = [];
|
||||
try { const nd = await api('/nodes?limit=2000', { ttl: CLIENT_TTL.nodeList }); allNodes = nd.nodes || []; } catch {}
|
||||
try { const nd = await api('/nodes?limit=2000' + RegionFilter.regionQueryString(), { ttl: CLIENT_TTL.nodeList }); allNodes = nd.nodes || []; } catch {}
|
||||
renderHashMatrix(data.topHops, allNodes);
|
||||
renderCollisions(data.topHops, allNodes);
|
||||
}
|
||||
@@ -962,11 +962,12 @@
|
||||
async function renderSubpaths(el) {
|
||||
el.innerHTML = '<div class="text-center text-muted" style="padding:40px">Analyzing route patterns…</div>';
|
||||
try {
|
||||
const rq = RegionFilter.regionQueryString();
|
||||
const [d2, d3, d4, d5] = await Promise.all([
|
||||
api('/analytics/subpaths?minLen=2&maxLen=2&limit=50', { ttl: CLIENT_TTL.analyticsRF }),
|
||||
api('/analytics/subpaths?minLen=3&maxLen=3&limit=30', { ttl: CLIENT_TTL.analyticsRF }),
|
||||
api('/analytics/subpaths?minLen=4&maxLen=4&limit=20', { ttl: CLIENT_TTL.analyticsRF }),
|
||||
api('/analytics/subpaths?minLen=5&maxLen=8&limit=15', { ttl: CLIENT_TTL.analyticsRF })
|
||||
api('/analytics/subpaths?minLen=2&maxLen=2&limit=50' + rq, { ttl: CLIENT_TTL.analyticsRF }),
|
||||
api('/analytics/subpaths?minLen=3&maxLen=3&limit=30' + rq, { ttl: CLIENT_TTL.analyticsRF }),
|
||||
api('/analytics/subpaths?minLen=4&maxLen=4&limit=20' + rq, { ttl: CLIENT_TTL.analyticsRF }),
|
||||
api('/analytics/subpaths?minLen=5&maxLen=8&limit=15' + rq, { ttl: CLIENT_TTL.analyticsRF })
|
||||
]);
|
||||
|
||||
function renderTable(data, title) {
|
||||
@@ -1165,10 +1166,11 @@
|
||||
async function renderNodesTab(el) {
|
||||
el.innerHTML = '<div style="padding:40px;text-align:center;color:var(--text-muted)">Loading node analytics…</div>';
|
||||
try {
|
||||
const rq = RegionFilter.regionQueryString();
|
||||
const [nodesResp, bulkHealth, netStatus] = await Promise.all([
|
||||
api('/nodes?limit=200&sortBy=lastSeen', { ttl: CLIENT_TTL.nodeList }),
|
||||
api('/nodes/bulk-health?limit=50', { ttl: CLIENT_TTL.analyticsRF }),
|
||||
api('/nodes/network-status', { ttl: CLIENT_TTL.analyticsRF })
|
||||
api('/nodes?limit=200&sortBy=lastSeen' + rq, { ttl: CLIENT_TTL.nodeList }),
|
||||
api('/nodes/bulk-health?limit=50' + rq, { ttl: CLIENT_TTL.analyticsRF }),
|
||||
api('/nodes/network-status' + rq, { ttl: CLIENT_TTL.analyticsRF })
|
||||
]);
|
||||
const nodes = nodesResp.nodes || nodesResp;
|
||||
const myNodes = JSON.parse(localStorage.getItem('meshcore-my-nodes') || '[]');
|
||||
|
||||
+1
-1
@@ -88,7 +88,7 @@
|
||||
<script src="channels.js?v=1774075538" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774290000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774350000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774335600" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774353600" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774072222" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774290000" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774028201" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
|
||||
@@ -1131,10 +1131,32 @@ app.get('/api/nodes/search', (req, res) => {
|
||||
// Bulk health summary for analytics — single query approach (MUST be before :pubkey routes)
|
||||
app.get('/api/nodes/bulk-health', (req, res) => {
|
||||
const limit = Math.min(Number(req.query.limit) || 50, 200);
|
||||
const _ck = 'bulk-health:' + limit;
|
||||
const regionKey = req.query.region || '';
|
||||
const _ck = 'bulk-health:' + limit + ':r=' + regionKey;
|
||||
const _c = cache.get(_ck); if (_c) return res.json(_c);
|
||||
|
||||
const nodes = db.db.prepare(`SELECT * FROM nodes ORDER BY last_seen DESC LIMIT ?`).all(limit);
|
||||
// Region filtering
|
||||
const regionObsIds = getObserverIdsForRegions(req.query.region);
|
||||
let regionNodeKeys = null;
|
||||
let regionalHashes = null;
|
||||
if (regionObsIds) {
|
||||
regionalHashes = new Set();
|
||||
for (const obsId of regionObsIds) {
|
||||
const obs = pktStore.byObserver.get(obsId);
|
||||
if (obs) for (const o of obs) regionalHashes.add(o.hash);
|
||||
}
|
||||
regionNodeKeys = new Set();
|
||||
for (const [pubkey, hashes] of pktStore._nodeHashIndex) {
|
||||
for (const h of hashes) {
|
||||
if (regionalHashes.has(h)) { regionNodeKeys.add(pubkey); break; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let nodes = db.db.prepare(`SELECT * FROM nodes ORDER BY last_seen DESC LIMIT ?`).all(regionNodeKeys ? 500 : limit);
|
||||
if (regionNodeKeys) {
|
||||
nodes = nodes.filter(n => regionNodeKeys.has(n.public_key)).slice(0, limit);
|
||||
}
|
||||
if (nodes.length === 0) { cache.set(_ck, [], TTL.bulkHealth); return res.json([]); }
|
||||
|
||||
const todayStart = new Date();
|
||||
@@ -1192,7 +1214,25 @@ app.get('/api/nodes/bulk-health', (req, res) => {
|
||||
|
||||
app.get('/api/nodes/network-status', (req, res) => {
|
||||
const now = Date.now();
|
||||
const allNodes = db.db.prepare('SELECT public_key, name, role, last_seen FROM nodes').all();
|
||||
let allNodes = db.db.prepare('SELECT public_key, name, role, last_seen FROM nodes').all();
|
||||
|
||||
// Region filtering
|
||||
const regionObsIds = getObserverIdsForRegions(req.query.region);
|
||||
if (regionObsIds) {
|
||||
const regionalHashes = new Set();
|
||||
for (const obsId of regionObsIds) {
|
||||
const obs = pktStore.byObserver.get(obsId);
|
||||
if (obs) for (const o of obs) regionalHashes.add(o.hash);
|
||||
}
|
||||
const regionNodeKeys = new Set();
|
||||
for (const [pubkey, hashes] of pktStore._nodeHashIndex) {
|
||||
for (const h of hashes) {
|
||||
if (regionalHashes.has(h)) { regionNodeKeys.add(pubkey); break; }
|
||||
}
|
||||
}
|
||||
allNodes = allNodes.filter(n => regionNodeKeys.has(n.public_key));
|
||||
}
|
||||
|
||||
let active = 0, degraded = 0, silent = 0;
|
||||
const roleCounts = {};
|
||||
allNodes.forEach(n => {
|
||||
@@ -2409,13 +2449,60 @@ function computeAllSubpaths() {
|
||||
|
||||
// Subpath frequency analysis — reads from pre-computed master
|
||||
app.get('/api/analytics/subpaths', (req, res) => {
|
||||
const _ck = 'analytics:subpaths:' + (req.query.minLen||2) + ':' + (req.query.maxLen||8) + ':' + (req.query.limit||100);
|
||||
const regionKey = req.query.region || '';
|
||||
const _ck = 'analytics:subpaths:' + (req.query.minLen||2) + ':' + (req.query.maxLen||8) + ':' + (req.query.limit||100) + ':r=' + regionKey;
|
||||
const _c = cache.get(_ck); if (_c) return res.json(_c);
|
||||
|
||||
const minLen = Math.max(2, Number(req.query.minLen) || 2);
|
||||
const maxLen = Number(req.query.maxLen) || 8;
|
||||
const limit = Number(req.query.limit) || 100;
|
||||
|
||||
const regionObsIds = getObserverIdsForRegions(req.query.region);
|
||||
if (regionObsIds) {
|
||||
// Region-filtered subpath computation
|
||||
const regionalHashes = new Set();
|
||||
for (const obsId of regionObsIds) {
|
||||
const obs = pktStore.byObserver.get(obsId);
|
||||
if (obs) for (const o of obs) regionalHashes.add(o.hash);
|
||||
}
|
||||
const packets = pktStore.filter(p => p.path_json && p.path_json !== '[]' && regionalHashes.has(p.hash));
|
||||
const allNodes = db.db.prepare('SELECT public_key, name, lat, lon FROM nodes WHERE name IS NOT NULL').all();
|
||||
const subpathsByLen = {};
|
||||
let totalPaths = 0;
|
||||
for (const pkt of packets) {
|
||||
let hops;
|
||||
try { hops = JSON.parse(pkt.path_json); } catch { continue; }
|
||||
if (!Array.isArray(hops) || hops.length < 2) continue;
|
||||
totalPaths++;
|
||||
const resolved = disambiguateHops(hops, allNodes);
|
||||
const named = resolved.map(r => r.name);
|
||||
for (let len = minLen; len <= Math.min(maxLen, named.length); len++) {
|
||||
if (!subpathsByLen[len]) subpathsByLen[len] = {};
|
||||
for (let start = 0; start <= named.length - len; start++) {
|
||||
const sub = named.slice(start, start + len).join(' \u2192 ');
|
||||
const raw = hops.slice(start, start + len).join(',');
|
||||
if (!subpathsByLen[len][sub]) subpathsByLen[len][sub] = { count: 0, raw };
|
||||
subpathsByLen[len][sub].count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
const merged = {};
|
||||
for (let len = minLen; len <= maxLen; len++) {
|
||||
const bucket = subpathsByLen[len] || {};
|
||||
for (const [path, data] of Object.entries(bucket)) {
|
||||
if (!merged[path]) merged[path] = { count: 0, raw: data.raw };
|
||||
merged[path].count += data.count;
|
||||
}
|
||||
}
|
||||
const ranked = Object.entries(merged)
|
||||
.map(([path, data]) => ({ path, rawHops: data.raw.split(','), count: data.count, hops: path.split(' \u2192 ').length, pct: totalPaths > 0 ? Math.round(data.count / totalPaths * 1000) / 10 : 0 }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, limit);
|
||||
const result = { subpaths: ranked, totalPaths };
|
||||
cache.set(_ck, result, TTL.analyticsSubpaths);
|
||||
return res.json(result);
|
||||
}
|
||||
|
||||
const { subpathsByLen, totalPaths } = computeAllSubpaths();
|
||||
|
||||
// Merge requested length ranges
|
||||
|
||||
Reference in New Issue
Block a user