9 Commits

17 changed files with 728 additions and 95 deletions

33
app.go
View File

@@ -186,6 +186,39 @@ func (a *App) ToggleCharacterWaypointEnabled(characterID int64) error {
return a.ssi.ToggleCharacterWaypointEnabled(characterID)
}
// GetSystemJumps fetches system jump statistics from ESI
func (a *App) GetSystemJumps() ([]SystemJumps, error) {
fmt.Printf("🔍 App.GetSystemJumps() called - this should ONLY happen when toggle is ON!\n")
if a.ssi == nil {
return nil, errors.New("ESI not initialised")
}
ctx, cancel := context.WithTimeout(a.ctx, 15*time.Second)
defer cancel()
return a.ssi.GetSystemJumps(ctx)
}
// GetSystemKills fetches system kill statistics from ESI
func (a *App) GetSystemKills() ([]SystemKills, error) {
fmt.Printf("🔍 App.GetSystemKills() called - this should ONLY happen when toggle is ON!\n")
if a.ssi == nil {
return nil, errors.New("ESI not initialised")
}
ctx, cancel := context.WithTimeout(a.ctx, 15*time.Second)
defer cancel()
return a.ssi.GetSystemKills(ctx)
}
// ResolveSystemIDByName resolves a system name to its ID
func (a *App) ResolveSystemIDByName(systemName string) (int64, error) {
fmt.Printf("🔍 App.ResolveSystemIDByName() called for system: %s\n", systemName)
if a.ssi == nil {
return 0, errors.New("ESI not initialised")
}
ctx, cancel := context.WithTimeout(a.ctx, 5*time.Second)
defer cancel()
return a.ssi.ResolveSystemIDByName(ctx, systemName)
}
// SystemRegion holds system + region names from local DB
type SystemRegion struct {
System string `json:"system"`

View File

@@ -95,6 +95,19 @@ type esiCharacterLocationResponse struct {
StructureID int64 `json:"structure_id"`
}
// ESI Statistics data structures
type SystemJumps struct {
SystemID int64 `json:"system_id"`
ShipJumps int64 `json:"ship_jumps"`
}
type SystemKills struct {
SystemID int64 `json:"system_id"`
ShipKills int64 `json:"ship_kills"`
PodKills int64 `json:"pod_kills"`
NpcKills int64 `json:"npc_kills"`
}
func NewESISSO(clientID string, redirectURI string, scopes []string) *ESISSO {
s := &ESISSO{
clientID: clientID,
@@ -177,6 +190,72 @@ func (s *ESISSO) GetCharacterLocations(ctx context.Context) ([]CharacterLocation
return out, nil
}
// GetSystemJumps fetches system jump statistics from ESI
func (s *ESISSO) GetSystemJumps(ctx context.Context) ([]SystemJumps, error) {
fmt.Printf("🚀 ESI API REQUEST: Fetching system jumps data from https://esi.evetech.net/v2/universe/system_jumps\n")
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, esiBase+"/v2/universe/system_jumps", nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Compatibility-Date", "2025-08-26")
req.Header.Set("X-Tenant", "tranquility")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ESI API returned status %d", resp.StatusCode)
}
var jumps []SystemJumps
if err := json.NewDecoder(resp.Body).Decode(&jumps); err != nil {
return nil, err
}
fmt.Printf("✅ ESI API SUCCESS: Fetched %d system jumps entries\n", len(jumps))
return jumps, nil
}
// GetSystemKills fetches system kill statistics from ESI
func (s *ESISSO) GetSystemKills(ctx context.Context) ([]SystemKills, error) {
fmt.Printf("⚔️ ESI API REQUEST: Fetching system kills data from https://esi.evetech.net/v2/universe/system_kills\n")
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, esiBase+"/v2/universe/system_kills", nil)
if err != nil {
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("X-Compatibility-Date", "2025-08-26")
req.Header.Set("X-Tenant", "tranquility")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("ESI API returned status %d", resp.StatusCode)
}
var kills []SystemKills
if err := json.NewDecoder(resp.Body).Decode(&kills); err != nil {
return nil, err
}
fmt.Printf("✅ ESI API SUCCESS: Fetched %d system kills entries\n", len(kills))
return kills, nil
}
func (s *ESISSO) saveToken() {
if s.db == nil || s.characterID == 0 {
return

View File

@@ -7,6 +7,7 @@ import { SystemView } from "./pages/SystemView";
import NotFound from "./pages/NotFound";
import "./App.css";
import { SearchDialog } from "@/components/SearchDialog";
import { SignatureRules } from "./pages/SignatureRules";
const queryClient = new QueryClient();
@@ -19,6 +20,7 @@ function App() {
<Route path="/regions/:region" element={<RegionPage />} />
<Route path="/regions/:region/:system" element={<SystemView />} />
<Route path="/systems/:system" element={<SystemView />} />
<Route path="/settings/signature-rules" element={<SignatureRules />} />
<Route path="*" element={<NotFound />} />
</Routes>
<Toaster />

View File

@@ -98,6 +98,7 @@ export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => {
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-white">{title}</h1>
<div className="flex items-center gap-3">
<Button size="sm" variant="outline" className="border-purple-500/40 text-purple-200" onClick={() => navigate('/settings/signature-rules')}>Rules</Button>
{chars.length > 0 && (
<div
className="grid gap-1 flex-1 justify-end"
@@ -110,8 +111,7 @@ export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => {
<span
key={c.character_id}
onClick={() => handleCharacterClick(c)}
className={`px-3 py-1 text-xs cursor-pointer transition-colors text-center overflow-hidden text-ellipsis ${
c.waypoint_enabled
className={`px-3 py-1 text-xs cursor-pointer transition-colors text-center overflow-hidden text-ellipsis ${c.waypoint_enabled
? 'bg-purple-500/20 text-purple-200 border border-purple-400/40 hover:bg-purple-500/30'
: 'bg-gray-500/20 text-gray-400 border border-gray-400/40 hover:bg-gray-500/30'
}`}

View File

@@ -16,6 +16,12 @@ interface MapNodeProps {
signatures?: number;
isDraggable?: boolean;
disableNavigate?: boolean;
jumps?: number;
kills?: number;
showJumps?: boolean;
showKills?: boolean;
viewBoxWidth?: number; // Add viewBox width for scaling calculations
labelScale?: number;
}
export const MapNode: React.FC<MapNodeProps> = ({
@@ -33,6 +39,12 @@ export const MapNode: React.FC<MapNodeProps> = ({
signatures,
isDraggable = false,
disableNavigate = false,
jumps,
kills,
showJumps = false,
showKills = false,
viewBoxWidth = 1200,
labelScale = 1,
}) => {
const [isHovered, setIsHovered] = useState(false);
const [isDragging, setIsDragging] = useState(false);
@@ -177,7 +189,7 @@ export const MapNode: React.FC<MapNodeProps> = ({
className="transition-all duration-300"
/>
{/* Node label */}
{/* Node label - fixed visual size regardless of zoom */}
<text
x="0"
y={textOffset}
@@ -187,23 +199,92 @@ 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={{ textShadow: '2px 2px 4px rgba(0,0,0,0.8)' }}
style={{
textShadow: '2px 2px 4px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
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(
<text
key="signatures"
x="0"
y={textOffset + 15}
y={currentY}
textAnchor="middle"
fill="#a3a3a3"
fontSize="12"
className="pointer-events-none select-none"
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.8)' }}
style={{
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke'
}}
transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
>
{signatures !== undefined && signatures > 0 && `📡 ${signatures}`}
📡 {signatures}
</text>
);
currentY += 15;
}
// Add jumps if enabled and present
if (showJumps && jumps !== undefined) {
textElements.push(
<text
key="jumps"
x="0"
y={currentY}
textAnchor="middle"
fill="#60a5fa"
fontSize="10"
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})`}
>
🚀 {jumps}
</text>
);
currentY += 15;
}
// Add kills if enabled and present
if (showKills && kills !== undefined) {
textElements.push(
<text
key="kills"
x="0"
y={currentY}
textAnchor="middle"
fill="#f87171"
fontSize="10"
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})`}
>
{kills}
</text>
);
}
return textElements;
})()}
</g>
);
}

View File

@@ -1,4 +1,4 @@
import React, { useState, useRef, useCallback, useEffect } from 'react';
import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { MapNode } from '@/components/MapNode';
import { SystemContextMenu } from '@/components/SystemContextMenu';
@@ -11,6 +11,8 @@ import { Header } from './Header';
import { ListCharacters, StartESILogin, SetDestinationForAll, PostRouteForAllByNames, GetCharacterLocations } from 'wailsjs/go/main/App';
import { toast } from '@/hooks/use-toast';
import { getSystemsRegions } from '@/utils/systemApi';
import { useSystemJumps, useSystemKills, resolveSystemID } from '@/hooks/useSystemStatistics';
import { StatisticsToggle } from './StatisticsToggle';
// Interaction/indicator constants
const SELECT_HOLD_MS = 300;
@@ -31,6 +33,7 @@ interface RegionMapProps {
focusSystem?: string;
isCompact?: boolean;
isWormholeRegion?: boolean;
header?: boolean;
}
interface ContextMenuState {
@@ -91,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);
@@ -114,6 +117,14 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
const [charLocs, setCharLocs] = useState<Array<{ character_id: number; character_name: string; solar_system_name: string }>>([]);
const [focusUntil, setFocusUntil] = useState<number | null>(null);
// 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);
const [indicatedSystem, setIndicatedSystem] = useState<string | null>(null);
@@ -171,11 +182,36 @@ 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);
useEffect(() => {
if (!isLoading && error == null && rsystems && rsystems.size > 0)
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>();
for (const systemName of rsystems.keys()) {
try {
const id = await resolveSystemID(systemName);
if (id) {
newCache.set(systemName, id);
}
} catch (error) {
console.warn(`Failed to resolve system ID for ${systemName}:`, error);
}
}
setSystemIDCache(newCache);
};
resolveSystemIDs();
}
}, [rsystems, isLoading, error]);
useEffect(() => {
if (!systems || systems.size === 0) return;
const positions = computeNodePositions(systems);
@@ -501,6 +537,52 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
return nearestName;
};
// Create lookup maps for system statistics
const jumpsBySystemID = useMemo(() => {
if (!jumpsData) return new Map();
const map = new Map<number, number>();
jumpsData.forEach(jump => {
map.set(jump.system_id, jump.ship_jumps);
});
return map;
}, [jumpsData]);
const killsBySystemID = useMemo(() => {
if (!killsData) return new Map();
const map = new Map<number, number>();
killsData.forEach(kill => {
map.set(kill.system_id, kill.ship_kills);
});
return map;
}, [killsData]);
// 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;
};
// Commit shift selection: toggle all systems within radius
const commitShiftSelection = useCallback(() => {
if (!shiftCenter || shiftRadius <= 0) return;
@@ -940,6 +1022,7 @@ 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 && (
<Header
title={`Region: ${regionName}`}
breadcrumbs={[
@@ -947,6 +1030,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
{ label: regionName }
]}
/>
)}
<svg
ref={svgRef}
width="100%"
@@ -1025,6 +1109,12 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
signatures={system.signatures}
isDraggable={isWormholeRegion}
disableNavigate={viaMode}
jumps={getSystemJumps(system.solarSystemName)}
kills={getSystemKills(system.solarSystemName)}
showJumps={showJumps}
showKills={showKills}
viewBoxWidth={viewBox.width}
labelScale={isCompact ? 2.0 : 1}
/>
))}
@@ -1136,6 +1226,16 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
</div>
)}
{/* Statistics Toggle - positioned to avoid overlaps */}
<div className="absolute bottom-4 left-4">
<StatisticsToggle
jumpsEnabled={showJumps}
killsEnabled={showKills}
onJumpsToggle={setShowJumps}
onKillsToggle={setShowKills}
/>
</div>
{/* Context Menu */}
{contextMenu && (
<SystemContextMenu

View File

@@ -75,10 +75,12 @@ export const SignatureCard = ({ signature, onDelete, onUpdate }: SignatureCardPr
{signature.signame || 'Unnamed Signature'}
</h3>
{signature.note && (
<div className="mt-2">
<Badge variant="outline" className="bg-blue-900/50 text-blue-200 border-blue-500 px-3 py-1 text-sm font-semibold">
{signature.note}
<div className="mt-2 flex flex-wrap gap-1 justify-center">
{signature.note.split(';').filter(Boolean).map((note, index) => (
<Badge key={index} variant="outline" className="bg-blue-900/50 text-blue-200 border-blue-500 px-3 py-1 text-sm font-semibold">
{note.trim()}
</Badge>
))}
</div>
)}
</div>

View File

@@ -117,9 +117,13 @@ export const SignatureListItem = ({ signature, onDelete, onUpdate }: SignatureLi
)}
</h3>
{signature.note && (
<Badge variant="outline" className="bg-blue-900/50 text-blue-200 border-blue-500 px-2 py-0.5 text-sm font-semibold ml-2">
{signature.note}
<div className="flex flex-wrap gap-1 ml-2">
{signature.note.split(';').filter(Boolean).map((note, index) => (
<Badge key={index} variant="outline" className="bg-blue-900/50 text-blue-200 border-blue-500 px-2 py-0.5 text-sm font-semibold">
{note.trim()}
</Badge>
))}
</div>
)}
</div>
</div>

View File

@@ -0,0 +1,44 @@
import React from 'react';
interface StatisticsToggleProps {
jumpsEnabled: boolean;
killsEnabled: boolean;
onJumpsToggle: (enabled: boolean) => void;
onKillsToggle: (enabled: boolean) => void;
}
export const StatisticsToggle: React.FC<StatisticsToggleProps> = ({
jumpsEnabled,
killsEnabled,
onJumpsToggle,
onKillsToggle,
}) => {
return (
<div className="bg-slate-800/90 backdrop-blur-sm rounded-lg p-2 shadow-lg border border-slate-700">
<div className="flex gap-2">
<button
onClick={() => onJumpsToggle(!jumpsEnabled)}
className={`p-2 rounded transition-colors ${
jumpsEnabled
? 'bg-blue-600/20 text-blue-400 hover:bg-blue-600/30'
: 'bg-gray-600/20 text-gray-400 hover:bg-gray-600/30'
}`}
title={jumpsEnabled ? 'Hide Jumps' : 'Show Jumps'}
>
🚀
</button>
<button
onClick={() => onKillsToggle(!killsEnabled)}
className={`p-2 rounded transition-colors ${
killsEnabled
? 'bg-red-600/20 text-red-400 hover:bg-red-600/30'
: 'bg-gray-600/20 text-gray-400 hover:bg-gray-600/30'
}`}
title={killsEnabled ? 'Hide Kills' : 'Show Kills'}
>
</button>
</div>
</div>
);
};

View File

@@ -0,0 +1,41 @@
import { useQuery } from '@tanstack/react-query';
import * as App from 'wailsjs/go/main/App';
// Helper function to resolve system name to ID
export const resolveSystemID = async (systemName: string): Promise<number | null> => {
try {
const id = await App.ResolveSystemIDByName(systemName);
return id;
} catch (error) {
console.warn(`Failed to resolve system ID for ${systemName}:`, error);
return null;
}
};
export const useSystemJumps = (enabled: boolean = false) => {
console.log('useSystemJumps called with enabled:', enabled);
return useQuery({
queryKey: ['systemJumps'],
queryFn: () => {
console.log('🚀 FETCHING SYSTEM JUMPS DATA - API REQUEST MADE!');
return App.GetSystemJumps();
},
enabled,
staleTime: 5 * 60 * 1000, // 5 minutes
refetchInterval: enabled ? 5 * 60 * 1000 : false, // Only refetch when enabled
});
};
export const useSystemKills = (enabled: boolean = false) => {
console.log('useSystemKills called with enabled:', enabled);
return useQuery({
queryKey: ['systemKills'],
queryFn: () => {
console.log('⚔️ FETCHING SYSTEM KILLS DATA - API REQUEST MADE!');
return App.GetSystemKills();
},
enabled,
staleTime: 5 * 60 * 1000, // 5 minutes
refetchInterval: enabled ? 5 * 60 * 1000 : false, // Only refetch when enabled
});
};

View File

@@ -11,8 +11,13 @@ export enum Collections {
Mfas = "_mfas",
Otps = "_otps",
Superusers = "_superusers",
IndBillitem = "ind_billItem",
IndChar = "ind_char",
IndJob = "ind_job",
IndTransaction = "ind_transaction",
Regionview = "regionview",
Signature = "signature",
SignatureNoteRules = "signature_note_rules",
Sigview = "sigview",
System = "system",
WormholeSystems = "wormholeSystems",
@@ -94,6 +99,74 @@ export type SuperusersRecord = {
verified?: boolean
}
export type IndBillitemRecord = {
created?: IsoDateString
id: string
name: string
quantity: number
updated?: IsoDateString
}
export type IndCharRecord = {
created?: IsoDateString
id: string
name: string
updated?: IsoDateString
}
export enum IndJobStatusOptions {
"Planned" = "Planned",
"Acquisition" = "Acquisition",
"Running" = "Running",
"Done" = "Done",
"Selling" = "Selling",
"Closed" = "Closed",
"Tracked" = "Tracked",
"Staging" = "Staging",
"Inbound" = "Inbound",
"Outbound" = "Outbound",
"Delivered" = "Delivered",
"Queued" = "Queued",
}
export type IndJobRecord = {
billOfMaterials?: RecordIdString[]
character?: RecordIdString
consumedMaterials?: RecordIdString[]
created?: IsoDateString
expenditures?: RecordIdString[]
id: string
income?: RecordIdString[]
jobEnd?: IsoDateString
jobStart?: IsoDateString
outputItem: string
outputQuantity: number
parallel?: number
produced?: number
projectedCost?: number
projectedRevenue?: number
runtime?: number
saleEnd?: IsoDateString
saleStart?: IsoDateString
status: IndJobStatusOptions
updated?: IsoDateString
}
export type IndTransactionRecord = {
buyer?: string
corporation?: string
created?: IsoDateString
date: IsoDateString
id: string
itemName: string
job?: RecordIdString
location?: string
quantity: number
totalPrice: number
unitPrice: number
updated?: IsoDateString
wallet?: string
}
export type RegionviewRecord = {
id: string
sigcount?: number
@@ -114,6 +187,15 @@ export type SignatureRecord = {
updated?: IsoDateString
}
export type SignatureNoteRulesRecord = {
created?: IsoDateString
enabled?: boolean
id: string
note: string
regex: string
updated?: IsoDateString
}
export type SigviewRecord = {
created?: IsoDateString
dangerous?: boolean
@@ -153,8 +235,13 @@ export type ExternalauthsResponse<Texpand = unknown> = Required<ExternalauthsRec
export type MfasResponse<Texpand = unknown> = Required<MfasRecord> & BaseSystemFields<Texpand>
export type OtpsResponse<Texpand = unknown> = Required<OtpsRecord> & BaseSystemFields<Texpand>
export type SuperusersResponse<Texpand = unknown> = Required<SuperusersRecord> & AuthSystemFields<Texpand>
export type IndBillitemResponse<Texpand = unknown> = Required<IndBillitemRecord> & BaseSystemFields<Texpand>
export type IndCharResponse<Texpand = unknown> = Required<IndCharRecord> & BaseSystemFields<Texpand>
export type IndJobResponse<Texpand = unknown> = Required<IndJobRecord> & BaseSystemFields<Texpand>
export type IndTransactionResponse<Texpand = unknown> = Required<IndTransactionRecord> & BaseSystemFields<Texpand>
export type RegionviewResponse<Texpand = unknown> = Required<RegionviewRecord> & BaseSystemFields<Texpand>
export type SignatureResponse<Texpand = unknown> = Required<SignatureRecord> & BaseSystemFields<Texpand>
export type SignatureNoteRulesResponse<Texpand = unknown> = Required<SignatureNoteRulesRecord> & BaseSystemFields<Texpand>
export type SigviewResponse<Texpand = unknown> = Required<SigviewRecord> & BaseSystemFields<Texpand>
export type SystemResponse<Texpand = unknown> = Required<SystemRecord> & BaseSystemFields<Texpand>
export type WormholeSystemsResponse<Texpand = unknown> = Required<WormholeSystemsRecord> & BaseSystemFields<Texpand>
@@ -167,8 +254,13 @@ export type CollectionRecords = {
_mfas: MfasRecord
_otps: OtpsRecord
_superusers: SuperusersRecord
ind_billItem: IndBillitemRecord
ind_char: IndCharRecord
ind_job: IndJobRecord
ind_transaction: IndTransactionRecord
regionview: RegionviewRecord
signature: SignatureRecord
signature_note_rules: SignatureNoteRulesRecord
sigview: SigviewRecord
system: SystemRecord
wormholeSystems: WormholeSystemsRecord
@@ -180,8 +272,13 @@ export type CollectionResponses = {
_mfas: MfasResponse
_otps: OtpsResponse
_superusers: SuperusersResponse
ind_billItem: IndBillitemResponse
ind_char: IndCharResponse
ind_job: IndJobResponse
ind_transaction: IndTransactionResponse
regionview: RegionviewResponse
signature: SignatureResponse
signature_note_rules: SignatureNoteRulesResponse
sigview: SigviewResponse
system: SystemResponse
wormholeSystems: WormholeSystemsResponse
@@ -196,8 +293,13 @@ export type TypedPocketBase = PocketBase & {
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>

View File

@@ -0,0 +1,124 @@
import { useEffect, useState } from 'react';
import pb from '@/lib/pocketbase';
import { Header } from '@/components/Header';
import { Button } from '@/components/ui/button';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Input } from '@/components/ui/input';
import { Switch } from '@/components/ui/switch';
import { toast } from '@/hooks/use-toast';
import { SignatureNoteRulesResponse, Collections } from '@/lib/pbtypes';
export const SignatureRules = () => {
const [rules, setRules] = useState<SignatureNoteRulesResponse[]>([]);
const [loading, setLoading] = useState(false);
const [creating, setCreating] = useState({ regex: '', note: '' });
const load = async () => {
setLoading(true);
try {
const list = await pb.collection(Collections.SignatureNoteRules).getFullList<SignatureNoteRulesResponse>({ batch: 1000, sort: '-updated' });
setRules(list);
} catch (e: any) {
toast({ title: 'Load failed', description: String(e), variant: 'destructive' });
} finally {
setLoading(false);
}
};
useEffect(() => { load(); }, []);
const handleCreate = async () => {
if (!creating.regex.trim() || !creating.note.trim()) return;
try {
await pb.collection(Collections.SignatureNoteRules).create({ regex: creating.regex.trim(), note: creating.note.trim(), enabled: true });
setCreating({ regex: '', note: '' });
await load();
toast({ title: 'Rule added', description: 'New rule created.' });
} catch (e: any) {
toast({ title: 'Create failed', description: String(e), variant: 'destructive' });
}
};
const handleUpdate = async (id: string, patch: Partial<SignatureNoteRulesResponse>) => {
try {
await pb.collection(Collections.SignatureNoteRules).update(id, patch);
await load();
} catch (e: any) {
toast({ title: 'Update failed', description: String(e), variant: 'destructive' });
}
};
const handleDelete = async (id: string) => {
try {
await pb.collection(Collections.SignatureNoteRules).delete(id);
await load();
toast({ title: 'Rule deleted' });
} catch (e: any) {
toast({ title: 'Delete failed', description: String(e), variant: 'destructive' });
}
};
return (
<div className="h-screen w-screen bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 overflow-hidden">
<div className="h-full flex flex-col">
<Header title="Signature Rules" breadcrumbs={[{ label: 'Universe', path: '/' }, { label: 'Signature Rules' }]} />
<div className="flex-1 overflow-auto p-4 space-y-4">
<div className="bg-black/20 border border-purple-500/30 rounded p-4 space-y-3">
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Input
type="text"
placeholder="Regex (e.g. ^Angel.*Outpost$|Guristas.*)"
value={creating.regex}
onChange={e => setCreating({ ...creating, regex: e.target.value })}
className="font-mono"
/>
<Input placeholder="Note/Tag (e.g. 3/10)" value={creating.note} onChange={e => setCreating({ ...creating, note: e.target.value })} />
<Button onClick={handleCreate} disabled={loading}>Add Rule</Button>
</div>
</div>
<div className="bg-black/20 border border-purple-500/30 rounded">
<Table>
<TableHeader>
<TableRow>
<TableHead className="text-slate-300">Enabled</TableHead>
<TableHead className="text-slate-300">Regex</TableHead>
<TableHead className="text-slate-300">Note</TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rules.map(r => (
<TableRow key={r.id}>
<TableCell>
<Switch checked={!!r.enabled} onCheckedChange={(v) => handleUpdate(r.id, { enabled: v })} />
</TableCell>
<TableCell className="max-w-0">
<Input
type="text"
value={r.regex}
onChange={e => setRules(prev => prev.map(x => x.id === r.id ? { ...x, regex: e.target.value } : x))}
onBlur={e => handleUpdate(r.id, { regex: e.currentTarget.value })}
className="font-mono"
/>
</TableCell>
<TableCell className="max-w-0">
<Input
value={r.note}
onChange={e => setRules(prev => prev.map(x => x.id === r.id ? { ...x, note: e.target.value } : x))}
onBlur={e => handleUpdate(r.id, { note: e.currentTarget.value })}
/>
</TableCell>
<TableCell className="text-right">
<Button variant="destructive" onClick={() => handleDelete(r.id)}>Delete</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</div>
</div>
);
};

View File

@@ -8,7 +8,7 @@ import { Header } from "@/components/Header";
import { parseSignature, parseScannedPercentage } from "@/utils/signatureParser";
import { getSystemId } from "@/utils/systemApi";
import pb from "@/lib/pocketbase";
import { SigviewRecord as Signature, SignatureRecord } from "@/lib/pbtypes";
import { SigviewRecord as Signature, SignatureRecord, SignatureNoteRulesResponse, Collections } from "@/lib/pbtypes";
export const SystemView = () => {
const { system, region } = useParams();
@@ -163,12 +163,17 @@ export const SystemView = () => {
try {
const systemId = await getSystemId(system);
let rules: Array<Pick<SignatureNoteRulesResponse, 'regex' | 'note' | 'enabled'>> = [];
try {
const list = await pb.collection(Collections.SignatureNoteRules).getFullList<SignatureNoteRulesResponse>({ batch: 1000 });
rules = list.filter(r => r.enabled).map(r => ({ regex: r.regex, note: r.note, enabled: r.enabled }));
} catch { }
const lines = pastedText.trim().split('\n').filter(line => line.trim());
const parsedSignatures: Omit<Signature, 'id'>[] = [];
// Parse all signatures
for (const line of lines) {
const parsed = parseSignature(line);
const parsed = parseSignature(line, rules);
if (parsed) {
parsedSignatures.push({
...parsed,
@@ -276,6 +281,7 @@ export const SystemView = () => {
regionName={region}
focusSystem={system}
isCompact={true}
header={false}
/>
</div>
</div>

View File

@@ -1,52 +1,7 @@
import { SigviewRecord as Signature } from "@/lib/pbtypes";
import { SigviewRecord as Signature, SignatureNoteRulesResponse } from "@/lib/pbtypes";
const oneOutOfTen = [
"Minmatar Contracted Bio-Farm",
"Old Meanie - Cultivation Center",
"Pith Robux Asteroid Mining & Co.",
"Sansha Military Outpost",
"Serpentis Drug Outlet",
];
const twoOutOfTen = [
"Angel Creo-Corp Mining",
"Blood Raider Human Farm",
"Pith Merchant Depot",
"Sansha Acclimatization Facility",
"Serpentis Live Cargo Distribution Facilities",
"Rogue Drone Infestation Sprout",
];
const threeOutOfTen = [
"Angel Repurposed Outpost",
"Blood Raider Intelligence Collection Point",
"Guristas Guerilla Grounds",
"Sansha's Command Relay Outpost",
"Serpentis Narcotic Warehouses",
"Rogue Drone Asteroid Infestation",
];
const fourOutOfTen = [
"Angel Cartel Occupied Mining Colony",
"Mul-Zatah Monastery",
"Guristas Scout Outpost",
"Sansha's Nation Occupied Mining Colony",
"Serpentis Phi-Outpost",
"Drone Infested Mine",
];
const fiveOutOfTen = [
"Angel's Red Light District",
"Blood Raider Psychotropics Depot",
"Guristas Hallucinogen Supply Waypoint",
"Sansha's Nation Neural Paralytic Facility",
"Serpentis Corporation Hydroponics Site",
"Outgrowth Rogue Drone Hive",
];
function isFourOutOfTen(signature: string): boolean {
return fourOutOfTen.some((s) => signature.includes(s));
}
function isFiveOutOfTen(signature: string): boolean {
return fiveOutOfTen.some((s) => signature.includes(s));
}
export const parseSignature = (text: string): Omit<Signature, 'system' | 'id' | 'sysid'> | null => {
export const parseSignature = (text: string, rules?: Array<Pick<SignatureNoteRulesResponse, 'regex' | 'note' | 'enabled'>>): Omit<Signature, 'system' | 'id' | 'sysid'> | null => {
const parts = text.split('\t');
if (parts.length < 4) return null;
@@ -56,15 +11,25 @@ export const parseSignature = (text: string): Omit<Signature, 'system' | 'id' |
return null;
}
let note = "";
const isFour = isFourOutOfTen(parts[3]);
if (isFour) {
note = "4/10";
const appliedNotes: string[] = [];
if (rules && rules.length > 0) {
for (const rule of rules) {
if (rule && rule.enabled) {
try {
const re = new RegExp(rule.regex, 'i');
if (re.test(parts[3])) {
appliedNotes.push(rule.note);
}
const isFive = isFiveOutOfTen(parts[3]);
if (isFive) {
note = "5/10";
} catch {
// invalid regex - ignore
}
}
}
}
const dedupedNotes = Array.from(new Set(appliedNotes)).filter(Boolean);
const note = dedupedNotes.join(';');
return {
identifier: parts[0],

View File

@@ -10,6 +10,10 @@ export function ESILoginStatus():Promise<string>;
export function GetCharacterLocations():Promise<Array<main.CharacterLocation>>;
export function GetSystemJumps():Promise<Array<main.SystemJumps>>;
export function GetSystemKills():Promise<Array<main.SystemKills>>;
export function Greet(arg1:string):Promise<string>;
export function ListCharacters():Promise<Array<main.CharacterInfo>>;
@@ -18,6 +22,8 @@ export function ListSystemsWithRegions():Promise<Array<main.SystemRegion>>;
export function PostRouteForAllByNames(arg1:string,arg2:Array<string>):Promise<void>;
export function ResolveSystemIDByName(arg1:string):Promise<number>;
export function SetDestinationForAll(arg1:string,arg2:boolean,arg3:boolean):Promise<void>;
export function StartESILogin():Promise<string>;

View File

@@ -18,6 +18,14 @@ export function GetCharacterLocations() {
return window['go']['main']['App']['GetCharacterLocations']();
}
export function GetSystemJumps() {
return window['go']['main']['App']['GetSystemJumps']();
}
export function GetSystemKills() {
return window['go']['main']['App']['GetSystemKills']();
}
export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
@@ -34,6 +42,10 @@ export function PostRouteForAllByNames(arg1, arg2) {
return window['go']['main']['App']['PostRouteForAllByNames'](arg1, arg2);
}
export function ResolveSystemIDByName(arg1) {
return window['go']['main']['App']['ResolveSystemIDByName'](arg1);
}
export function SetDestinationForAll(arg1, arg2, arg3) {
return window['go']['main']['App']['SetDestinationForAll'](arg1, arg2, arg3);
}

View File

@@ -55,6 +55,38 @@ export namespace main {
return a;
}
}
export class SystemJumps {
system_id: number;
ship_jumps: number;
static createFrom(source: any = {}) {
return new SystemJumps(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.system_id = source["system_id"];
this.ship_jumps = source["ship_jumps"];
}
}
export class SystemKills {
system_id: number;
ship_kills: number;
pod_kills: number;
npc_kills: number;
static createFrom(source: any = {}) {
return new SystemKills(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.system_id = source["system_id"];
this.ship_kills = source["ship_kills"];
this.pod_kills = source["pod_kills"];
this.npc_kills = source["npc_kills"];
}
}
export class SystemRegion {
system: string;
region: string;