package main import ( "context" "encoding/json" "fmt" "log" "net/http" "os" "os/signal" "syscall" "strings" "time" "github.com/joho/godotenv" "eve_haul_go/internal/api" "eve_haul_go/internal/core" "eve_haul_go/internal/esi" ) type EventStreamData struct { Route []core.RouteStep `json:"route"` RouteInfo *core.RouteInfo `json:"route_info"` Log []string `json:"log"` } func main() { if err := godotenv.Overload("../.env"); err != nil { fmt.Printf("Error loading .env file: %v\n", err) } log.SetFlags(log.LstdFlags | log.Lmicroseconds) os.Chdir("../backend/src") // Match Python's working directory so data/cache works // Check if setup is needed dataReady, authReady := isSetupReady() var manager *core.Manager var managerReady bool = false if dataReady && authReady { log.Println("Prerequisites found. Initializing Loader...") dl := core.NewDataLoader("../data") items, err := dl.LoadItems() if err != nil { log.Fatalf("Error loading items: %v", err) } log.Printf("Loaded %d items", len(items)) stations, solarSystems, err := dl.LoadMapData() if err != nil { log.Fatalf("Error loading map data: %v", err) } log.Printf("Loaded %d stations", len(stations)) jumps, err := dl.LoadStarGateConnections() if err != nil { log.Fatalf("Error loading stargate jumps: %v", err) } ship := &core.Ship{ MaxWarpSpeed: 4.55, MaxSubwarpSpeed: 166.9, PlayerCost: 15000000.0 / 3600.0, RiskCost: 60000000.0, GankersAreas: map[string]float64{"Ahbazon": 0.5}, TaxRate: 8.0, ProfitThreshold: 100000.0, MaxCapital: 20000000, SnipeThreshold: 20000000.0, Cargo: 4000, } graph := core.NewGraph(stations, solarSystems, ship) graph.Build(jumps) log.Printf("Graph built with %d nodes", len(graph.Adj)) if os.Getenv("character_id") == "" { log.Fatalf("character_id missing in .env but auth present") } client := esi.NewEsiClient("./cache/auth.json") manager = core.NewManager(client, graph) market, err := core.NewMarket(items, client) if err == nil { manager.Market = market } else { log.Printf("Warning: Failed to init market: %v", err) } go func() { for { log.Println("Starting manager update") if err := manager.Update(context.Background()); err != nil { log.Printf("An error occurred during manager update: %v", err) } log.Println("Manager update completed") time.Sleep(2 * time.Second) } }() managerReady = true } else { log.Println("Missing data or auth. Starting in setup mode...") } mux := httpNewServeOptionsMux(manager, managerReady) server := &http.Server{ Addr: ":8000", Handler: mux, } go func() { log.Println("Listening on :8000") if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("Server failed: %v", err) } }() quit := make(chan os.Signal, 1) signal.Notify(quit, os.Interrupt, syscall.SIGTERM) <-quit log.Println("Shutting down server gracefully...") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { log.Printf("Server forced to shutdown: %v", err) os.Exit(0) } log.Println("Server exited") } func isSetupReady() (bool, bool) { dataReady := true authReady := true if _, err := os.Stat("../data/mapDenormalize.csv"); err != nil { dataReady = false } if _, err := os.Stat("../data/mapJumps.csv"); err != nil { dataReady = false } if _, err := os.Stat("../data/invTypes.csv"); err != nil { dataReady = false } if _, err := os.Stat("../data/mapRegions.csv"); err != nil { dataReady = false } if _, err := os.Stat("./cache/auth.json"); err != nil { authReady = false } return dataReady, authReady } type serveMuxCORS struct { mux *http.ServeMux } func (s *serveMuxCORS) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "http://localhost:3000") w.Header().Set("Access-Control-Allow-Credentials", "true") 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 } s.mux.ServeHTTP(w, r) } func httpNewServeOptionsMux(manager *core.Manager, ready bool) http.Handler { mux := http.NewServeMux() // Setup Routes mux.HandleFunc("/api/setup/status", api.StatusHandler) mux.HandleFunc("/api/setup/data", api.DownloadDataHandler) mux.HandleFunc("/api/setup/auth/url", api.GetAuthURLHandler) mux.HandleFunc("/api/setup/auth/callback", api.CompleteAuthHandler) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(map[string]bool{"ready": ready}) }) if !ready { // Degraded mode routes mux.HandleFunc("/data_stream", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") fmt.Fprintf(w, "data: %s\n\n", `{"route": [], "route_info": null, "log": ["System pending setup."]}`) w.(http.Flusher).Flush() }) return &serveMuxCORS{mux: mux} } mux.HandleFunc("/route", func(w http.ResponseWriter, r *http.Request) { if manager.Route == nil { json.NewEncoder(w).Encode([]core.RouteStep{}) return } json.NewEncoder(w).Encode(manager.Route) }) mux.HandleFunc("/route_info", func(w http.ResponseWriter, r *http.Request) { if manager.RouteInfo == nil { json.NewEncoder(w).Encode(nil) return } json.NewEncoder(w).Encode(manager.RouteInfo) }) mux.HandleFunc("/create_route", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } go manager.CreateRoute(context.Background()) json.NewEncoder(w).Encode(map[string]string{"status": "Route creation initiated"}) }) mux.HandleFunc("/data_stream", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/event-stream") w.Header().Set("Cache-Control", "no-cache") w.Header().Set("Connection", "keep-alive") flusher, ok := w.(http.Flusher) if !ok { http.Error(w, "Streaming unsupported", http.StatusInternalServerError) return } for { data := EventStreamData{ Route: manager.Route, RouteInfo: manager.RouteInfo, Log: manager.Log, } b, _ := json.Marshal(data) fmt.Fprintf(w, "data: %s\n\n", string(b)) flusher.Flush() select { case <-r.Context().Done(): return case <-time.After(1 * time.Second): } } }) mux.HandleFunc("/logs", func(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(manager.Log) }) mux.HandleFunc("/open_market_window/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } parts := strings.Split(r.URL.Path, "/") if len(parts) > 2 { typeID := parts[2] _ = typeID log.Printf("Opened market window for type_id %s", typeID) } json.NewEncoder(w).Encode(map[string]string{"status": "opened"}) }) mux.HandleFunc("/settings", func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { json.NewEncoder(w).Encode(manager.Ship) return } if r.Method == http.MethodPost { var newSettings core.Ship if err := json.NewDecoder(r.Body).Decode(&newSettings); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } manager.Ship = newSettings manager.Graph.Ship = &manager.Ship json.NewEncoder(w).Encode(map[string]interface{}{ "status": "success", "settings": manager.Ship, }) return } http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) }) mux.HandleFunc("/set_waypoints", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } err := manager.SetWaypoints() if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } json.NewEncoder(w).Encode(map[string]string{"status": "success"}) }) return &serveMuxCORS{mux: mux} }