From 1b9e7e2726e964db0649573e70c7f4607fa14bb2 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 14 Jun 2025 16:06:15 +0000 Subject: [PATCH] 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// format. --- src/App.tsx | 30 ++-- src/components/RegionMap.tsx | 13 +- src/components/SignatureIngest.tsx | 233 +++++++++++++++++++++++++++++ src/components/ui/switch.tsx | 1 + src/pages/SystemView.tsx | 18 +-- 5 files changed, 263 insertions(+), 32 deletions(-) create mode 100644 src/components/SignatureIngest.tsx diff --git a/src/App.tsx b/src/App.tsx index 4a7e188..3b4f8f2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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 ( + + } /> } /> + } /> } /> - {/* ADD ALL CUSTOM ROUTES ABOVE THE CATCH-ALL "*" ROUTE */} } /> - - - -); + + + + ); +} export default App; diff --git a/src/components/RegionMap.tsx b/src/components/RegionMap.tsx index 01b65c5..30ec396 100644 --- a/src/components/RegionMap.tsx +++ b/src/components/RegionMap.tsx @@ -57,11 +57,7 @@ const fetchRegionData = async (regionName: string): Promise => { return systems; }; -export const RegionMap: React.FC = ({ - 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 = ({ }, [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 = ({ ); }; + +export default RegionMap; diff --git a/src/components/SignatureIngest.tsx b/src/components/SignatureIngest.tsx new file mode 100644 index 0000000..947db99 --- /dev/null +++ b/src/components/SignatureIngest.tsx @@ -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 | 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 => { + 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 => { + // 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 => { + 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 ( + + + + + Ingest Signatures + + + +
+ +