From bb409a2e007b1c7b782790dc4681b46d8a2a7bce Mon Sep 17 00:00:00 2001 From: you Date: Mon, 23 Mar 2026 20:32:56 +0000 Subject: [PATCH] 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. --- public/index.html | 48 +++++++++++++++++++++++------------------------ public/nodes.js | 15 +++++++++------ server.js | 9 +++++++++ 3 files changed, 42 insertions(+), 30 deletions(-) diff --git a/public/index.html b/public/index.html index b4599afd..d173c80f 100644 --- a/public/index.html +++ b/public/index.html @@ -22,9 +22,9 @@ - - - + + + @@ -81,26 +81,26 @@
- - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/public/nodes.js b/public/nodes.js index 38abd26d..45f1cad9 100644 --- a/public/nodes.js +++ b/public/nodes.js @@ -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 @@ ${favStar(n.public_key, 'node-fav')}${isClaimed ? '★ ' : ''}${n.name || '(unnamed)'} ${truncate(n.public_key, 16)} ${n.role} - ${timeAgo(n.last_seen)} + ${timeAgo(n.last_heard || n.last_seen)} ${n.advert_count || 0} `; }).join(''); diff --git a/server.js b/server.js index 10fe2c04..6cfb0954 100644 --- a/server.js +++ b/server.js @@ -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 });