Fix aircraft track propagation issues in web frontend
This commit addresses issue #23 where aircraft track changes were not propagating properly to the web frontend. The fixes include: **Server-side improvements:** - Enhanced WebSocket broadcast reliability with timeout-based queueing - Increased broadcast channel buffer size (1000 -> 2000) - Improved error handling and connection management - Added write timeouts to prevent slow clients from blocking updates - Enhanced connection cleanup and ping/pong handling - Added debug endpoint /api/debug/websocket for troubleshooting - Relaxed position validation thresholds for better track acceptance **Frontend improvements:** - Enhanced WebSocket manager with exponential backoff reconnection - Improved aircraft position update detection and logging - Fixed position update logic to always propagate changes to map - Better coordinate validation and error reporting - Enhanced debugging with detailed console logging - Fixed track rotation update thresholds and logic - Improved marker lifecycle management and cleanup - Better handling of edge cases in aircraft state transitions **Key bug fixes:** - Removed overly aggressive position change detection that blocked updates - Fixed track rotation sensitivity (5° -> 10° threshold) - Enhanced coordinate validation to handle null/undefined values - Improved WebSocket message ordering and processing - Fixed marker position updates to always propagate to Leaflet These changes ensure reliable real-time aircraft tracking with proper position, heading, and altitude updates across multiple data sources. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8ffb657711
commit
1fe15c06a3
6 changed files with 216 additions and 49 deletions
|
|
@ -117,7 +117,7 @@ func NewWebServer(host string, port int, merger *merger.Merger, staticFiles embe
|
|||
ReadBufferSize: 8192,
|
||||
WriteBufferSize: 8192,
|
||||
},
|
||||
broadcastChan: make(chan []byte, 1000),
|
||||
broadcastChan: make(chan []byte, 2000), // Increased buffer size to handle bursts
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
|
@ -198,6 +198,7 @@ func (s *Server) setupRoutes() http.Handler {
|
|||
api.HandleFunc("/aircraft", s.handleGetAircraft).Methods("GET")
|
||||
api.HandleFunc("/aircraft/{icao}", s.handleGetAircraftDetails).Methods("GET")
|
||||
api.HandleFunc("/debug/aircraft", s.handleDebugAircraft).Methods("GET")
|
||||
api.HandleFunc("/debug/websocket", s.handleDebugWebSocket).Methods("GET")
|
||||
api.HandleFunc("/sources", s.handleGetSources).Methods("GET")
|
||||
api.HandleFunc("/stats", s.handleGetStats).Methods("GET")
|
||||
api.HandleFunc("/origin", s.handleGetOrigin).Methods("GET")
|
||||
|
|
@ -259,7 +260,7 @@ func (s *Server) handleHealthCheck(w http.ResponseWriter, r *http.Request) {
|
|||
sources := s.merger.GetSources()
|
||||
stats := s.addServerStats(s.merger.GetStatistics())
|
||||
aircraft := s.merger.GetAircraft()
|
||||
|
||||
|
||||
// Check if we have any active sources
|
||||
activeSources := 0
|
||||
for _, source := range sources {
|
||||
|
|
@ -267,7 +268,7 @@ func (s *Server) handleHealthCheck(w http.ResponseWriter, r *http.Request) {
|
|||
activeSources++
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Determine health status
|
||||
status := "healthy"
|
||||
statusCode := http.StatusOK
|
||||
|
|
@ -275,26 +276,26 @@ func (s *Server) handleHealthCheck(w http.ResponseWriter, r *http.Request) {
|
|||
status = "degraded"
|
||||
statusCode = http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
|
||||
response := map[string]interface{}{
|
||||
"status": status,
|
||||
"status": status,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"sources": map[string]interface{}{
|
||||
"total": len(sources),
|
||||
"total": len(sources),
|
||||
"active": activeSources,
|
||||
},
|
||||
"aircraft": map[string]interface{}{
|
||||
"count": len(aircraft),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// Add statistics if available
|
||||
if stats != nil {
|
||||
if totalMessages, ok := stats["total_messages"]; ok {
|
||||
response["messages"] = totalMessages
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(statusCode)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
|
|
@ -621,26 +622,38 @@ func (s *Server) sendInitialData(conn *websocket.Conn) {
|
|||
//
|
||||
// This routine:
|
||||
// - Listens for broadcast messages on the broadcastChan
|
||||
// - Sends messages to all connected WebSocket clients
|
||||
// - Sends messages to all connected WebSocket clients with write timeouts
|
||||
// - Handles client connection cleanup on write errors
|
||||
// - Respects the shutdown signal from stopChan
|
||||
//
|
||||
// Using a dedicated routine for broadcasting ensures efficient message
|
||||
// distribution without blocking the update generation.
|
||||
// ENHANCED: Added write timeouts and better error handling to prevent
|
||||
// slow clients from blocking updates to other clients.
|
||||
func (s *Server) broadcastRoutine() {
|
||||
for {
|
||||
select {
|
||||
case <-s.stopChan:
|
||||
return
|
||||
case data := <-s.broadcastChan:
|
||||
s.wsClientsMu.RLock()
|
||||
s.wsClientsMu.Lock()
|
||||
// Create list of clients to remove (to avoid modifying map during iteration)
|
||||
var toRemove []*websocket.Conn
|
||||
|
||||
for conn := range s.wsClients {
|
||||
// Set write timeout to prevent slow clients from blocking
|
||||
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
conn.Close()
|
||||
delete(s.wsClients, conn)
|
||||
// Mark for removal but don't modify map during iteration
|
||||
toRemove = append(toRemove, conn)
|
||||
}
|
||||
}
|
||||
s.wsClientsMu.RUnlock()
|
||||
|
||||
// Clean up failed connections
|
||||
for _, conn := range toRemove {
|
||||
conn.Close()
|
||||
delete(s.wsClients, conn)
|
||||
}
|
||||
|
||||
s.wsClientsMu.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -676,11 +689,10 @@ func (s *Server) periodicUpdateRoutine() {
|
|||
// 2. Filters aircraft to only include "useful" ones (with position or callsign)
|
||||
// 3. Formats the data as a WebSocketMessage with type "aircraft_update"
|
||||
// 4. Converts ICAO addresses to hex strings for JSON compatibility
|
||||
// 5. Queues the message for broadcast (non-blocking)
|
||||
// 5. Queues the message for broadcast (blocking with timeout)
|
||||
//
|
||||
// If the broadcast channel is full, the update is dropped to prevent blocking.
|
||||
// This ensures the system continues operating even if WebSocket clients
|
||||
// cannot keep up with updates.
|
||||
// IMPORTANT: Changed from non-blocking to blocking with timeout to prevent
|
||||
// dropping aircraft track updates when the channel is busy.
|
||||
func (s *Server) broadcastUpdate() {
|
||||
aircraft := s.merger.GetAircraft()
|
||||
sources := s.merger.GetSources()
|
||||
|
|
@ -707,10 +719,14 @@ func (s *Server) broadcastUpdate() {
|
|||
}
|
||||
|
||||
if data, err := json.Marshal(msg); err == nil {
|
||||
// Use timeout to prevent indefinite blocking while ensuring updates aren't dropped
|
||||
timeout := time.After(100 * time.Millisecond)
|
||||
select {
|
||||
case s.broadcastChan <- data:
|
||||
default:
|
||||
// Channel full, skip this update
|
||||
// Successfully queued
|
||||
case <-timeout:
|
||||
// Log dropped updates for debugging
|
||||
log.Printf("WARNING: Broadcast channel full, dropping update with %d aircraft", len(aircraftMap))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -863,3 +879,22 @@ func (s *Server) handleDebugAircraft(w http.ResponseWriter, r *http.Request) {
|
|||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// handleDebugWebSocket serves the /api/debug/websocket endpoint.
|
||||
// Returns WebSocket connection statistics for debugging connection issues.
|
||||
func (s *Server) handleDebugWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
s.wsClientsMu.RLock()
|
||||
clientCount := len(s.wsClients)
|
||||
s.wsClientsMu.RUnlock()
|
||||
|
||||
response := map[string]interface{}{
|
||||
"timestamp": time.Now().Unix(),
|
||||
"connected_clients": clientCount,
|
||||
"broadcast_chan_len": len(s.broadcastChan),
|
||||
"broadcast_chan_cap": cap(s.broadcastChan),
|
||||
"broadcast_chan_full": len(s.broadcastChan) == cap(s.broadcastChan),
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue