Geraete-Aenderungen (Name/IP) jetzt auch nur als Vorschlag statt automatisch, klarere FritzBox-Zaehlung

This commit is contained in:
2026-07-22 16:53:38 +02:00
parent 5f150119b2
commit 54f224b650
5 changed files with 172 additions and 16 deletions

View File

@@ -5,9 +5,19 @@ import type { Device, Service } from "@launchpad/shared";
import { useDevices } from "../../hooks/useDevices.js";
import { AdminPageHeader } from "./AdminPageHeader.js";
interface DeviceChange {
deviceId: string;
deviceHostname: string;
field: "hostname" | "ip";
current: string;
suggested: string;
}
interface FritzBoxScanResult {
found: number;
uniqueDevices: number;
staleDevices: Device[];
deviceChanges: DeviceChange[];
}
async function scanFritzBox(): Promise<FritzBoxScanResult> {
@@ -19,6 +29,18 @@ async function scanFritzBox(): Promise<FritzBoxScanResult> {
return body;
}
async function patchDevice(id: string, patch: Record<string, unknown>) {
const res = await fetch(`/api/devices/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
if (!res.ok) {
throw new Error(`Gerät konnte nicht aktualisiert werden (HTTP ${res.status})`);
}
return res.json();
}
async function deleteDeviceRequest(id: string) {
const res = await fetch(`/api/devices/${id}`, { method: "DELETE" });
if (!res.ok && res.status !== 404) {
@@ -184,6 +206,62 @@ function StaleServicesReview({
);
}
function DeviceChangesReview({
deviceChanges,
onDone,
}: {
deviceChanges: DeviceChange[];
onDone: () => void;
}) {
const queryClient = useQueryClient();
const [handled, setHandled] = useState<Set<string>>(new Set());
const applyMutation = useMutation({
mutationFn: (change: DeviceChange) => patchDevice(change.deviceId, { [change.field]: change.suggested }),
onSuccess: (_data, change) => {
setHandled((prev) => new Set(prev).add(`${change.deviceId}:${change.field}`));
queryClient.invalidateQueries({ queryKey: ["devices"] });
},
});
const remaining = deviceChanges.filter((c) => !handled.has(`${c.deviceId}:${c.field}`));
if (remaining.length === 0) return null;
const fieldLabel = (field: DeviceChange["field"]) => (field === "hostname" ? "Name" : "IP");
return (
<div className="mt-3 rounded-xl border border-blue-500/30 bg-blue-500/5 p-3 text-xs">
<p className="mb-2 font-medium text-blue-700 dark:text-blue-400">
{remaining.length} Geräte-Änderung(en) weichen von den gespeicherten Werten ab:
</p>
<ul className="max-h-60 space-y-1 overflow-y-auto">
{remaining.map((c) => (
<li key={`${c.deviceId}:${c.field}`} className="flex items-center justify-between gap-2">
<span className="text-black/70 dark:text-white/70">
{c.deviceHostname} {fieldLabel(c.field)}: {c.current}" → „{c.suggested}"
</span>
<div className="flex gap-1">
<Button
size="sm"
variant="ghost"
onClick={() => setHandled((prev) => new Set(prev).add(`${c.deviceId}:${c.field}`))}
>
Behalten
</Button>
<Button size="sm" variant="primary" onClick={() => applyMutation.mutate(c)} disabled={applyMutation.isPending}>
Übernehmen
</Button>
</div>
</li>
))}
</ul>
<button onClick={onDone} className="mt-2 text-black/40 underline dark:text-white/40">
Hinweis schließen
</button>
</div>
);
}
function StaleDevicesReview({ staleDevices, onDone }: { staleDevices: Device[]; onDone: () => void }) {
const queryClient = useQueryClient();
const [handled, setHandled] = useState<Set<string>>(new Set());
@@ -242,11 +320,13 @@ export function ScannerPage() {
const [bulkStaleServices, setBulkStaleServices] = useState<Service[]>([]);
const [bulkNameChanges, setBulkNameChanges] = useState<ScanNameChange[]>([]);
const [staleDevices, setStaleDevices] = useState<Device[]>([]);
const [deviceChanges, setDeviceChanges] = useState<DeviceChange[]>([]);
const fritzboxMutation = useMutation({
mutationFn: scanFritzBox,
onSuccess: (result) => {
setStaleDevices(result.staleDevices);
setDeviceChanges(result.deviceChanges);
queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["logs"] });
return result;
@@ -316,12 +396,20 @@ export function ScannerPage() {
</Button>
{fritzboxMutation.isSuccess ? (
<p className="mt-2 text-sm text-emerald-600 dark:text-emerald-400">
{fritzboxMutation.data.found} Gerät(e) gefunden.
{fritzboxMutation.data.found} Eintrag/Einträge von der FritzBox,{" "}
{fritzboxMutation.data.uniqueDevices} eindeutige Geräte
{fritzboxMutation.data.found !== fritzboxMutation.data.uniqueDevices
? " (mehrere Einträge desselben Geräts wurden zusammengeführt)"
: ""}
.
</p>
) : null}
{fritzboxMutation.isError ? (
<p className="mt-2 text-sm text-red-500">{(fritzboxMutation.error as Error).message}</p>
) : null}
{deviceChanges.length > 0 ? (
<DeviceChangesReview deviceChanges={deviceChanges} onDone={() => setDeviceChanges([])} />
) : null}
{staleDevices.length > 0 ? (
<StaleDevicesReview staleDevices={staleDevices} onDone={() => setStaleDevices([])} />
) : null}