mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-05-14 23:25:44 +00:00
fix: nodes list shows actual last heard time, not just last advert
Server now computes last_heard from in-memory packet store (all traffic types) and includes it in /api/nodes response. Client prefers last_heard over DB last_seen for display, sort, filter, and status calculation. Fixes inconsistency where list showed '5d ago' but side pane showed '26m ago' for the same node.
This commit is contained in:
+24
-24
@@ -22,9 +22,9 @@
|
||||
<meta name="twitter:title" content="MeshCore Analyzer">
|
||||
<meta name="twitter:description" content="Real-time MeshCore LoRa mesh network analyzer — live packet visualization, node tracking, channel decryption, and route analysis.">
|
||||
<meta name="twitter:image" content="https://raw.githubusercontent.com/Kpa-clawbot/meshcore-analyzer/master/public/og-image.png">
|
||||
<link rel="stylesheet" href="style.css?v=1774237752">
|
||||
<link rel="stylesheet" href="home.css?v=1774295848">
|
||||
<link rel="stylesheet" href="live.css?v=1774295848">
|
||||
<link rel="stylesheet" href="style.css?v=1774297967">
|
||||
<link rel="stylesheet" href="home.css?v=1774297967">
|
||||
<link rel="stylesheet" href="live.css?v=1774297967">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin="anonymous">
|
||||
@@ -81,26 +81,26 @@
|
||||
<main id="app" role="main"></main>
|
||||
|
||||
<script src="vendor/qrcode.js"></script>
|
||||
<script src="roles.js?v=1774295848"></script>
|
||||
<script src="customize.js?v=1774238281" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774325000"></script>
|
||||
<script src="hop-resolver.js?v=1774223973"></script>
|
||||
<script src="hop-display.js?v=1774221932"></script>
|
||||
<script src="app.js?v=1774238281"></script>
|
||||
<script src="home.js?v=1774295848"></script>
|
||||
<script src="packets.js?v=1774295848"></script>
|
||||
<script src="map.js?v=1774295848" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774331200" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774497660" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774295848" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774208460" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774207165" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774229396" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774295848" 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=1774219440" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774126708" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774295848" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="roles.js?v=1774297967"></script>
|
||||
<script src="customize.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="region-filter.js?v=1774297967"></script>
|
||||
<script src="hop-resolver.js?v=1774297967"></script>
|
||||
<script src="hop-display.js?v=1774297967"></script>
|
||||
<script src="app.js?v=1774297967"></script>
|
||||
<script src="home.js?v=1774297967"></script>
|
||||
<script src="packets.js?v=1774297967"></script>
|
||||
<script src="map.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="channels.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="nodes.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="traces.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="analytics.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-v1-constellation.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="audio-lab.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="live.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observers.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="observer-detail.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="node-analytics.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
<script src="perf.js?v=1774297967" onerror="console.error('Failed to load:', this.src)"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+9
-6
@@ -56,8 +56,8 @@
|
||||
va = (a.role || '').toLowerCase(); vb = (b.role || '').toLowerCase();
|
||||
return va < vb ? -dir : va > vb ? dir : 0;
|
||||
} else if (col === 'last_seen') {
|
||||
va = a.last_seen ? new Date(a.last_seen).getTime() : 0;
|
||||
vb = b.last_seen ? new Date(b.last_seen).getTime() : 0;
|
||||
va = a.last_heard ? new Date(a.last_heard).getTime() : a.last_seen ? new Date(a.last_seen).getTime() : 0;
|
||||
vb = b.last_heard ? new Date(b.last_heard).getTime() : b.last_seen ? new Date(b.last_seen).getTime() : 0;
|
||||
return (va - vb) * dir;
|
||||
} else if (col === 'advert_count') {
|
||||
va = a.advert_count || 0; vb = b.advert_count || 0;
|
||||
@@ -90,8 +90,8 @@
|
||||
// Single source of truth for all status-related info
|
||||
const role = (n.role || '').toLowerCase();
|
||||
const roleColor = ROLE_COLORS[n.role] || '#6b7280';
|
||||
// Accept pre-resolved lastHeard from health data, or fall back to last_seen
|
||||
const lastHeardTime = n._lastHeard || n.last_seen;
|
||||
// Prefer last_heard (from in-memory packets) > _lastHeard (health API) > last_seen (DB)
|
||||
const lastHeardTime = n._lastHeard || n.last_heard || n.last_seen;
|
||||
const lastHeardMs = lastHeardTime ? new Date(lastHeardTime).getTime() : 0;
|
||||
const status = getNodeStatus(role, lastHeardMs);
|
||||
const statusLabel = status === 'active' ? '🟢 Active' : '⚪ Stale';
|
||||
@@ -430,7 +430,10 @@
|
||||
}
|
||||
if (lastHeard) {
|
||||
const ms = { '1h': 3600000, '6h': 21600000, '24h': 86400000, '7d': 604800000, '30d': 2592000000 }[lastHeard];
|
||||
if (ms) filtered = filtered.filter(n => n.last_seen && (Date.now() - new Date(n.last_seen).getTime()) < ms);
|
||||
if (ms) filtered = filtered.filter(n => {
|
||||
const t = n.last_heard || n.last_seen;
|
||||
return t && (Date.now() - new Date(t).getTime()) < ms;
|
||||
});
|
||||
}
|
||||
nodes = filtered;
|
||||
|
||||
@@ -586,7 +589,7 @@
|
||||
<td>${favStar(n.public_key, 'node-fav')}${isClaimed ? '<span class="claimed-badge" title="My Mesh">★</span> ' : ''}<strong>${n.name || '(unnamed)'}</strong></td>
|
||||
<td class="mono">${truncate(n.public_key, 16)}</td>
|
||||
<td><span class="badge" style="background:${roleColor}20;color:${roleColor}">${n.role}</span></td>
|
||||
<td class="${lastSeenClass}">${timeAgo(n.last_seen)}</td>
|
||||
<td class="${lastSeenClass}">${timeAgo(n.last_heard || n.last_seen)}</td>
|
||||
<td>${n.advert_count || 0}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
|
||||
@@ -1301,6 +1301,15 @@ app.get('/api/nodes', (req, res) => {
|
||||
const allSizes = _hashSizeAllMap.get(node.public_key);
|
||||
node.hash_size_inconsistent = _isHashSizeFlipFlop(node.public_key);
|
||||
if (allSizes && allSizes.size > 1) node.hash_sizes_seen = [...allSizes].sort();
|
||||
// Compute lastHeard from in-memory packets (more accurate than DB last_seen)
|
||||
const nodePkts = pktStore.byNode.get(node.public_key);
|
||||
if (nodePkts && nodePkts.length > 0) {
|
||||
let latest = null;
|
||||
for (const p of nodePkts) {
|
||||
if (!latest || p.timestamp > latest) latest = p.timestamp;
|
||||
}
|
||||
if (latest) node.last_heard = latest;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({ nodes, total, counts });
|
||||
|
||||
Reference in New Issue
Block a user