Fix: visible-Flag für Scan-Müll, editierbare Hostname/Port/HTTPS, Sidebar fixiert, Suche nur bei Eingabe

This commit is contained in:
2026-07-19 12:27:45 +02:00
parent d65897b8e1
commit 20ea4d382d
12 changed files with 238 additions and 44 deletions

View File

@@ -11,6 +11,11 @@ interface ServicePatch {
alias?: string[];
order?: number;
favorite?: boolean;
visible?: boolean;
hostname?: string;
port?: number;
https?: boolean;
url?: string;
}
async function patchService(id: string, patch: ServicePatch): Promise<Service> {
@@ -38,6 +43,12 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
const [category, setCategory] = useState(service.category ?? "");
const [alias, setAlias] = useState(service.alias.join(", "));
const [order, setOrder] = useState(String(service.order));
const [hostname, setHostname] = useState(service.hostname);
const [port, setPort] = useState(String(service.port));
const [https, setHttps] = useState(service.https);
const portNumber = Number(port) || service.port;
const previewUrl = `${https ? "https" : "http"}://${hostname || service.hostname}:${portNumber}`;
const mutation = useMutation({
mutationFn: () =>
@@ -49,6 +60,10 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
.map((a) => a.trim())
.filter(Boolean),
order: Number(order) || 0,
hostname: hostname.trim(),
port: portNumber,
https,
url: previewUrl,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["services"] });
@@ -99,12 +114,55 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
dark:border-white/10 dark:bg-white/10 dark:text-white"
/>
</div>
<Button size="sm" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending}>
Speichern
</Button>
<Button size="sm" variant="ghost" onClick={onDone}>
Abbrechen
</Button>
<div className="w-full border-t border-black/10 pt-3 dark:border-white/10" />
<div>
<label className="mb-1 block text-xs text-black/50 dark:text-white/50">
Hostname / IP
</label>
<input
value={hostname}
onChange={(e) => setHostname(e.target.value)}
className="rounded-lg border border-black/10 bg-white px-2 py-1 text-sm
dark:border-white/10 dark:bg-white/10 dark:text-white"
/>
</div>
<div>
<label className="mb-1 block text-xs text-black/50 dark:text-white/50">Port</label>
<input
value={port}
onChange={(e) => setPort(e.target.value)}
type="number"
min={1}
max={65535}
className="w-24 rounded-lg border border-black/10 bg-white px-2 py-1 text-sm
dark:border-white/10 dark:bg-white/10 dark:text-white"
/>
</div>
<div>
<label className="mb-1 flex items-center gap-1.5 text-xs text-black/50 dark:text-white/50">
<input
type="checkbox"
checked={https}
onChange={(e) => setHttps(e.target.checked)}
className="rounded"
/>
HTTPS
</label>
</div>
<div className="text-xs text-black/40 dark:text-white/40">
Öffnet: <span className="font-mono">{previewUrl}</span>
</div>
<div className="flex w-full gap-2 pt-1">
<Button size="sm" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending}>
Speichern
</Button>
<Button size="sm" variant="ghost" onClick={onDone}>
Abbrechen
</Button>
</div>
</div>
</td>
</tr>
@@ -120,6 +178,11 @@ function ServiceRow({ service }: { service: Service }) {
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["services"] }),
});
const visibleMutation = useMutation({
mutationFn: () => patchService(service.id, { visible: !service.visible }),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["services"] }),
});
const deleteMutation = useMutation({
mutationFn: () => deleteServiceRequest(service.id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["services"] }),
@@ -130,19 +193,42 @@ function ServiceRow({ service }: { service: Service }) {
}
return (
<tr className="border-b border-black/5 last:border-0 dark:border-white/5">
<tr
className={`border-b border-black/5 last:border-0 dark:border-white/5 ${
service.visible ? "" : "opacity-50"
}`}
>
<td className="px-4 py-3">
<button
onClick={() => favoriteMutation.mutate()}
aria-label={service.favorite ? "Favorit entfernen" : "Als Favorit markieren"}
className={`text-lg ${service.favorite ? "text-amber-500" : "text-black/15 hover:text-amber-400 dark:text-white/15"}`}
>
</button>
<div className="flex items-center gap-1.5">
<button
onClick={() => favoriteMutation.mutate()}
aria-label={service.favorite ? "Favorit entfernen" : "Als Favorit markieren"}
className={`text-lg ${service.favorite ? "text-amber-500" : "text-black/15 hover:text-amber-400 dark:text-white/15"}`}
>
</button>
<button
onClick={() => visibleMutation.mutate()}
aria-label={service.visible ? "In der Suche ausblenden" : "In der Suche einblenden"}
title={service.visible ? "In der Suche sichtbar" : "In der Suche ausgeblendet"}
className={`text-base ${service.visible ? "text-black/40 dark:text-white/40" : "text-black/60 dark:text-white/60"}`}
>
{service.visible ? "👁️" : "🙈"}
</button>
</div>
</td>
<td className="px-4 py-3">
<div className="font-medium text-black dark:text-white">{service.displayName}</div>
<div className="text-xs text-black/40 dark:text-white/40">{service.hostname}</div>
<div className="flex items-center gap-2">
<span className="font-medium text-black dark:text-white">{service.displayName}</span>
{!service.visible ? (
<span className="rounded-full bg-black/10 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-black/50 dark:bg-white/10 dark:text-white/50">
ausgeblendet
</span>
) : null}
</div>
<div className="text-xs text-black/40 dark:text-white/40">
{service.hostname}:{service.port}
</div>
</td>
<td className="px-4 py-3 text-black/60 dark:text-white/60">{service.category ?? ""}</td>
<td className="px-4 py-3 text-black/60 dark:text-white/60">
@@ -155,7 +241,7 @@ function ServiceRow({ service }: { service: Service }) {
rel="noopener noreferrer"
className="text-black/60 underline decoration-black/20 hover:text-black dark:text-white/60 dark:decoration-white/20 dark:hover:text-white"
>
öffnen
öffnen{service.https ? " (https)" : ""}
</a>
</td>
<td className="px-4 py-3">
@@ -174,12 +260,17 @@ function ServiceRow({ service }: { service: Service }) {
export function ServicesPage() {
const { data: services, isLoading, isError } = useServices();
const hiddenCount = services?.filter((s) => !s.visible).length ?? 0;
return (
<div>
<AdminPageHeader
title="Dienste"
description="Name, Kategorie, Alias und Reihenfolge bleiben bei erneuten Scans erhalten."
description={
hiddenCount > 0
? `Name, Kategorie, Alias, IP/Port/Protokoll und Reihenfolge bleiben bei erneuten Scans erhalten. ${hiddenCount} Dienst(e) sind aktuell in der Suche ausgeblendet (🙈).`
: "Name, Kategorie, Alias, IP/Port/Protokoll und Reihenfolge bleiben bei erneuten Scans erhalten."
}
/>
{isLoading ? (