round30: alle drei Scanner als Hintergrund-Job, Dienst-Duplikate strukturell verhindert (Migration+Constraint+Geraete-Sperre), zweiter Erkennungsversuch bei leerem Ersttreffer

This commit is contained in:
2026-07-24 15:14:00 +02:00
parent 2106c17301
commit 87fed21348
7 changed files with 308 additions and 140 deletions

View File

@@ -22,16 +22,18 @@ export function useScanActivity(): ScanActivity {
async function poll() {
try {
const [devicesRes, apisRes] = await Promise.all([
const [devicesRes, apisRes, fritzboxRes] = await Promise.all([
fetch("/api/scan/devices/all/status"),
fetch("/api/scan/apis/status"),
fetch("/api/scan/fritzbox/status"),
]);
if (cancelled) return;
const devicesStatus = devicesRes.ok ? await devicesRes.json() : null;
const apisStatus = apisRes.ok ? await apisRes.json() : null;
const fritzboxStatus = fritzboxRes.ok ? await fritzboxRes.json() : null;
const running = !!devicesStatus?.running || !!apisStatus?.running;
const running = !!devicesStatus?.running || !!apisStatus?.running || !!fritzboxStatus?.running;
const lastResult = devicesStatus?.lastResult;
const pendingCount = lastResult

View File

@@ -18,22 +18,6 @@ interface DeviceChange {
suggested: string;
}
interface FritzBoxScanResult {
found: number;
uniqueDevices: number;
staleDevices: Device[];
deviceChanges: DeviceChange[];
}
async function scanFritzBox(): Promise<FritzBoxScanResult> {
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;
}
async function patchDevice(id: string, patch: Record<string, unknown>) {
const res = await fetch(`/api/devices/${id}`, {
method: "PATCH",
@@ -441,18 +425,56 @@ export function ScannerPage() {
null
);
const fritzboxMutation = useMutation({
mutationFn: scanFritzBox,
onSuccess: (result) => {
setFritzboxSummary({ found: result.found, uniqueDevices: result.uniqueDevices });
setFritzboxError(null);
setStaleDevices(result.staleDevices);
setDeviceChanges(result.deviceChanges);
queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["logs"] });
},
onError: (error: Error) => setFritzboxError(error.message),
});
const [fritzboxRunning, setFritzboxRunning] = useState(false);
const lastFritzboxHandledAt = useRef<string | null>(null);
useEffect(() => {
let cancelled = false;
async function poll() {
try {
const res = await fetch("/api/scan/fritzbox/status");
if (!res.ok || cancelled) return;
const s = await res.json();
setFritzboxRunning(s.running);
if (!s.running && s.lastResult && s.startedAt !== lastFritzboxHandledAt.current) {
lastFritzboxHandledAt.current = s.startedAt;
setFritzboxSummary({ found: s.lastResult.found, uniqueDevices: s.lastResult.uniqueDevices });
setFritzboxError(null);
setStaleDevices(s.lastResult.staleDevices);
setDeviceChanges(s.lastResult.deviceChanges);
queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["logs"] });
} else if (!s.running && s.lastError && s.startedAt !== lastFritzboxHandledAt.current) {
lastFritzboxHandledAt.current = s.startedAt;
setFritzboxError(s.lastError);
}
} catch {
// Netzwerk-Hänger beim Poll soll nicht die Seite stören.
}
}
poll();
const interval = setInterval(poll, 1500);
return () => {
cancelled = true;
clearInterval(interval);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
async function startFritzboxScan() {
setFritzboxError(null);
const res = await fetch("/api/scan/fritzbox", { method: "POST" });
if (!res.ok) {
const body = await res.json().catch(() => ({}));
setFritzboxError(body.error ?? `Fehlgeschlagen (HTTP ${res.status})`);
return;
}
setFritzboxRunning(true);
}
// Geräteübergreifender Sammel-Scan: läuft als Hintergrund-Job im Backend
// (siehe apps/backend/src/scanJobs.ts), nicht mehr als Schleife aus
@@ -548,10 +570,10 @@ export function ScannerPage() {
<Button
variant="primary"
className="mt-4"
onClick={() => fritzboxMutation.mutate()}
disabled={fritzboxMutation.isPending}
onClick={startFritzboxScan}
disabled={fritzboxRunning}
>
{fritzboxMutation.isPending ? "Scanne …" : "FritzBox jetzt scannen"}
{fritzboxRunning ? "Scanne …" : "FritzBox jetzt scannen"}
</Button>
{fritzboxSummary ? (
<p className="mt-2 text-sm text-emerald-600 dark:text-emerald-400">