mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-14 22:25:06 +00:00
perf: bulk health endpoint — single API call replaces 50 individual health requests for Nodes tab
This commit is contained in:
+8
-10
@@ -1140,20 +1140,18 @@
|
||||
async function renderNodesTab(el) {
|
||||
el.innerHTML = '<div style="padding:40px;text-align:center;color:var(--text-muted)">Loading node analytics…</div>';
|
||||
try {
|
||||
const nodesResp = await api('/nodes?limit=200&sortBy=lastSeen');
|
||||
const [nodesResp, bulkHealth] = await Promise.all([
|
||||
api('/nodes?limit=200&sortBy=lastSeen'),
|
||||
api('/nodes/bulk-health?limit=50')
|
||||
]);
|
||||
const nodes = nodesResp.nodes || nodesResp;
|
||||
const myNodes = JSON.parse(localStorage.getItem('meshcore-my-nodes') || '[]');
|
||||
const myKeys = new Set(myNodes.map(n => n.pubkey));
|
||||
|
||||
// Fetch health data for top nodes (limit to avoid hammering)
|
||||
const topNodes = nodes.slice(0, 50);
|
||||
const healthResults = await Promise.allSettled(
|
||||
topNodes.map(n => api('/nodes/' + encodeURIComponent(n.public_key) + '/health').then(h => ({ ...n, health: h })))
|
||||
);
|
||||
const enriched = healthResults
|
||||
.filter(r => r.status === 'fulfilled')
|
||||
.map(r => r.value)
|
||||
.filter(n => n.health);
|
||||
// Map bulk health by pubkey
|
||||
const healthMap = {};
|
||||
bulkHealth.forEach(h => { healthMap[h.public_key] = h; });
|
||||
const enriched = nodes.filter(n => healthMap[n.public_key]).map(n => ({ ...n, health: { stats: healthMap[n.public_key].stats, observers: healthMap[n.public_key].observers } }));
|
||||
|
||||
// Compute rankings
|
||||
const byPackets = [...enriched].sort((a, b) => (b.health.stats.totalPackets || 0) - (a.health.stats.totalPackets || 0));
|
||||
|
||||
+2
-2
@@ -83,9 +83,9 @@
|
||||
<script src="channels.js?v=1774079160" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1773959793" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774079160" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1773960314" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1773960384" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774079160" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774079160" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1773960314" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1773960384" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1266,6 +1266,47 @@ app.get('/api/nodes/:pubkey/health', (req, res) => {
|
||||
res.json(health);
|
||||
});
|
||||
|
||||
// Bulk health summary for analytics — single query approach
|
||||
app.get('/api/nodes/bulk-health', (req, res) => {
|
||||
const limit = Math.min(Number(req.query.limit) || 50, 200);
|
||||
const nodes = db.db.prepare(`SELECT * FROM nodes ORDER BY last_seen DESC LIMIT ?`).all(limit);
|
||||
const todayStart = new Date();
|
||||
todayStart.setUTCHours(0, 0, 0, 0);
|
||||
const todayISO = todayStart.toISOString();
|
||||
|
||||
const results = nodes.map(node => {
|
||||
const pk = node.public_key;
|
||||
const keyPattern = `%${pk}%`;
|
||||
const namePattern = node.name ? `%${node.name.replace(/[%_]/g, '')}%` : null;
|
||||
const where = namePattern
|
||||
? `(decoded_json LIKE @k OR decoded_json LIKE @n)`
|
||||
: `decoded_json LIKE @k`;
|
||||
const p = namePattern ? { k: keyPattern, n: namePattern } : { k: keyPattern };
|
||||
|
||||
const observerRows = db.db.prepare(`
|
||||
SELECT observer_id, observer_name, AVG(snr) as avgSnr, AVG(rssi) as avgRssi, COUNT(*) as packetCount
|
||||
FROM packets WHERE ${where} AND observer_id IS NOT NULL GROUP BY observer_id ORDER BY packetCount DESC
|
||||
`).all(p);
|
||||
|
||||
const totalPackets = db.db.prepare(`SELECT COUNT(*) as c FROM packets WHERE ${where}`).get(p).c;
|
||||
const packetsToday = db.db.prepare(`SELECT COUNT(*) as c FROM packets WHERE ${where} AND timestamp > @s`).get({ ...p, s: todayISO }).c;
|
||||
const avgSnr = db.db.prepare(`SELECT AVG(snr) as v FROM packets WHERE ${where}`).get(p).v;
|
||||
const lastHeard = db.db.prepare(`SELECT MAX(timestamp) as v FROM packets WHERE ${where}`).get(p).v;
|
||||
|
||||
return {
|
||||
public_key: pk,
|
||||
name: node.name,
|
||||
role: node.role,
|
||||
lat: node.lat,
|
||||
lon: node.lon,
|
||||
stats: { totalPackets, packetsToday, avgSnr, lastHeard },
|
||||
observers: observerRows
|
||||
};
|
||||
});
|
||||
|
||||
res.json(results);
|
||||
});
|
||||
|
||||
app.get('/api/nodes/:pubkey/analytics', (req, res) => {
|
||||
const days = Math.min(Math.max(Number(req.query.days) || 7, 1), 365);
|
||||
const data = db.getNodeAnalytics(req.params.pubkey, days);
|
||||
|
||||
Reference in New Issue
Block a user