// Package server provides HTTP and WebSocket services for the SkyView application. // // This package implements the web server that serves both static assets and real-time // aircraft data via REST API endpoints and WebSocket connections. It handles: // - Static web file serving from embedded assets // - RESTful API endpoints for aircraft, sources, and statistics // - Real-time WebSocket streaming for live aircraft updates // - CORS handling for cross-origin requests // - Coverage and heatmap data generation for visualization // // The server integrates with the merger component to access consolidated aircraft // data from multiple sources and provides various data formats optimized for // web consumption. package server import ( "context" "embed" "encoding/json" "fmt" "log" "net/http" "path" "strconv" "strings" "sync" "time" "github.com/gorilla/mux" "github.com/gorilla/websocket" "skyview/internal/merger" ) // OriginConfig represents the geographical reference point configuration. // This is used as the center point for the web map interface and for // distance calculations in coverage analysis. type OriginConfig struct { Latitude float64 `json:"latitude"` // Reference latitude in decimal degrees Longitude float64 `json:"longitude"` // Reference longitude in decimal degrees Name string `json:"name,omitempty"` // Descriptive name for the origin point } // Server handles HTTP requests and WebSocket connections for the SkyView web interface. // It serves static web assets, provides RESTful API endpoints for aircraft data, // and maintains real-time WebSocket connections for live updates. // // The server architecture uses: // - Gorilla mux for HTTP routing // - Gorilla WebSocket for real-time communication // - Embedded filesystem for static asset serving // - Concurrent broadcast system for WebSocket clients // - CORS support for cross-origin web applications type Server struct { host string // Bind address for HTTP server port int // TCP port for HTTP server merger *merger.Merger // Data source for aircraft information staticFiles embed.FS // Embedded static web assets server *http.Server // HTTP server instance origin OriginConfig // Geographic reference point // WebSocket management wsClients map[*websocket.Conn]bool // Active WebSocket client connections wsClientsMu sync.RWMutex // Protects wsClients map upgrader websocket.Upgrader // HTTP to WebSocket protocol upgrader // Broadcast channels for real-time updates broadcastChan chan []byte // Channel for broadcasting updates to all clients stopChan chan struct{} // Shutdown signal channel } // WebSocketMessage represents the standard message format for WebSocket communication. // All messages sent to clients follow this structure to provide consistent // message handling and enable message type discrimination on the client side. type WebSocketMessage struct { Type string `json:"type"` // Message type ("initial_data", "aircraft_update", etc.) Timestamp int64 `json:"timestamp"` // Unix timestamp when message was created Data interface{} `json:"data"` // Message payload (varies by type) } // AircraftUpdate represents the complete aircraft data payload sent via WebSocket. // This structure contains all information needed by the web interface to display // current aircraft positions, source status, and system statistics. type AircraftUpdate struct { Aircraft map[string]*merger.AircraftState `json:"aircraft"` // Current aircraft keyed by ICAO hex string Sources []*merger.Source `json:"sources"` // Active data sources with status Stats map[string]interface{} `json:"stats"` // System statistics and metrics } // NewWebServer creates a new HTTP server instance for serving the SkyView web interface. // // The server is configured with: // - WebSocket upgrader allowing all origins (suitable for development) // - Buffered broadcast channel for efficient message distribution // - Read/Write buffers optimized for aircraft data messages // // Parameters: // - host: Bind address (empty for all interfaces, "localhost" for local only) // - port: TCP port number for the HTTP server // - merger: Data merger instance providing aircraft information // - staticFiles: Embedded filesystem containing web assets // - origin: Geographic reference point for the map interface // // Returns a configured but not yet started server instance. func NewWebServer(host string, port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server { return &Server{ host: host, port: port, merger: merger, staticFiles: staticFiles, origin: origin, wsClients: make(map[*websocket.Conn]bool), upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true // Allow all origins in development }, ReadBufferSize: 8192, WriteBufferSize: 8192, }, broadcastChan: make(chan []byte, 2000), // Increased buffer size to handle bursts stopChan: make(chan struct{}), } } // Start begins serving HTTP requests and WebSocket connections. // // This method starts several background routines: // 1. Broadcast routine - handles WebSocket message distribution // 2. Periodic update routine - sends regular updates to WebSocket clients // 3. HTTP server - serves API endpoints and static files // // The method blocks until the server encounters an error or is shut down. // Use Stop() for graceful shutdown. // // Returns an error if the server fails to start or encounters a fatal error. func (s *Server) Start() error { // Start broadcast routine go s.broadcastRoutine() // Start periodic updates go s.periodicUpdateRoutine() // Setup routes router := s.setupRoutes() // Format address correctly for IPv6 addr := fmt.Sprintf("%s:%d", s.host, s.port) if strings.Contains(s.host, ":") { // IPv6 address needs brackets addr = fmt.Sprintf("[%s]:%d", s.host, s.port) } s.server = &http.Server{ Addr: addr, Handler: router, } return s.server.ListenAndServe() } // Stop gracefully shuts down the server and all background routines. // // This method: // 1. Signals all background routines to stop via stopChan // 2. Shuts down the HTTP server with a 5-second timeout // 3. Closes WebSocket connections // // The shutdown is designed to be safe and allow in-flight requests to complete. func (s *Server) Stop() { close(s.stopChan) if s.server != nil { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() s.server.Shutdown(ctx) } } // setupRoutes configures the HTTP routing for all server endpoints. // // The routing structure includes: // - /api/* - RESTful API endpoints for data access // - /ws - WebSocket endpoint for real-time updates // - /static/* - Static file serving // - / - Main application page // // All routes are wrapped with CORS middleware for cross-origin support. // // Returns a configured HTTP handler ready for use with the HTTP server. func (s *Server) setupRoutes() http.Handler { router := mux.NewRouter() // Health check endpoint for load balancers/monitoring router.HandleFunc("/health", s.handleHealthCheck).Methods("GET") // API routes api := router.PathPrefix("/api").Subrouter() 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") api.HandleFunc("/coverage/{sourceId}", s.handleGetCoverage).Methods("GET") api.HandleFunc("/heatmap/{sourceId}", s.handleGetHeatmap).Methods("GET") // WebSocket router.HandleFunc("/ws", s.handleWebSocket) // Static files router.PathPrefix("/static/").Handler(s.staticFileHandler()) router.HandleFunc("/favicon.ico", s.handleFavicon) // Main page router.HandleFunc("/", s.handleIndex) // Enable CORS return s.enableCORS(router) } // isAircraftUseful determines if an aircraft has enough data to be useful for the frontend. // // DESIGN NOTE: We WANT reasonable aircraft to appear in our table view, even if they // don't have enough data to appear on the map. This provides users visibility into // all tracked aircraft, not just those with complete position data. // // Aircraft are considered useful if they have ANY of: // - Valid position data (both latitude and longitude non-zero) -> Can show on map // - Callsign (flight identification) -> Can show in table with "No position" status // - Altitude information -> Can show in table as "Aircraft at X feet" // - Any other identifying information that makes it a "real" aircraft // // This inclusive approach ensures the table view shows all aircraft we're tracking, // while the map view only shows those with valid positions (handled by frontend filtering). func (s *Server) isAircraftUseful(aircraft *merger.AircraftState) bool { // Aircraft is useful if it has any meaningful data: hasValidPosition := aircraft.Latitude != 0 && aircraft.Longitude != 0 hasCallsign := aircraft.Callsign != "" hasAltitude := aircraft.Altitude != 0 hasSquawk := aircraft.Squawk != "" // Include aircraft with any identifying or operational data return hasValidPosition || hasCallsign || hasAltitude || hasSquawk } // handleHealthCheck serves the /health endpoint for monitoring and load balancers. // Returns a simple health status with basic service information. // // Response includes: // - status: "healthy" or "degraded" // - uptime: server uptime in seconds // - sources: number of active sources and their connection status // - aircraft: current aircraft count // // The endpoint returns: // - 200 OK when the service is healthy // - 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.addServerStats(s.merger.GetStatistics()) aircraft := s.merger.GetAircraft() // Check if we have any active sources activeSources := 0 for _, source := range sources { if source.Active { activeSources++ } } // Determine health status status := "healthy" statusCode := http.StatusOK if activeSources == 0 && len(sources) > 0 { status = "degraded" statusCode = http.StatusServiceUnavailable } response := map[string]interface{}{ "status": status, "timestamp": time.Now().Unix(), "sources": map[string]interface{}{ "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) } // handleGetAircraft serves the /api/aircraft endpoint. // Returns all currently tracked aircraft with their latest state information. // // Only "useful" aircraft are returned - those with position data or callsign. // This filters out incomplete aircraft that only have altitude or squawk codes, // which are not actionable for frontend mapping and flight tracking. // // The response includes: // - timestamp: Unix timestamp of the response // - aircraft: Map of aircraft keyed by ICAO hex strings // - count: Total number of useful aircraft (filtered count) // // Aircraft ICAO addresses are converted from uint32 to 6-digit hex strings // for consistent JSON representation (e.g., 0xABC123 -> "ABC123"). func (s *Server) handleGetAircraft(w http.ResponseWriter, r *http.Request) { aircraft := s.merger.GetAircraft() // Convert ICAO keys to hex strings for JSON and filter useful aircraft aircraftMap := make(map[string]*merger.AircraftState) for icao, state := range aircraft { if s.isAircraftUseful(state) { aircraftMap[fmt.Sprintf("%06X", icao)] = state } } response := map[string]interface{}{ "timestamp": time.Now().Unix(), "aircraft": aircraftMap, "count": len(aircraftMap), // Count of filtered useful aircraft } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } // handleGetAircraftDetails serves the /api/aircraft/{icao} endpoint. // Returns detailed information for a specific aircraft identified by ICAO address. // // The ICAO parameter should be a 6-digit hexadecimal string (e.g., "ABC123"). // Returns 400 Bad Request for invalid ICAO format. // Returns 404 Not Found if the aircraft is not currently tracked. // // On success, returns the complete AircraftState for the requested aircraft. func (s *Server) handleGetAircraftDetails(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) icaoStr := vars["icao"] // Parse ICAO hex string icao, err := strconv.ParseUint(icaoStr, 16, 32) if err != nil { http.Error(w, "Invalid ICAO address", http.StatusBadRequest) return } aircraft := s.merger.GetAircraft() if state, exists := aircraft[uint32(icao)]; exists { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(state) } else { http.Error(w, "Aircraft not found", http.StatusNotFound) } } // handleGetSources serves the /api/sources endpoint. // Returns information about all configured data sources and their current status. // // The response includes: // - sources: Array of source configurations with connection status // - count: Total number of configured sources // // This endpoint is useful for monitoring source connectivity and debugging // multi-source setups. func (s *Server) handleGetSources(w http.ResponseWriter, r *http.Request) { sources := s.merger.GetSources() w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "sources": sources, "count": len(sources), }) } // handleGetStats serves the /api/stats endpoint. // Returns system statistics and performance metrics from the data merger. // // Statistics may include: // - Message processing rates // - Aircraft count by source // - Connection status // - Data quality metrics // // The exact statistics depend on the merger implementation. func (s *Server) handleGetStats(w http.ResponseWriter, r *http.Request) { stats := s.addServerStats(s.merger.GetStatistics()) w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(stats) } // handleGetOrigin serves the /api/origin endpoint. // Returns the configured geographical reference point used by the system. // // The origin point is used for: // - Default map center in the web interface // - Distance calculations in coverage analysis // - Range circle calculations func (s *Server) handleGetOrigin(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(s.origin) } // handleGetCoverage serves the /api/coverage/{sourceId} endpoint. // Returns coverage data for a specific source based on aircraft positions and signal strength. // // The coverage data includes all positions where the specified source has received // aircraft signals, along with signal strength and distance information. // This is useful for visualizing receiver coverage patterns and range. // // Parameters: // - sourceId: URL parameter identifying the source // // Returns array of coverage points with lat/lon, signal strength, distance, and altitude. func (s *Server) handleGetCoverage(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) sourceID := vars["sourceId"] // Generate coverage data based on signal strength aircraft := s.merger.GetAircraft() coveragePoints := make([]map[string]interface{}, 0) for _, state := range aircraft { if srcData, exists := state.Sources[sourceID]; exists { coveragePoints = append(coveragePoints, map[string]interface{}{ "lat": state.Latitude, "lon": state.Longitude, "signal": srcData.SignalLevel, "distance": srcData.Distance, "altitude": state.Altitude, }) } } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "source": sourceID, "points": coveragePoints, }) } // handleGetHeatmap serves the /api/heatmap/{sourceId} endpoint. // Generates a grid-based heatmap visualization of signal coverage for a specific source. // // The heatmap is computed by: // 1. Finding geographic bounds of all aircraft positions for the source // 2. Creating a 100x100 grid covering the bounds // 3. Accumulating signal strength values in each grid cell // 4. Returning the grid data with boundary coordinates // // This provides a density-based visualization of where the source receives // the strongest signals, useful for coverage analysis and antenna optimization. // // Parameters: // - sourceId: URL parameter identifying the source // // Returns grid data array and geographic bounds for visualization. func (s *Server) handleGetHeatmap(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) sourceID := vars["sourceId"] // Generate heatmap data grid aircraft := s.merger.GetAircraft() heatmapData := make(map[string]interface{}) // Simple grid-based heatmap grid := make([][]float64, 100) for i := range grid { grid[i] = make([]float64, 100) } // Find bounds minLat, maxLat := 90.0, -90.0 minLon, maxLon := 180.0, -180.0 for _, state := range aircraft { if _, exists := state.Sources[sourceID]; exists { if state.Latitude < minLat { minLat = state.Latitude } if state.Latitude > maxLat { maxLat = state.Latitude } if state.Longitude < minLon { minLon = state.Longitude } if state.Longitude > maxLon { maxLon = state.Longitude } } } // Fill grid for _, state := range aircraft { if srcData, exists := state.Sources[sourceID]; exists { latIdx := int((state.Latitude - minLat) / (maxLat - minLat) * 99) lonIdx := int((state.Longitude - minLon) / (maxLon - minLon) * 99) if latIdx >= 0 && latIdx < 100 && lonIdx >= 0 && lonIdx < 100 { grid[latIdx][lonIdx] += srcData.SignalLevel } } } heatmapData["grid"] = grid heatmapData["bounds"] = map[string]float64{ "minLat": minLat, "maxLat": maxLat, "minLon": minLon, "maxLon": maxLon, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(heatmapData) } // handleWebSocket manages WebSocket connections for real-time aircraft data streaming. // // This handler: // 1. Upgrades the HTTP connection to WebSocket protocol // 2. Registers the client for broadcast updates // 3. Sends initial data snapshot to the client // 4. Handles client messages (currently just ping/pong for keepalive) // 5. Cleans up the connection when the client disconnects // // WebSocket clients receive periodic updates with current aircraft positions, // source status, and system statistics. The connection is kept alive until // the client disconnects or the server shuts down. func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { conn, err := s.upgrader.Upgrade(w, r, nil) if err != nil { log.Printf("WebSocket upgrade error: %v", err) return } defer conn.Close() // Register client s.wsClientsMu.Lock() s.wsClients[conn] = true s.wsClientsMu.Unlock() // Send initial data s.sendInitialData(conn) // Handle client messages (ping/pong) for { _, _, err := conn.ReadMessage() if err != nil { break } } // Unregister client s.wsClientsMu.Lock() delete(s.wsClients, conn) s.wsClientsMu.Unlock() } // sendInitialData sends a complete data snapshot to a newly connected WebSocket client. // // This includes: // - All currently tracked aircraft with their state information // - Status of all configured data sources // - Current system statistics // // 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.addServerStats(s.merger.GetStatistics()) // Convert ICAO keys to hex strings and filter useful aircraft aircraftMap := make(map[string]*merger.AircraftState) for icao, state := range aircraft { if s.isAircraftUseful(state) { aircraftMap[fmt.Sprintf("%06X", icao)] = state } } update := AircraftUpdate{ Aircraft: aircraftMap, Sources: sources, Stats: stats, } msg := WebSocketMessage{ Type: "initial_data", Timestamp: time.Now().Unix(), Data: update, } conn.WriteJSON(msg) } // broadcastRoutine runs in a dedicated goroutine to distribute WebSocket messages. // // This routine: // - Listens for broadcast messages on the broadcastChan // - Sends messages to all connected WebSocket clients with write timeouts // - Handles client connection cleanup on write errors // - Respects the shutdown signal from stopChan // // 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.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 { // Mark for removal but don't modify map during iteration toRemove = append(toRemove, conn) } } // Clean up failed connections for _, conn := range toRemove { conn.Close() delete(s.wsClients, conn) } s.wsClientsMu.Unlock() } } } // periodicUpdateRoutine generates regular WebSocket updates for all connected clients. // // Updates are sent every second and include: // - Current aircraft positions and state // - Data source status updates // - Fresh system statistics // // The routine uses a ticker for consistent timing and respects the shutdown // signal. Updates are queued through broadcastUpdate() which handles the // actual message formatting and distribution. func (s *Server) periodicUpdateRoutine() { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-s.stopChan: return case <-ticker.C: s.broadcastUpdate() } } } // broadcastUpdate creates and queues an aircraft update message for WebSocket clients. // // This function: // 1. Collects current aircraft data from the merger // 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 (blocking with timeout) // // 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() stats := s.addServerStats(s.merger.GetStatistics()) // Convert ICAO keys to hex strings and filter useful aircraft aircraftMap := make(map[string]*merger.AircraftState) for icao, state := range aircraft { if s.isAircraftUseful(state) { aircraftMap[fmt.Sprintf("%06X", icao)] = state } } update := AircraftUpdate{ Aircraft: aircraftMap, Sources: sources, Stats: stats, } msg := WebSocketMessage{ Type: "aircraft_update", Timestamp: time.Now().Unix(), Data: update, } 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: // Successfully queued case <-timeout: // Log dropped updates for debugging log.Printf("WARNING: Broadcast channel full, dropping update with %d aircraft", len(aircraftMap)) } } } // handleIndex serves the main application page at the root URL. // Returns the embedded index.html file which contains the aircraft tracking interface. // // Returns 404 if the index.html file is not found in the embedded assets. func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) { data, err := s.staticFiles.ReadFile("static/index.html") if err != nil { http.Error(w, "Page not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "text/html") w.Write(data) } // handleFavicon serves the favicon.ico file for browser tab icons. // Returns the embedded favicon file with appropriate content-type header. // // Returns 404 if the favicon.ico file is not found in the embedded assets. func (s *Server) handleFavicon(w http.ResponseWriter, r *http.Request) { data, err := s.staticFiles.ReadFile("static/favicon.ico") if err != nil { http.Error(w, "Favicon not found", http.StatusNotFound) return } w.Header().Set("Content-Type", "image/x-icon") w.Write(data) } // staticFileHandler creates an HTTP handler for serving embedded static files. // // This handler: // - Maps URL paths from /static/* to embedded file paths // - Sets appropriate Content-Type headers based on file extension // - Adds cache control headers for client-side caching (1 hour) // - Returns 404 for missing files // // The handler serves files from the embedded filesystem, enabling // single-binary deployment without external static file dependencies. func (s *Server) staticFileHandler() http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Remove /static/ prefix from URL path to get the actual file path filePath := "static" + r.URL.Path[len("/static"):] data, err := s.staticFiles.ReadFile(filePath) if err != nil { http.NotFound(w, r) return } // Set content type ext := path.Ext(filePath) contentType := getContentType(ext) w.Header().Set("Content-Type", contentType) // Cache control w.Header().Set("Cache-Control", "public, max-age=3600") w.Write(data) }) } // getContentType returns the appropriate MIME type for a file extension. // Supports common web file types used in the SkyView interface: // - HTML, CSS, JavaScript files // - JSON data files // - Image formats (SVG, PNG, JPEG, ICO) // // Returns "application/octet-stream" for unknown extensions. func getContentType(ext string) string { switch ext { case ".html": return "text/html" case ".css": return "text/css" case ".js": return "application/javascript" case ".json": return "application/json" case ".svg": return "image/svg+xml" case ".png": return "image/png" case ".jpg", ".jpeg": return "image/jpeg" case ".ico": return "image/x-icon" default: return "application/octet-stream" } } // enableCORS wraps an HTTP handler with Cross-Origin Resource Sharing headers. // // This middleware: // - Allows requests from any origin (*) // - Supports GET, POST, PUT, DELETE, and OPTIONS methods // - Permits Content-Type and Authorization headers // - Handles preflight OPTIONS requests // // CORS is enabled to support web applications hosted on different domains // than the SkyView server, which is common in development and some deployment scenarios. func (s *Server) enableCORS(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } handler.ServeHTTP(w, r) }) } // handleDebugAircraft serves the /api/debug/aircraft endpoint. // Returns all aircraft (filtered and unfiltered) for debugging position issues. func (s *Server) handleDebugAircraft(w http.ResponseWriter, r *http.Request) { aircraft := s.merger.GetAircraft() // All aircraft (unfiltered) allAircraftMap := make(map[string]*merger.AircraftState) for icao, state := range aircraft { allAircraftMap[fmt.Sprintf("%06X", icao)] = state } // Filtered aircraft (useful ones) filteredAircraftMap := make(map[string]*merger.AircraftState) for icao, state := range aircraft { if s.isAircraftUseful(state) { filteredAircraftMap[fmt.Sprintf("%06X", icao)] = state } } response := map[string]interface{}{ "timestamp": time.Now().Unix(), "all_aircraft": allAircraftMap, "filtered_aircraft": filteredAircraftMap, "all_count": len(allAircraftMap), "filtered_count": len(filteredAircraftMap), } 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) }