5 Commits

8 changed files with 212 additions and 89 deletions

7
app.go
View File

@@ -36,7 +36,7 @@ func (a *App) startup(ctx context.Context) {
}
// Add location read scope so we can fetch character locations
a.ssi = NewESISSO(clientID, redirectURI, []string{
ssi, err := NewESISSO(clientID, redirectURI, []string{
"esi-location.read_location.v1",
"esi-location.read_ship_type.v1",
"esi-mail.organize_mail.v1",
@@ -76,6 +76,11 @@ func (a *App) startup(ctx context.Context) {
"esi-characters.read_titles.v1",
"esi-characters.read_fw_stats.v1",
})
if err != nil {
fmt.Printf("ERROR: Failed to initialize ESI SSO: %v\n", err)
return
}
a.ssi = ssi
}
// Greet returns a greeting for the given name

View File

@@ -74,6 +74,10 @@ type ESIToken struct {
CreatedAt time.Time
}
func (ESIToken) TableName() string {
return "esi_tokens"
}
type CharacterInfo struct {
CharacterID int64 `json:"character_id"`
CharacterName string `json:"character_name"`
@@ -108,28 +112,42 @@ type SystemKills struct {
NpcKills int64 `json:"npc_kills"`
}
func NewESISSO(clientID string, redirectURI string, scopes []string) *ESISSO {
func NewESISSO(clientID string, redirectURI string, scopes []string) (*ESISSO, error) {
s := &ESISSO{
clientID: clientID,
redirectURI: redirectURI,
scopes: scopes,
}
_ = s.initDB()
return s
if err := s.initDB(); err != nil {
return nil, fmt.Errorf("failed to initialize database: %w", err)
}
return s, nil
}
func (s *ESISSO) initDB() error {
home, err := os.UserHomeDir()
if err != nil {
return err
return fmt.Errorf("failed to get user home directory: %w", err)
}
dbPath := filepath.Join(home, ".industrializer", "sqlite-latest.sqlite")
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return err
return fmt.Errorf("failed to open database at %s: %w", dbPath, err)
}
if err := db.AutoMigrate(&ESIToken{}); err != nil {
return err
sqlDB, err := db.DB()
if err != nil {
return fmt.Errorf("failed to get underlying sql.DB: %w", err)
}
if err := sqlDB.Ping(); err != nil {
return fmt.Errorf("failed to ping database: %w", err)
}
if !db.Migrator().HasTable(&ESIToken{}) {
if err := db.Migrator().CreateTable(&ESIToken{}); err != nil {
return fmt.Errorf("failed to create table esi_tokens: %w", err)
}
}
s.db = db
return nil

View File

@@ -14,6 +14,7 @@ interface MapNodeProps {
type: 'region' | 'system';
security?: number;
signatures?: number;
jove_observatory?: boolean;
isDraggable?: boolean;
disableNavigate?: boolean;
jumps?: number;
@@ -21,6 +22,7 @@ interface MapNodeProps {
showJumps?: boolean;
showKills?: boolean;
viewBoxWidth?: number; // Add viewBox width for scaling calculations
labelScale?: number;
}
export const MapNode: React.FC<MapNodeProps> = ({
@@ -36,6 +38,7 @@ export const MapNode: React.FC<MapNodeProps> = ({
type,
security,
signatures,
jove_observatory,
isDraggable = false,
disableNavigate = false,
jumps,
@@ -43,6 +46,7 @@ export const MapNode: React.FC<MapNodeProps> = ({
showJumps = false,
showKills = false,
viewBoxWidth = 1200,
labelScale = 1,
}) => {
const [isHovered, setIsHovered] = useState(false);
const [isDragging, setIsDragging] = useState(false);
@@ -197,23 +201,22 @@ export const MapNode: React.FC<MapNodeProps> = ({
fontWeight="bold"
className={`transition-all duration-300 ${isHovered ? 'fill-purple-200' : 'fill-white'
} pointer-events-none select-none`}
style={{
style={{
textShadow: '2px 2px 4px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
transformOrigin="0 0"
transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
>
{name} {security !== undefined && (
<tspan fill={getSecurityColor(security)}>{security.toFixed(1)}</tspan>
)}
</text>
{/* Dynamic text positioning based on what's shown */}
{(() => {
let currentY = textOffset + 15;
const textElements = [];
// Add signatures if present
if (signatures !== undefined && signatures > 0) {
textElements.push(
@@ -225,19 +228,41 @@ export const MapNode: React.FC<MapNodeProps> = ({
fill="#a3a3a3"
fontSize="12"
className="pointer-events-none select-none"
style={{
style={{
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
transformOrigin="0 0"
transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
>
📡 {signatures}
</text>
);
currentY += 15;
}
// Add jove observatory icon if present
if (jove_observatory) {
textElements.push(
<text
key="jove_observatory"
x="0"
y={currentY}
textAnchor="middle"
fill="#fbbf24"
fontSize="12"
className="pointer-events-none select-none"
style={{
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
>
🔭
</text>
);
currentY += 15;
}
// Add jumps if enabled and present
if (showJumps && jumps !== undefined) {
textElements.push(
@@ -249,19 +274,18 @@ export const MapNode: React.FC<MapNodeProps> = ({
fill="#60a5fa"
fontSize="10"
className="pointer-events-none select-none"
style={{
style={{
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
transformOrigin="0 0"
transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
>
🚀 {jumps}
</text>
);
currentY += 15;
}
// Add kills if enabled and present
if (showKills && kills !== undefined) {
textElements.push(
@@ -273,18 +297,17 @@ export const MapNode: React.FC<MapNodeProps> = ({
fill="#f87171"
fontSize="10"
className="pointer-events-none select-none"
style={{
style={{
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
transformOrigin="0 0"
transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
>
{kills}
</text>
);
}
return textElements;
})()}
</g>

View File

@@ -33,6 +33,7 @@ interface RegionMapProps {
focusSystem?: string;
isCompact?: boolean;
isWormholeRegion?: boolean;
header?: boolean;
}
interface ContextMenuState {
@@ -93,7 +94,7 @@ const ensureUniversePositions = async () => {
}
};
export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormholeRegion = false }: RegionMapProps) => {
export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormholeRegion = false, header = true }: RegionMapProps) => {
const navigate = useNavigate();
const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 1200, height: 800 });
const [isPanning, setIsPanning] = useState(false);
@@ -119,10 +120,10 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
// Statistics state - MUST default to false to avoid API spam!
const [showJumps, setShowJumps] = useState(false);
const [showKills, setShowKills] = useState(false);
// System ID cache for statistics lookup
const [systemIDCache, setSystemIDCache] = useState<Map<string, number>>(new Map());
// New: selection/aim state for left-click aimbot behavior
const [isSelecting, setIsSelecting] = useState(false);
@@ -181,7 +182,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
}, [viaMode, viaDest, viaQueue]);
const { data: rsystems, isLoading, error } = useRegionData(regionName);
// Fetch statistics data - only when toggles are enabled
const { data: jumpsData } = useSystemJumps(showJumps);
const { data: killsData } = useSystemKills(showKills);
@@ -189,7 +190,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
useEffect(() => {
if (!isLoading && error == null && rsystems && rsystems.size > 0) {
setSystems(rsystems);
// Pre-resolve all system IDs for statistics lookup
const resolveSystemIDs = async () => {
const newCache = new Map<string, number>();
@@ -205,7 +206,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
}
setSystemIDCache(newCache);
};
resolveSystemIDs();
}
}, [rsystems, isLoading, error]);
@@ -558,26 +559,26 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
// Helper functions to get statistics for a system
const getSystemJumps = (systemName: string): number | undefined => {
if (!showJumps) return undefined;
const systemID = systemIDCache.get(systemName);
if (!systemID) return undefined;
const jumps = jumpsBySystemID.get(systemID);
if (!jumps || jumps === 0) return undefined;
console.log(`🚀 Found ${jumps} jumps for ${systemName} (ID: ${systemID})`);
return jumps;
};
const getSystemKills = (systemName: string): number | undefined => {
if (!showKills) return undefined;
const systemID = systemIDCache.get(systemName);
if (!systemID) return undefined;
const kills = killsBySystemID.get(systemID);
if (!kills || kills === 0) return undefined;
console.log(`⚔️ Found ${kills} kills for ${systemName} (ID: ${systemID})`);
return kills;
};
@@ -1021,13 +1022,15 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
return (
<div className="w-full h-full bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 relative">
<Header
title={`Region: ${regionName}`}
breadcrumbs={[
{ label: "Universe", path: "/" },
{ label: regionName }
]}
/>
{header && (
<Header
title={`Region: ${regionName}`}
breadcrumbs={[
{ label: "Universe", path: "/" },
{ label: regionName }
]}
/>
)}
<svg
ref={svgRef}
width="100%"
@@ -1104,6 +1107,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
type="system"
security={system.security}
signatures={system.signatures}
jove_observatory={system.jove_observatory}
isDraggable={isWormholeRegion}
disableNavigate={viaMode}
jumps={getSystemJumps(system.solarSystemName)}
@@ -1111,6 +1115,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
showJumps={showJumps}
showKills={showKills}
viewBoxWidth={viewBox.width}
labelScale={isCompact ? 2.0 : 1}
/>
))}

View File

@@ -1,5 +1,7 @@
import { System } from '@/lib/types';
import { useQuery } from '@tanstack/react-query';
import pb from '@/lib/pocketbase';
import { SystemResponse } from '@/lib/pbtypes';
const pocketbaseUrl = `https://evebase.site.quack-lab.dev/api/collections/regionview/records`;
@@ -23,6 +25,26 @@ const fetchRegionData = async (regionName: string): Promise<Map<string, System>>
system.signatures = systemSigs.sigcount;
}
}
const systemNames = Array.from(systemsMap.keys());
if (systemNames.length > 0) {
const filter = systemNames.map(name => `name='${name}'`).join(' || ');
try {
const systemRecords = await pb.collection('system').getFullList<SystemResponse>({
filter: `(${filter})`,
batch: 1000
});
for (const record of systemRecords) {
const system = systemsMap.get(record.name);
if (system && record.jove_observatory) {
system.jove_observatory = record.jove_observatory;
}
}
} catch (error) {
console.warn('Failed to fetch jove_observatory data:', error);
}
}
return systemsMap;
};

View File

@@ -25,7 +25,9 @@ export enum Collections {
// Alias types for improved usability
export type IsoDateString = string
export type IsoAutoDateString = string & { readonly autodate: unique symbol }
export type RecordIdString = string
export type FileNameString = string & { readonly filename: unique symbol }
export type HTMLString = string
type ExpandType<T> = unknown extends T
@@ -52,66 +54,66 @@ export type AuthSystemFields<T = unknown> = {
export type AuthoriginsRecord = {
collectionRef: string
created?: IsoDateString
created: IsoAutoDateString
fingerprint: string
id: string
recordRef: string
updated?: IsoDateString
updated: IsoAutoDateString
}
export type ExternalauthsRecord = {
collectionRef: string
created?: IsoDateString
created: IsoAutoDateString
id: string
provider: string
providerId: string
recordRef: string
updated?: IsoDateString
updated: IsoAutoDateString
}
export type MfasRecord = {
collectionRef: string
created?: IsoDateString
created: IsoAutoDateString
id: string
method: string
recordRef: string
updated?: IsoDateString
updated: IsoAutoDateString
}
export type OtpsRecord = {
collectionRef: string
created?: IsoDateString
created: IsoAutoDateString
id: string
password: string
recordRef: string
sentTo?: string
updated?: IsoDateString
updated: IsoAutoDateString
}
export type SuperusersRecord = {
created?: IsoDateString
created: IsoAutoDateString
email: string
emailVisibility?: boolean
id: string
password: string
tokenKey: string
updated?: IsoDateString
updated: IsoAutoDateString
verified?: boolean
}
export type IndBillitemRecord = {
created?: IsoDateString
created: IsoAutoDateString
id: string
name: string
quantity: number
updated?: IsoDateString
updated: IsoAutoDateString
}
export type IndCharRecord = {
created?: IsoDateString
created: IsoAutoDateString
id: string
name: string
updated?: IsoDateString
updated: IsoAutoDateString
}
export enum IndJobStatusOptions {
@@ -132,7 +134,7 @@ export type IndJobRecord = {
billOfMaterials?: RecordIdString[]
character?: RecordIdString
consumedMaterials?: RecordIdString[]
created?: IsoDateString
created: IsoAutoDateString
expenditures?: RecordIdString[]
id: string
income?: RecordIdString[]
@@ -148,13 +150,13 @@ export type IndJobRecord = {
saleEnd?: IsoDateString
saleStart?: IsoDateString
status: IndJobStatusOptions
updated?: IsoDateString
updated: IsoAutoDateString
}
export type IndTransactionRecord = {
buyer?: string
corporation?: string
created?: IsoDateString
created: IsoAutoDateString
date: IsoDateString
id: string
itemName: string
@@ -163,7 +165,7 @@ export type IndTransactionRecord = {
quantity: number
totalPrice: number
unitPrice: number
updated?: IsoDateString
updated: IsoAutoDateString
wallet?: string
}
@@ -175,7 +177,7 @@ export type RegionviewRecord = {
}
export type SignatureRecord = {
created?: IsoDateString
created: IsoAutoDateString
dangerous?: boolean
id: string
identifier: string
@@ -184,20 +186,20 @@ export type SignatureRecord = {
scanned?: string
system: RecordIdString
type?: string
updated?: IsoDateString
updated: IsoAutoDateString
}
export type SignatureNoteRulesRecord = {
created?: IsoDateString
created: IsoAutoDateString
enabled?: boolean
id: string
note: string
regex: string
updated?: IsoDateString
updated: IsoAutoDateString
}
export type SigviewRecord = {
created?: IsoDateString
created: IsoAutoDateString
dangerous?: boolean
id: string
identifier: string
@@ -207,24 +209,25 @@ export type SigviewRecord = {
sysid?: RecordIdString
system: string
type?: string
updated?: IsoDateString
updated: IsoAutoDateString
}
export type SystemRecord = {
connectedTo?: string
created?: IsoDateString
created: IsoAutoDateString
id: string
jove_observatory?: boolean
name: string
region: string
updated?: IsoDateString
updated: IsoAutoDateString
}
export type WormholeSystemsRecord = {
connectedSystems?: string
created?: IsoDateString
created: IsoAutoDateString
id: string
solarSystemName: string
updated?: IsoDateString
updated: IsoAutoDateString
x: number
y: number
}
@@ -284,23 +287,68 @@ export type CollectionResponses = {
wormholeSystems: WormholeSystemsResponse
}
// Utility types for create/update operations
type ProcessCreateAndUpdateFields<T> = Omit<{
// Omit AutoDate fields
[K in keyof T as Extract<T[K], IsoAutoDateString> extends never ? K : never]:
// Convert FileNameString to File
T[K] extends infer U ?
U extends (FileNameString | FileNameString[]) ?
U extends any[] ? File[] : File
: U
: never
}, 'id'>
// Create type for Auth collections
export type CreateAuth<T> = {
id?: RecordIdString
email: string
emailVisibility?: boolean
password: string
passwordConfirm: string
verified?: boolean
} & ProcessCreateAndUpdateFields<T>
// Create type for Base collections
export type CreateBase<T> = {
id?: RecordIdString
} & ProcessCreateAndUpdateFields<T>
// Update type for Auth collections
export type UpdateAuth<T> = Partial<
Omit<ProcessCreateAndUpdateFields<T>, keyof AuthSystemFields>
> & {
email?: string
emailVisibility?: boolean
oldPassword?: string
password?: string
passwordConfirm?: string
verified?: boolean
}
// Update type for Base collections
export type UpdateBase<T> = Partial<
Omit<ProcessCreateAndUpdateFields<T>, keyof BaseSystemFields>
>
// Get the correct create type for any collection
export type Create<T extends keyof CollectionResponses> =
CollectionResponses[T] extends AuthSystemFields
? CreateAuth<CollectionRecords[T]>
: CreateBase<CollectionRecords[T]>
// Get the correct update type for any collection
export type Update<T extends keyof CollectionResponses> =
CollectionResponses[T] extends AuthSystemFields
? UpdateAuth<CollectionRecords[T]>
: UpdateBase<CollectionRecords[T]>
// Type for usage with type asserted PocketBase instance
// https://github.com/pocketbase/js-sdk#specify-typescript-definitions
export type TypedPocketBase = PocketBase & {
collection(idOrName: '_authOrigins'): RecordService<AuthoriginsResponse>
collection(idOrName: '_externalAuths'): RecordService<ExternalauthsResponse>
collection(idOrName: '_mfas'): RecordService<MfasResponse>
collection(idOrName: '_otps'): RecordService<OtpsResponse>
collection(idOrName: '_superusers'): RecordService<SuperusersResponse>
collection(idOrName: 'ind_billItem'): RecordService<IndBillitemResponse>
collection(idOrName: 'ind_char'): RecordService<IndCharResponse>
collection(idOrName: 'ind_job'): RecordService<IndJobResponse>
collection(idOrName: 'ind_transaction'): RecordService<IndTransactionResponse>
collection(idOrName: 'regionview'): RecordService<RegionviewResponse>
collection(idOrName: 'signature'): RecordService<SignatureResponse>
collection(idOrName: 'signature_note_rules'): RecordService<SignatureNoteRulesResponse>
collection(idOrName: 'sigview'): RecordService<SigviewResponse>
collection(idOrName: 'system'): RecordService<SystemResponse>
collection(idOrName: 'wormholeSystems'): RecordService<WormholeSystemsResponse>
}
export type TypedPocketBase = {
collection<T extends keyof CollectionResponses>(
idOrName: T
): RecordService<CollectionResponses[T]>
} & PocketBase

View File

@@ -10,6 +10,7 @@ export interface System {
y: number;
security?: number;
signatures?: number;
jove_observatory?: boolean;
connectedSystems: string;
}

View File

@@ -281,6 +281,7 @@ export const SystemView = () => {
regionName={region}
focusSystem={system}
isCompact={true}
header={false}
/>
</div>
</div>