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