generated from Dicken/dickendock
round26: Ping vor Portscan, Liste neuer Dienste, Suchmaschinen-Integration (OpenSearch), API-Scanner
This commit is contained in:
@@ -4,6 +4,8 @@ import { faBan, faCheck, faTrash, faXmark } from "@fortawesome/free-solid-svg-ic
|
||||
import { Button } from "@launchpad/ui";
|
||||
import type { Device, Service } from "@launchpad/shared";
|
||||
import { useDevices } from "../../hooks/useDevices.js";
|
||||
import { useServices } from "../../hooks/useServices.js";
|
||||
import { useDetectedApis, type DetectedApiEntry } from "../../hooks/useDetectedApis.js";
|
||||
import { usePersistedState } from "../../hooks/usePersistedState.js";
|
||||
import { AdminPageHeader } from "./AdminPageHeader.js";
|
||||
|
||||
@@ -90,6 +92,7 @@ async function scanDeviceById(id: string, signal?: AbortSignal) {
|
||||
created: number;
|
||||
updated: number;
|
||||
staleServices: Service[];
|
||||
newServices: Service[];
|
||||
nameChanges: ScanNameChange[];
|
||||
deviceNameSuggestion: string | null;
|
||||
};
|
||||
@@ -166,6 +169,25 @@ function NameChangesReview({
|
||||
);
|
||||
}
|
||||
|
||||
function NewServicesList({ newServices }: { newServices: Service[] }) {
|
||||
if (newServices.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-xl border border-emerald-500/30 bg-emerald-500/5 p-3 text-xs">
|
||||
<p className="mb-2 font-medium text-emerald-700 dark:text-emerald-400">
|
||||
{newServices.length} neue(r) Dienst(e) gefunden:
|
||||
</p>
|
||||
<ul className="max-h-60 space-y-1 overflow-y-auto">
|
||||
{newServices.map((s) => (
|
||||
<li key={s.id} className="text-black/70 dark:text-white/70">
|
||||
{s.displayName} ({s.hostname}:{s.port})
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StaleServicesReview({
|
||||
staleServices,
|
||||
onResolve,
|
||||
@@ -305,6 +327,93 @@ function StaleDevicesReview({
|
||||
);
|
||||
}
|
||||
|
||||
function ApiScannerCard() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: services } = useServices();
|
||||
const { data: detectedApis } = useDetectedApis();
|
||||
const [status, setStatus] = usePersistedState<string | null>("scanner:apiStatus", null);
|
||||
|
||||
const scanMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await fetch("/api/scan/apis", { method: "POST" });
|
||||
if (!res.ok) throw new Error(`Fehlgeschlagen (HTTP ${res.status})`);
|
||||
return res.json() as Promise<{ checked: number; servicesWithApi: number; totalFound: number }>;
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
setStatus(
|
||||
`Fertig: ${result.checked} Dienst(e) geprüft, bei ${result.servicesWithApi} Dienst(en) ${result.totalFound} API-Endpunkt(e) gefunden.`
|
||||
);
|
||||
queryClient.invalidateQueries({ queryKey: ["detected-apis"] });
|
||||
},
|
||||
onError: (err: Error) => setStatus(err.message),
|
||||
});
|
||||
|
||||
const serviceById = new Map((services ?? []).map((s) => [s.id, s]));
|
||||
const groups = new Map<string, DetectedApiEntry[]>();
|
||||
for (const entry of detectedApis ?? []) {
|
||||
const list = groups.get(entry.serviceId) ?? [];
|
||||
list.push(entry);
|
||||
groups.set(entry.serviceId, list);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-black/10 p-5 dark:border-white/10 sm:col-span-2">
|
||||
<h2 className="font-medium text-black dark:text-white">API-Scanner</h2>
|
||||
<p className="mt-1 text-sm text-black/50 dark:text-white/50">
|
||||
Eigener, separater Scan: prüft alle bekannten Dienste auf gängige API-Pfade
|
||||
(OpenAPI/Swagger, GraphQL, REST) und listet die Funde auf. Läuft nur auf Knopfdruck,
|
||||
nie zusammen mit dem normalen Geräte-Scan.
|
||||
</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
className="mt-4"
|
||||
onClick={() => {
|
||||
setStatus(null);
|
||||
scanMutation.mutate();
|
||||
}}
|
||||
disabled={scanMutation.isPending}
|
||||
>
|
||||
{scanMutation.isPending ? "Scanne …" : "APIs jetzt scannen"}
|
||||
</Button>
|
||||
{status ? <p className="mt-2 text-sm text-black/50 dark:text-white/50">{status}</p> : null}
|
||||
|
||||
{groups.size > 0 ? (
|
||||
<ul className="mt-4 max-h-72 space-y-2 overflow-y-auto text-sm">
|
||||
{Array.from(groups.entries()).map(([serviceId, entries]) => {
|
||||
const service = serviceById.get(serviceId);
|
||||
return (
|
||||
<li key={serviceId} className="rounded-lg border border-black/5 p-2 dark:border-white/5">
|
||||
<div className="font-medium text-black dark:text-white">
|
||||
{service?.displayName ?? "Unbekannter Dienst"}
|
||||
</div>
|
||||
<ul className="mt-1 flex flex-wrap gap-2">
|
||||
{entries.map((e) => (
|
||||
<li key={e.id}>
|
||||
<a
|
||||
href={`${service?.url ?? ""}${e.path}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="rounded-full bg-black/5 px-2 py-0.5 text-xs text-black/60
|
||||
hover:bg-black/10 dark:bg-white/10 dark:text-white/60 dark:hover:bg-white/20"
|
||||
>
|
||||
{e.type} · {e.path}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
) : (
|
||||
<p className="mt-4 text-sm text-black/30 dark:text-white/30">
|
||||
Noch keine APIs gefunden. Einmal scannen, um loszulegen.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function ScannerPage() {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: devices } = useDevices();
|
||||
@@ -315,6 +424,10 @@ export function ScannerPage() {
|
||||
"scanner:bulkStaleServices",
|
||||
[]
|
||||
);
|
||||
const [bulkNewServices, setBulkNewServices] = usePersistedState<Service[]>(
|
||||
"scanner:bulkNewServices",
|
||||
[]
|
||||
);
|
||||
const [bulkNameChanges, setBulkNameChanges] = usePersistedState<ScanNameChange[]>(
|
||||
"scanner:bulkNameChanges",
|
||||
[]
|
||||
@@ -352,11 +465,13 @@ async function scanAllDevices() {
|
||||
setBulkRunning(true);
|
||||
setBulkStaleServices([]);
|
||||
setBulkNameChanges([]);
|
||||
setBulkNewServices([]);
|
||||
let created = 0;
|
||||
let updated = 0;
|
||||
let scannedCount = 0;
|
||||
const allStale: Service[] = [];
|
||||
const allNameChanges: ScanNameChange[] = [];
|
||||
const allNew: Service[] = [];
|
||||
|
||||
for (const device of devices) {
|
||||
if (controller.signal.aborted) break;
|
||||
@@ -367,6 +482,7 @@ async function scanAllDevices() {
|
||||
scannedCount++;
|
||||
allStale.push(...result.staleServices);
|
||||
allNameChanges.push(...result.nameChanges);
|
||||
allNew.push(...result.newServices);
|
||||
setBulkStatus(`Scanne ${device.hostname} … (${created} neu, ${updated} aktualisiert bisher)`);
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === "AbortError") break;
|
||||
@@ -386,6 +502,7 @@ async function scanAllDevices() {
|
||||
);
|
||||
setBulkStaleServices(allStale);
|
||||
setBulkNameChanges(allNameChanges);
|
||||
setBulkNewServices(allNew);
|
||||
setBulkRunning(false);
|
||||
bulkAbortController = null;
|
||||
queryClient.invalidateQueries({ queryKey: ["devices"] });
|
||||
@@ -469,6 +586,7 @@ async function scanAllDevices() {
|
||||
{bulkStatus ? (
|
||||
<p className="mt-2 text-sm text-black/50 dark:text-white/50">{bulkStatus}</p>
|
||||
) : null}
|
||||
<NewServicesList newServices={bulkNewServices} />
|
||||
<NameChangesReview
|
||||
nameChanges={bulkNameChanges}
|
||||
onResolve={(key) => setBulkNameChanges((prev) => prev.filter((c) => nameChangeKey(c) !== key))}
|
||||
@@ -478,6 +596,8 @@ async function scanAllDevices() {
|
||||
onResolve={(id) => setBulkStaleServices((prev) => prev.filter((s) => s.id !== id))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ApiScannerCard />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user