diff --git a/public/analytics.js b/public/analytics.js
index 554e9909..229c6814 100644
--- a/public/analytics.js
+++ b/public/analytics.js
@@ -772,7 +772,7 @@
`;
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 = '
Analyzing route patterns…
';
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 = 'Loading node analytics…
';
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') || '[]');
diff --git a/public/index.html b/public/index.html
index 8bdf6ff8..f24fb488 100644
--- a/public/index.html
+++ b/public/index.html
@@ -88,7 +88,7 @@
-
+
diff --git a/server.js b/server.js
index 0ebbc2a3..a7dc36d2 100644
--- a/server.js
+++ b/server.js
@@ -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