7 Commits

11 changed files with 467 additions and 211 deletions

View File

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

View File

@@ -98,23 +98,23 @@ export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-white">{title}</h1> <h1 className="text-2xl font-bold text-white">{title}</h1>
<div className="flex items-center gap-3"> <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 && ( {chars.length > 0 && (
<div <div
className="grid gap-1 flex-1 justify-end" className="grid gap-1 flex-1 justify-end"
style={{ style={{
gridTemplateColumns: `repeat(${Math.ceil(chars.length / 2)}, 1fr)`, gridTemplateColumns: `repeat(${Math.ceil(chars.length / 2)}, 1fr)`,
gridTemplateRows: 'repeat(2, auto)' gridTemplateRows: 'repeat(2, auto)'
}} }}
> >
{chars.map((c) => ( {chars.map((c) => (
<span <span
key={c.character_id} key={c.character_id}
onClick={() => handleCharacterClick(c)} onClick={() => handleCharacterClick(c)}
className={`px-3 py-1 text-xs cursor-pointer transition-colors text-center overflow-hidden text-ellipsis ${ className={`px-3 py-1 text-xs cursor-pointer transition-colors text-center overflow-hidden text-ellipsis ${c.waypoint_enabled
c.waypoint_enabled ? 'bg-purple-500/20 text-purple-200 border border-purple-400/40 hover:bg-purple-500/30'
? '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' : 'bg-gray-500/20 text-gray-400 border border-gray-400/40 hover:bg-gray-500/30'
}`} }`}
title={`Click to ${c.waypoint_enabled ? 'disable' : 'enable'} waypoints for ${c.character_name}`} title={`Click to ${c.waypoint_enabled ? 'disable' : 'enable'} waypoints for ${c.character_name}`}
> >
{c.character_name} {c.character_name}

View File

@@ -21,6 +21,7 @@ interface MapNodeProps {
showJumps?: boolean; showJumps?: boolean;
showKills?: boolean; showKills?: boolean;
viewBoxWidth?: number; // Add viewBox width for scaling calculations viewBoxWidth?: number; // Add viewBox width for scaling calculations
labelScale?: number;
} }
export const MapNode: React.FC<MapNodeProps> = ({ export const MapNode: React.FC<MapNodeProps> = ({
@@ -43,6 +44,7 @@ export const MapNode: React.FC<MapNodeProps> = ({
showJumps = false, showJumps = false,
showKills = false, showKills = false,
viewBoxWidth = 1200, viewBoxWidth = 1200,
labelScale = 1,
}) => { }) => {
const [isHovered, setIsHovered] = useState(false); const [isHovered, setIsHovered] = useState(false);
const [isDragging, setIsDragging] = useState(false); const [isDragging, setIsDragging] = useState(false);
@@ -197,72 +199,92 @@ export const MapNode: React.FC<MapNodeProps> = ({
fontWeight="bold" fontWeight="bold"
className={`transition-all duration-300 ${isHovered ? 'fill-purple-200' : 'fill-white' className={`transition-all duration-300 ${isHovered ? 'fill-purple-200' : 'fill-white'
} pointer-events-none select-none`} } pointer-events-none select-none`}
style={{ style={{
textShadow: '2px 2px 4px rgba(0,0,0,0.8)', textShadow: '2px 2px 4px rgba(0,0,0,0.8)',
vectorEffect: 'non-scaling-stroke' vectorEffect: 'non-scaling-stroke'
}} }}
transform={`scale(${1 / (1200 / viewBoxWidth)})`} transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
transformOrigin="0 0"
> >
{name} {security !== undefined && ( {name} {security !== undefined && (
<tspan fill={getSecurityColor(security)}>{security.toFixed(1)}</tspan> <tspan fill={getSecurityColor(security)}>{security.toFixed(1)}</tspan>
)} )}
</text> </text>
<text
x="0" {/* Dynamic text positioning based on what's shown */}
y={textOffset + 15} {(() => {
textAnchor="middle" let currentY = textOffset + 15;
fill="#a3a3a3" const textElements = [];
fontSize="12"
className="pointer-events-none select-none" // Add signatures if present
style={{ if (signatures !== undefined && signatures > 0) {
textShadow: '1px 1px 2px rgba(0,0,0,0.8)', textElements.push(
vectorEffect: 'non-scaling-stroke' <text
}} key="signatures"
transform={`scale(${1 / (1200 / viewBoxWidth)})`} x="0"
transformOrigin="0 0" y={currentY}
> textAnchor="middle"
{signatures !== undefined && signatures > 0 && `📡 ${signatures}`} fill="#a3a3a3"
</text> fontSize="12"
className="pointer-events-none select-none"
{/* Statistics display - fixed visual size regardless of zoom */} style={{
{showJumps && jumps !== undefined && ( textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
<text vectorEffect: 'non-scaling-stroke'
x="0" }}
y={textOffset + 30} transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
textAnchor="middle" >
fill="#60a5fa" 📡 {signatures}
fontSize="10" </text>
className="pointer-events-none select-none" );
style={{ currentY += 15;
textShadow: '1px 1px 2px rgba(0,0,0,0.8)', }
vectorEffect: 'non-scaling-stroke'
}} // Add jumps if enabled and present
transform={`scale(${1 / (1200 / viewBoxWidth)})`} if (showJumps && jumps !== undefined) {
transformOrigin="0 0" textElements.push(
> <text
🚀 {jumps} key="jumps"
</text> x="0"
)} y={currentY}
textAnchor="middle"
{showKills && kills !== undefined && ( fill="#60a5fa"
<text fontSize="10"
x="0" className="pointer-events-none select-none"
y={textOffset + 45} style={{
textAnchor="middle" textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
fill="#f87171" vectorEffect: 'non-scaling-stroke'
fontSize="10" }}
className="pointer-events-none select-none" transform={`scale(${(1 / (1200 / viewBoxWidth)) * labelScale})`}
style={{ >
textShadow: '1px 1px 2px rgba(0,0,0,0.8)', 🚀 {jumps}
vectorEffect: 'non-scaling-stroke' </text>
}} );
transform={`scale(${1 / (1200 / viewBoxWidth)})`} currentY += 15;
transformOrigin="0 0" }
>
{kills} // Add kills if enabled and present
</text> 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> </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 { useNavigate } from 'react-router-dom';
import { MapNode } from '@/components/MapNode'; import { MapNode } from '@/components/MapNode';
import { SystemContextMenu } from '@/components/SystemContextMenu'; import { SystemContextMenu } from '@/components/SystemContextMenu';
@@ -11,7 +11,7 @@ import { Header } from './Header';
import { ListCharacters, StartESILogin, SetDestinationForAll, PostRouteForAllByNames, GetCharacterLocations } from 'wailsjs/go/main/App'; import { ListCharacters, StartESILogin, SetDestinationForAll, PostRouteForAllByNames, GetCharacterLocations } from 'wailsjs/go/main/App';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { getSystemsRegions } from '@/utils/systemApi'; import { getSystemsRegions } from '@/utils/systemApi';
import { useSystemJumps, useSystemKills } from '@/hooks/useSystemStatistics'; import { useSystemJumps, useSystemKills, resolveSystemID } from '@/hooks/useSystemStatistics';
import { StatisticsToggle } from './StatisticsToggle'; import { StatisticsToggle } from './StatisticsToggle';
// Interaction/indicator constants // Interaction/indicator constants
@@ -33,6 +33,7 @@ interface RegionMapProps {
focusSystem?: string; focusSystem?: string;
isCompact?: boolean; isCompact?: boolean;
isWormholeRegion?: boolean; isWormholeRegion?: boolean;
header?: boolean;
} }
interface ContextMenuState { 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 navigate = useNavigate();
const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 1200, height: 800 }); const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 1200, height: 800 });
const [isPanning, setIsPanning] = useState(false); const [isPanning, setIsPanning] = useState(false);
@@ -119,10 +120,11 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
// Statistics state - MUST default to false to avoid API spam! // Statistics state - MUST default to false to avoid API spam!
const [showJumps, setShowJumps] = useState(false); const [showJumps, setShowJumps] = useState(false);
const [showKills, setShowKills] = useState(false); const [showKills, setShowKills] = useState(false);
// Cache for system name to ID mappings // System ID cache for statistics lookup
const [systemIDCache, setSystemIDCache] = useState<Map<string, number>>(new Map()); const [systemIDCache, setSystemIDCache] = useState<Map<string, number>>(new Map());
// New: selection/aim state for left-click aimbot behavior // New: selection/aim state for left-click aimbot behavior
const [isSelecting, setIsSelecting] = useState(false); const [isSelecting, setIsSelecting] = useState(false);
const [indicatedSystem, setIndicatedSystem] = useState<string | null>(null); const [indicatedSystem, setIndicatedSystem] = useState<string | null>(null);
@@ -180,19 +182,35 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
}, [viaMode, viaDest, viaQueue]); }, [viaMode, viaDest, viaQueue]);
const { data: rsystems, isLoading, error } = useRegionData(regionName); const { data: rsystems, isLoading, error } = useRegionData(regionName);
// Fetch statistics data - only when toggles are enabled // Fetch statistics data - only when toggles are enabled
const { data: jumpsData } = useSystemJumps(showJumps); const { data: jumpsData } = useSystemJumps(showJumps);
const { data: killsData } = useSystemKills(showKills); const { data: killsData } = useSystemKills(showKills);
useEffect(() => { useEffect(() => {
if (!isLoading && error == null && rsystems && rsystems.size > 0) if (!isLoading && error == null && rsystems && rsystems.size > 0) {
setSystems(rsystems); 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]); }, [rsystems, isLoading, error]);
// For now, we'll use a simplified approach without system ID resolution
// The ESI data will be displayed for systems that have data, but we won't
// be able to match system names to IDs until the binding issue is resolved
useEffect(() => { useEffect(() => {
if (!systems || systems.size === 0) return; if (!systems || systems.size === 0) return;
@@ -519,45 +537,50 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
return nearestName; 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 // Helper functions to get statistics for a system
const getSystemJumps = (systemName: string): number | undefined => { const getSystemJumps = (systemName: string): number | undefined => {
if (!jumpsData || !showJumps) return undefined; if (!showJumps) return undefined;
// For demonstration, show the first few systems with jump data const systemID = systemIDCache.get(systemName);
// This is a temporary solution until system ID resolution is fixed if (!systemID) return undefined;
const systemNames = Array.from(systems.keys());
const systemIndex = systemNames.indexOf(systemName); const jumps = jumpsBySystemID.get(systemID);
if (!jumps || jumps === 0) return undefined;
if (systemIndex >= 0 && systemIndex < jumpsData.length) {
const jumps = jumpsData[systemIndex].ship_jumps; console.log(`🚀 Found ${jumps} jumps for ${systemName} (ID: ${systemID})`);
// Don't show 0 values - return undefined so nothing is rendered return jumps;
if (jumps === 0) return undefined;
console.log(`🚀 Found ${jumps} jumps for ${systemName} (using index ${systemIndex})`);
return jumps;
}
return undefined;
}; };
const getSystemKills = (systemName: string): number | undefined => { const getSystemKills = (systemName: string): number | undefined => {
if (!killsData || !showKills) return undefined; if (!showKills) return undefined;
// For demonstration, show the first few systems with kill data const systemID = systemIDCache.get(systemName);
// This is a temporary solution until system ID resolution is fixed if (!systemID) return undefined;
const systemNames = Array.from(systems.keys());
const systemIndex = systemNames.indexOf(systemName); const kills = killsBySystemID.get(systemID);
if (!kills || kills === 0) return undefined;
if (systemIndex >= 0 && systemIndex < killsData.length) {
const kills = killsData[systemIndex].ship_kills; console.log(`⚔️ Found ${kills} kills for ${systemName} (ID: ${systemID})`);
// Don't show 0 values - return undefined so nothing is rendered return kills;
if (kills === 0) return undefined;
console.log(`⚔️ Found ${kills} kills for ${systemName} (using index ${systemIndex})`);
return kills;
}
return undefined;
}; };
// Commit shift selection: toggle all systems within radius // Commit shift selection: toggle all systems within radius
@@ -999,13 +1022,15 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
return ( return (
<div className="w-full h-full bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 relative"> <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}`} <Header
breadcrumbs={[ title={`Region: ${regionName}`}
{ label: "Universe", path: "/" }, breadcrumbs={[
{ label: regionName } { label: "Universe", path: "/" },
]} { label: regionName }
/> ]}
/>
)}
<svg <svg
ref={svgRef} ref={svgRef}
width="100%" width="100%"
@@ -1089,6 +1114,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
showJumps={showJumps} showJumps={showJumps}
showKills={showKills} showKills={showKills}
viewBoxWidth={viewBox.width} viewBoxWidth={viewBox.width}
labelScale={isCompact ? 2.0 : 1}
/> />
))} ))}
@@ -1200,13 +1226,15 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
</div> </div>
)} )}
{/* Statistics Toggle */} {/* Statistics Toggle - positioned to avoid overlaps */}
<StatisticsToggle <div className="absolute bottom-4 left-4">
jumpsEnabled={showJumps} <StatisticsToggle
killsEnabled={showKills} jumpsEnabled={showJumps}
onJumpsToggle={setShowJumps} killsEnabled={showKills}
onKillsToggle={setShowKills} onJumpsToggle={setShowJumps}
/> onKillsToggle={setShowKills}
/>
</div>
{/* Context Menu */} {/* Context Menu */}
{contextMenu && ( {contextMenu && (

View File

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

View File

@@ -117,9 +117,13 @@ export const SignatureListItem = ({ signature, onDelete, onUpdate }: SignatureLi
)} )}
</h3> </h3>
{signature.note && ( {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"> <div className="flex flex-wrap gap-1 ml-2">
{signature.note} {signature.note.split(';').filter(Boolean).map((note, index) => (
</Badge> <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>
</div> </div>

View File

@@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import { Switch } from '@/components/ui/switch';
interface StatisticsToggleProps { interface StatisticsToggleProps {
jumpsEnabled: boolean; jumpsEnabled: boolean;
@@ -15,28 +14,30 @@ export const StatisticsToggle: React.FC<StatisticsToggleProps> = ({
onKillsToggle, onKillsToggle,
}) => { }) => {
return ( return (
<div className="absolute top-2 left-2 bg-slate-800/90 backdrop-blur-sm rounded-lg p-3 shadow-lg border border-slate-700"> <div className="bg-slate-800/90 backdrop-blur-sm rounded-lg p-2 shadow-lg border border-slate-700">
<div className="flex flex-col gap-3"> <div className="flex gap-2">
<div className="flex items-center gap-3"> <button
<Switch onClick={() => onJumpsToggle(!jumpsEnabled)}
id="jumps-toggle" className={`p-2 rounded transition-colors ${
checked={jumpsEnabled} jumpsEnabled
onCheckedChange={onJumpsToggle} ? 'bg-blue-600/20 text-blue-400 hover:bg-blue-600/30'
/> : 'bg-gray-600/20 text-gray-400 hover:bg-gray-600/30'
<label htmlFor="jumps-toggle" className="text-sm font-medium text-white"> }`}
🚀 Show Jumps title={jumpsEnabled ? 'Hide Jumps' : 'Show Jumps'}
</label> >
</div> 🚀
<div className="flex items-center gap-3"> </button>
<Switch <button
id="kills-toggle" onClick={() => onKillsToggle(!killsEnabled)}
checked={killsEnabled} className={`p-2 rounded transition-colors ${
onCheckedChange={onKillsToggle} killsEnabled
/> ? 'bg-red-600/20 text-red-400 hover:bg-red-600/30'
<label htmlFor="kills-toggle" className="text-sm font-medium text-white"> : 'bg-gray-600/20 text-gray-400 hover:bg-gray-600/30'
Show Kills }`}
</label> title={killsEnabled ? 'Hide Kills' : 'Show Kills'}
</div> >
</button>
</div> </div>
</div> </div>
); );

View File

@@ -11,8 +11,13 @@ export enum Collections {
Mfas = "_mfas", Mfas = "_mfas",
Otps = "_otps", Otps = "_otps",
Superusers = "_superusers", Superusers = "_superusers",
IndBillitem = "ind_billItem",
IndChar = "ind_char",
IndJob = "ind_job",
IndTransaction = "ind_transaction",
Regionview = "regionview", Regionview = "regionview",
Signature = "signature", Signature = "signature",
SignatureNoteRules = "signature_note_rules",
Sigview = "sigview", Sigview = "sigview",
System = "system", System = "system",
WormholeSystems = "wormholeSystems", WormholeSystems = "wormholeSystems",
@@ -94,6 +99,74 @@ export type SuperusersRecord = {
verified?: boolean 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 = { export type RegionviewRecord = {
id: string id: string
sigcount?: number sigcount?: number
@@ -114,6 +187,15 @@ export type SignatureRecord = {
updated?: IsoDateString updated?: IsoDateString
} }
export type SignatureNoteRulesRecord = {
created?: IsoDateString
enabled?: boolean
id: string
note: string
regex: string
updated?: IsoDateString
}
export type SigviewRecord = { export type SigviewRecord = {
created?: IsoDateString created?: IsoDateString
dangerous?: boolean dangerous?: boolean
@@ -153,8 +235,13 @@ export type ExternalauthsResponse<Texpand = unknown> = Required<ExternalauthsRec
export type MfasResponse<Texpand = unknown> = Required<MfasRecord> & BaseSystemFields<Texpand> export type MfasResponse<Texpand = unknown> = Required<MfasRecord> & BaseSystemFields<Texpand>
export type OtpsResponse<Texpand = unknown> = Required<OtpsRecord> & BaseSystemFields<Texpand> export type OtpsResponse<Texpand = unknown> = Required<OtpsRecord> & BaseSystemFields<Texpand>
export type SuperusersResponse<Texpand = unknown> = Required<SuperusersRecord> & AuthSystemFields<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 RegionviewResponse<Texpand = unknown> = Required<RegionviewRecord> & BaseSystemFields<Texpand>
export type SignatureResponse<Texpand = unknown> = Required<SignatureRecord> & 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 SigviewResponse<Texpand = unknown> = Required<SigviewRecord> & BaseSystemFields<Texpand>
export type SystemResponse<Texpand = unknown> = Required<SystemRecord> & BaseSystemFields<Texpand> export type SystemResponse<Texpand = unknown> = Required<SystemRecord> & BaseSystemFields<Texpand>
export type WormholeSystemsResponse<Texpand = unknown> = Required<WormholeSystemsRecord> & BaseSystemFields<Texpand> export type WormholeSystemsResponse<Texpand = unknown> = Required<WormholeSystemsRecord> & BaseSystemFields<Texpand>
@@ -167,8 +254,13 @@ export type CollectionRecords = {
_mfas: MfasRecord _mfas: MfasRecord
_otps: OtpsRecord _otps: OtpsRecord
_superusers: SuperusersRecord _superusers: SuperusersRecord
ind_billItem: IndBillitemRecord
ind_char: IndCharRecord
ind_job: IndJobRecord
ind_transaction: IndTransactionRecord
regionview: RegionviewRecord regionview: RegionviewRecord
signature: SignatureRecord signature: SignatureRecord
signature_note_rules: SignatureNoteRulesRecord
sigview: SigviewRecord sigview: SigviewRecord
system: SystemRecord system: SystemRecord
wormholeSystems: WormholeSystemsRecord wormholeSystems: WormholeSystemsRecord
@@ -180,8 +272,13 @@ export type CollectionResponses = {
_mfas: MfasResponse _mfas: MfasResponse
_otps: OtpsResponse _otps: OtpsResponse
_superusers: SuperusersResponse _superusers: SuperusersResponse
ind_billItem: IndBillitemResponse
ind_char: IndCharResponse
ind_job: IndJobResponse
ind_transaction: IndTransactionResponse
regionview: RegionviewResponse regionview: RegionviewResponse
signature: SignatureResponse signature: SignatureResponse
signature_note_rules: SignatureNoteRulesResponse
sigview: SigviewResponse sigview: SigviewResponse
system: SystemResponse system: SystemResponse
wormholeSystems: WormholeSystemsResponse wormholeSystems: WormholeSystemsResponse
@@ -196,8 +293,13 @@ export type TypedPocketBase = PocketBase & {
collection(idOrName: '_mfas'): RecordService<MfasResponse> collection(idOrName: '_mfas'): RecordService<MfasResponse>
collection(idOrName: '_otps'): RecordService<OtpsResponse> collection(idOrName: '_otps'): RecordService<OtpsResponse>
collection(idOrName: '_superusers'): RecordService<SuperusersResponse> 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: 'regionview'): RecordService<RegionviewResponse>
collection(idOrName: 'signature'): RecordService<SignatureResponse> collection(idOrName: 'signature'): RecordService<SignatureResponse>
collection(idOrName: 'signature_note_rules'): RecordService<SignatureNoteRulesResponse>
collection(idOrName: 'sigview'): RecordService<SigviewResponse> collection(idOrName: 'sigview'): RecordService<SigviewResponse>
collection(idOrName: 'system'): RecordService<SystemResponse> collection(idOrName: 'system'): RecordService<SystemResponse>
collection(idOrName: 'wormholeSystems'): RecordService<WormholeSystemsResponse> 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 { parseSignature, parseScannedPercentage } from "@/utils/signatureParser";
import { getSystemId } from "@/utils/systemApi"; import { getSystemId } from "@/utils/systemApi";
import pb from "@/lib/pocketbase"; 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 = () => { export const SystemView = () => {
const { system, region } = useParams(); const { system, region } = useParams();
@@ -163,12 +163,17 @@ export const SystemView = () => {
try { try {
const systemId = await getSystemId(system); 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 lines = pastedText.trim().split('\n').filter(line => line.trim());
const parsedSignatures: Omit<Signature, 'id'>[] = []; const parsedSignatures: Omit<Signature, 'id'>[] = [];
// Parse all signatures // Parse all signatures
for (const line of lines) { for (const line of lines) {
const parsed = parseSignature(line); const parsed = parseSignature(line, rules);
if (parsed) { if (parsed) {
parsedSignatures.push({ parsedSignatures.push({
...parsed, ...parsed,
@@ -276,6 +281,7 @@ export const SystemView = () => {
regionName={region} regionName={region}
focusSystem={system} focusSystem={system}
isCompact={true} isCompact={true}
header={false}
/> />
</div> </div>
</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'); const parts = text.split('\t');
if (parts.length < 4) return null; if (parts.length < 4) return null;
@@ -56,16 +11,26 @@ export const parseSignature = (text: string): Omit<Signature, 'system' | 'id' |
return null; return null;
} }
let note = ""; const appliedNotes: string[] = [];
const isFour = isFourOutOfTen(parts[3]);
if (isFour) { if (rules && rules.length > 0) {
note = "4/10"; for (const rule of rules) {
} if (rule && rule.enabled) {
const isFive = isFiveOutOfTen(parts[3]); try {
if (isFive) { const re = new RegExp(rule.regex, 'i');
note = "5/10"; if (re.test(parts[3])) {
appliedNotes.push(rule.note);
}
} catch {
// invalid regex - ignore
}
}
}
} }
const dedupedNotes = Array.from(new Set(appliedNotes)).filter(Boolean);
const note = dedupedNotes.join(';');
return { return {
identifier: parts[0], identifier: parts[0],
type: parts[2], type: parts[2],