2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 22:09:37 +02:00
|
|
|
package server
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"embed"
|
|
|
|
|
"encoding/json"
|
2025-08-23 23:51:37 +02:00
|
|
|
"fmt"
|
2025-08-23 22:09:37 +02:00
|
|
|
"log"
|
|
|
|
|
"net/http"
|
2025-08-23 22:14:19 +02:00
|
|
|
"path"
|
2025-08-23 23:51:37 +02:00
|
|
|
"strconv"
|
2025-08-24 18:36:14 +02:00
|
|
|
"strings"
|
2025-08-23 22:09:37 +02:00
|
|
|
"sync"
|
2025-08-23 23:51:37 +02:00
|
|
|
"time"
|
2025-08-23 22:09:37 +02:00
|
|
|
|
|
|
|
|
"github.com/gorilla/mux"
|
|
|
|
|
"github.com/gorilla/websocket"
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
"skyview/internal/merger"
|
2025-08-23 22:09:37 +02:00
|
|
|
)
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-24 00:24:45 +02:00
|
|
|
type OriginConfig struct {
|
2025-08-24 18:36:14 +02:00
|
|
|
Latitude float64 `json:"latitude"` // Reference latitude in decimal degrees
|
|
|
|
|
Longitude float64 `json:"longitude"` // Reference longitude in decimal degrees
|
2025-08-24 10:29:25 +02:00
|
|
|
Name string `json:"name,omitempty"` // Descriptive name for the origin point
|
2025-08-24 00:24:45 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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
|
2025-08-23 22:09:37 +02:00
|
|
|
type Server struct {
|
2025-08-24 18:36:14 +02:00
|
|
|
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
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// WebSocket management
|
2025-08-24 10:29:25 +02:00
|
|
|
wsClients map[*websocket.Conn]bool // Active WebSocket client connections
|
|
|
|
|
wsClientsMu sync.RWMutex // Protects wsClients map
|
|
|
|
|
upgrader websocket.Upgrader // HTTP to WebSocket protocol upgrader
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// Broadcast channels for real-time updates
|
2025-08-24 18:36:14 +02:00
|
|
|
broadcastChan chan []byte // Channel for broadcasting updates to all clients
|
|
|
|
|
stopChan chan struct{} // Shutdown signal channel
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 22:09:37 +02:00
|
|
|
type WebSocketMessage struct {
|
2025-08-24 10:29:25 +02:00
|
|
|
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)
|
2025-08-23 23:51:37 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
type AircraftUpdate struct {
|
2025-08-24 10:29:25 +02:00
|
|
|
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
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 18:36:14 +02:00
|
|
|
// NewWebServer creates a new HTTP server instance for serving the SkyView web interface.
|
2025-08-24 10:29:25 +02:00
|
|
|
//
|
|
|
|
|
// 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:
|
2025-08-24 18:36:14 +02:00
|
|
|
// - host: Bind address (empty for all interfaces, "localhost" for local only)
|
2025-08-24 10:29:25 +02:00
|
|
|
// - 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.
|
2025-08-24 18:36:14 +02:00
|
|
|
func NewWebServer(host string, port int, merger *merger.Merger, staticFiles embed.FS, origin OriginConfig) *Server {
|
2025-08-23 23:51:37 +02:00
|
|
|
return &Server{
|
2025-08-24 18:36:14 +02:00
|
|
|
host: host,
|
2025-08-24 00:57:49 +02:00
|
|
|
port: port,
|
|
|
|
|
merger: merger,
|
|
|
|
|
staticFiles: staticFiles,
|
|
|
|
|
origin: origin,
|
|
|
|
|
wsClients: make(map[*websocket.Conn]bool),
|
2025-08-23 22:09:37 +02:00
|
|
|
upgrader: websocket.Upgrader{
|
|
|
|
|
CheckOrigin: func(r *http.Request) bool {
|
2025-08-23 23:51:37 +02:00
|
|
|
return true // Allow all origins in development
|
2025-08-23 22:09:37 +02:00
|
|
|
},
|
2025-08-24 17:54:17 +02:00
|
|
|
ReadBufferSize: 8192,
|
|
|
|
|
WriteBufferSize: 8192,
|
2025-08-23 22:09:37 +02:00
|
|
|
},
|
2025-08-25 10:14:03 +02:00
|
|
|
broadcastChan: make(chan []byte, 2000), // Increased buffer size to handle bursts
|
2025-08-23 23:51:37 +02:00
|
|
|
stopChan: make(chan struct{}),
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
2025-08-23 23:51:37 +02:00
|
|
|
}
|
2025-08-23 22:09:37 +02:00
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// Start begins serving HTTP requests and WebSocket connections.
|
|
|
|
|
//
|
|
|
|
|
// This method starts several background routines:
|
2025-08-24 18:36:14 +02:00
|
|
|
// 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
|
2025-08-24 10:29:25 +02:00
|
|
|
//
|
|
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) Start() error {
|
|
|
|
|
// Start broadcast routine
|
|
|
|
|
go s.broadcastRoutine()
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Start periodic updates
|
|
|
|
|
go s.periodicUpdateRoutine()
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Setup routes
|
|
|
|
|
router := s.setupRoutes()
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-24 18:36:14 +02:00
|
|
|
// 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)
|
|
|
|
|
}
|
2025-08-24 18:52:39 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
s.server = &http.Server{
|
2025-08-24 18:36:14 +02:00
|
|
|
Addr: addr,
|
2025-08-23 23:51:37 +02:00
|
|
|
Handler: router,
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
return s.server.ListenAndServe()
|
|
|
|
|
}
|
2025-08-23 22:09:37 +02:00
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// Stop gracefully shuts down the server and all background routines.
|
|
|
|
|
//
|
|
|
|
|
// This method:
|
2025-08-24 18:36:14 +02:00
|
|
|
// 1. Signals all background routines to stop via stopChan
|
|
|
|
|
// 2. Shuts down the HTTP server with a 5-second timeout
|
|
|
|
|
// 3. Closes WebSocket connections
|
2025-08-24 10:29:25 +02:00
|
|
|
//
|
|
|
|
|
// The shutdown is designed to be safe and allow in-flight requests to complete.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) Stop() {
|
|
|
|
|
close(s.stopChan)
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
if s.server != nil {
|
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
|
|
|
defer cancel()
|
|
|
|
|
s.server.Shutdown(ctx)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-23 22:09:37 +02:00
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) setupRoutes() http.Handler {
|
2025-08-23 22:09:37 +02:00
|
|
|
router := mux.NewRouter()
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-24 20:20:46 +02:00
|
|
|
// Health check endpoint for load balancers/monitoring
|
|
|
|
|
router.HandleFunc("/health", s.handleHealthCheck).Methods("GET")
|
|
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// API routes
|
|
|
|
|
api := router.PathPrefix("/api").Subrouter()
|
|
|
|
|
api.HandleFunc("/aircraft", s.handleGetAircraft).Methods("GET")
|
|
|
|
|
api.HandleFunc("/aircraft/{icao}", s.handleGetAircraftDetails).Methods("GET")
|
2025-08-24 14:04:17 +02:00
|
|
|
api.HandleFunc("/debug/aircraft", s.handleDebugAircraft).Methods("GET")
|
2025-08-25 10:14:03 +02:00
|
|
|
api.HandleFunc("/debug/websocket", s.handleDebugWebSocket).Methods("GET")
|
2025-08-23 23:51:37 +02:00
|
|
|
api.HandleFunc("/sources", s.handleGetSources).Methods("GET")
|
|
|
|
|
api.HandleFunc("/stats", s.handleGetStats).Methods("GET")
|
2025-08-24 00:24:45 +02:00
|
|
|
api.HandleFunc("/origin", s.handleGetOrigin).Methods("GET")
|
2025-08-23 23:51:37 +02:00
|
|
|
api.HandleFunc("/coverage/{sourceId}", s.handleGetCoverage).Methods("GET")
|
|
|
|
|
api.HandleFunc("/heatmap/{sourceId}", s.handleGetHeatmap).Methods("GET")
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// WebSocket
|
|
|
|
|
router.HandleFunc("/ws", s.handleWebSocket)
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Static files
|
|
|
|
|
router.PathPrefix("/static/").Handler(s.staticFileHandler())
|
|
|
|
|
router.HandleFunc("/favicon.ico", s.handleFavicon)
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Main page
|
|
|
|
|
router.HandleFunc("/", s.handleIndex)
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Enable CORS
|
2025-08-23 22:09:37 +02:00
|
|
|
return s.enableCORS(router)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 14:04:17 +02:00
|
|
|
// isAircraftUseful determines if an aircraft has enough data to be useful for the frontend.
|
|
|
|
|
//
|
2025-08-24 18:36:14 +02:00
|
|
|
// 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
|
2025-08-24 14:04:17 +02:00
|
|
|
// 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
|
2025-08-24 18:36:14 +02:00
|
|
|
// - Callsign (flight identification) -> Can show in table with "No position" status
|
2025-08-24 14:04:17 +02:00
|
|
|
// - 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 != ""
|
2025-08-24 18:36:14 +02:00
|
|
|
|
2025-08-24 14:04:17 +02:00
|
|
|
// Include aircraft with any identifying or operational data
|
|
|
|
|
return hasValidPosition || hasCallsign || hasAltitude || hasSquawk
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 20:20:46 +02:00
|
|
|
// 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()
|
2025-08-24 21:56:16 +02:00
|
|
|
stats := s.addServerStats(s.merger.GetStatistics())
|
2025-08-24 20:20:46 +02:00
|
|
|
aircraft := s.merger.GetAircraft()
|
2025-08-25 10:14:03 +02:00
|
|
|
|
2025-08-24 20:20:46 +02:00
|
|
|
// Check if we have any active sources
|
|
|
|
|
activeSources := 0
|
|
|
|
|
for _, source := range sources {
|
|
|
|
|
if source.Active {
|
|
|
|
|
activeSources++
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-25 10:14:03 +02:00
|
|
|
|
2025-08-24 20:20:46 +02:00
|
|
|
// Determine health status
|
|
|
|
|
status := "healthy"
|
|
|
|
|
statusCode := http.StatusOK
|
|
|
|
|
if activeSources == 0 && len(sources) > 0 {
|
|
|
|
|
status = "degraded"
|
|
|
|
|
statusCode = http.StatusServiceUnavailable
|
|
|
|
|
}
|
2025-08-25 10:14:03 +02:00
|
|
|
|
2025-08-24 20:20:46 +02:00
|
|
|
response := map[string]interface{}{
|
2025-08-25 10:14:03 +02:00
|
|
|
"status": status,
|
2025-08-24 20:20:46 +02:00
|
|
|
"timestamp": time.Now().Unix(),
|
|
|
|
|
"sources": map[string]interface{}{
|
2025-08-25 10:14:03 +02:00
|
|
|
"total": len(sources),
|
2025-08-24 20:20:46 +02:00
|
|
|
"active": activeSources,
|
|
|
|
|
},
|
|
|
|
|
"aircraft": map[string]interface{}{
|
|
|
|
|
"count": len(aircraft),
|
|
|
|
|
},
|
|
|
|
|
}
|
2025-08-25 10:14:03 +02:00
|
|
|
|
2025-08-24 20:20:46 +02:00
|
|
|
// Add statistics if available
|
|
|
|
|
if stats != nil {
|
|
|
|
|
if totalMessages, ok := stats["total_messages"]; ok {
|
|
|
|
|
response["messages"] = totalMessages
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-25 10:14:03 +02:00
|
|
|
|
2025-08-24 20:20:46 +02:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
w.WriteHeader(statusCode)
|
|
|
|
|
json.NewEncoder(w).Encode(response)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// handleGetAircraft serves the /api/aircraft endpoint.
|
|
|
|
|
// Returns all currently tracked aircraft with their latest state information.
|
|
|
|
|
//
|
2025-08-24 14:04:17 +02:00
|
|
|
// 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.
|
|
|
|
|
//
|
2025-08-24 10:29:25 +02:00
|
|
|
// The response includes:
|
|
|
|
|
// - timestamp: Unix timestamp of the response
|
|
|
|
|
// - aircraft: Map of aircraft keyed by ICAO hex strings
|
2025-08-24 14:04:17 +02:00
|
|
|
// - count: Total number of useful aircraft (filtered count)
|
2025-08-24 10:29:25 +02:00
|
|
|
//
|
|
|
|
|
// Aircraft ICAO addresses are converted from uint32 to 6-digit hex strings
|
|
|
|
|
// for consistent JSON representation (e.g., 0xABC123 -> "ABC123").
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) handleGetAircraft(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
aircraft := s.merger.GetAircraft()
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-24 14:04:17 +02:00
|
|
|
// Convert ICAO keys to hex strings for JSON and filter useful aircraft
|
2025-08-23 23:51:37 +02:00
|
|
|
aircraftMap := make(map[string]*merger.AircraftState)
|
|
|
|
|
for icao, state := range aircraft {
|
2025-08-24 14:04:17 +02:00
|
|
|
if s.isAircraftUseful(state) {
|
|
|
|
|
aircraftMap[fmt.Sprintf("%06X", icao)] = state
|
|
|
|
|
}
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
response := map[string]interface{}{
|
|
|
|
|
"timestamp": time.Now().Unix(),
|
|
|
|
|
"aircraft": aircraftMap,
|
2025-08-24 14:04:17 +02:00
|
|
|
"count": len(aircraftMap), // Count of filtered useful aircraft
|
2025-08-23 23:51:37 +02:00
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
json.NewEncoder(w).Encode(response)
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) handleGetAircraftDetails(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
vars := mux.Vars(r)
|
|
|
|
|
icaoStr := vars["icao"]
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Parse ICAO hex string
|
|
|
|
|
icao, err := strconv.ParseUint(icaoStr, 16, 32)
|
2025-08-23 22:43:25 +02:00
|
|
|
if err != nil {
|
2025-08-23 23:51:37 +02:00
|
|
|
http.Error(w, "Invalid ICAO address", http.StatusBadRequest)
|
2025-08-23 22:43:25 +02:00
|
|
|
return
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
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)
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
2025-08-23 23:51:37 +02:00
|
|
|
}
|
2025-08-23 22:09:37 +02:00
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) handleGetSources(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
sources := s.merger.GetSources()
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 22:09:37 +02:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2025-08-23 23:51:37 +02:00
|
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
|
|
|
"sources": sources,
|
|
|
|
|
"count": len(sources),
|
|
|
|
|
})
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) handleGetStats(w http.ResponseWriter, r *http.Request) {
|
2025-08-24 21:56:16 +02:00
|
|
|
stats := s.addServerStats(s.merger.GetStatistics())
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 22:09:37 +02:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
json.NewEncoder(w).Encode(stats)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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
|
2025-08-24 00:24:45 +02:00
|
|
|
func (s *Server) handleGetOrigin(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
json.NewEncoder(w).Encode(s.origin)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) handleGetCoverage(w http.ResponseWriter, r *http.Request) {
|
2025-08-23 22:52:16 +02:00
|
|
|
vars := mux.Vars(r)
|
2025-08-23 23:51:37 +02:00
|
|
|
sourceID := vars["sourceId"]
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Generate coverage data based on signal strength
|
|
|
|
|
aircraft := s.merger.GetAircraft()
|
|
|
|
|
coveragePoints := make([]map[string]interface{}, 0)
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
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,
|
|
|
|
|
})
|
|
|
|
|
}
|
2025-08-23 22:52:16 +02:00
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 22:52:16 +02:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
2025-08-23 23:51:37 +02:00
|
|
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
|
|
|
|
"source": sourceID,
|
|
|
|
|
"points": coveragePoints,
|
|
|
|
|
})
|
2025-08-23 22:52:16 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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:
|
2025-08-24 18:36:14 +02:00
|
|
|
// 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
|
2025-08-24 10:29:25 +02:00
|
|
|
//
|
|
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) handleGetHeatmap(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
vars := mux.Vars(r)
|
|
|
|
|
sourceID := vars["sourceId"]
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Generate heatmap data grid
|
|
|
|
|
aircraft := s.merger.GetAircraft()
|
|
|
|
|
heatmapData := make(map[string]interface{})
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Simple grid-based heatmap
|
|
|
|
|
grid := make([][]float64, 100)
|
|
|
|
|
for i := range grid {
|
|
|
|
|
grid[i] = make([]float64, 100)
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Find bounds
|
|
|
|
|
minLat, maxLat := 90.0, -90.0
|
|
|
|
|
minLon, maxLon := 180.0, -180.0
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// 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)
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
if latIdx >= 0 && latIdx < 100 && lonIdx >= 0 && lonIdx < 100 {
|
|
|
|
|
grid[latIdx][lonIdx] += srcData.SignalLevel
|
|
|
|
|
}
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
heatmapData["grid"] = grid
|
|
|
|
|
heatmapData["bounds"] = map[string]float64{
|
|
|
|
|
"minLat": minLat,
|
|
|
|
|
"maxLat": maxLat,
|
|
|
|
|
"minLon": minLon,
|
|
|
|
|
"maxLon": maxLon,
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
json.NewEncoder(w).Encode(heatmapData)
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// handleWebSocket manages WebSocket connections for real-time aircraft data streaming.
|
|
|
|
|
//
|
|
|
|
|
// This handler:
|
2025-08-24 18:36:14 +02:00
|
|
|
// 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
|
2025-08-24 10:29:25 +02:00
|
|
|
//
|
|
|
|
|
// 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.
|
2025-08-23 22:09:37 +02:00
|
|
|
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()
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Register client
|
|
|
|
|
s.wsClientsMu.Lock()
|
2025-08-23 22:09:37 +02:00
|
|
|
s.wsClients[conn] = true
|
2025-08-23 23:51:37 +02:00
|
|
|
s.wsClientsMu.Unlock()
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Send initial data
|
|
|
|
|
s.sendInitialData(conn)
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Handle client messages (ping/pong)
|
2025-08-23 22:09:37 +02:00
|
|
|
for {
|
|
|
|
|
_, _, err := conn.ReadMessage()
|
|
|
|
|
if err != nil {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Unregister client
|
|
|
|
|
s.wsClientsMu.Lock()
|
|
|
|
|
delete(s.wsClients, conn)
|
|
|
|
|
s.wsClientsMu.Unlock()
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-24 21:56:16 +02:00
|
|
|
// 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
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) sendInitialData(conn *websocket.Conn) {
|
|
|
|
|
aircraft := s.merger.GetAircraft()
|
|
|
|
|
sources := s.merger.GetSources()
|
2025-08-24 21:56:16 +02:00
|
|
|
stats := s.addServerStats(s.merger.GetStatistics())
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-24 14:04:17 +02:00
|
|
|
// Convert ICAO keys to hex strings and filter useful aircraft
|
2025-08-23 23:51:37 +02:00
|
|
|
aircraftMap := make(map[string]*merger.AircraftState)
|
|
|
|
|
for icao, state := range aircraft {
|
2025-08-24 14:04:17 +02:00
|
|
|
if s.isAircraftUseful(state) {
|
|
|
|
|
aircraftMap[fmt.Sprintf("%06X", icao)] = state
|
|
|
|
|
}
|
2025-08-23 23:51:37 +02:00
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
update := AircraftUpdate{
|
|
|
|
|
Aircraft: aircraftMap,
|
|
|
|
|
Sources: sources,
|
|
|
|
|
Stats: stats,
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
msg := WebSocketMessage{
|
|
|
|
|
Type: "initial_data",
|
|
|
|
|
Timestamp: time.Now().Unix(),
|
|
|
|
|
Data: update,
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
conn.WriteJSON(msg)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// broadcastRoutine runs in a dedicated goroutine to distribute WebSocket messages.
|
|
|
|
|
//
|
|
|
|
|
// This routine:
|
|
|
|
|
// - Listens for broadcast messages on the broadcastChan
|
2025-08-25 10:14:03 +02:00
|
|
|
// - Sends messages to all connected WebSocket clients with write timeouts
|
2025-08-24 10:29:25 +02:00
|
|
|
// - Handles client connection cleanup on write errors
|
|
|
|
|
// - Respects the shutdown signal from stopChan
|
|
|
|
|
//
|
2025-08-25 10:14:03 +02:00
|
|
|
// ENHANCED: Added write timeouts and better error handling to prevent
|
|
|
|
|
// slow clients from blocking updates to other clients.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) broadcastRoutine() {
|
|
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-s.stopChan:
|
|
|
|
|
return
|
|
|
|
|
case data := <-s.broadcastChan:
|
2025-08-25 10:14:03 +02:00
|
|
|
s.wsClientsMu.Lock()
|
|
|
|
|
// Create list of clients to remove (to avoid modifying map during iteration)
|
|
|
|
|
var toRemove []*websocket.Conn
|
|
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
for conn := range s.wsClients {
|
2025-08-25 10:14:03 +02:00
|
|
|
// Set write timeout to prevent slow clients from blocking
|
|
|
|
|
conn.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
2025-08-23 23:51:37 +02:00
|
|
|
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
2025-08-25 10:14:03 +02:00
|
|
|
// Mark for removal but don't modify map during iteration
|
|
|
|
|
toRemove = append(toRemove, conn)
|
2025-08-23 23:51:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
2025-08-25 10:14:03 +02:00
|
|
|
|
|
|
|
|
// Clean up failed connections
|
|
|
|
|
for _, conn := range toRemove {
|
|
|
|
|
conn.Close()
|
|
|
|
|
delete(s.wsClients, conn)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s.wsClientsMu.Unlock()
|
2025-08-23 23:51:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-08-23 22:09:37 +02:00
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) periodicUpdateRoutine() {
|
|
|
|
|
ticker := time.NewTicker(1 * time.Second)
|
|
|
|
|
defer ticker.Stop()
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
for {
|
|
|
|
|
select {
|
|
|
|
|
case <-s.stopChan:
|
|
|
|
|
return
|
|
|
|
|
case <-ticker.C:
|
|
|
|
|
s.broadcastUpdate()
|
2025-08-23 22:09:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// broadcastUpdate creates and queues an aircraft update message for WebSocket clients.
|
|
|
|
|
//
|
|
|
|
|
// This function:
|
2025-08-24 18:36:14 +02:00
|
|
|
// 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
|
2025-08-25 10:14:03 +02:00
|
|
|
// 5. Queues the message for broadcast (blocking with timeout)
|
2025-08-24 10:29:25 +02:00
|
|
|
//
|
2025-08-25 10:14:03 +02:00
|
|
|
// IMPORTANT: Changed from non-blocking to blocking with timeout to prevent
|
|
|
|
|
// dropping aircraft track updates when the channel is busy.
|
2025-08-23 23:51:37 +02:00
|
|
|
func (s *Server) broadcastUpdate() {
|
|
|
|
|
aircraft := s.merger.GetAircraft()
|
|
|
|
|
sources := s.merger.GetSources()
|
2025-08-24 21:56:16 +02:00
|
|
|
stats := s.addServerStats(s.merger.GetStatistics())
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-24 14:04:17 +02:00
|
|
|
// Convert ICAO keys to hex strings and filter useful aircraft
|
2025-08-23 23:51:37 +02:00
|
|
|
aircraftMap := make(map[string]*merger.AircraftState)
|
|
|
|
|
for icao, state := range aircraft {
|
2025-08-24 14:04:17 +02:00
|
|
|
if s.isAircraftUseful(state) {
|
|
|
|
|
aircraftMap[fmt.Sprintf("%06X", icao)] = state
|
|
|
|
|
}
|
2025-08-23 23:51:37 +02:00
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
update := AircraftUpdate{
|
|
|
|
|
Aircraft: aircraftMap,
|
|
|
|
|
Sources: sources,
|
|
|
|
|
Stats: stats,
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
msg := WebSocketMessage{
|
|
|
|
|
Type: "aircraft_update",
|
|
|
|
|
Timestamp: time.Now().Unix(),
|
|
|
|
|
Data: update,
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
if data, err := json.Marshal(msg); err == nil {
|
2025-08-25 10:14:03 +02:00
|
|
|
// Use timeout to prevent indefinite blocking while ensuring updates aren't dropped
|
|
|
|
|
timeout := time.After(100 * time.Millisecond)
|
2025-08-23 23:51:37 +02:00
|
|
|
select {
|
|
|
|
|
case s.broadcastChan <- data:
|
2025-08-25 10:14:03 +02:00
|
|
|
// Successfully queued
|
|
|
|
|
case <-timeout:
|
|
|
|
|
// Log dropped updates for debugging
|
|
|
|
|
log.Printf("WARNING: Broadcast channel full, dropping update with %d aircraft", len(aircraftMap))
|
2025-08-23 23:51:37 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
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
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
w.Header().Set("Content-Type", "text/html")
|
|
|
|
|
w.Write(data)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
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
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
w.Header().Set("Content-Type", "image/x-icon")
|
|
|
|
|
w.Write(data)
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 22:14:19 +02:00
|
|
|
func (s *Server) staticFileHandler() http.Handler {
|
|
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
2025-08-23 23:57:32 +02:00
|
|
|
// Remove /static/ prefix from URL path to get the actual file path
|
|
|
|
|
filePath := "static" + r.URL.Path[len("/static"):]
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 22:14:19 +02:00
|
|
|
data, err := s.staticFiles.ReadFile(filePath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
http.NotFound(w, r)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Set content type
|
2025-08-23 22:14:19 +02:00
|
|
|
ext := path.Ext(filePath)
|
2025-08-23 23:51:37 +02:00
|
|
|
contentType := getContentType(ext)
|
2025-08-23 22:14:19 +02:00
|
|
|
w.Header().Set("Content-Type", contentType)
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 23:51:37 +02:00
|
|
|
// Cache control
|
|
|
|
|
w.Header().Set("Cache-Control", "public, max-age=3600")
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 22:14:19 +02:00
|
|
|
w.Write(data)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 23:51:37 +02:00
|
|
|
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"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-08-24 10:29:25 +02:00
|
|
|
// 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.
|
2025-08-23 22:09:37 +02:00
|
|
|
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")
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 22:09:37 +02:00
|
|
|
if r.Method == "OPTIONS" {
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-08-24 00:57:49 +02:00
|
|
|
|
2025-08-23 22:09:37 +02:00
|
|
|
handler.ServeHTTP(w, r)
|
|
|
|
|
})
|
2025-08-24 00:57:49 +02:00
|
|
|
}
|
2025-08-24 14:04:17 +02:00
|
|
|
|
|
|
|
|
// 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{}{
|
2025-08-24 18:36:14 +02:00
|
|
|
"timestamp": time.Now().Unix(),
|
|
|
|
|
"all_aircraft": allAircraftMap,
|
2025-08-24 14:04:17 +02:00
|
|
|
"filtered_aircraft": filteredAircraftMap,
|
2025-08-24 18:36:14 +02:00
|
|
|
"all_count": len(allAircraftMap),
|
|
|
|
|
"filtered_count": len(filteredAircraftMap),
|
2025-08-24 14:04:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
|
json.NewEncoder(w).Encode(response)
|
|
|
|
|
}
|
2025-08-25 10:14:03 +02:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|