From 6414ea72f1d0c9e0e1425e1e739f9005956b71ac Mon Sep 17 00:00:00 2001 From: Ole-Morten Duesund Date: Sun, 24 Aug 2025 21:56:16 +0200 Subject: [PATCH] Add active WebSocket client tracking and display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track and display the number of active WebSocket connections viewing the aircraft tracker. Features: Backend: - Add getActiveClientCount() method for thread-safe client counting - Include active_clients count in all statistics responses - Integrate with existing stats API endpoints Frontend: - Display "X viewer(s)" in header stats with proper plural/singular grammar - Add "Active Viewers" card to Statistics view - Real-time updates as clients connect/disconnect - Fallback to 1 when stats not yet available This adds a social element showing "Now being watched by X users" style information prominently in the interface. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- assets/static/index.html | 5 +++++ assets/static/js/modules/ui-manager.js | 12 ++++++++++++ internal/server/server.go | 21 +++++++++++++++++---- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/assets/static/index.html b/assets/static/index.html index 2420bf6..da510e5 100644 --- a/assets/static/index.html +++ b/assets/static/index.html @@ -54,6 +54,7 @@
0 aircraft 0 sources + 1 viewer Connecting...
@@ -195,6 +196,10 @@

Active Sources

0
+
+

Active Viewers

+
1
+

Messages/sec

0
diff --git a/assets/static/js/modules/ui-manager.js b/assets/static/js/modules/ui-manager.js index c4e7569..9697471 100644 --- a/assets/static/js/modules/ui-manager.js +++ b/assets/static/js/modules/ui-manager.js @@ -241,6 +241,7 @@ export class UIManager { updateStatistics() { const totalAircraftEl = document.getElementById('total-aircraft'); const activeSourcesEl = document.getElementById('active-sources'); + const activeViewersEl = document.getElementById('active-viewers'); const maxRangeEl = document.getElementById('max-range'); const messagesSecEl = document.getElementById('messages-sec'); @@ -248,6 +249,9 @@ export class UIManager { if (activeSourcesEl) { activeSourcesEl.textContent = Array.from(this.sourcesData.values()).filter(s => s.active).length; } + if (activeViewersEl) { + activeViewersEl.textContent = this.stats.active_clients || 1; + } // Calculate max range let maxDistance = 0; @@ -267,10 +271,18 @@ export class UIManager { updateHeaderInfo() { const aircraftCountEl = document.getElementById('aircraft-count'); const sourcesCountEl = document.getElementById('sources-count'); + const activeClientsEl = document.getElementById('active-clients'); if (aircraftCountEl) aircraftCountEl.textContent = `${this.aircraftData.size} aircraft`; if (sourcesCountEl) sourcesCountEl.textContent = `${this.sourcesData.size} sources`; + // Update active clients count + const clientCount = this.stats.active_clients || 1; + if (activeClientsEl) { + const clientText = clientCount === 1 ? 'viewer' : 'viewers'; + activeClientsEl.textContent = `${clientCount} ${clientText}`; + } + this.updateClocks(); } diff --git a/internal/server/server.go b/internal/server/server.go index 2197f85..6b43d1e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -257,7 +257,7 @@ func (s *Server) isAircraftUseful(aircraft *merger.AircraftState) bool { // - 503 Service Unavailable when the service is degraded (no active sources) func (s *Server) handleHealthCheck(w http.ResponseWriter, r *http.Request) { sources := s.merger.GetSources() - stats := s.merger.GetStatistics() + stats := s.addServerStats(s.merger.GetStatistics()) aircraft := s.merger.GetAircraft() // Check if we have any active sources @@ -393,7 +393,7 @@ func (s *Server) handleGetSources(w http.ResponseWriter, r *http.Request) { // // The exact statistics depend on the merger implementation. func (s *Server) handleGetStats(w http.ResponseWriter, r *http.Request) { - stats := s.merger.GetStatistics() + stats := s.addServerStats(s.merger.GetStatistics()) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(stats) @@ -576,10 +576,23 @@ func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { // ICAO addresses are converted to hex strings for consistent JSON representation. // This initial data allows the client to immediately display current aircraft // without waiting for the next periodic update. +// getActiveClientCount returns the number of currently connected WebSocket clients. +func (s *Server) getActiveClientCount() int { + s.wsClientsMu.RLock() + defer s.wsClientsMu.RUnlock() + return len(s.wsClients) +} + +// addServerStats adds server-specific statistics to the merger stats. +func (s *Server) addServerStats(stats map[string]interface{}) map[string]interface{} { + stats["active_clients"] = s.getActiveClientCount() + return stats +} + func (s *Server) sendInitialData(conn *websocket.Conn) { aircraft := s.merger.GetAircraft() sources := s.merger.GetSources() - stats := s.merger.GetStatistics() + stats := s.addServerStats(s.merger.GetStatistics()) // Convert ICAO keys to hex strings and filter useful aircraft aircraftMap := make(map[string]*merger.AircraftState) @@ -671,7 +684,7 @@ func (s *Server) periodicUpdateRoutine() { func (s *Server) broadcastUpdate() { aircraft := s.merger.GetAircraft() sources := s.merger.GetSources() - stats := s.merger.GetStatistics() + stats := s.addServerStats(s.merger.GetStatistics()) // Convert ICAO keys to hex strings and filter useful aircraft aircraftMap := make(map[string]*merger.AircraftState)