Commit 6: Adminbereich (TanStack Router, 8 Menüpunkte, Scan-Logs)

This commit is contained in:
2026-07-19 02:53:04 +02:00
parent b0ff551ca0
commit 2f31996bbb
31 changed files with 1693 additions and 112 deletions

View File

@@ -0,0 +1,121 @@
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@launchpad/ui";
import { useDevices } from "../../hooks/useDevices.js";
import { AdminPageHeader } from "./AdminPageHeader.js";
async function scanFritzBox() {
const res = await fetch("/api/scan/fritzbox", { method: "POST" });
const body = await res.json();
if (!res.ok) {
throw new Error(body.error ?? `FritzBox-Scan fehlgeschlagen (HTTP ${res.status})`);
}
return body as { found: number };
}
async function scanDeviceById(id: string) {
const res = await fetch(`/api/scan/devices/${id}`, { method: "POST" });
const body = await res.json();
if (!res.ok) {
throw new Error(body.detail ?? body.error ?? `Scan fehlgeschlagen (HTTP ${res.status})`);
}
return body as { created: number; updated: number };
}
export function ScannerPage() {
const queryClient = useQueryClient();
const { data: devices } = useDevices();
const [bulkStatus, setBulkStatus] = useState<string | null>(null);
const [bulkRunning, setBulkRunning] = useState(false);
const fritzboxMutation = useMutation({
mutationFn: scanFritzBox,
onSuccess: (result) => {
queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["logs"] });
return result;
},
});
async function scanAllDevices() {
if (!devices || devices.length === 0) return;
setBulkRunning(true);
let created = 0;
let updated = 0;
for (const device of devices) {
try {
const result = await scanDeviceById(device.id);
created += result.created;
updated += result.updated;
setBulkStatus(`Scanne ${device.hostname} … (${created} neu, ${updated} aktualisiert bisher)`);
} catch {
// einzelnes fehlgeschlagenes Gerät soll den Rest nicht abbrechen
}
}
setBulkStatus(`Fertig: ${devices.length} Gerät(e) gescannt, ${created} neue Dienste, ${updated} aktualisiert.`);
setBulkRunning(false);
queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["services"] });
queryClient.invalidateQueries({ queryKey: ["logs"] });
}
return (
<div>
<AdminPageHeader
title="Scanner"
description="Scans laufen ausschließlich manuell nie automatisch oder zeitgesteuert."
/>
<div className="grid gap-4 sm:grid-cols-2">
<div className="rounded-2xl border border-black/10 p-5 dark:border-white/10">
<h2 className="font-medium text-black dark:text-white">FritzBox-Scan</h2>
<p className="mt-1 text-sm text-black/50 dark:text-white/50">
Liest die Geräteliste der FritzBox per TR-064 und legt/aktualisiert Geräte.
Erfordert <code className="rounded bg-black/5 px-1 dark:bg-white/10">FRITZBOX_HOST</code>,{" "}
<code className="rounded bg-black/5 px-1 dark:bg-white/10">FRITZBOX_USERNAME</code> und{" "}
<code className="rounded bg-black/5 px-1 dark:bg-white/10">FRITZBOX_PASSWORD</code> in
der <code className="rounded bg-black/5 px-1 dark:bg-white/10">.env</code>.
</p>
<Button
variant="primary"
className="mt-4"
onClick={() => fritzboxMutation.mutate()}
disabled={fritzboxMutation.isPending}
>
{fritzboxMutation.isPending ? "Scanne …" : "FritzBox jetzt scannen"}
</Button>
{fritzboxMutation.isSuccess ? (
<p className="mt-2 text-sm text-emerald-600 dark:text-emerald-400">
{fritzboxMutation.data.found} Gerät(e) gefunden.
</p>
) : null}
{fritzboxMutation.isError ? (
<p className="mt-2 text-sm text-red-500">{(fritzboxMutation.error as Error).message}</p>
) : null}
</div>
<div className="rounded-2xl border border-black/10 p-5 dark:border-white/10">
<h2 className="font-medium text-black dark:text-white">Alle Geräte scannen</h2>
<p className="mt-1 text-sm text-black/50 dark:text-white/50">
Führt den Netzwerk-Scan (DNS, Ports, Titel/Favicon, Softwareerkennung) nacheinander
für alle {devices?.length ?? 0} bekannten Geräte aus. Für ein einzelnes Gerät lieber
den Button in der Geräte-Tabelle nutzen.
</p>
<Button
variant="primary"
className="mt-4"
onClick={scanAllDevices}
disabled={bulkRunning || !devices || devices.length === 0}
>
{bulkRunning ? "Scanne …" : "Alle Geräte jetzt scannen"}
</Button>
{bulkStatus ? (
<p className="mt-2 text-sm text-black/50 dark:text-white/50">{bulkStatus}</p>
) : null}
</div>
</div>
</div>
);
}