From fe19c283c46cfb1405ba92346f9bf59a3a3923c1 Mon Sep 17 00:00:00 2001 From: accius Date: Tue, 3 Feb 2026 14:38:55 -0500 Subject: [PATCH 1/2] Update server.js --- server.js | 76 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 75 insertions(+), 1 deletion(-) diff --git a/server.js b/server.js index e8237e5..747c2e8 100644 --- a/server.js +++ b/server.js @@ -263,6 +263,71 @@ function logErrorOnce(category, message) { return false; } +// ============================================ +// VISITOR TRACKING +// ============================================ +// Lightweight in-memory visitor counter — tracks unique IPs per day +// No cookies, no external analytics, no persistent storage +// Resets on server restart; logs daily summary + +const visitorStats = { + today: new Date().toISOString().slice(0, 10), // YYYY-MM-DD + uniqueIPs: new Set(), + totalRequests: 0, + history: [] // Last 30 days of { date, uniqueVisitors, totalRequests } +}; + +function rolloverVisitorStats() { + const now = new Date().toISOString().slice(0, 10); + if (now !== visitorStats.today) { + // Save yesterday's stats to history + visitorStats.history.push({ + date: visitorStats.today, + uniqueVisitors: visitorStats.uniqueIPs.size, + totalRequests: visitorStats.totalRequests + }); + // Keep only last 30 days + if (visitorStats.history.length > 30) { + visitorStats.history = visitorStats.history.slice(-30); + } + console.log(`[Visitors] Daily summary for ${visitorStats.today}: ${visitorStats.uniqueIPs.size} unique visitors, ${visitorStats.totalRequests} total requests`); + // Reset for new day + visitorStats.today = now; + visitorStats.uniqueIPs = new Set(); + visitorStats.totalRequests = 0; + } +} + +// Visitor tracking middleware — only counts page loads and API config fetches +// (not every API poll, which would inflate the count) +app.use((req, res, next) => { + rolloverVisitorStats(); + + // Only count meaningful "visits" — initial page load or config fetch + // This avoids counting every 5-second DX cluster poll as a "visit" + const countableRoutes = ['/', '/index.html', '/api/config']; + if (countableRoutes.includes(req.path)) { + const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.ip || req.connection?.remoteAddress || 'unknown'; + const isNew = !visitorStats.uniqueIPs.has(ip); + visitorStats.uniqueIPs.add(ip); + visitorStats.totalRequests++; + + if (isNew) { + logInfo(`[Visitors] New visitor today (#${visitorStats.uniqueIPs.size}) from ${ip.replace(/\d+$/, 'x')}`); + } + } + + next(); +}); + +// Log visitor count every hour +setInterval(() => { + rolloverVisitorStats(); + if (visitorStats.uniqueIPs.size > 0) { + console.log(`[Visitors] Today so far: ${visitorStats.uniqueIPs.size} unique visitors, ${visitorStats.totalRequests} requests`); + } +}, 60 * 60 * 1000); + // Serve static files // dist/ contains the built React app (from npm run build) // public/ contains the fallback page if build hasn't run @@ -3593,11 +3658,20 @@ function getLastWeekendOfMonth(year, month) { // ============================================ app.get('/api/health', (req, res) => { + rolloverVisitorStats(); res.json({ status: 'ok', version: APP_VERSION, uptime: process.uptime(), - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), + visitors: { + today: { + date: visitorStats.today, + uniqueVisitors: visitorStats.uniqueIPs.size, + totalRequests: visitorStats.totalRequests + }, + history: visitorStats.history + } }); }); From 683137477ab0bf7a386e8dea793f8d02bd16fc2e Mon Sep 17 00:00:00 2001 From: accius Date: Tue, 3 Feb 2026 14:44:50 -0500 Subject: [PATCH 2/2] Update server.js --- server.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/server.js b/server.js index 747c2e8..2b344c1 100644 --- a/server.js +++ b/server.js @@ -274,6 +274,9 @@ const visitorStats = { today: new Date().toISOString().slice(0, 10), // YYYY-MM-DD uniqueIPs: new Set(), totalRequests: 0, + allTimeVisitors: 0, // Cumulative unique visitors since server start + allTimeRequests: 0, // Cumulative requests since server start + serverStarted: new Date().toISOString(), history: [] // Last 30 days of { date, uniqueVisitors, totalRequests } }; @@ -290,8 +293,11 @@ function rolloverVisitorStats() { if (visitorStats.history.length > 30) { visitorStats.history = visitorStats.history.slice(-30); } - console.log(`[Visitors] Daily summary for ${visitorStats.today}: ${visitorStats.uniqueIPs.size} unique visitors, ${visitorStats.totalRequests} total requests`); - // Reset for new day + const avg = visitorStats.history.length > 0 + ? Math.round(visitorStats.history.reduce((sum, d) => sum + d.uniqueVisitors, 0) / visitorStats.history.length) + : 0; + console.log(`[Visitors] Daily summary for ${visitorStats.today}: ${visitorStats.uniqueIPs.size} unique visitors, ${visitorStats.totalRequests} requests | All-time: ${visitorStats.allTimeVisitors} visitors, ${visitorStats.allTimeRequests} requests | ${visitorStats.history.length}-day avg: ${avg}/day`); + // Reset daily counters for new day visitorStats.today = now; visitorStats.uniqueIPs = new Set(); visitorStats.totalRequests = 0; @@ -311,9 +317,11 @@ app.use((req, res, next) => { const isNew = !visitorStats.uniqueIPs.has(ip); visitorStats.uniqueIPs.add(ip); visitorStats.totalRequests++; + visitorStats.allTimeRequests++; if (isNew) { - logInfo(`[Visitors] New visitor today (#${visitorStats.uniqueIPs.size}) from ${ip.replace(/\d+$/, 'x')}`); + visitorStats.allTimeVisitors++; + logInfo(`[Visitors] New visitor today (#${visitorStats.uniqueIPs.size}, #${visitorStats.allTimeVisitors} all-time) from ${ip.replace(/\d+$/, 'x')}`); } } @@ -323,8 +331,11 @@ app.use((req, res, next) => { // Log visitor count every hour setInterval(() => { rolloverVisitorStats(); - if (visitorStats.uniqueIPs.size > 0) { - console.log(`[Visitors] Today so far: ${visitorStats.uniqueIPs.size} unique visitors, ${visitorStats.totalRequests} requests`); + if (visitorStats.uniqueIPs.size > 0 || visitorStats.allTimeVisitors > 0) { + const avg = visitorStats.history.length > 0 + ? Math.round(visitorStats.history.reduce((sum, d) => sum + d.uniqueVisitors, 0) / visitorStats.history.length) + : visitorStats.uniqueIPs.size; + console.log(`[Visitors] Today so far: ${visitorStats.uniqueIPs.size} unique, ${visitorStats.totalRequests} requests | All-time: ${visitorStats.allTimeVisitors} visitors | Avg: ${avg}/day`); } }, 60 * 60 * 1000); @@ -3659,6 +3670,9 @@ function getLastWeekendOfMonth(year, month) { app.get('/api/health', (req, res) => { rolloverVisitorStats(); + const avg = visitorStats.history.length > 0 + ? Math.round(visitorStats.history.reduce((sum, d) => sum + d.uniqueVisitors, 0) / visitorStats.history.length) + : visitorStats.uniqueIPs.size; res.json({ status: 'ok', version: APP_VERSION, @@ -3670,6 +3684,12 @@ app.get('/api/health', (req, res) => { uniqueVisitors: visitorStats.uniqueIPs.size, totalRequests: visitorStats.totalRequests }, + allTime: { + since: visitorStats.serverStarted, + uniqueVisitors: visitorStats.allTimeVisitors, + totalRequests: visitorStats.allTimeRequests + }, + dailyAverage: avg, history: visitorStats.history } });