package main import ( "encoding/json" "fmt" "net/http" "os" "sync" "time" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" ) type HTTPServer struct { Router *mux.Router FlexRepo *FlexDXClusterRepository ContactRepo *Log4OMContactsRepository TCPServer *TCPServer TCPClient *TCPClient FlexClient *FlexClient Port string Log *log.Logger statsCache Stats statsMutex sync.RWMutex lastUpdate time.Time } type Stats struct { TotalSpots int `json:"totalSpots"` ActiveSpotters int `json:"activeSpotters"` NewDXCC int `json:"newDXCC"` ConnectedClients int `json:"connectedClients"` TotalContacts int `json:"totalContacts"` ClusterStatus string `json:"clusterStatus"` FlexStatus string `json:"flexStatus"` MyCallsign string `json:"myCallsign"` Mode string `json:"mode"` Filters Filters `json:"filters"` } type Filters struct { Skimmer bool `json:"skimmer"` FT8 bool `json:"ft8"` FT4 bool `json:"ft4"` } type APIResponse struct { Success bool `json:"success"` Data interface{} `json:"data,omitempty"` Error string `json:"error,omitempty"` Message string `json:"message,omitempty"` } type SendCallsignRequest struct { Callsign string `json:"callsign"` } func NewHTTPServer(flexRepo *FlexDXClusterRepository, contactRepo *Log4OMContactsRepository, tcpServer *TCPServer, tcpClient *TCPClient, flexClient *FlexClient, port string) *HTTPServer { server := &HTTPServer{ Router: mux.NewRouter(), FlexRepo: flexRepo, ContactRepo: contactRepo, TCPServer: tcpServer, TCPClient: tcpClient, FlexClient: flexClient, Port: port, Log: Log, } server.setupRoutes() return server } func (s *HTTPServer) setupRoutes() { // Enable CORS s.Router.Use(s.corsMiddleware) // API Routes api := s.Router.PathPrefix("/api").Subrouter() api.HandleFunc("/stats", s.getStats).Methods("GET", "OPTIONS") api.HandleFunc("/spots", s.getSpots).Methods("GET", "OPTIONS") api.HandleFunc("/spots/{id}", s.getSpotByID).Methods("GET", "OPTIONS") api.HandleFunc("/spotters", s.getTopSpotters).Methods("GET", "OPTIONS") api.HandleFunc("/contacts", s.getContacts).Methods("GET", "OPTIONS") api.HandleFunc("/send-command", s.sendCommand).Methods("POST", "OPTIONS") api.HandleFunc("/filters", s.updateFilters).Methods("POST", "OPTIONS") api.HandleFunc("/shutdown", s.shutdownApp).Methods("POST", "OPTIONS") api.HandleFunc("/send-callsign", s.handleSendCallsign).Methods("POST", "OPTIONS") // Serve static files (dashboard) s.Router.PathPrefix("/").Handler(http.FileServer(http.Dir("./static"))) } func (s *HTTPServer) corsMiddleware(next 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 } next.ServeHTTP(w, r) }) } func (s *HTTPServer) getStats(w http.ResponseWriter, r *http.Request) { // Cache stats for 5 seconds to avoid overwhelming the database s.statsMutex.RLock() if time.Since(s.lastUpdate) < 5*time.Second { stats := s.statsCache s.statsMutex.RUnlock() s.sendJSON(w, APIResponse{Success: true, Data: stats}) return } s.statsMutex.RUnlock() // Calculate fresh stats s.statsMutex.Lock() defer s.statsMutex.Unlock() allSpots := s.FlexRepo.GetAllSpots("0") spotters := s.FlexRepo.GetSpotters() contacts := s.ContactRepo.CountEntries() newDXCCCount := 0 for _, spot := range allSpots { if spot.NewDXCC { newDXCCCount++ } } clusterStatus := "disconnected" if s.TCPClient != nil && s.TCPClient.LoggedIn { clusterStatus = "connected" } flexStatus := "disconnected" if s.FlexClient != nil && s.FlexClient.IsConnected { flexStatus = "connected" } s.statsCache = Stats{ TotalSpots: len(allSpots), ActiveSpotters: len(spotters), NewDXCC: newDXCCCount, ConnectedClients: len(s.TCPServer.Clients), TotalContacts: contacts, ClusterStatus: clusterStatus, FlexStatus: flexStatus, MyCallsign: Cfg.General.Callsign, // Nouveau Filters: Filters{ Skimmer: Cfg.Cluster.Skimmer, FT8: Cfg.Cluster.FT8, FT4: Cfg.Cluster.FT4, }, } s.lastUpdate = time.Now() s.sendJSON(w, APIResponse{Success: true, Data: s.statsCache}) } func (s *HTTPServer) getSpots(w http.ResponseWriter, r *http.Request) { limitStr := r.URL.Query().Get("limit") if limitStr == "" { limitStr = "50" } spots := s.FlexRepo.GetAllSpots(limitStr) s.sendJSON(w, APIResponse{Success: true, Data: spots}) } func (s *HTTPServer) getSpotByID(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) id := vars["id"] spot, err := s.FlexRepo.FindSpotByFlexSpotNumber(id) if err != nil { s.sendJSON(w, APIResponse{Success: false, Error: "Spot not found"}) return } s.sendJSON(w, APIResponse{Success: true, Data: spot}) } func (s *HTTPServer) getTopSpotters(w http.ResponseWriter, r *http.Request) { spotters := s.FlexRepo.GetSpotters() s.sendJSON(w, APIResponse{Success: true, Data: spotters}) } func (s *HTTPServer) getContacts(w http.ResponseWriter, r *http.Request) { count := s.ContactRepo.CountEntries() data := map[string]interface{}{ "totalContacts": count, } s.sendJSON(w, APIResponse{Success: true, Data: data}) } type CommandRequest struct { Command string `json:"command"` } func (s *HTTPServer) sendCommand(w http.ResponseWriter, r *http.Request) { var req CommandRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendJSON(w, APIResponse{Success: false, Error: "Invalid request"}) return } if req.Command == "" { s.sendJSON(w, APIResponse{Success: false, Error: "Command is required"}) return } // Send command to cluster via TCPClient s.TCPClient.CmdChan <- req.Command s.Log.Infof("Command sent via HTTP API: %s", req.Command) s.sendJSON(w, APIResponse{Success: true, Data: map[string]string{"message": "Command sent successfully"}}) } type FilterRequest struct { Skimmer *bool `json:"skimmer,omitempty"` FT8 *bool `json:"ft8,omitempty"` FT4 *bool `json:"ft4,omitempty"` } func (s *HTTPServer) updateFilters(w http.ResponseWriter, r *http.Request) { var req FilterRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendJSON(w, APIResponse{Success: false, Error: "Invalid request"}) return } commands := []string{} if req.Skimmer != nil { if *req.Skimmer { commands = append(commands, "set/skimmer") Cfg.Cluster.Skimmer = true } else { commands = append(commands, "set/noskimmer") Cfg.Cluster.Skimmer = false } } if req.FT8 != nil { if *req.FT8 { commands = append(commands, "set/ft8") Cfg.Cluster.FT8 = true } else { commands = append(commands, "set/noft8") Cfg.Cluster.FT8 = false } } if req.FT4 != nil { if *req.FT4 { commands = append(commands, "set/ft4") Cfg.Cluster.FT4 = true } else { commands = append(commands, "set/noft4") Cfg.Cluster.FT4 = false } } for _, cmd := range commands { s.TCPClient.CmdChan <- cmd } s.sendJSON(w, APIResponse{Success: true, Data: map[string]string{"message": "Filters updated successfully"}}) } func (s *HTTPServer) handleSendCallsign(w http.ResponseWriter, r *http.Request) { var req struct { Callsign string `json:"callsign"` Frequency string `json:"frequency"` Mode string `json:"mode"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { s.sendJSON(w, APIResponse{Success: false, Error: "Invalid request"}) return } if req.Callsign == "" { s.sendJSON(w, APIResponse{Success: false, Error: "Callsign is required"}) return } // Envoyer l'indicatif à Log4OM via UDP SendUDPMessage("" + req.Callsign) s.Log.Infof("Sent callsign %s to Log4OM via UDP (127.0.0.1:2241)", req.Callsign) // Tuner la radio Flex sur la fréquence si elle est fournie if req.Frequency != "" && s.FlexClient != nil && s.FlexClient.IsConnected { // Commande TUNE pour le slice 0 selon l'API FlexRadio tuneCmd := fmt.Sprintf("C%v|slice tune 0 %s", CommandNumber, req.Frequency) s.FlexClient.Write(tuneCmd) CommandNumber++ time.Sleep(time.Millisecond * 500) modeCmd := fmt.Sprintf("C%v|slice s 0 mode=%s", CommandNumber, req.Mode) s.FlexClient.Write(modeCmd) CommandNumber++ s.Log.Infof("Sent TUNE command to Flex: %s", tuneCmd) } s.sendJSON(w, APIResponse{ Success: true, Message: "Callsign sent to Log4OM and radio tuned", Data: map[string]string{"callsign": req.Callsign, "frequency": req.Frequency}, }) } func (s *HTTPServer) sendJSON(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) } func (s *HTTPServer) shutdownApp(w http.ResponseWriter, r *http.Request) { s.Log.Info("Shutdown request received from dashboard") s.sendJSON(w, APIResponse{Success: true, Data: map[string]string{"message": "Shutting down FlexDXCluster"}}) // Shutdown gracefully after a short delay to allow response to be sent go func() { time.Sleep(500 * time.Millisecond) s.Log.Info("Initiating shutdown...") os.Exit(0) }() } func (s *HTTPServer) Start() { s.Log.Infof("HTTP Server starting on port %s", s.Port) s.Log.Infof("Dashboard available at http://localhost:%s", s.Port) if err := http.ListenAndServe(":"+s.Port, s.Router); err != nil { s.Log.Fatalf("Failed to start HTTP server: %v", err) } }