Implement signature ingestion and cleanup

- Add functionality to paste and parse signatures from text input.
- Implement a "clean" toggle to delete signatures not present in the pasted input.
- Update routing to use /regions/<region>/<system> format.
This commit is contained in:
gpt-engineer-app[bot]
2025-06-14 16:06:15 +00:00
parent 99ffba4f28
commit 1b9e7e2726
5 changed files with 263 additions and 32 deletions

View File

@@ -1,32 +1,30 @@
import { Toaster } from "@/components/ui/toaster";
import { Toaster as Sonner } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Toaster } from "@/components/ui/toaster";
import Index from "./pages/Index";
import RegionPage from "./pages/RegionPage";
import NotFound from "./pages/NotFound";
import SystemView from "./pages/SystemView";
import NotFound from "./pages/NotFound";
import "./App.css";
const queryClient = new QueryClient();
const App = () => (
function App() {
return (
<QueryClientProvider client={queryClient}>
<TooltipProvider>
<Toaster />
<Sonner />
<BrowserRouter>
<Router>
<Routes>
<Route path="/" element={<Index />} />
<Route path="/regions/:region" element={<RegionPage />} />
<Route path="/regions/:region/:system" element={<SystemView />} />
<Route path="/systems/:system" element={<SystemView />} />
{/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */}
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
</TooltipProvider>
<Toaster />
</Router>
</QueryClientProvider>
);
}
export default App;

View File

@@ -57,11 +57,7 @@ const fetchRegionData = async (regionName: string): Promise<SolarSystem[]> => {
return systems;
};
export const RegionMap: React.FC<RegionMapProps> = ({
regionName,
focusSystem,
isCompact = false
}) => {
const RegionMap = ({ regionName, focusSystem, isCompact = false }: RegionMapProps) => {
const navigate = useNavigate();
const [viewBox, setViewBox] = useState({ x: 0, y: 0, width: 1200, height: 800 });
const [isPanning, setIsPanning] = useState(false);
@@ -135,8 +131,9 @@ export const RegionMap: React.FC<RegionMapProps> = ({
}, [systems, focusSystem, isCompact]);
const handleSystemClick = (systemName: string) => {
// Pass the region name when navigating to a system
navigate(`/systems/${systemName}?region=${regionName}`);
if (focusSystem === systemName) return;
navigate(`/regions/${regionName}/${systemName}`);
};
const handleMouseDown = useCallback((e: React.MouseEvent) => {
@@ -378,3 +375,5 @@ export const RegionMap: React.FC<RegionMapProps> = ({
</div>
);
};
export default RegionMap;

View File

@@ -0,0 +1,233 @@
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
import { Upload, Trash2 } from "lucide-react";
import { toast } from "@/hooks/use-toast";
import pb from "@/lib/pocketbase";
import { useQueryClient } from "@tanstack/react-query";
interface Signature {
identifier: string;
type: string;
signame: string;
system: string;
sysid: string;
dangerous?: boolean;
}
interface SignatureIngestProps {
system: string;
region: string;
}
const SignatureIngest = ({ system, region }: SignatureIngestProps) => {
const [pasteData, setPasteData] = useState("");
const [cleanMode, setCleanMode] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const queryClient = useQueryClient();
const parseSignature = (text: string): Omit<Signature, 'system' | 'sysid'> | null => {
const parts = text.split('\t');
if (parts.length < 4) return null;
return {
identifier: parts[0],
type: parts[2],
signame: parts[3],
dangerous: false // TODO: Implement dangerous signature detection
};
};
const getSystemId = async (systemName: string): Promise<string> => {
const url = `https://evebase.site.quack-lab.dev/api/collections/regionview/records?filter=(sysname='${encodeURIComponent(systemName)}')`;
const response = await fetch(url);
const data = await response.json();
if (data.items && data.items.length > 0) {
return data.items[0].id;
}
throw new Error(`System ${systemName} not found`);
};
const saveSignature = async (signature: Signature): Promise<void> => {
// Check if signature already exists
const existingUrl = `https://evebase.site.quack-lab.dev/api/collections/sigview/records?filter=(identifier='${signature.identifier}' && system='${signature.system}')`;
const existingResponse = await fetch(existingUrl);
const existingData = await existingResponse.json();
if (existingData.items && existingData.items.length > 0) {
// Update existing signature
const existingId = existingData.items[0].id;
const updateUrl = `https://evebase.site.quack-lab.dev/api/collections/sigview/records/${existingId}`;
const updateResponse = await fetch(updateUrl, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
signame: signature.signame,
type: signature.type,
dangerous: signature.dangerous
})
});
if (!updateResponse.ok) {
throw new Error(`Failed to update signature: ${updateResponse.status}`);
}
} else {
// Create new signature
const createUrl = 'https://evebase.site.quack-lab.dev/api/collections/sigview/records';
const createResponse = await fetch(createUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(signature)
});
if (!createResponse.ok) {
throw new Error(`Failed to create signature: ${createResponse.status}`);
}
}
};
const deleteSignature = async (signatureId: string): Promise<void> => {
const deleteUrl = `https://evebase.site.quack-lab.dev/api/collections/sigview/records/${signatureId}`;
const response = await fetch(deleteUrl, { method: 'DELETE' });
if (!response.ok && response.status !== 204) {
throw new Error(`Failed to delete signature: ${response.status}`);
}
};
const handleSubmit = async () => {
if (!pasteData.trim()) {
toast({
title: "No Data",
description: "Please paste signature data to submit.",
variant: "destructive"
});
return;
}
setIsSubmitting(true);
try {
const systemId = await getSystemId(system);
const lines = pasteData.trim().split('\n').filter(line => line.trim());
const parsedSignatures: Signature[] = [];
// Parse all signatures
for (const line of lines) {
const parsed = parseSignature(line);
if (parsed) {
parsedSignatures.push({
...parsed,
system,
sysid: systemId
});
}
}
if (parsedSignatures.length === 0) {
toast({
title: "No Valid Signatures",
description: "No valid signatures found in the pasted data.",
variant: "destructive"
});
return;
}
// If clean mode is enabled, get existing signatures and delete ones not in the new list
if (cleanMode) {
const existingUrl = `https://evebase.site.quack-lab.dev/api/collections/sigview/records?filter=(system='${encodeURIComponent(system)}')`;
const existingResponse = await fetch(existingUrl);
const existingData = await existingResponse.json();
const newIdentifiers = new Set(parsedSignatures.map(sig => sig.identifier));
const toDelete = existingData.items?.filter((item: any) => !newIdentifiers.has(item.identifier)) || [];
// Delete signatures not in the new list
for (const item of toDelete) {
await deleteSignature(item.id);
}
}
// Save all new/updated signatures
for (const signature of parsedSignatures) {
await saveSignature(signature);
}
// Invalidate queries to refresh the data
queryClient.invalidateQueries({ queryKey: ['signatures', system] });
toast({
title: "Success",
description: `${parsedSignatures.length} signatures processed${cleanMode ? ' (clean mode)' : ''}.`
});
setPasteData("");
} catch (error) {
console.error('Failed to submit signatures:', error);
toast({
title: "Error",
description: error instanceof Error ? error.message : "Failed to submit signatures.",
variant: "destructive"
});
} finally {
setIsSubmitting(false);
}
};
const handlePaste = (e: React.ClipboardEvent) => {
const pastedText = e.clipboardData.getData('text');
setPasteData(prev => prev + (prev ? '\n' : '') + pastedText);
};
return (
<Card className="bg-slate-800/30 border-slate-700">
<CardHeader>
<CardTitle className="text-white flex items-center gap-2">
<Upload className="h-5 w-5 text-blue-400" />
Ingest Signatures
</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="signature-data" className="text-slate-200">
Paste signature data (Ctrl+V)
</Label>
<Textarea
id="signature-data"
placeholder="Paste signatures here (one per line, tab-separated)"
value={pasteData}
onChange={(e) => setPasteData(e.target.value)}
onPaste={handlePaste}
className="bg-slate-700 border-slate-600 text-white placeholder:text-slate-400 min-h-[120px]"
/>
</div>
<div className="flex items-center space-x-2">
<Switch
id="clean-mode"
checked={cleanMode}
onCheckedChange={setCleanMode}
/>
<Label htmlFor="clean-mode" className="text-slate-200 flex items-center gap-2">
<Trash2 className="h-4 w-4" />
Clean mode (delete signatures not in list)
</Label>
</div>
<Button
onClick={handleSubmit}
disabled={isSubmitting || !pasteData.trim()}
className="w-full bg-blue-600 hover:bg-blue-700"
>
{isSubmitting ? "Processing..." : "Submit Signatures"}
</Button>
</CardContent>
</Card>
);
};
export default SignatureIngest;

View File

@@ -1,3 +1,4 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"

View File

@@ -1,13 +1,12 @@
import { useParams, useNavigate, useSearchParams } from "react-router-dom";
import { useParams, useNavigate } from "react-router-dom";
import SystemTracker from "@/components/SystemTracker";
import SignatureIngest from "@/components/SignatureIngest";
import { RegionMap } from "@/components/RegionMap";
const SystemView = () => {
const { system } = useParams();
const { system, region } = useParams();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const regionName = searchParams.get('region');
if (!system) {
navigate("/");
@@ -23,22 +22,23 @@ const SystemView = () => {
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main content - signatures */}
<div className="lg:col-span-2">
{/* Main content - signatures and ingestion */}
<div className="lg:col-span-2 space-y-6">
<SystemTracker system={system} />
<SignatureIngest system={system} region={region || ''} />
</div>
{/* Regional overview map */}
<div className="lg:col-span-1">
{regionName ? (
{region ? (
<div className="h-96 border border-purple-500/30 rounded-lg overflow-hidden">
<div className="p-2 border-b border-purple-500/30 bg-black/20 backdrop-blur-sm">
<h3 className="text-white text-sm font-semibold">{regionName} Region</h3>
<h3 className="text-white text-sm font-semibold">{region} Region</h3>
<p className="text-purple-200 text-xs">Click systems to navigate Current: {system}</p>
</div>
<div className="h-[calc(100%-3rem)]">
<RegionMap
regionName={regionName}
regionName={region}
focusSystem={system}
isCompact={true}
/>