package server import ( "context" "embed" "encoding/json" "log" "net/http" "sync" "github.com/gorilla/mux" "github.com/gorilla/websocket" "skyview/internal/client" "skyview/internal/config" "skyview/internal/parser" ) type Server struct { config *config.Config staticFiles embed.FS upgrader websocket.Upgrader wsClients map[*websocket.Conn]bool wsClientsMux sync.RWMutex dump1090 *client.Dump1090Client ctx context.Context } type WebSocketMessage struct { Type string `json:"type"` Data interface{} `json:"data"` } func New(cfg *config.Config, staticFiles embed.FS, ctx context.Context) http.Handler { s := &Server{ config: cfg, staticFiles: staticFiles, upgrader: websocket.Upgrader{ CheckOrigin: func(r *http.Request) bool { return true }, }, wsClients: make(map[*websocket.Conn]bool), dump1090: client.NewDump1090Client(cfg), ctx: ctx, } if err := s.dump1090.Start(ctx); err != nil { log.Printf("Failed to start dump1090 client: %v", err) } go s.subscribeToAircraftUpdates() router := mux.NewRouter() router.HandleFunc("/", s.serveIndex).Methods("GET") router.HandleFunc("/ws", s.handleWebSocket).Methods("GET") apiRouter := router.PathPrefix("/api").Subrouter() apiRouter.HandleFunc("/aircraft", s.getAircraft).Methods("GET") apiRouter.HandleFunc("/stats", s.getStats).Methods("GET") apiRouter.HandleFunc("/config", s.getConfig).Methods("GET") router.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(s.staticFiles)))) return s.enableCORS(router) } func (s *Server) serveIndex(w http.ResponseWriter, r *http.Request) { data, err := s.staticFiles.ReadFile("static/index.html") if err != nil { http.Error(w, "Failed to read index.html", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/html") w.Write(data) } func (s *Server) getAircraft(w http.ResponseWriter, r *http.Request) { data := s.dump1090.GetAircraftData() response := map[string]interface{}{ "now": data.Now, "messages": data.Messages, "aircraft": s.aircraftMapToSlice(data.Aircraft), } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) } func (s *Server) getStats(w http.ResponseWriter, r *http.Request) { data := s.dump1090.GetAircraftData() stats := map[string]interface{}{ "total": map[string]interface{}{ "aircraft": len(data.Aircraft), "messages": map[string]interface{}{ "total": data.Messages, "last1min": data.Messages, }, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(stats) } func (s *Server) getConfig(w http.ResponseWriter, r *http.Request) { configData := map[string]interface{}{ "origin": map[string]interface{}{ "latitude": s.config.Origin.Latitude, "longitude": s.config.Origin.Longitude, "name": s.config.Origin.Name, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(configData) } func (s *Server) aircraftMapToSlice(aircraftMap map[string]parser.Aircraft) []parser.Aircraft { aircraft := make([]parser.Aircraft, 0, len(aircraftMap)) for _, a := range aircraftMap { aircraft = append(aircraft, a) } return aircraft } func (s *Server) subscribeToAircraftUpdates() { updates := s.dump1090.Subscribe() for data := range updates { message := WebSocketMessage{ Type: "aircraft_update", Data: map[string]interface{}{ "now": data.Now, "messages": data.Messages, "aircraft": s.aircraftMapToSlice(data.Aircraft), }, } s.broadcastToWebSocketClients(message) } } 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() s.wsClientsMux.Lock() s.wsClients[conn] = true s.wsClientsMux.Unlock() defer func() { s.wsClientsMux.Lock() delete(s.wsClients, conn) s.wsClientsMux.Unlock() }() data := s.dump1090.GetAircraftData() initialMessage := WebSocketMessage{ Type: "aircraft_update", Data: map[string]interface{}{ "now": data.Now, "messages": data.Messages, "aircraft": s.aircraftMapToSlice(data.Aircraft), }, } conn.WriteJSON(initialMessage) for { _, _, err := conn.ReadMessage() if err != nil { break } } } func (s *Server) broadcastToWebSocketClients(message WebSocketMessage) { s.wsClientsMux.RLock() defer s.wsClientsMux.RUnlock() for client := range s.wsClients { if err := client.WriteJSON(message); err != nil { client.Close() delete(s.wsClients, client) } } } 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) }) }