diff --git a/app.go b/app.go index 7159af6..ee9c8d8 100644 --- a/app.go +++ b/app.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "time" "github.com/wailsapp/wails/v2/pkg/runtime" ) @@ -126,3 +127,11 @@ func (a *App) ListCharacters() ([]CharacterInfo, error) { } return list, nil } + +// GetCharacterLocations exposes current locations for all characters +func (a *App) GetCharacterLocations() ([]CharacterLocation, error) { + if a.ssi == nil { return nil, errors.New("ESI not initialised") } + ctx, cancel := context.WithTimeout(a.ctx, 6*time.Second) + defer cancel() + return a.ssi.GetCharacterLocations(ctx) +} diff --git a/esi_sso.go b/esi_sso.go index 9095a62..6b8d743 100644 --- a/esi_sso.go +++ b/esi_sso.go @@ -78,6 +78,21 @@ type CharacterInfo struct { CharacterName string `json:"character_name"` } +// CharacterLocation represents a character's current location +type CharacterLocation struct { + CharacterID int64 `json:"character_id"` + CharacterName string `json:"character_name"` + SolarSystemID int64 `json:"solar_system_id"` + SolarSystemName string `json:"solar_system_name"` + RetrievedAt time.Time `json:"retrieved_at"` +} + +type esiCharacterLocationResponse struct { + SolarSystemID int64 `json:"solar_system_id"` + StationID int64 `json:"station_id"` + StructureID int64 `json:"structure_id"` +} + func NewESISSO(clientID string, redirectURI string, scopes []string) *ESISSO { s := &ESISSO{ clientID: clientID, @@ -105,6 +120,43 @@ func (s *ESISSO) initDB() error { return nil } +// resolveSystemNameByID returns the system name for an ID from the local DB, or empty if not found +func (s *ESISSO) resolveSystemNameByID(id int64) string { + if s.db == nil || id == 0 { return "" } + var ss SolarSystem + if err := s.db.Select("solarSystemName").First(&ss, "solarSystemID = ?", id).Error; err != nil { return "" } + return ss.SolarSystemName +} + +// GetCharacterLocations returns current locations for all stored characters +func (s *ESISSO) GetCharacterLocations(ctx context.Context) ([]CharacterLocation, error) { + if s.db == nil { return nil, errors.New("db not initialised") } + var tokens []ESIToken + if err := s.db.Find(&tokens).Error; err != nil { return nil, err } + out := make([]CharacterLocation, 0, len(tokens)) + client := &http.Client{ Timeout: 5 * time.Second } + for i := range tokens { + t := &tokens[i] + tok, err := s.ensureAccessTokenFor(ctx, t) + if err != nil { continue } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, esiBase+"/v2/characters/"+strconv.FormatInt(t.CharacterID,10)+"/location", nil) + if err != nil { continue } + req.Header.Set("Accept", "application/json") + req.Header.Set("Authorization", "Bearer "+tok) + resp, err := client.Do(req) + if err != nil { continue } + func() { + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { return } + var lr esiCharacterLocationResponse + if err := json.NewDecoder(resp.Body).Decode(&lr); err != nil { return } + name := s.resolveSystemNameByID(lr.SolarSystemID) + out = append(out, CharacterLocation{ CharacterID: t.CharacterID, CharacterName: t.CharacterName, SolarSystemID: lr.SolarSystemID, SolarSystemName: name, RetrievedAt: time.Now() }) + }() + } + return out, nil +} + func (s *ESISSO) saveToken() { if s.db == nil || s.characterID == 0 { return diff --git a/frontend/src/components/RegionMap.tsx b/frontend/src/components/RegionMap.tsx index 8b9ebe1..7418a29 100644 --- a/frontend/src/components/RegionMap.tsx +++ b/frontend/src/components/RegionMap.tsx @@ -8,7 +8,7 @@ import { loadWormholeSystems, saveWormholeSystem, deleteWormholeSystem } from '@ import { System, Position, Connection as ConnectionType } from '@/lib/types'; import { getSecurityColor } from '@/utils/securityColors'; import { Header } from './Header'; -import { ListCharacters, StartESILogin, SetDestinationForAll, PostRouteForAllByNames } from 'wailsjs/go/main/App'; +import { ListCharacters, StartESILogin, SetDestinationForAll, PostRouteForAllByNames, GetCharacterLocations } from 'wailsjs/go/main/App'; import { toast } from '@/hooks/use-toast'; import { getSystemsRegions } from '@/utils/systemApi'; @@ -97,6 +97,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho type OffIndicator = { from: string; toRegion: string; count: number; color: string; angle: number; sampleTo?: string }; const [offRegionIndicators, setOffRegionIndicators] = useState([]); const [meanInboundAngle, setMeanInboundAngle] = useState>({}); + const [charLocs, setCharLocs] = useState>([]); useEffect(() => { const onKeyDown = async (e: KeyboardEvent) => { @@ -154,6 +155,24 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho setMeanInboundAngle(angleMap); }, [systems]); + // Poll character locations every 7s and store those in this region + useEffect(() => { + let timer: any; + const tick = async () => { + try { + const locs = await GetCharacterLocations(); + const here = locs.filter(l => !!l.solar_system_name && systems.has(l.solar_system_name)); + setCharLocs(here.map(l => ({ character_id: l.character_id, character_name: l.character_name, solar_system_name: l.solar_system_name }))); + } catch (_) { + // ignore + } finally { + timer = setTimeout(tick, 7000); + } + }; + tick(); + return () => { if (timer) clearTimeout(timer); }; + }, [systems]); + // Compute off-region indicators: dedupe per (from, toRegion), compute avg color, and angle via universe centroids useEffect(() => { const computeOffRegion = async () => { @@ -644,6 +663,19 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho /> ))} + {/* Character location markers */} + {charLocs.map((c, idx) => { + const pos = positions[c.solar_system_name]; + if (!pos) return null; + const yoff = -18 - (idx % 3) * 10; // stagger small vertical offsets if multiple in same system + return ( + + + {c.character_name} + + ); + })} + {/* Off-region indicators: labeled arrows pointing toward the destination region */} {offRegionIndicators.map((ind, idx) => { const pos = positions[ind.from]; diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index a3f24ec..84cc0c6 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -8,6 +8,8 @@ export function ESILoggedIn():Promise; export function ESILoginStatus():Promise; +export function GetCharacterLocations():Promise>; + export function Greet(arg1:string):Promise; export function ListCharacters():Promise>; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 6742dd8..6f49768 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -14,6 +14,10 @@ export function ESILoginStatus() { return window['go']['main']['App']['ESILoginStatus'](); } +export function GetCharacterLocations() { + return window['go']['main']['App']['GetCharacterLocations'](); +} + export function Greet(arg1) { return window['go']['main']['App']['Greet'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index 6ad18e2..b3073b4 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -14,6 +14,45 @@ export namespace main { this.character_name = source["character_name"]; } } + export class CharacterLocation { + character_id: number; + character_name: string; + solar_system_id: number; + solar_system_name: string; + // Go type: time + retrieved_at: any; + + static createFrom(source: any = {}) { + return new CharacterLocation(source); + } + + constructor(source: any = {}) { + if ('string' === typeof source) source = JSON.parse(source); + this.character_id = source["character_id"]; + this.character_name = source["character_name"]; + this.solar_system_id = source["solar_system_id"]; + this.solar_system_name = source["solar_system_name"]; + this.retrieved_at = this.convertValues(source["retrieved_at"], null); + } + + convertValues(a: any, classs: any, asMap: boolean = false): any { + if (!a) { + return a; + } + if (a.slice && a.map) { + return (a as any[]).map(elem => this.convertValues(elem, classs)); + } else if ("object" === typeof a) { + if (asMap) { + for (const key of Object.keys(a)) { + a[key] = new classs(a[key]); + } + return a; + } + return new classs(a); + } + return a; + } + } } diff --git a/signalerr.exe b/signalerr.exe new file mode 100644 index 0000000..c29fb54 Binary files /dev/null and b/signalerr.exe differ