round22: Startseite kompakter, Ping-Zeitstempel-Bug behoben, echte Favicon-Deduplizierung per Bildinhalt, automatischer Favicon-Abgleich aus Icon-Datenbank + Nachruest-Button

This commit is contained in:
2026-07-23 20:58:58 +02:00
parent 6a66128172
commit b098007742
8 changed files with 181 additions and 49 deletions

View File

@@ -334,7 +334,7 @@ export function HomePage() {
dark:bg-white/[0.02]"
>
{hasFavorites ? (
<div className="flex flex-col gap-4 p-4">
<div className="flex flex-col gap-3 p-3">
{favoriteServices.length > 0 ? (
<FavoritesBar
items={favoriteServices}
@@ -363,7 +363,7 @@ export function HomePage() {
) : null}
{recentVisits && recentVisits.length > 0 ? (
<div className="p-4">
<div className="p-3">
<FavoritesBar
items={recentVisits}
label="Zuletzt besucht"
@@ -376,7 +376,7 @@ export function HomePage() {
</div>
) : null}
<div className="p-4">
<div className="p-3">
<div className="mb-2.5 flex items-center justify-between gap-2">
<span className="text-xs font-medium uppercase tracking-wide text-black/35 dark:text-white/35">
Später lesen
@@ -384,23 +384,23 @@ export function HomePage() {
<ReadLaterBox />
</div>
{hasReadLaterChips ? (
<div className="flex flex-wrap gap-2.5">
<div className="flex flex-wrap gap-2">
{readLaterItems!.slice(0, readLaterLimit).map((item) => (
<button
key={item.id}
onClick={() => window.open(item.url, "_blank", "noopener,noreferrer")}
title={item.displayName}
className="group flex w-16 flex-col items-center gap-1.5 rounded-xl p-1.5
className="group flex w-14 flex-col items-center gap-1 rounded-xl p-1
transition-colors hover:bg-black/[0.04] dark:hover:bg-white/[0.06]"
>
<span
className="flex h-11 w-11 items-center justify-center rounded-xl border
className="flex h-9 w-9 items-center justify-center rounded-xl border
border-black/10 bg-white/80 shadow-sm transition-transform
group-hover:scale-105 dark:border-white/10 dark:bg-white/5"
>
<Favicon src={item.favicon} fallbackLetter={item.displayName} size="md" />
</span>
<span className="w-full truncate text-center text-[11px] leading-tight text-black/55 dark:text-white/55">
<span className="w-full truncate text-center text-[10px] leading-tight text-black/55 dark:text-white/55">
{item.displayName}
</span>
</button>

View File

@@ -503,6 +503,46 @@ function SortableHeader({
);
}
function BackfillFaviconsButton() {
const queryClient = useQueryClient();
const [message, setMessage] = useState<string | null>(null);
const mutation = useMutation({
mutationFn: async () => {
const res = await fetch("/api/services/backfill-favicons", { method: "POST" });
if (!res.ok) throw new Error(`Fehlgeschlagen (HTTP ${res.status})`);
return res.json() as Promise<{ checked: number; updated: number }>;
},
onSuccess: (result) => {
setMessage(
result.updated > 0
? `${result.updated} von ${result.checked} Diensten mit einem passenden Favicon aus der Icon-Datenbank ergänzt.`
: `Keine passenden Treffer gefunden (${result.checked} Dienste ohne Favicon geprüft).`
);
queryClient.invalidateQueries({ queryKey: ["services"] });
},
onError: () => setMessage("Fehlgeschlagen - ist die Icon-Datenbank erreichbar (Internetzugang)?"),
});
return (
<div className="flex items-center gap-2 pb-2">
{message ? <span className="text-xs text-black/40 dark:text-white/40">{message}</span> : null}
<Button
size="sm"
variant="ghost"
onClick={() => {
setMessage(null);
mutation.mutate();
}}
disabled={mutation.isPending}
title="Für alle Dienste ohne Favicon einen passenden Treffer aus der Icon-Datenbank suchen"
>
{mutation.isPending ? "Suche …" : "Fehlende Favicons ergänzen"}
</Button>
</div>
);
}
export function ServicesPage() {
const { data: services, isLoading, isError } = useServices();
const { data: devices } = useDevices();
@@ -633,27 +673,30 @@ export function ServicesPage() {
description="Spaltenköpfe anklickbar zum Sortieren; Drag & Drop (⠿⠿) nur in der Standard-Reihenfolge. Name, Kategorie, Alias, IP/Port/Protokoll und Reihenfolge bleiben bei erneuten Scans erhalten."
/>
<div className="mb-4 flex gap-1 border-b border-black/10 dark:border-white/10">
<button
onClick={() => setActiveTab("visible")}
className={`px-3 py-2 text-sm font-medium ${
activeTab === "visible"
? "border-b-2 border-black text-black dark:border-white dark:text-white"
: "text-black/40 hover:text-black/70 dark:text-white/40 dark:hover:text-white/70"
}`}
>
Sichtbar ({visibleCount})
</button>
<button
onClick={() => setActiveTab("hidden")}
className={`px-3 py-2 text-sm font-medium ${
activeTab === "hidden"
? "border-b-2 border-black text-black dark:border-white dark:text-white"
: "text-black/40 hover:text-black/70 dark:text-white/40 dark:hover:text-white/70"
}`}
>
<FontAwesomeIcon icon={faEyeSlash} /> Ausgeblendet ({hiddenCount})
</button>
<div className="mb-4 flex items-center justify-between gap-2 border-b border-black/10 dark:border-white/10">
<div className="flex gap-1">
<button
onClick={() => setActiveTab("visible")}
className={`px-3 py-2 text-sm font-medium ${
activeTab === "visible"
? "border-b-2 border-black text-black dark:border-white dark:text-white"
: "text-black/40 hover:text-black/70 dark:text-white/40 dark:hover:text-white/70"
}`}
>
Sichtbar ({visibleCount})
</button>
<button
onClick={() => setActiveTab("hidden")}
className={`px-3 py-2 text-sm font-medium ${
activeTab === "hidden"
? "border-b-2 border-black text-black dark:border-white dark:text-white"
: "text-black/40 hover:text-black/70 dark:text-white/40 dark:hover:text-white/70"
}`}
>
<FontAwesomeIcon icon={faEyeSlash} /> Ausgeblendet ({hiddenCount})
</button>
</div>
<BackfillFaviconsButton />
</div>
{sortColumn ? (