diff --git a/apps/backend/src/scanner/networkScanner.ts b/apps/backend/src/scanner/networkScanner.ts index 1e40878..767ce1e 100644 --- a/apps/backend/src/scanner/networkScanner.ts +++ b/apps/backend/src/scanner/networkScanner.ts @@ -91,12 +91,22 @@ export async function scanDeviceServices( const suggestedHostname = dnsResult ? null : await reverseLookup(device.ip); const candidatePorts = Array.from(new Set([80, 443, ...extraPorts])); + + // Die offenen Ports werden PARALLEL geprüft, nicht nacheinander - bei + // einem nicht erreichbaren/gefilterten Gerät (z. B. ein schlafender + // Laptop) würde ein sequenzieller Durchlauf sonst bis zu + // Portanzahl × Timeout dauern (bei 20 Ports und 800ms Timeout: 16+ + // Sekunden PRO GERÄT) und den Scan wie hängengeblieben wirken lassen. + // Danach wird nur noch für die tatsächlich offenen Ports (meist wenige) + // die eigentliche HTTP-Abfrage gemacht. + const openChecks = await Promise.all( + candidatePorts.map(async (port) => ({ port, open: await isPortOpen(device.ip, port) })) + ); + const openPorts = openChecks.filter((c) => c.open).map((c) => c.port); + const found: DiscoveredService[] = []; - for (const port of candidatePorts) { - const open = await isPortOpen(device.ip, port); - if (!open) continue; - + for (const port of openPorts) { let isHttps = port === 443 || port === 9443 || port === 8006 || port === 8443; let baseUrl = `${isHttps ? "https" : "http"}://${address}:${port}`; let probe = await probeHttp(baseUrl); diff --git a/apps/frontend/src/routes/admin/DevicesPage.tsx b/apps/frontend/src/routes/admin/DevicesPage.tsx index 0c72e30..c7ce664 100644 --- a/apps/frontend/src/routes/admin/DevicesPage.tsx +++ b/apps/frontend/src/routes/admin/DevicesPage.tsx @@ -79,8 +79,8 @@ interface ScanResult { deviceNameSuggestion: string | null; } -async function scanDevice(id: string): Promise { - const res = await fetch(`/api/scan/devices/${id}`, { method: "POST" }); +async function scanDevice(id: string, signal?: AbortSignal): Promise { + const res = await fetch(`/api/scan/devices/${id}`, { method: "POST", signal }); const body = await res.json(); if (!res.ok) { throw new Error(body.detail ?? body.error ?? `Scan fehlgeschlagen (HTTP ${res.status})`); @@ -323,9 +323,14 @@ function DeviceRow({ device }: { device: DeviceWithServices }) { const [deviceNameSuggestion, setDeviceNameSuggestion] = useState(null); const [expanded, setExpanded] = useState(false); const [editing, setEditing] = useState(false); + const [scanAbort, setScanAbort] = useState(null); const scanMutation = useMutation({ - mutationFn: () => scanDevice(device.id), + mutationFn: () => { + const controller = new AbortController(); + setScanAbort(controller); + return scanDevice(device.id, controller.signal); + }, onSuccess: (result) => { const portsText = result.ports.length > 0 ? result.ports.join(", ") : "keine"; const pending = result.nameChanges.length + result.staleServices.length; @@ -339,7 +344,10 @@ function DeviceRow({ device }: { device: DeviceWithServices }) { queryClient.invalidateQueries({ queryKey: ["devices"] }); queryClient.invalidateQueries({ queryKey: ["services"] }); }, - onError: (err: Error) => setScanMessage(err.message), + onError: (err: Error) => { + setScanMessage(err.name === "AbortError" ? "Scan abgebrochen." : err.message); + }, + onSettled: () => setScanAbort(null), }); const deleteMutation = useMutation({ @@ -412,6 +420,17 @@ function DeviceRow({ device }: { device: DeviceWithServices }) { > {scanMutation.isPending ? "Scanne …" : "Jetzt scannen"} + {scanMutation.isPending && scanAbort ? ( + + ) : null} +
+ + {bulkRunning ? ( + + ) : null} +
{bulkStatus ? (

{bulkStatus}

) : null}