generated from Dicken/dickendock
round15: manuelle Aenderungen nicht mehr nachfragen, 7-Tage-Schwelle fuer verschwundene Geraete, Icon-Buttons mit Tooltip, fixierte mobile Kopfleiste, Scroll-to-top-Button
This commit is contained in:
35
apps/frontend/src/components/ScrollToTopButton.tsx
Normal file
35
apps/frontend/src/components/ScrollToTopButton.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
/**
|
||||
* Schwebender Button unten rechts, der zum Seitenanfang zurückscrollt.
|
||||
* Erscheint erst, sobald tatsächlich runtergescrollt wurde (kein Sinn, ihn
|
||||
* direkt am Anfang der Seite schon anzuzeigen).
|
||||
*/
|
||||
export function ScrollToTopButton() {
|
||||
const [visible, setVisible] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
function onScroll() {
|
||||
setVisible(window.scrollY > 300);
|
||||
}
|
||||
onScroll();
|
||||
window.addEventListener("scroll", onScroll, { passive: true });
|
||||
return () => window.removeEventListener("scroll", onScroll);
|
||||
}, []);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={() => window.scrollTo({ top: 0, behavior: "smooth" })}
|
||||
title="Nach oben scrollen"
|
||||
aria-label="Nach oben scrollen"
|
||||
className="fixed bottom-5 right-5 z-30 flex h-11 w-11 items-center justify-center rounded-full
|
||||
border border-black/10 bg-white/90 text-lg text-black/70 shadow-lg backdrop-blur-md
|
||||
transition-opacity hover:bg-white dark:border-white/10 dark:bg-neutral-900/90
|
||||
dark:text-white/70 dark:hover:bg-neutral-900"
|
||||
>
|
||||
⬆️
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useState } from "react";
|
||||
import { Link, Outlet, useRouterState } from "@tanstack/react-router";
|
||||
import { StatusBadge } from "@launchpad/ui";
|
||||
import { useBackendHealth } from "../../hooks/useBackendHealth.js";
|
||||
import { ScrollToTopButton } from "../../components/ScrollToTopButton.js";
|
||||
|
||||
const NAV_ITEMS = [
|
||||
{ to: "/", label: "Startseite", icon: "🏠" },
|
||||
@@ -30,10 +31,13 @@ export function AdminLayout() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-neutral-50 dark:bg-neutral-950">
|
||||
{/* Mobile-Kopfleiste mit Hamburger-Button, nur unterhalb des sm-Breakpoints sichtbar */}
|
||||
{/* Mobile-Kopfleiste mit Hamburger-Button, nur unterhalb des sm-Breakpoints
|
||||
sichtbar. Fixiert am oberen Rand, damit sie bei langen Seiten (z. B.
|
||||
Dienste-Tabelle) beim Scrollen sichtbar bleibt statt mit dem Inhalt
|
||||
mitzuwandern - main hat dafür unten zusätzliches pt-16 auf Mobil. */}
|
||||
<div
|
||||
className="flex items-center gap-3 border-b border-black/10 bg-white/80 px-4 py-3
|
||||
backdrop-blur-md dark:border-white/10 dark:bg-neutral-950/80 sm:hidden"
|
||||
className="fixed inset-x-0 top-0 z-20 flex items-center gap-3 border-b border-black/10
|
||||
bg-white/80 px-4 py-3 backdrop-blur-md dark:border-white/10 dark:bg-neutral-950/80 sm:hidden"
|
||||
>
|
||||
<button
|
||||
onClick={() => setMobileOpen(true)}
|
||||
@@ -114,10 +118,14 @@ export function AdminLayout() {
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* sm:ml-56 gleicht die jetzt fixierte Sidebar aus (statt Flexbox-Nebeneinander) */}
|
||||
<main className="overflow-x-hidden p-4 sm:ml-56 sm:p-8">
|
||||
{/* pt-16 auf Mobil gleicht die jetzt fixierte Kopfleiste aus (sonst
|
||||
würde der Seiteninhalt anfangs darunter verschwinden);
|
||||
sm:ml-56 gleicht die fixierte Sidebar aus (statt Flexbox-Nebeneinander) */}
|
||||
<main className="overflow-x-hidden p-4 pt-16 sm:ml-56 sm:p-8">
|
||||
<Outlet />
|
||||
</main>
|
||||
|
||||
<ScrollToTopButton />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -260,12 +260,8 @@ function EditForm({ bookmark, onDone }: { bookmark: Bookmark; onDone: () => void
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={onDone}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button size="icon" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending} title="Speichern" aria-label="Speichern">💾</Button>
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Abbrechen" aria-label="Abbrechen">✕</Button>
|
||||
{mutation.isError ? (
|
||||
<span className="text-xs text-red-500">{(mutation.error as Error).message}</span>
|
||||
) : null}
|
||||
@@ -358,12 +354,8 @@ function BookmarkRow({
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button size="sm" onClick={() => setEditing(true)}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button size="sm" variant="danger" onClick={() => deleteMutation.mutate()} disabled={deleteMutation.isPending}>
|
||||
Löschen
|
||||
</Button>
|
||||
<Button size="icon" onClick={() => setEditing(true)} title="Bearbeiten" aria-label="Bearbeiten">✏️</Button>
|
||||
<Button size="icon" variant="danger" onClick={() => deleteMutation.mutate()} disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen">🗑️</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -125,22 +125,18 @@ function CategoryRow({
|
||||
className="flex-1 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"
|
||||
/>
|
||||
<Button size="sm" variant="primary" onClick={() => renameMutation.mutate()} disabled={renameMutation.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={() => setEditing(false)}>
|
||||
Abbrechen
|
||||
<Button size="icon" variant="primary" onClick={() => renameMutation.mutate()} disabled={renameMutation.isPending} title="Speichern" aria-label="Speichern">💾</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => setEditing(false)} title="Abbrechen" aria-label="Abbrechen">
|
||||
✕
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="flex-1 text-sm font-medium text-black dark:text-white">{category.name}</span>
|
||||
<Button size="sm" onClick={() => setEditing(true)}>
|
||||
Umbenennen
|
||||
</Button>
|
||||
<Button size="sm" variant="danger" onClick={() => deleteMutation.mutate()} disabled={deleteMutation.isPending}>
|
||||
Löschen
|
||||
<Button size="icon" onClick={() => setEditing(true)} title="Umbenennen" aria-label="Umbenennen">
|
||||
✏️
|
||||
</Button>
|
||||
<Button size="icon" variant="danger" onClick={() => deleteMutation.mutate()} disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen">🗑️</Button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
|
||||
@@ -162,12 +162,8 @@ function EditDeviceForm({ device, onDone }: { device: DeviceWithServices; onDone
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
<Button size="sm" variant="ghost" onClick={onDone}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button size="icon" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending} title="Speichern" aria-label="Speichern">💾</Button>
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Abbrechen" aria-label="Abbrechen">✕</Button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -203,59 +199,36 @@ function NameChangesReview({
|
||||
|
||||
const fieldLabel = (field: ScanNameChange["field"]) =>
|
||||
field === "displayName" ? "Name" : "Kategorie";
|
||||
const manual = remaining.filter((c) => c.manualOverride);
|
||||
const real = remaining.filter((c) => !c.manualOverride);
|
||||
|
||||
function renderList(list: ScanNameChange[]) {
|
||||
return (
|
||||
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">
|
||||
{list.map((c) => (
|
||||
{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"
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
onClick={() => setHandled((prev) => new Set(prev).add(`${c.serviceId}:${c.field}`))}
|
||||
>
|
||||
Behalten
|
||||
</Button>
|
||||
title="Behalten (Vorschlag verwerfen)"
|
||||
aria-label="Behalten (Vorschlag verwerfen)"
|
||||
>✕</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="primary"
|
||||
onClick={() => applyMutation.mutate(c)}
|
||||
disabled={applyMutation.isPending}
|
||||
>
|
||||
Übernehmen
|
||||
</Button>
|
||||
disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen">✓</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2 space-y-2">
|
||||
{real.length > 0 ? (
|
||||
<div className="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">
|
||||
{real.length} erkannte Änderung(en) weichen vom gespeicherten Wert ab:
|
||||
</p>
|
||||
{renderList(real)}
|
||||
</div>
|
||||
) : null}
|
||||
{manual.length > 0 ? (
|
||||
<div className="rounded-xl border border-black/10 bg-black/[0.02] p-3 text-xs dark:border-white/10 dark:bg-white/[0.02]">
|
||||
<p className="mb-2 font-medium text-black/60 dark:text-white/60">
|
||||
{manual.length} Abweichung(en), weil du diesen Wert bereits manuell angepasst hast:
|
||||
</p>
|
||||
{renderList(manual)}
|
||||
</div>
|
||||
) : null}
|
||||
<button onClick={onDone} className="text-black/40 underline dark:text-white/40">
|
||||
<button onClick={onDone} className="mt-2 text-black/40 underline dark:text-white/40">
|
||||
Hinweis schließen
|
||||
</button>
|
||||
</div>
|
||||
@@ -288,12 +261,8 @@ function DeviceNameSuggestionBanner({
|
||||
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>
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)">✕</Button>
|
||||
<Button size="icon" variant="primary" onClick={() => applyMutation.mutate()} disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen">✓</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -327,17 +296,12 @@ function StaleServicesReview({ staleServices, onDone }: { staleServices: Service
|
||||
{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="icon" variant="ghost" onClick={() => setHandled((prev) => new Set(prev).add(s.id))} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)">✕</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="danger"
|
||||
onClick={() => deleteMutation.mutate(s.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
Entfernen
|
||||
</Button>
|
||||
disabled={deleteMutation.isPending} title="Entfernen" aria-label="Entfernen">🗑️</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -424,9 +388,7 @@ function DeviceRow({ device }: { device: DeviceWithServices }) {
|
||||
<td className="px-4 py-3 text-black/60 dark:text-white/60">{device.services.length}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button size="sm" onClick={() => setEditing(true)}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button size="icon" onClick={() => setEditing(true)} title="Bearbeiten" aria-label="Bearbeiten">✏️</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@@ -438,13 +400,10 @@ function DeviceRow({ device }: { device: DeviceWithServices }) {
|
||||
{scanMutation.isPending ? "Scanne …" : "Jetzt scannen"}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="danger"
|
||||
onClick={() => deleteMutation.mutate()}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen">🗑️</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -77,17 +77,13 @@ function EditRow({ item, onDone }: { item: ReadLaterItem; onDone: () => void })
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="ghost" onClick={onDone}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Abbrechen" aria-label="Abbrechen">✕</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="primary"
|
||||
onClick={() => mutation.mutate()}
|
||||
disabled={mutation.isPending || !displayName.trim() || !url.trim()}
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
>💾</Button>
|
||||
</div>
|
||||
</div>
|
||||
{mutation.isError ? (
|
||||
@@ -140,9 +136,7 @@ function ReadLaterRow({ item }: { item: ReadLaterItem }) {
|
||||
<td className="px-4 py-3 text-xs text-black/50 dark:text-white/50">{formatDate(item.savedAt)}</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
<div className="flex justify-end gap-1">
|
||||
<Button size="sm" variant="ghost" onClick={() => setEditing(true)}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => setEditing(true)} title="Bearbeiten" aria-label="Bearbeiten">✏️</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
@@ -153,13 +147,13 @@ function ReadLaterRow({ item }: { item: ReadLaterItem }) {
|
||||
🔖 Zu Lesezeichen
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="danger"
|
||||
onClick={() => deleteMutation.mutate()}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
title="Löschen"
|
||||
aria-label="Löschen"
|
||||
>🗑️</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -127,57 +127,30 @@ function NameChangesReview({
|
||||
if (nameChanges.length === 0) return null;
|
||||
|
||||
const fieldLabel = (field: ScanNameChange["field"]) => (field === "displayName" ? "Name" : "Kategorie");
|
||||
const manual = nameChanges.filter((c) => c.manualOverride);
|
||||
const real = nameChanges.filter((c) => !c.manualOverride);
|
||||
|
||||
function renderList(list: ScanNameChange[]) {
|
||||
return (
|
||||
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">
|
||||
{nameChanges.length} erkannte Änderung(en) weichen vom gespeicherten Wert ab:
|
||||
</p>
|
||||
<ul className="max-h-60 space-y-1 overflow-y-auto">
|
||||
{list.map((c) => (
|
||||
{nameChanges.map((c) => (
|
||||
<li key={nameChangeKey(c)} 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={() => onResolve(nameChangeKey(c))}>
|
||||
Behalten
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(nameChangeKey(c))} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)">✕</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="primary"
|
||||
onClick={() => applyMutation.mutate(c)}
|
||||
disabled={applyMutation.isPending}
|
||||
>
|
||||
Übernehmen
|
||||
</Button>
|
||||
disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen">✓</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3 space-y-3">
|
||||
{real.length > 0 ? (
|
||||
<div className="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">
|
||||
{real.length} erkannte Änderung(en) weichen vom gespeicherten Wert ab:
|
||||
</p>
|
||||
{renderList(real)}
|
||||
</div>
|
||||
) : null}
|
||||
{manual.length > 0 ? (
|
||||
<div className="rounded-xl border border-black/10 bg-black/[0.02] p-3 text-xs dark:border-white/10 dark:bg-white/[0.02]">
|
||||
<p className="mb-2 font-medium text-black/60 dark:text-white/60">
|
||||
{manual.length} Abweichung(en), weil du diesen Wert bereits manuell angepasst hast – der
|
||||
Scan schlägt lediglich erneut den automatisch erkannten Wert vor, es hat sich nichts am
|
||||
Dienst selbst geändert:
|
||||
</p>
|
||||
{renderList(manual)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -214,17 +187,12 @@ function StaleServicesReview({
|
||||
{s.displayName} ({s.hostname}:{s.port})
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="ghost" onClick={() => onResolve(s.id)}>
|
||||
Behalten
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(s.id)} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)">✕</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="danger"
|
||||
onClick={() => deleteMutation.mutate(s.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
Entfernen
|
||||
</Button>
|
||||
disabled={deleteMutation.isPending} title="Entfernen" aria-label="Entfernen">🗑️</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -266,17 +234,12 @@ function DeviceChangesReview({
|
||||
{c.deviceHostname} – {fieldLabel(c.field)}: „{c.current}" → „{c.suggested}"
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="ghost" onClick={() => onResolve(`${c.deviceId}:${c.field}`)}>
|
||||
Behalten
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(`${c.deviceId}:${c.field}`)} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)">✕</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="primary"
|
||||
onClick={() => applyMutation.mutate(c)}
|
||||
disabled={applyMutation.isPending}
|
||||
>
|
||||
Übernehmen
|
||||
</Button>
|
||||
disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen">✓</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -317,17 +280,12 @@ function StaleDevicesReview({
|
||||
{d.hostname} ({d.ip})
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Button size="sm" variant="ghost" onClick={() => onResolve(d.id)}>
|
||||
Behalten
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(d.id)} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)">✕</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="danger"
|
||||
onClick={() => deleteMutation.mutate(d.id)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
Löschen
|
||||
</Button>
|
||||
disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen">🗑️</Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -229,12 +229,8 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center 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>
|
||||
<Button size="icon" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending} title="Speichern" aria-label="Speichern">💾</Button>
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Abbrechen" aria-label="Abbrechen">✕</Button>
|
||||
{mutation.isError ? (
|
||||
<span className="text-xs text-red-500">{(mutation.error as Error).message}</span>
|
||||
) : null}
|
||||
@@ -330,6 +326,11 @@ function ServiceRow({
|
||||
<Favicon src={service.favicon} fallbackLetter={service.displayName} size="sm" />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-black dark:text-white">{service.displayName}</span>
|
||||
{service.displayNameEditedManually ? (
|
||||
<span title="Name wurde manuell angepasst – Scan-Vorschläge dafür werden nicht mehr angezeigt">
|
||||
✍️
|
||||
</span>
|
||||
) : null}
|
||||
{!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
|
||||
@@ -353,7 +354,16 @@ function ServiceRow({
|
||||
<td className="px-4 py-3 font-mono text-xs text-black/60 dark:text-white/60">
|
||||
{deviceIp ?? "–"}
|
||||
</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">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
{service.category ?? "–"}
|
||||
{service.categoryEditedManually ? (
|
||||
<span title="Kategorie wurde manuell angepasst – Scan-Vorschläge dafür werden nicht mehr angezeigt">
|
||||
✍️
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 font-mono text-xs text-black/40 dark:text-white/40">
|
||||
{deviceMac ?? "–"}
|
||||
</td>
|
||||
@@ -384,12 +394,8 @@ function ServiceRow({
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button size="sm" onClick={() => setEditing(true)}>
|
||||
Bearbeiten
|
||||
</Button>
|
||||
<Button size="sm" variant="danger" onClick={() => deleteMutation.mutate()} disabled={deleteMutation.isPending}>
|
||||
Löschen
|
||||
</Button>
|
||||
<Button size="icon" onClick={() => setEditing(true)} title="Bearbeiten" aria-label="Bearbeiten">✏️</Button>
|
||||
<Button size="icon" variant="danger" onClick={() => deleteMutation.mutate()} disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen">🗑️</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
Reference in New Issue
Block a user