Add active WebSocket client tracking and display

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 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-24 21:56:16 +02:00
commit 6414ea72f1
3 changed files with 34 additions and 4 deletions

View file

@ -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)