fix: filter totalNodes to active nodes (7-day window), fixes #133

The /api/stats endpoint returned totalNodes from SELECT COUNT(*) FROM nodes,
which counts every node ever seen. On long-running instances this climbs to
6800+ for a ~200-400 node mesh.

Changes:
- totalNodes now counts only nodes with last_seen within the last 7 days
- Added totalNodesAllTime field for the full historical count
- Role counts (repeaters, rooms, etc.) also filtered to 7-day window
- Added countActiveNodes and countActiveNodesByRole prepared statements
- Added 6 tests verifying active vs all-time node counting

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Kpa-clawbot
2026-03-27 01:56:38 -07:00
co-authored by Copilot
parent e18a73e1f2
commit ac7bd13ae8
3 changed files with 35 additions and 3 deletions
+5 -1
View File
@@ -346,6 +346,8 @@ const stmts = {
getObservers: db.prepare(`SELECT * FROM observers ORDER BY last_seen DESC`),
countPackets: db.prepare(`SELECT COUNT(*) as count FROM observations`),
countNodes: db.prepare(`SELECT COUNT(*) as count FROM nodes`),
countActiveNodes: db.prepare(`SELECT COUNT(*) as count FROM nodes WHERE last_seen > ?`),
countActiveNodesByRole: db.prepare(`SELECT COUNT(*) as count FROM nodes WHERE role = ? AND last_seen > ?`),
countObservers: db.prepare(`SELECT COUNT(*) as count FROM observers`),
countRecentPackets: schemaVersion >= 3
? db.prepare(`SELECT COUNT(*) as count FROM observations WHERE timestamp > CAST(strftime('%s', ?) AS INTEGER)`)
@@ -579,6 +581,7 @@ function getObservers() {
function getStats() {
const oneHourAgo = new Date(Date.now() - 3600000).toISOString();
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 3600000).toISOString();
// Try to get transmission count from normalized schema
let totalTransmissions = null;
try {
@@ -588,7 +591,8 @@ function getStats() {
totalPackets: totalTransmissions || stmts.countPackets.get().count,
totalTransmissions,
totalObservations: stmts.countPackets.get().count,
totalNodes: stmts.countNodes.get().count,
totalNodes: stmts.countActiveNodes.get(sevenDaysAgo).count,
totalNodesAllTime: stmts.countNodes.get().count,
totalObservers: stmts.countObservers.get().count,
packetsLastHour: stmts.countRecentPackets.get(oneHourAgo).count,
};
+3 -2
View File
@@ -876,10 +876,11 @@ app.use(express.json());
app.get('/api/stats', (req, res) => {
const stats = db.getStats();
// Get role counts
// Get role counts (active nodes only — same 7-day window as totalNodes)
const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 3600000).toISOString();
const counts = {};
for (const role of ['repeater', 'room', 'companion', 'sensor']) {
const r = db.db.prepare(`SELECT COUNT(*) as count FROM nodes WHERE role = ?`).get(role);
const r = db.db.prepare(`SELECT COUNT(*) as count FROM nodes WHERE role = ? AND last_seen > ?`).get(role, sevenDaysAgo);
counts[role + 's'] = r.count;
}
res.json({ ...stats, counts });
+27
View File
@@ -213,6 +213,33 @@ console.log('\ngetStats:');
assert(stats.totalObservers >= 1, 'totalObservers');
assert(typeof stats.totalPackets === 'number', 'totalPackets is number');
assert(typeof stats.packetsLastHour === 'number', 'packetsLastHour is number');
assert(typeof stats.totalNodesAllTime === 'number', 'totalNodesAllTime is number');
assert(stats.totalNodesAllTime >= stats.totalNodes, 'totalNodesAllTime >= totalNodes');
}
// --- getStats active node filtering ---
console.log('\ngetStats active node filtering:');
{
// Insert a node with last_seen 30 days ago (should be excluded from totalNodes)
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 3600000).toISOString();
db.upsertNode({ public_key: 'deadnode0000000000000000deadnode00', name: 'DeadNode', role: 'repeater', last_seen: thirtyDaysAgo, first_seen: thirtyDaysAgo });
// Insert a node with last_seen now (should be included)
db.upsertNode({ public_key: 'livenode0000000000000000livenode00', name: 'LiveNode', role: 'companion', last_seen: new Date().toISOString() });
const stats = db.getStats();
assert(stats.totalNodesAllTime > stats.totalNodes, 'dead node excluded from totalNodes but included in totalNodesAllTime');
// Verify the dead node is in totalNodesAllTime
const allTime = stats.totalNodesAllTime;
assert(allTime >= 3, 'totalNodesAllTime includes dead + live nodes');
// Verify active count doesn't include the 30-day-old node
// The dead node's last_seen is 30 days ago, window is 7 days
const nodeInDb = db.getNode('deadnode0000000000000000deadnode00');
assert(nodeInDb !== null, 'dead node exists in DB');
const liveNode = db.getNode('livenode0000000000000000livenode00');
assert(liveNode !== null, 'live node exists in DB');
}
// --- getNodeHealth ---