diff --git a/apps/backend/src/routes/scan.ts b/apps/backend/src/routes/scan.ts index 24aa029..38f9aa0 100644 --- a/apps/backend/src/routes/scan.ts +++ b/apps/backend/src/routes/scan.ts @@ -21,6 +21,7 @@ import { requestCancel, tryLockDevice, unlockDevice, + getScanningDeviceIds, } from "../scanJobs.js"; import type { Device } from "@launchpad/shared"; @@ -271,6 +272,15 @@ export async function scanRoutes(app: FastifyInstance): Promise { return { dismissed: true }; }); + // Geräteübergreifende Sichtbarkeit für manuelle Einzelgeräte-Scans (siehe + // getScanningDeviceIds() in scanJobs.ts) - wird von der Seitenleiste + // gepollt, damit "Ein Scan läuft gerade" auch für einen per Button auf der + // Geräteseite gestarteten Scan angezeigt wird, nicht nur für den + // Sammel-Scan. + app.get("/api/scan/devices/active", async () => { + return { deviceIds: getScanningDeviceIds() }; + }); + app.post("/api/scan/devices/:id", async (request, reply) => { const { id } = request.params as { id: string }; const { full } = request.query as { full?: string }; diff --git a/apps/backend/src/scanJobs.ts b/apps/backend/src/scanJobs.ts index 67d42e9..8968758 100644 --- a/apps/backend/src/scanJobs.ts +++ b/apps/backend/src/scanJobs.ts @@ -100,3 +100,18 @@ export function tryLockDevice(deviceId: string): boolean { export function unlockDevice(deviceId: string): void { devicesCurrentlyScanning.delete(deviceId); } + +/** + * Liefert die IDs aller Geräte, die GERADE JETZT gescannt werden - egal ob + * über den Einzelgerät-Endpunkt (manueller "Jetzt scannen"-Button auf der + * Geräteseite) oder als Teil eines laufenden Sammel-Scans. Genutzt für die + * geräteübergreifende Anzeige "Ein Scan läuft gerade" in der Seitenleiste + * (siehe AdminLayout.tsx/useScanActivity.ts) - vorher war ein einzelner + * manueller Geräte-Scan NUR im Browser sichtbar, der ihn ausgelöst hat (der + * Einzelgerät-Endpunkt ist eine simple, blockierende Anfrage ohne eigenen + * Job-Status wie der Sammel-Scan), auf jedem anderen Browser/Gerät gab es + * dafür überhaupt keine Anzeige. + */ +export function getScanningDeviceIds(): string[] { + return Array.from(devicesCurrentlyScanning); +} diff --git a/apps/frontend/src/hooks/useScanActivity.ts b/apps/frontend/src/hooks/useScanActivity.ts index f68793b..3089df0 100644 --- a/apps/frontend/src/hooks/useScanActivity.ts +++ b/apps/frontend/src/hooks/useScanActivity.ts @@ -22,18 +22,29 @@ export function useScanActivity(): ScanActivity { async function poll() { try { - const [devicesRes, apisRes, fritzboxRes] = await Promise.all([ + const [devicesRes, apisRes, fritzboxRes, activeDevicesRes] = await Promise.all([ fetch("/api/scan/devices/all/status"), fetch("/api/scan/apis/status"), fetch("/api/scan/fritzbox/status"), + fetch("/api/scan/devices/active"), ]); 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; + // Manuelle Einzelgeräte-Scans (Button auf der Geräteseite) laufen + // NICHT über den Sammel-Scan-Job, sondern über eine einfache, + // blockierende Anfrage - ohne diesen zusätzlichen Abruf wäre so ein + // Scan für jeden anderen Browser/jedes andere Gerät unsichtbar + // gewesen (siehe Bugreport). + const activeDevices = activeDevicesRes.ok ? await activeDevicesRes.json() : null; - const running = !!devicesStatus?.running || !!apisStatus?.running || !!fritzboxStatus?.running; + const running = + !!devicesStatus?.running || + !!apisStatus?.running || + !!fritzboxStatus?.running || + (activeDevices?.deviceIds?.length ?? 0) > 0; const lastResult = devicesStatus?.lastResult; const pendingCount = lastResult diff --git a/apps/frontend/src/routes/admin/DevicesPage.tsx b/apps/frontend/src/routes/admin/DevicesPage.tsx index df53ebd..81cde8d 100644 --- a/apps/frontend/src/routes/admin/DevicesPage.tsx +++ b/apps/frontend/src/routes/admin/DevicesPage.tsx @@ -343,9 +343,16 @@ function DeviceRow({ device }: { device: DeviceWithServices }) { setDeviceNameSuggestion(result.deviceNameSuggestion); queryClient.invalidateQueries({ queryKey: ["devices"] }); queryClient.invalidateQueries({ queryKey: ["services"] }); + // War bisher vergessen: der Scan selbst schreibt zwar schon einen + // Log-Eintrag (siehe performDeviceScan in routes/scan.ts, gilt für + // Einzelgerät- UND Sammel-Scan gleichermaßen), aber ohne diese + // Invalidierung blieb eine bereits geöffnete Admin -> Logs-Seite auf + // dem alten Stand, bis man sie manuell neu lädt (siehe Bugreport). + queryClient.invalidateQueries({ queryKey: ["logs"] }); }, onError: (err: Error) => { setScanMessage(err.name === "AbortError" ? "Scan abgebrochen." : err.message); + queryClient.invalidateQueries({ queryKey: ["logs"] }); }, onSettled: () => setScanAbort(null), });