round59: Serverseitige, geraeteuebergreifende Bestaetigung von Scan-Vorschlaegen statt nur lokalem sessionStorage

This commit is contained in:
2026-07-26 14:00:10 +02:00
parent 403a052a0f
commit a7036849b1
5 changed files with 282 additions and 35 deletions

View File

@@ -68,7 +68,33 @@ async function patchService(id: string, patch: Record<string, unknown>) {
}
function nameChangeKey(c: ScanNameChange): string {
return `${c.serviceId}:${c.field}`;
return `${c.serviceId}:${c.field}:${c.suggested}`;
}
function deviceChangeKey(c: { deviceId: string; field: string; suggested: string }): string {
return `${c.deviceId}:${c.field}:${c.suggested}`;
}
/**
* Meldet eine Bestätigung/Ablehnung eines Scan-Vorschlags SERVERSEITIG
* (siehe routes/scan.ts POST /api/scan/dismiss), damit sie für ALLE
* Browser/Geräte gilt, nicht nur lokal im aufrufenden Browser
* (sessionStorage) - siehe Bugreport.
*/
async function dismissSuggestion(kind: string, key: string): Promise<void> {
try {
await fetch("/api/scan/dismiss", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ kind, key }),
});
} catch {
// Netzwerk-Hänger hier soll die Bedienung nicht blockieren - die lokale
// Ausblendung (siehe Aufrufer) greift trotzdem sofort; bleibt der
// Dismiss serverseitig aus, taucht der Vorschlag beim nächsten Scan
// erneut auf, ist also kein Datenverlust, nur ein einmal erneutes
// Bestätigen nötig.
}
}
/**
@@ -276,7 +302,7 @@ function DeviceChangesReview({
const applyMutation = useMutation({
mutationFn: (change: DeviceChange) => patchDevice(change.deviceId, { [change.field]: change.suggested }),
onSuccess: (_data, change) => {
onResolve(`${change.deviceId}:${change.field}`);
onResolve(deviceChangeKey(change));
queryClient.invalidateQueries({ queryKey: ["devices"] });
},
});
@@ -292,7 +318,7 @@ function DeviceChangesReview({
{deviceChanges.length} Geräte-Änderung(en) weichen von den gespeicherten Werten ab:
</p>
<button
onClick={() => deviceChanges.forEach((c) => onResolve(`${c.deviceId}:${c.field}`))}
onClick={() => deviceChanges.forEach((c) => onResolve(deviceChangeKey(c)))}
className="shrink-0 text-black/40 underline hover:text-black/60 dark:text-white/40 dark:hover:text-white/60"
>
Alle bestätigen
@@ -300,12 +326,12 @@ function DeviceChangesReview({
</div>
<ul className="max-h-60 space-y-1 overflow-y-auto">
{deviceChanges.map((c) => (
<li key={`${c.deviceId}:${c.field}`} className="flex items-center justify-between gap-2">
<li key={deviceChangeKey(c)} 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="icon" variant="ghost" onClick={() => onResolve(`${c.deviceId}:${c.field}`)} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)"><FontAwesomeIcon icon={faBan} /></Button>
<Button size="icon" variant="ghost" onClick={() => onResolve(deviceChangeKey(c))} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)"><FontAwesomeIcon icon={faBan} /></Button>
<Button
size="icon"
variant="primary"
@@ -716,11 +742,17 @@ export function ScannerPage() {
{fritzboxError ? <p className="mt-2 text-sm text-red-500">{fritzboxError}</p> : null}
<DeviceChangesReview
deviceChanges={deviceChanges}
onResolve={(key) => setDeviceChanges((prev) => prev.filter((c) => `${c.deviceId}:${c.field}` !== key))}
onResolve={(key) => {
void dismissSuggestion("device_change", key);
setDeviceChanges((prev) => prev.filter((c) => deviceChangeKey(c) !== key));
}}
/>
<StaleDevicesReview
staleDevices={staleDevices}
onResolve={(id) => setStaleDevices((prev) => prev.filter((d) => d.id !== id))}
onResolve={(id) => {
void dismissSuggestion("stale_device", id);
setStaleDevices((prev) => prev.filter((d) => d.id !== id));
}}
/>
</div>
@@ -757,15 +789,24 @@ export function ScannerPage() {
) : null}
<NewServicesList
newServices={bulkNewServices}
onResolve={(id) => setBulkNewServices((prev) => prev.filter((s) => s.id !== id))}
onResolve={(id) => {
void dismissSuggestion("new_service", id);
setBulkNewServices((prev) => prev.filter((s) => s.id !== id));
}}
/>
<NameChangesReview
nameChanges={bulkNameChanges}
onResolve={(key) => setBulkNameChanges((prev) => prev.filter((c) => nameChangeKey(c) !== key))}
onResolve={(key) => {
void dismissSuggestion("name_change", key);
setBulkNameChanges((prev) => prev.filter((c) => nameChangeKey(c) !== key));
}}
/>
<StaleServicesReview
staleServices={bulkStaleServices}
onResolve={(id) => setBulkStaleServices((prev) => prev.filter((s) => s.id !== id))}
onResolve={(id) => {
void dismissSuggestion("stale_service", id);
setBulkStaleServices((prev) => prev.filter((s) => s.id !== id));
}}
/>
</div>