feat(esi): improve ESI SSO login flow and system name resolution
This commit introduces several improvements to the ESI (EVE Server Interface) Single Sign-On (SSO) flow and system name resolution: **ESI SSO Login Flow:** - **Asynchronous Callback Server:** The `StartCallbackServer` function is now deprecated in favor of `StartCallbackServerAsync`. This allows the callback server to run in the background without blocking the main application thread, improving responsiveness. - **Improved Login Status Polling:** After initiating the ESI login, the frontend now polls the `ESILoggedIn` status for a short period. This ensures that the UI reflects the login status more accurately and promptly after the user completes the authentication flow in their browser. - **Error Handling:** Added more specific error messages for failed token exchanges and invalid SSO responses. **System Name Resolution:** - **Multi-stage Resolution:** The `ResolveSystemIDByName` function now employs a more robust, multi-stage approach to find system IDs: 1. It first attempts to use the `universe/ids` endpoint for direct name-to-ID mapping, which is generally more accurate. 2. If that fails, it falls back to a `strict` search via the `search` endpoint. 3. As a final fallback, it performs a non-strict search and then resolves the names of the returned IDs to find an exact case-insensitive match. If no exact match is found, it returns the first result. - **Logging:** Added more detailed logging for each stage of the system name resolution process, aiding in debugging. - **ESI API Headers:** Ensured that necessary headers like `Accept` and `X-User-Agent` are correctly set for ESI API requests. **Frontend Changes:** - **Import `ESILoggedIn`:** The `ESILoggedIn` function is now imported into the `Header.tsx` component. - **Updated Toast Message:** The toast message for setting a destination now includes the system name for better context in case of errors.
This commit is contained in:
		
							
								
								
									
										12
									
								
								app.go
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								app.go
									
									
									
									
									
								
							@@ -51,7 +51,9 @@ func (a *App) StartESILogin() (string, error) {
 | 
				
			|||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return "", err
 | 
							return "", err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	go func() { _ = a.ssi.StartCallbackServer() }()
 | 
						if err := a.ssi.StartCallbackServerAsync(); err != nil {
 | 
				
			||||||
 | 
							return "", err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
	runtime.BrowserOpenURL(a.ctx, url)
 | 
						runtime.BrowserOpenURL(a.ctx, url)
 | 
				
			||||||
	return url, nil
 | 
						return url, nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -68,6 +70,14 @@ func (a *App) ESILoginStatus() string {
 | 
				
			|||||||
	return "not logged in"
 | 
						return "not logged in"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// ESILoggedIn returns true if a valid access token is present
 | 
				
			||||||
 | 
					func (a *App) ESILoggedIn() bool {
 | 
				
			||||||
 | 
						if a.ssi == nil {
 | 
				
			||||||
 | 
							return false
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return a.ssi.Status().LoggedIn
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// SetDestination posts a waypoint to ESI to set destination
 | 
					// SetDestination posts a waypoint to ESI to set destination
 | 
				
			||||||
func (a *App) SetDestination(destinationID int64, clearOthers bool, addToBeginning bool) error {
 | 
					func (a *App) SetDestination(destinationID int64, clearOthers bool, addToBeginning bool) error {
 | 
				
			||||||
	if a.ssi == nil {
 | 
						if a.ssi == nil {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										192
									
								
								esi_sso.go
									
									
									
									
									
								
							
							
						
						
									
										192
									
								
								esi_sso.go
									
									
									
									
									
								
							@@ -84,8 +84,62 @@ func (s *ESISSO) BuildAuthorizeURL() (string, error) {
 | 
				
			|||||||
	return issuerAuthorizeURL + "?" + q.Encode(), nil
 | 
						return issuerAuthorizeURL + "?" + q.Encode(), nil
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// StartCallbackServer starts a temporary local HTTP server to receive the SSO callback
 | 
					// StartCallbackServerAsync starts the callback server in the background and returns immediately
 | 
				
			||||||
func (s *ESISSO) StartCallbackServer() error {
 | 
					func (s *ESISSO) StartCallbackServerAsync() error {
 | 
				
			||||||
 | 
						u, err := url.Parse(s.redirectURI)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if u.Scheme != "http" && u.Scheme != "https" {
 | 
				
			||||||
 | 
							return errors.New("redirect URI must be http(s)")
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						hostPort := u.Host
 | 
				
			||||||
 | 
						if !strings.Contains(hostPort, ":") {
 | 
				
			||||||
 | 
							if u.Scheme == "https" {
 | 
				
			||||||
 | 
								hostPort += ":443"
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								hostPort += ":80"
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						mux := http.NewServeMux()
 | 
				
			||||||
 | 
						mux.HandleFunc(u.Path, func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
 | 
							if r.Method != http.MethodGet {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusMethodNotAllowed)
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							q := r.URL.Query()
 | 
				
			||||||
 | 
							code := q.Get("code")
 | 
				
			||||||
 | 
							st := q.Get("state")
 | 
				
			||||||
 | 
							if code == "" || st == "" || st != s.state {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusBadRequest)
 | 
				
			||||||
 | 
								_, _ = w.Write([]byte("Invalid SSO response"))
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if err := s.exchangeToken(r.Context(), code); err != nil {
 | 
				
			||||||
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
 | 
								_, _ = w.Write([]byte("Token exchange failed: " + err.Error()))
 | 
				
			||||||
 | 
								return
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							_, _ = io.WriteString(w, "Login successful. You can close this window.")
 | 
				
			||||||
 | 
							go func() {
 | 
				
			||||||
 | 
								time.Sleep(200 * time.Millisecond)
 | 
				
			||||||
 | 
								_ = s.server.Shutdown(context.Background())
 | 
				
			||||||
 | 
							}()
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						ln, err := net.Listen("tcp", hostPort)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						s.server = &http.Server{Handler: mux}
 | 
				
			||||||
 | 
						go func() { _ = s.server.Serve(ln) }()
 | 
				
			||||||
 | 
						return nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Deprecated: blocking start; prefer StartCallbackServerAsync
 | 
				
			||||||
 | 
					func (s *ESISSO) StartCallbackServer() error {
 | 
				
			||||||
	u, err := url.Parse(s.redirectURI)
 | 
						u, err := url.Parse(s.redirectURI)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
@@ -93,10 +147,8 @@ func (s *ESISSO) StartCallbackServer() error {
 | 
				
			|||||||
	if u.Scheme != "http" && u.Scheme != "https" {
 | 
						if u.Scheme != "http" && u.Scheme != "https" {
 | 
				
			||||||
		return errors.New("redirect URI must be http(s)")
 | 
							return errors.New("redirect URI must be http(s)")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	// Bind exact host:port
 | 
					 | 
				
			||||||
	hostPort := u.Host
 | 
						hostPort := u.Host
 | 
				
			||||||
	if !strings.Contains(hostPort, ":") {
 | 
						if !strings.Contains(hostPort, ":") {
 | 
				
			||||||
		// default ports
 | 
					 | 
				
			||||||
		if u.Scheme == "https" {
 | 
							if u.Scheme == "https" {
 | 
				
			||||||
			hostPort += ":443"
 | 
								hostPort += ":443"
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
@@ -106,7 +158,6 @@ func (s *ESISSO) StartCallbackServer() error {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	mux := http.NewServeMux()
 | 
						mux := http.NewServeMux()
 | 
				
			||||||
	mux.HandleFunc(u.Path, func(w http.ResponseWriter, r *http.Request) {
 | 
						mux.HandleFunc(u.Path, func(w http.ResponseWriter, r *http.Request) {
 | 
				
			||||||
		// Receive code
 | 
					 | 
				
			||||||
		if r.Method != http.MethodGet {
 | 
							if r.Method != http.MethodGet {
 | 
				
			||||||
			w.WriteHeader(http.StatusMethodNotAllowed)
 | 
								w.WriteHeader(http.StatusMethodNotAllowed)
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
@@ -119,7 +170,6 @@ func (s *ESISSO) StartCallbackServer() error {
 | 
				
			|||||||
			_, _ = w.Write([]byte("Invalid SSO response"))
 | 
								_, _ = w.Write([]byte("Invalid SSO response"))
 | 
				
			||||||
			return
 | 
								return
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		// Exchange token
 | 
					 | 
				
			||||||
		if err := s.exchangeToken(r.Context(), code); err != nil {
 | 
							if err := s.exchangeToken(r.Context(), code); err != nil {
 | 
				
			||||||
			w.WriteHeader(http.StatusInternalServerError)
 | 
								w.WriteHeader(http.StatusInternalServerError)
 | 
				
			||||||
			_, _ = w.Write([]byte("Token exchange failed: " + err.Error()))
 | 
								_, _ = w.Write([]byte("Token exchange failed: " + err.Error()))
 | 
				
			||||||
@@ -127,7 +177,6 @@ func (s *ESISSO) StartCallbackServer() error {
 | 
				
			|||||||
		}
 | 
							}
 | 
				
			||||||
		_, _ = io.WriteString(w, "Login successful. You can close this window.")
 | 
							_, _ = io.WriteString(w, "Login successful. You can close this window.")
 | 
				
			||||||
		go func() {
 | 
							go func() {
 | 
				
			||||||
			// stop shortly after responding
 | 
					 | 
				
			||||||
			time.Sleep(200 * time.Millisecond)
 | 
								time.Sleep(200 * time.Millisecond)
 | 
				
			||||||
			_ = s.server.Shutdown(context.Background())
 | 
								_ = s.server.Shutdown(context.Background())
 | 
				
			||||||
		}()
 | 
							}()
 | 
				
			||||||
@@ -262,14 +311,17 @@ func (s *ESISSO) PostWaypoint(destinationID int64, clearOthers bool, addToBeginn
 | 
				
			|||||||
	q.Set("destination_id", strconv.FormatInt(destinationID, 10))
 | 
						q.Set("destination_id", strconv.FormatInt(destinationID, 10))
 | 
				
			||||||
	q.Set("add_to_beginning", strconv.FormatBool(addToBeginning))
 | 
						q.Set("add_to_beginning", strconv.FormatBool(addToBeginning))
 | 
				
			||||||
	q.Set("clear_other_waypoints", strconv.FormatBool(clearOthers))
 | 
						q.Set("clear_other_waypoints", strconv.FormatBool(clearOthers))
 | 
				
			||||||
	endpoint := esiBase + "/v2/ui/autopilot/waypoint/" + "?" + q.Encode()
 | 
						q.Set("datasource", "tranquility")
 | 
				
			||||||
 | 
						endpoint := esiBase + "/v2/ui/autopilot/waypoint?" + q.Encode()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fmt.Printf("ESI: POST waypoint dest=%d clear=%v addToBeginning=%v\n", destinationID, clearOthers, addToBeginning)
 | 
				
			||||||
	req, err := http.NewRequest(http.MethodPost, endpoint, nil)
 | 
						req, err := http.NewRequest(http.MethodPost, endpoint, nil)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return err
 | 
							return err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	req.Header.Set("Authorization", "Bearer "+tok)
 | 
						req.Header.Set("Authorization", "Bearer "+tok)
 | 
				
			||||||
	req.Header.Set("Accept", "application/json")
 | 
						req.Header.Set("Accept", "application/json")
 | 
				
			||||||
 | 
						req.Header.Set("X-User-Agent", "signalerr/1.0")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	resp, err := http.DefaultClient.Do(req)
 | 
						resp, err := http.DefaultClient.Do(req)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
@@ -277,9 +329,11 @@ func (s *ESISSO) PostWaypoint(destinationID int64, clearOthers bool, addToBeginn
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
	defer resp.Body.Close()
 | 
						defer resp.Body.Close()
 | 
				
			||||||
	if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusOK {
 | 
						if resp.StatusCode == http.StatusNoContent || resp.StatusCode == http.StatusOK {
 | 
				
			||||||
 | 
							fmt.Println("ESI: waypoint set OK", resp.Status)
 | 
				
			||||||
		return nil
 | 
							return nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	b, _ := io.ReadAll(resp.Body)
 | 
						b, _ := io.ReadAll(resp.Body)
 | 
				
			||||||
 | 
						fmt.Printf("ESI: waypoint failed %s body=%s\n", resp.Status, string(b))
 | 
				
			||||||
	return fmt.Errorf("waypoint failed: %s: %s", resp.Status, string(b))
 | 
						return fmt.Errorf("waypoint failed: %s: %s", resp.Status, string(b))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -302,43 +356,139 @@ func (s *ESISSO) Status() SSOStatus {
 | 
				
			|||||||
	}
 | 
						}
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// ResolveSystemIDByName searches ESI for a solar system by exact name and returns its ID
 | 
					// ResolveSystemIDByName searches ESI for a solar system by name and returns its ID
 | 
				
			||||||
func (s *ESISSO) ResolveSystemIDByName(ctx context.Context, name string) (int64, error) {
 | 
					func (s *ESISSO) ResolveSystemIDByName(ctx context.Context, name string) (int64, error) {
 | 
				
			||||||
	name = strings.TrimSpace(name)
 | 
						name = strings.TrimSpace(name)
 | 
				
			||||||
	if name == "" {
 | 
						if name == "" {
 | 
				
			||||||
		return 0, errors.New("empty system name")
 | 
							return 0, errors.New("empty system name")
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						// 1) Prefer universe/ids (name->id) for accuracy
 | 
				
			||||||
 | 
						type idsReq struct {
 | 
				
			||||||
 | 
							Names []string `json:"names"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						body, _ := json.Marshal(idsReq{Names: []string{name}})
 | 
				
			||||||
 | 
						idsURL := esiBase + "/v3/universe/ids/?datasource=tranquility"
 | 
				
			||||||
 | 
						fmt.Printf("ESI: resolve system id via universe/ids for name=%q\n", name)
 | 
				
			||||||
 | 
						req, err := http.NewRequestWithContext(ctx, http.MethodPost, idsURL, strings.NewReader(string(body)))
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							req.Header.Set("Content-Type", "application/json")
 | 
				
			||||||
 | 
							req.Header.Set("Accept", "application/json")
 | 
				
			||||||
 | 
							resp, errDo := http.DefaultClient.Do(req)
 | 
				
			||||||
 | 
							if errDo == nil {
 | 
				
			||||||
 | 
								defer resp.Body.Close()
 | 
				
			||||||
 | 
								if resp.StatusCode == http.StatusOK {
 | 
				
			||||||
 | 
									var idsResp struct {
 | 
				
			||||||
 | 
										Systems []struct {
 | 
				
			||||||
 | 
											ID   int64  `json:"id"`
 | 
				
			||||||
 | 
											Name string `json:"name"`
 | 
				
			||||||
 | 
										} `json:"systems"`
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
									if err := json.NewDecoder(resp.Body).Decode(&idsResp); err == nil {
 | 
				
			||||||
 | 
										if len(idsResp.Systems) > 0 {
 | 
				
			||||||
 | 
											fmt.Printf("ESI: universe/ids hit: %s -> %d\n", idsResp.Systems[0].Name, idsResp.Systems[0].ID)
 | 
				
			||||||
 | 
											return idsResp.Systems[0].ID, nil
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// 2) Fallback: strict search
 | 
				
			||||||
	q := url.Values{}
 | 
						q := url.Values{}
 | 
				
			||||||
	q.Set("categories", "solar_system")
 | 
						q.Set("categories", "solar_system")
 | 
				
			||||||
	q.Set("search", name)
 | 
						q.Set("search", name)
 | 
				
			||||||
	q.Set("strict", "true")
 | 
						q.Set("strict", "true")
 | 
				
			||||||
	endpoint := esiBase + "/v3/search/?" + q.Encode()
 | 
						searchURL := esiBase + "/v3/search/?" + q.Encode()
 | 
				
			||||||
 | 
						fmt.Printf("ESI: strict search for %q\n", name)
 | 
				
			||||||
 | 
						req2, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL, nil)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						req2.Header.Set("Accept", "application/json")
 | 
				
			||||||
 | 
						resp2, err := http.DefaultClient.Do(req2)
 | 
				
			||||||
 | 
						if err == nil {
 | 
				
			||||||
 | 
							defer resp2.Body.Close()
 | 
				
			||||||
 | 
							if resp2.StatusCode == http.StatusOK {
 | 
				
			||||||
 | 
								var payload struct {
 | 
				
			||||||
 | 
									SolarSystem []int64 `json:"solar_system"`
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								if err := json.NewDecoder(resp2.Body).Decode(&payload); err == nil && len(payload.SolarSystem) > 0 {
 | 
				
			||||||
 | 
									fmt.Printf("ESI: strict search hit: %d\n", payload.SolarSystem[0])
 | 
				
			||||||
 | 
									return payload.SolarSystem[0], nil
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
 | 
						// 3) Fallback: non-strict search then best-effort name match via universe/names
 | 
				
			||||||
 | 
						q.Set("strict", "false")
 | 
				
			||||||
 | 
						searchURL2 := esiBase + "/v3/search/?" + q.Encode()
 | 
				
			||||||
 | 
						fmt.Printf("ESI: non-strict search for %q\n", name)
 | 
				
			||||||
 | 
						req3, err := http.NewRequestWithContext(ctx, http.MethodGet, searchURL2, nil)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return 0, err
 | 
							return 0, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	req.Header.Set("Accept", "application/json")
 | 
						req3.Header.Set("Accept", "application/json")
 | 
				
			||||||
	resp, err := http.DefaultClient.Do(req)
 | 
						resp3, err := http.DefaultClient.Do(req3)
 | 
				
			||||||
	if err != nil {
 | 
						if err != nil {
 | 
				
			||||||
		return 0, err
 | 
							return 0, err
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	defer resp.Body.Close()
 | 
						defer resp3.Body.Close()
 | 
				
			||||||
	if resp.StatusCode != http.StatusOK {
 | 
						if resp3.StatusCode != http.StatusOK {
 | 
				
			||||||
		b, _ := io.ReadAll(resp.Body)
 | 
							b, _ := io.ReadAll(resp3.Body)
 | 
				
			||||||
		return 0, fmt.Errorf("search failed: %s: %s", resp.Status, string(b))
 | 
							return 0, fmt.Errorf("search failed: %s: %s", resp3.Status, string(b))
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	var payload struct {
 | 
						var payload struct {
 | 
				
			||||||
		SolarSystem []int64 `json:"solar_system"`
 | 
							SolarSystem []int64 `json:"solar_system"`
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
 | 
						if err := json.NewDecoder(resp3.Body).Decode(&payload); err != nil || len(payload.SolarSystem) == 0 {
 | 
				
			||||||
		return 0, err
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if len(payload.SolarSystem) == 0 {
 | 
					 | 
				
			||||||
		return 0, fmt.Errorf("system not found: %s", name)
 | 
							return 0, fmt.Errorf("system not found: %s", name)
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						// If one result, return it
 | 
				
			||||||
 | 
						if len(payload.SolarSystem) == 1 {
 | 
				
			||||||
 | 
							fmt.Printf("ESI: non-strict search single hit: %d\n", payload.SolarSystem[0])
 | 
				
			||||||
		return payload.SolarSystem[0], nil
 | 
							return payload.SolarSystem[0], nil
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
						// Multiple: resolve names and pick exact case-insensitive match if possible
 | 
				
			||||||
 | 
						ids := payload.SolarSystem
 | 
				
			||||||
 | 
						var idNamesReq = make([]int64, 0, len(ids))
 | 
				
			||||||
 | 
						idNamesReq = append(idNamesReq, ids...)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						namesURL := esiBase + "/v3/universe/names/?datasource=tranquility"
 | 
				
			||||||
 | 
						idsBody, _ := json.Marshal(idNamesReq)
 | 
				
			||||||
 | 
						req4, err := http.NewRequestWithContext(ctx, http.MethodPost, namesURL, strings.NewReader(string(idsBody)))
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						req4.Header.Set("Content-Type", "application/json")
 | 
				
			||||||
 | 
						req4.Header.Set("Accept", "application/json")
 | 
				
			||||||
 | 
						resp4, err := http.DefaultClient.Do(req4)
 | 
				
			||||||
 | 
						if err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						defer resp4.Body.Close()
 | 
				
			||||||
 | 
						if resp4.StatusCode != http.StatusOK {
 | 
				
			||||||
 | 
							b, _ := io.ReadAll(resp4.Body)
 | 
				
			||||||
 | 
							return 0, fmt.Errorf("names lookup failed: %s: %s", resp4.Status, string(b))
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						var namesResp []struct {
 | 
				
			||||||
 | 
							Category string `json:"category"`
 | 
				
			||||||
 | 
							ID       int64  `json:"id"`
 | 
				
			||||||
 | 
							Name     string `json:"name"`
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if err := json.NewDecoder(resp4.Body).Decode(&namesResp); err != nil {
 | 
				
			||||||
 | 
							return 0, err
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						lower := strings.ToLower(name)
 | 
				
			||||||
 | 
						for _, n := range namesResp {
 | 
				
			||||||
 | 
							if n.Category == "solar_system" && strings.ToLower(n.Name) == lower {
 | 
				
			||||||
 | 
								fmt.Printf("ESI: names resolved exact: %s -> %d\n", n.Name, n.ID)
 | 
				
			||||||
 | 
								return n.ID, nil
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						// Fallback: return first
 | 
				
			||||||
 | 
						fmt.Printf("ESI: names resolved fallback: returning %d for %q\n", namesResp[0].ID, name)
 | 
				
			||||||
 | 
						return namesResp[0].ID, nil
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Helpers
 | 
					// Helpers
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ import {
 | 
				
			|||||||
} from '@/components/ui/breadcrumb';
 | 
					} from '@/components/ui/breadcrumb';
 | 
				
			||||||
import { Button } from '@/components/ui/button';
 | 
					import { Button } from '@/components/ui/button';
 | 
				
			||||||
import { toast } from '@/hooks/use-toast';
 | 
					import { toast } from '@/hooks/use-toast';
 | 
				
			||||||
import { StartESILogin, ESILoginStatus } from 'wailsjs/go/main/App';
 | 
					import { StartESILogin, ESILoginStatus, ESILoggedIn } from 'wailsjs/go/main/App';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface HeaderProps {
 | 
					interface HeaderProps {
 | 
				
			||||||
  title: string;
 | 
					  title: string;
 | 
				
			||||||
@@ -32,7 +32,17 @@ export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => {
 | 
				
			|||||||
  const handleLogin = async () => {
 | 
					  const handleLogin = async () => {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await StartESILogin();
 | 
					      await StartESILogin();
 | 
				
			||||||
      toast({ title: 'EVE Login', description: 'Complete login in your browser. Reopen menu to refresh status.' });
 | 
					      toast({ title: 'EVE Login', description: 'Complete login in your browser.' });
 | 
				
			||||||
 | 
					      // Poll a few times to update status after redirect callback
 | 
				
			||||||
 | 
					      for (let i = 0; i < 20; i++) {
 | 
				
			||||||
 | 
					        const ok = await ESILoggedIn();
 | 
				
			||||||
 | 
					        if (ok) {
 | 
				
			||||||
 | 
					          const s = await ESILoginStatus();
 | 
				
			||||||
 | 
					          setStatus(s);
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        await new Promise(r => setTimeout(r, 500));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    } catch (e: any) {
 | 
					    } catch (e: any) {
 | 
				
			||||||
      toast({ title: 'Login failed', description: String(e), variant: 'destructive' });
 | 
					      toast({ title: 'Login failed', description: String(e), variant: 'destructive' });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -41,7 +41,7 @@ export const SystemContextMenu = ({ x, y, system, onRename, onDelete, onClearCon
 | 
				
			|||||||
      toast({ title: 'Destination set', description: `${system.solarSystemName}` });
 | 
					      toast({ title: 'Destination set', description: `${system.solarSystemName}` });
 | 
				
			||||||
      onClose();
 | 
					      onClose();
 | 
				
			||||||
    } catch (e: any) {
 | 
					    } catch (e: any) {
 | 
				
			||||||
      toast({ title: 'Failed to set destination', description: String(e), variant: 'destructive' });
 | 
					      toast({ title: 'Failed to set destination', description: `${system.solarSystemName}: ${String(e)}`, variant: 'destructive' });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								frontend/wailsjs/go/main/App.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								frontend/wailsjs/go/main/App.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -1,6 +1,8 @@
 | 
				
			|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
 | 
					// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
 | 
				
			||||||
// This file is automatically generated. DO NOT EDIT
 | 
					// This file is automatically generated. DO NOT EDIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ESILoggedIn():Promise<boolean>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ESILoginStatus():Promise<string>;
 | 
					export function ESILoginStatus():Promise<string>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function Greet(arg1:string):Promise<string>;
 | 
					export function Greet(arg1:string):Promise<string>;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,10 @@
 | 
				
			|||||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
 | 
					// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
 | 
				
			||||||
// This file is automatically generated. DO NOT EDIT
 | 
					// This file is automatically generated. DO NOT EDIT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function ESILoggedIn() {
 | 
				
			||||||
 | 
					  return window['go']['main']['App']['ESILoggedIn']();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function ESILoginStatus() {
 | 
					export function ESILoginStatus() {
 | 
				
			||||||
  return window['go']['main']['App']['ESILoginStatus']();
 | 
					  return window['go']['main']['App']['ESILoginStatus']();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user