diff options
| author | kboz <kutayboz@gmail.com> | 2026-09-27 22:36:34 +0200 |
|---|---|---|
| committer | kboz <kutayboz@gmail.com> | 2026-09-27 22:36:34 +0200 |
| commit | 1b59961ce2385252e3346ea6107b15a2c1feafe6 (patch) | |
| tree | fbe9e0cb968c47cff353e937e4dc835a64058c60 /backend-go/cmd | |
| parent | 29c4b57db9a34e0f815d93cac6a2cae543778a93 (diff) | |
| download | haul-1b59961ce2385252e3346ea6107b15a2c1feafe6.tar.gz | |
feat(backend): port route-planning backend to Go
Add a Go implementation mirroring the Python pipeline (loader, graph,
pathfinder, arbitrage, market, route, manager) under internal/core, with
ESI client/API integration and setup endpoints. Reuses backend/src as
the working directory so existing data/cache paths keep working.
Diffstat (limited to 'backend-go/cmd')
| -rw-r--r-- | backend-go/cmd/eve_haul_go/main.go | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/backend-go/cmd/eve_haul_go/main.go b/backend-go/cmd/eve_haul_go/main.go new file mode 100644 index 0000000..12f9b00 --- /dev/null +++ b/backend-go/cmd/eve_haul_go/main.go @@ -0,0 +1,306 @@ +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} +} |
