Dashboard-Zahlen (Kategorien, Read Later), Host/IP getrennte Spalten bei Diensten, Scanner-Uebersicht beim Massen-Scan

This commit is contained in:
2026-07-20 17:51:58 +02:00
parent 0580d46000
commit 724f076c8c
3 changed files with 111 additions and 18 deletions

View File

@@ -1,7 +1,7 @@
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@launchpad/ui";
import type { Device } from "@launchpad/shared";
import type { Device, Service } from "@launchpad/shared";
import { useDevices } from "../../hooks/useDevices.js";
import { AdminPageHeader } from "./AdminPageHeader.js";
@@ -26,13 +26,76 @@ async function deleteDeviceRequest(id: string) {
}
}
async function deleteServiceRequest(id: string) {
const res = await fetch(`/api/services/${id}`, { method: "DELETE" });
if (!res.ok && res.status !== 404) {
throw new Error(`Dienst konnte nicht gelöscht werden (HTTP ${res.status})`);
}
}
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 };
return body as { created: number; updated: number; staleServices: Service[] };
}
function StaleServicesReview({
staleServices,
onDone,
}: {
staleServices: Service[];
onDone: () => void;
}) {
const queryClient = useQueryClient();
const [handled, setHandled] = useState<Set<string>>(new Set());
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteServiceRequest(id),
onSuccess: (_data, id) => {
setHandled((prev) => new Set(prev).add(id));
queryClient.invalidateQueries({ queryKey: ["services"] });
},
});
const remaining = staleServices.filter((s) => !handled.has(s.id));
if (remaining.length === 0) return null;
return (
<div className="mt-3 rounded-xl border border-amber-500/30 bg-amber-500/5 p-3 text-xs">
<p className="mb-2 font-medium text-amber-700 dark:text-amber-400">
{remaining.length} Dienst(e) über alle gescannten Geräte hinweg nicht mehr gefunden (Port
nicht mehr offen):
</p>
<ul className="max-h-60 space-y-1 overflow-y-auto">
{remaining.map((s) => (
<li key={s.id} className="flex items-center justify-between gap-2">
<span className="text-black/70 dark:text-white/70">
{s.displayName} ({s.hostname}:{s.port})
</span>
<div className="flex gap-1">
<Button size="sm" variant="ghost" onClick={() => setHandled((prev) => new Set(prev).add(s.id))}>
Behalten
</Button>
<Button
size="sm"
variant="danger"
onClick={() => deleteMutation.mutate(s.id)}
disabled={deleteMutation.isPending}
>
Entfernen
</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 }) {
@@ -90,6 +153,7 @@ export function ScannerPage() {
const { data: devices } = useDevices();
const [bulkStatus, setBulkStatus] = useState<string | null>(null);
const [bulkRunning, setBulkRunning] = useState(false);
const [bulkStaleServices, setBulkStaleServices] = useState<Service[]>([]);
const [staleDevices, setStaleDevices] = useState<Device[]>([]);
const fritzboxMutation = useMutation({
@@ -105,21 +169,29 @@ export function ScannerPage() {
async function scanAllDevices() {
if (!devices || devices.length === 0) return;
setBulkRunning(true);
setBulkStaleServices([]);
let created = 0;
let updated = 0;
const allStale: Service[] = [];
for (const device of devices) {
try {
const result = await scanDeviceById(device.id);
created += result.created;
updated += result.updated;
allStale.push(...result.staleServices);
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.`);
setBulkStatus(
`Fertig: ${devices.length} Gerät(e) gescannt, ${created} neue Dienste, ${updated} aktualisiert${
allStale.length > 0 ? `, ${allStale.length} nicht mehr gefunden (siehe unten)` : ""
}.`
);
setBulkStaleServices(allStale);
setBulkRunning(false);
queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["services"] });
@@ -182,6 +254,12 @@ export function ScannerPage() {
{bulkStatus ? (
<p className="mt-2 text-sm text-black/50 dark:text-white/50">{bulkStatus}</p>
) : null}
{bulkStaleServices.length > 0 ? (
<StaleServicesReview
staleServices={bulkStaleServices}
onDone={() => setBulkStaleServices([])}
/>
) : null}
</div>
</div>
</div>