Fix: FritzBox faelschlich als Home Assistant erkannt + Reverse-DNS + Namens-Aenderungsvorschlaege beim Scan

This commit is contained in:
2026-07-22 15:58:10 +02:00
parent 7cf64404c5
commit 5f150119b2
8 changed files with 361 additions and 15 deletions

View File

@@ -33,13 +33,99 @@ async function deleteServiceRequest(id: string) {
}
}
interface ScanNameChange {
serviceId: string;
serviceHostname: string;
servicePort: number;
field: "displayName" | "category";
current: string | null;
suggested: string;
}
async function patchService(id: string, patch: Record<string, unknown>) {
const res = await fetch(`/api/services/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
if (!res.ok) {
throw new Error(`Dienst konnte nicht aktualisiert werden (HTTP ${res.status})`);
}
return res.json();
}
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; staleServices: Service[] };
return body as {
created: number;
updated: number;
staleServices: Service[];
nameChanges: ScanNameChange[];
deviceNameSuggestion: string | null;
};
}
function NameChangesReview({
nameChanges,
onDone,
}: {
nameChanges: ScanNameChange[];
onDone: () => void;
}) {
const queryClient = useQueryClient();
const [handled, setHandled] = useState<Set<string>>(new Set());
const applyMutation = useMutation({
mutationFn: (change: ScanNameChange) =>
patchService(change.serviceId, { [change.field]: change.suggested }),
onSuccess: (_data, change) => {
setHandled((prev) => new Set(prev).add(`${change.serviceId}:${change.field}`));
queryClient.invalidateQueries({ queryKey: ["services"] });
},
});
const remaining = nameChanges.filter((c) => !handled.has(`${c.serviceId}:${c.field}`));
if (remaining.length === 0) return null;
const fieldLabel = (field: ScanNameChange["field"]) => (field === "displayName" ? "Name" : "Kategorie");
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} erkannte Änderung(en) über alle gescannten Geräte hinweg weichen vom
gespeicherten Wert ab:
</p>
<ul className="max-h-60 space-y-1 overflow-y-auto">
{remaining.map((c) => (
<li key={`${c.serviceId}:${c.field}`} className="flex items-center justify-between gap-2">
<span className="text-black/70 dark:text-white/70">
{c.serviceHostname}:{c.servicePort} {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.serviceId}:${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 StaleServicesReview({
@@ -154,6 +240,7 @@ export function ScannerPage() {
const [bulkStatus, setBulkStatus] = useState<string | null>(null);
const [bulkRunning, setBulkRunning] = useState(false);
const [bulkStaleServices, setBulkStaleServices] = useState<Service[]>([]);
const [bulkNameChanges, setBulkNameChanges] = useState<ScanNameChange[]>([]);
const [staleDevices, setStaleDevices] = useState<Device[]>([]);
const fritzboxMutation = useMutation({
@@ -170,9 +257,11 @@ export function ScannerPage() {
if (!devices || devices.length === 0) return;
setBulkRunning(true);
setBulkStaleServices([]);
setBulkNameChanges([]);
let created = 0;
let updated = 0;
const allStale: Service[] = [];
const allNameChanges: ScanNameChange[] = [];
for (const device of devices) {
try {
@@ -180,6 +269,7 @@ export function ScannerPage() {
created += result.created;
updated += result.updated;
allStale.push(...result.staleServices);
allNameChanges.push(...result.nameChanges);
setBulkStatus(`Scanne ${device.hostname} … (${created} neu, ${updated} aktualisiert bisher)`);
} catch {
// einzelnes fehlgeschlagenes Gerät soll den Rest nicht abbrechen
@@ -188,10 +278,11 @@ export function ScannerPage() {
setBulkStatus(
`Fertig: ${devices.length} Gerät(e) gescannt, ${created} neue Dienste, ${updated} aktualisiert${
allStale.length > 0 ? `, ${allStale.length} nicht mehr gefunden (siehe unten)` : ""
}.`
allStale.length > 0 ? `, ${allStale.length} nicht mehr gefunden` : ""
}${allNameChanges.length > 0 ? `, ${allNameChanges.length} Namensänderung(en) vorgeschlagen` : ""} (siehe unten).`
);
setBulkStaleServices(allStale);
setBulkNameChanges(allNameChanges);
setBulkRunning(false);
queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["services"] });
@@ -254,6 +345,9 @@ export function ScannerPage() {
{bulkStatus ? (
<p className="mt-2 text-sm text-black/50 dark:text-white/50">{bulkStatus}</p>
) : null}
{bulkNameChanges.length > 0 ? (
<NameChangesReview nameChanges={bulkNameChanges} onDone={() => setBulkNameChanges([])} />
) : null}
{bulkStaleServices.length > 0 ? (
<StaleServicesReview
staleServices={bulkStaleServices}