generated from Dicken/dickendock
Fix: FritzBox faelschlich als Home Assistant erkannt + Reverse-DNS + Namens-Aenderungsvorschlaege beim Scan
This commit is contained in:
@@ -44,12 +44,35 @@ async function deleteServiceRequest(id: 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();
|
||||
}
|
||||
|
||||
interface ScanNameChange {
|
||||
serviceId: string;
|
||||
serviceHostname: string;
|
||||
servicePort: number;
|
||||
field: "displayName" | "category";
|
||||
current: string | null;
|
||||
suggested: string;
|
||||
}
|
||||
|
||||
interface ScanResult {
|
||||
scannedPorts: number;
|
||||
ports: number[];
|
||||
created: number;
|
||||
updated: number;
|
||||
staleServices: Service[];
|
||||
nameChanges: ScanNameChange[];
|
||||
deviceNameSuggestion: string | null;
|
||||
}
|
||||
|
||||
async function scanDevice(id: string): Promise<ScanResult> {
|
||||
@@ -150,6 +173,109 @@ function EditDeviceForm({ device, onDone }: { device: DeviceWithServices; onDone
|
||||
);
|
||||
}
|
||||
|
||||
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-2 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) weichen vom gespeicherten Wert ab:
|
||||
</p>
|
||||
<ul className="space-y-1">
|
||||
{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 DeviceNameSuggestionBanner({
|
||||
deviceId,
|
||||
suggestion,
|
||||
onDone,
|
||||
}: {
|
||||
deviceId: string;
|
||||
suggestion: string;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const applyMutation = useMutation({
|
||||
mutationFn: () => patchDevice(deviceId, { hostname: suggestion }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["devices"] });
|
||||
onDone();
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mt-2 rounded-xl border border-blue-500/30 bg-blue-500/5 p-3 text-xs">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-black/70 dark:text-white/70">
|
||||
Per Reverse-DNS gefunden: „{suggestion}" als Gerätename?
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="ghost" onClick={onDone}>
|
||||
Behalten
|
||||
</Button>
|
||||
<Button size="sm" variant="primary" onClick={() => applyMutation.mutate()} disabled={applyMutation.isPending}>
|
||||
Übernehmen
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StaleServicesReview({ staleServices, onDone }: { staleServices: Service[]; onDone: () => void }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [handled, setHandled] = useState<Set<string>>(new Set());
|
||||
@@ -203,6 +329,8 @@ function DeviceRow({ device }: { device: DeviceWithServices }) {
|
||||
const queryClient = useQueryClient();
|
||||
const [scanMessage, setScanMessage] = useState<string | null>(null);
|
||||
const [staleServices, setStaleServices] = useState<Service[]>([]);
|
||||
const [nameChanges, setNameChanges] = useState<ScanNameChange[]>([]);
|
||||
const [deviceNameSuggestion, setDeviceNameSuggestion] = useState<string | null>(null);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [editing, setEditing] = useState(false);
|
||||
|
||||
@@ -214,6 +342,8 @@ function DeviceRow({ device }: { device: DeviceWithServices }) {
|
||||
`Ports offen: ${portsText} · ${result.created} neu · ${result.updated} aktualisiert`
|
||||
);
|
||||
setStaleServices(result.staleServices);
|
||||
setNameChanges(result.nameChanges);
|
||||
setDeviceNameSuggestion(result.deviceNameSuggestion);
|
||||
queryClient.invalidateQueries({ queryKey: ["devices"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["services"] });
|
||||
},
|
||||
@@ -293,12 +423,22 @@ function DeviceRow({ device }: { device: DeviceWithServices }) {
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{(scanMessage || staleServices.length > 0) && (
|
||||
{(scanMessage || staleServices.length > 0 || nameChanges.length > 0 || deviceNameSuggestion) && (
|
||||
<tr className="border-b border-black/5 dark:border-white/5">
|
||||
<td colSpan={8} className="px-4 pb-3">
|
||||
{scanMessage ? (
|
||||
<p className="text-xs text-black/40 dark:text-white/40">{scanMessage}</p>
|
||||
) : null}
|
||||
{deviceNameSuggestion ? (
|
||||
<DeviceNameSuggestionBanner
|
||||
deviceId={device.id}
|
||||
suggestion={deviceNameSuggestion}
|
||||
onDone={() => setDeviceNameSuggestion(null)}
|
||||
/>
|
||||
) : null}
|
||||
{nameChanges.length > 0 ? (
|
||||
<NameChangesReview nameChanges={nameChanges} onDone={() => setNameChanges([])} />
|
||||
) : null}
|
||||
{staleServices.length > 0 ? (
|
||||
<StaleServicesReview staleServices={staleServices} onDone={() => setStaleServices([])} />
|
||||
) : null}
|
||||
|
||||
Reference in New Issue
Block a user