diff --git a/db.js b/db.js
index 24f7c580..590a250b 100644
--- a/db.js
+++ b/db.js
@@ -328,6 +328,10 @@ function getNodeHealth(pubkey) {
}
const avgHops = hopCount > 0 ? Math.round(totalHops / hopCount) : 0;
+ const totalPackets = db.prepare(`
+ SELECT COUNT(*) as count FROM packets WHERE ${whereClause}
+ `).get(params).count;
+
// Recent 10 packets
const recentPackets = db.prepare(`
SELECT * FROM packets WHERE ${whereClause} ORDER BY timestamp DESC LIMIT 10
@@ -336,7 +340,7 @@ function getNodeHealth(pubkey) {
return {
node,
observers,
- stats: { packetsToday, avgSnr: avgStats.avgSnr, avgHops, lastHeard },
+ stats: { totalPackets, packetsToday, avgSnr: avgStats.avgSnr, avgHops, lastHeard },
recentPackets,
};
}
diff --git a/public/index.html b/public/index.html
index 9128b578..752b4d75 100644
--- a/public/index.html
+++ b/public/index.html
@@ -80,7 +80,7 @@
-
+
diff --git a/public/nodes.js b/public/nodes.js
index 37be8fc7..d5b195a3 100644
--- a/public/nodes.js
+++ b/public/nodes.js
@@ -114,15 +114,31 @@
Stats
- - First Seen
- ${n.first_seen ? new Date(n.first_seen).toLocaleString() : '—'}
- Last Heard
- ${lastHeard ? timeAgo(lastHeard) : (n.last_seen ? timeAgo(n.last_seen) : '—')}
+ - First Seen
- ${n.first_seen ? new Date(n.first_seen).toLocaleString() : '—'}
- Total Packets
- ${stats.totalPackets || n.advert_count || 0}
- Packets Today
- ${stats.packetsToday || 0}
- - Observers
- ${observers.length || 0}${observers.length ? ' (' + observers.map(o => escapeHtml(o.observer_name || o.observer_id)).join(', ') + ')' : ''}
+ ${stats.avgSnr != null ? `- Avg SNR
- ${stats.avgSnr.toFixed(1)} dB
` : ''}
+ ${stats.avgHops ? `- Avg Hops
- ${stats.avgHops}
` : ''}
${hasLoc ? `- Location
- ${n.lat.toFixed(5)}, ${n.lon.toFixed(5)}
` : ''}
+ ${observers.length ? `
+
Heard By (${observers.length} observer${observers.length > 1 ? 's' : ''})
+
+ | Observer | Packets | Avg SNR | Avg RSSI |
+
+ ${observers.map(o => `
+ | ${escapeHtml(o.observer_name || o.observer_id)} |
+ ${o.packetCount} |
+ ${o.avgSnr != null ? o.avgSnr.toFixed(1) + ' dB' : '—'} |
+ ${o.avgRssi != null ? o.avgRssi.toFixed(0) + ' dBm' : '—'} |
+
`).join('')}
+
+
+
` : ''}
+
Recent Activity (${recent.length})
@@ -373,16 +389,29 @@
function renderDetail(panel, data) {
const n = data.node;
const adverts = data.recentAdverts || [];
- const recent = data.healthData?.recentPackets || [];
+ const h = data.healthData || {};
+ const stats = h.stats || {};
+ const observers = h.observers || [];
+ const recent = h.recentPackets || [];
const roleColor = ROLE_COLORS[n.role] || '#6b7280';
const hasLoc = n.lat != null && n.lon != null;
const nodeUrl = location.origin + '#/nodes/' + encodeURIComponent(n.public_key);
+ // Status calculation
+ const lastHeard = stats.lastHeard;
+ const statusAge = lastHeard ? (Date.now() - new Date(lastHeard).getTime()) : Infinity;
+ const role = (n.role || '').toLowerCase();
+ const isInfra = role === 'repeater' || role === 'room';
+ const degradedMs = isInfra ? 86400000 : 3600000;
+ const silentMs = isInfra ? 259200000 : 86400000;
+ const statusLabel = statusAge < degradedMs ? '🟢 Active' : statusAge < silentMs ? '🟡 Degraded' : '🔴 Silent';
+ const totalPackets = stats.totalPackets || n.advert_count || 0;
+
panel.innerHTML = `
${hasLoc ? `
` : ''}
-
${n.name || '(unnamed)'}
-
${n.role}
+
${escapeHtml(n.name || '(unnamed)')}
+
${n.role} ${statusLabel}
Public Key
@@ -391,15 +420,28 @@
-
Info
+
Overview
+ - Last Heard
- ${lastHeard ? timeAgo(lastHeard) : (n.last_seen ? timeAgo(n.last_seen) : '—')}
- First Seen
- ${n.first_seen ? new Date(n.first_seen).toLocaleString() : '—'}
- - Last Seen
- ${n.last_seen ? timeAgo(n.last_seen) : '—'}
- - Adverts
- ${n.advert_count || 0}
+ - Total Packets
- ${totalPackets}
+ - Packets Today
- ${stats.packetsToday || 0}
+ ${stats.avgSnr != null ? `- Avg SNR
- ${stats.avgSnr.toFixed(1)} dB
` : ''}
+ ${stats.avgHops ? `- Avg Hops
- ${stats.avgHops}
` : ''}
${hasLoc ? `- Location
- ${n.lat.toFixed(5)}, ${n.lon.toFixed(5)}
` : ''}
+ ${observers.length ? `
+
Heard By (${observers.length} observer${observers.length > 1 ? 's' : ''})
+
+ ${observers.map(o => `
+ ${escapeHtml(o.observer_name || o.observer_id)}
+ ${o.packetCount} pkts · ${o.avgSnr != null ? 'SNR ' + o.avgSnr.toFixed(1) + 'dB' : ''}${o.avgRssi != null ? ' · RSSI ' + o.avgRssi.toFixed(0) : ''}
+
`).join('')}
+
+
` : ''}
+
@@ -419,7 +461,6 @@
`;
}).join('') : '
No recent adverts
'}
- }).join('') : '
No recent adverts
'}