Implement note rules

So we may specify regex rules to map to notes
This commit is contained in:
2025-10-05 15:03:05 +02:00
parent f6450fdafb
commit 22a7d1ad45
8 changed files with 271 additions and 72 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,6 +98,7 @@ 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"
@@ -110,8 +111,7 @@ export const Header = ({ title, breadcrumbs = [] }: HeaderProps) => {
<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'
}`} }`}

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">
{note.trim()}
</Badge> </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 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> </Badge>
))}
</div>
)} )}
</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,119 @@
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 => handleUpdate(r.id, { regex: e.target.value })}
className="font-mono"
/>
</TableCell>
<TableCell className="max-w-0">
<Input value={r.note} onChange={e => handleUpdate(r.id, { note: e.target.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,

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,15 +11,25 @@ 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) {
try {
const re = new RegExp(rule.regex, 'i');
if (re.test(parts[3])) {
appliedNotes.push(rule.note);
} }
const isFive = isFiveOutOfTen(parts[3]); } catch {
if (isFive) { // invalid regex - ignore
note = "5/10";
} }
}
}
}
const dedupedNotes = Array.from(new Set(appliedNotes)).filter(Boolean);
const note = dedupedNotes.join(';');
return { return {
identifier: parts[0], identifier: parts[0],