Bugfixes (Dark Mode, Dropdown-Kontrast, IP-Suche), Lesezeichen-Ausbau (Farbe, Beschreibung, Favicon), Zuletzt-besucht, Spaeter-lesen, kombinierter Import-Export, Scanner-Reconciliation, Admin-Ueberarbeitung

This commit is contained in:
2026-07-20 07:52:38 +02:00
parent dc91a9aba9
commit 96cf29fef4
37 changed files with 1988 additions and 494 deletions

View File

@@ -1,16 +1,29 @@
import { useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { Button } from "@launchpad/ui";
import type { Device } from "@launchpad/shared";
import { useDevices } from "../../hooks/useDevices.js";
import { AdminPageHeader } from "./AdminPageHeader.js";
async function scanFritzBox() {
interface FritzBoxScanResult {
found: number;
staleDevices: Device[];
}
async function scanFritzBox(): Promise<FritzBoxScanResult> {
const res = await fetch("/api/scan/fritzbox", { method: "POST" });
const body = await res.json();
if (!res.ok) {
throw new Error(body.error ?? `FritzBox-Scan fehlgeschlagen (HTTP ${res.status})`);
}
return body as { found: number };
return body;
}
async function deleteDeviceRequest(id: string) {
const res = await fetch(`/api/devices/${id}`, { method: "DELETE" });
if (!res.ok && res.status !== 404) {
throw new Error(`Gerät konnte nicht gelöscht werden (HTTP ${res.status})`);
}
}
async function scanDeviceById(id: string) {
@@ -22,15 +35,67 @@ async function scanDeviceById(id: string) {
return body as { created: number; updated: number };
}
function StaleDevicesReview({ staleDevices, onDone }: { staleDevices: Device[]; onDone: () => void }) {
const queryClient = useQueryClient();
const [handled, setHandled] = useState<Set<string>>(new Set());
const deleteMutation = useMutation({
mutationFn: (id: string) => deleteDeviceRequest(id),
onSuccess: (_data, id) => {
setHandled((prev) => new Set(prev).add(id));
queryClient.invalidateQueries({ queryKey: ["devices"] });
},
});
const remaining = staleDevices.filter((d) => !handled.has(d.id));
if (remaining.length === 0) return null;
return (
<div className="mt-3 rounded-xl border border-amber-500/30 bg-amber-500/5 p-3 text-xs">
<p className="mb-2 font-medium text-amber-700 dark:text-amber-400">
{remaining.length} Gerät(e), die die FritzBox früher gemeldet hatte, diesmal aber nicht
mehr:
</p>
<ul className="space-y-1">
{remaining.map((d) => (
<li key={d.id} className="flex items-center justify-between gap-2">
<span className="text-black/70 dark:text-white/70">
{d.hostname} ({d.ip})
</span>
<div className="flex gap-1">
<Button size="sm" variant="ghost" onClick={() => setHandled((prev) => new Set(prev).add(d.id))}>
Behalten
</Button>
<Button
size="sm"
variant="danger"
onClick={() => deleteMutation.mutate(d.id)}
disabled={deleteMutation.isPending}
>
Löschen
</Button>
</div>
</li>
))}
</ul>
<button onClick={onDone} className="mt-2 text-black/40 underline dark:text-white/40">
Hinweis schließen
</button>
</div>
);
}
export function ScannerPage() {
const queryClient = useQueryClient();
const { data: devices } = useDevices();
const [bulkStatus, setBulkStatus] = useState<string | null>(null);
const [bulkRunning, setBulkRunning] = useState(false);
const [staleDevices, setStaleDevices] = useState<Device[]>([]);
const fritzboxMutation = useMutation({
mutationFn: scanFritzBox,
onSuccess: (result) => {
setStaleDevices(result.staleDevices);
queryClient.invalidateQueries({ queryKey: ["devices"] });
queryClient.invalidateQueries({ queryKey: ["logs"] });
return result;
@@ -94,6 +159,9 @@ export function ScannerPage() {
{fritzboxMutation.isError ? (
<p className="mt-2 text-sm text-red-500">{(fritzboxMutation.error as Error).message}</p>
) : null}
{staleDevices.length > 0 ? (
<StaleDevicesReview staleDevices={staleDevices} onDone={() => setStaleDevices([])} />
) : null}
</div>
<div className="rounded-2xl border border-black/10 p-5 dark:border-white/10">