diff --git a/apps/frontend/src/routes/admin/DashboardPage.tsx b/apps/frontend/src/routes/admin/DashboardPage.tsx index 783e2f9..0dca0de 100644 --- a/apps/frontend/src/routes/admin/DashboardPage.tsx +++ b/apps/frontend/src/routes/admin/DashboardPage.tsx @@ -3,6 +3,7 @@ import { useServices } from "../../hooks/useServices.js"; import { useDevices } from "../../hooks/useDevices.js"; import { useCategories } from "../../hooks/useCategories.js"; import { useBookmarks } from "../../hooks/useBookmarks.js"; +import { useReadLater } from "../../hooks/useReadLater.js"; import { AdminPageHeader } from "./AdminPageHeader.js"; function StatCard({ label, value }: { label: string; value: number | string }) { @@ -19,6 +20,7 @@ export function DashboardPage() { const { data: devices } = useDevices(); const { data: categories } = useCategories(); const { data: bookmarks } = useBookmarks(); + const { data: readLaterItems } = useReadLater(); const onlineDevices = devices?.filter((d) => d.online).length ?? 0; const favoriteServices = services?.filter((s) => s.favorite).length ?? 0; @@ -46,11 +48,13 @@ export function DashboardPage() { } /> -
+
+ +
@@ -88,15 +92,6 @@ export function DashboardPage() {
)} - -
-

- Kategorien -

-

- {categories?.length ?? 0} Kategorie(n) angelegt. -

-
); } diff --git a/apps/frontend/src/routes/admin/ScannerPage.tsx b/apps/frontend/src/routes/admin/ScannerPage.tsx index 001251e..13f3f06 100644 --- a/apps/frontend/src/routes/admin/ScannerPage.tsx +++ b/apps/frontend/src/routes/admin/ScannerPage.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@launchpad/ui"; -import type { Device } from "@launchpad/shared"; +import type { Device, Service } from "@launchpad/shared"; import { useDevices } from "../../hooks/useDevices.js"; import { AdminPageHeader } from "./AdminPageHeader.js"; @@ -26,13 +26,76 @@ async function deleteDeviceRequest(id: string) { } } +async function deleteServiceRequest(id: string) { + const res = await fetch(`/api/services/${id}`, { method: "DELETE" }); + if (!res.ok && res.status !== 404) { + throw new Error(`Dienst konnte nicht gelöscht werden (HTTP ${res.status})`); + } +} + async function scanDeviceById(id: string) { const res = await fetch(`/api/scan/devices/${id}`, { method: "POST" }); const body = await res.json(); if (!res.ok) { throw new Error(body.detail ?? body.error ?? `Scan fehlgeschlagen (HTTP ${res.status})`); } - return body as { created: number; updated: number }; + return body as { created: number; updated: number; staleServices: Service[] }; +} + +function StaleServicesReview({ + staleServices, + onDone, +}: { + staleServices: Service[]; + onDone: () => void; +}) { + const queryClient = useQueryClient(); + const [handled, setHandled] = useState>(new Set()); + + const deleteMutation = useMutation({ + mutationFn: (id: string) => deleteServiceRequest(id), + onSuccess: (_data, id) => { + setHandled((prev) => new Set(prev).add(id)); + queryClient.invalidateQueries({ queryKey: ["services"] }); + }, + }); + + const remaining = staleServices.filter((s) => !handled.has(s.id)); + if (remaining.length === 0) return null; + + return ( +
+

+ {remaining.length} Dienst(e) über alle gescannten Geräte hinweg nicht mehr gefunden (Port + nicht mehr offen): +

+ + +
+ ); } function StaleDevicesReview({ staleDevices, onDone }: { staleDevices: Device[]; onDone: () => void }) { @@ -90,6 +153,7 @@ export function ScannerPage() { const { data: devices } = useDevices(); const [bulkStatus, setBulkStatus] = useState(null); const [bulkRunning, setBulkRunning] = useState(false); + const [bulkStaleServices, setBulkStaleServices] = useState([]); const [staleDevices, setStaleDevices] = useState([]); const fritzboxMutation = useMutation({ @@ -105,21 +169,29 @@ export function ScannerPage() { async function scanAllDevices() { if (!devices || devices.length === 0) return; setBulkRunning(true); + setBulkStaleServices([]); let created = 0; let updated = 0; + const allStale: Service[] = []; for (const device of devices) { try { const result = await scanDeviceById(device.id); created += result.created; updated += result.updated; + allStale.push(...result.staleServices); setBulkStatus(`Scanne ${device.hostname} … (${created} neu, ${updated} aktualisiert bisher)`); } catch { // einzelnes fehlgeschlagenes Gerät soll den Rest nicht abbrechen } } - setBulkStatus(`Fertig: ${devices.length} Gerät(e) gescannt, ${created} neue Dienste, ${updated} aktualisiert.`); + setBulkStatus( + `Fertig: ${devices.length} Gerät(e) gescannt, ${created} neue Dienste, ${updated} aktualisiert${ + allStale.length > 0 ? `, ${allStale.length} nicht mehr gefunden (siehe unten)` : "" + }.` + ); + setBulkStaleServices(allStale); setBulkRunning(false); queryClient.invalidateQueries({ queryKey: ["devices"] }); queryClient.invalidateQueries({ queryKey: ["services"] }); @@ -182,6 +254,12 @@ export function ScannerPage() { {bulkStatus ? (

{bulkStatus}

) : null} + {bulkStaleServices.length > 0 ? ( + setBulkStaleServices([])} + /> + ) : null} diff --git a/apps/frontend/src/routes/admin/ServicesPage.tsx b/apps/frontend/src/routes/admin/ServicesPage.tsx index 2c2cd85..0997a2d 100644 --- a/apps/frontend/src/routes/admin/ServicesPage.tsx +++ b/apps/frontend/src/routes/admin/ServicesPage.tsx @@ -103,7 +103,7 @@ function CategorySelect({ ); } -const EDIT_FORM_COLSPAN = 11; +const EDIT_FORM_COLSPAN = 12; function EditForm({ service, onDone }: { service: Service; onDone: () => void }) { const queryClient = useQueryClient(); @@ -239,6 +239,7 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void }) function ServiceRow({ service, deviceMac, + deviceIp, draggable, onDragStart, onDragOver, @@ -247,6 +248,7 @@ function ServiceRow({ }: { service: Service; deviceMac: string | null; + deviceIp: string | null; draggable: boolean; onDragStart: (e: DragEvent) => void; onDragOver: (e: DragEvent) => void; @@ -328,6 +330,9 @@ function ServiceRow({ {service.hostname} + + {deviceIp ?? "–"} + {service.category ?? "–"} {deviceMac ?? "–"} @@ -371,7 +376,7 @@ function ServiceRow({ ); } -type SortColumn = "displayName" | "hostname" | "category" | "alias" | "port" | "https" | null; +type SortColumn = "displayName" | "hostname" | "ip" | "category" | "alias" | "port" | "https" | null; function SortableHeader({ label, @@ -417,6 +422,12 @@ export function ServicesPage() { return map; }, [devices]); + const ipByDeviceId = useMemo(() => { + const map: Record = {}; + for (const d of devices ?? []) map[d.id] = d.ip; + return map; + }, [devices]); + const reorderMutation = useMutation({ mutationFn: reorderServicesRequest, onSuccess: () => { @@ -440,6 +451,13 @@ export function ServicesPage() { case "hostname": cmp = a.hostname.localeCompare(b.hostname); break; + case "ip": + cmp = (ipByDeviceId[a.deviceId] ?? "").localeCompare( + ipByDeviceId[b.deviceId] ?? "", + undefined, + { numeric: true } + ); + break; case "category": cmp = (a.category ?? "").localeCompare(b.category ?? ""); break; @@ -456,7 +474,7 @@ export function ServicesPage() { return sortDirection === "asc" ? cmp : -cmp; }); return sorted; - }, [baseList, sortColumn, sortDirection]); + }, [baseList, sortColumn, sortDirection, ipByDeviceId]); function handleHeaderClick(column: SortColumn) { if (sortColumn === column) { @@ -526,14 +544,15 @@ export function ServicesPage() { ) : list.length > 0 ? (
- +
@@ -549,6 +568,7 @@ export function ServicesPage() { key={service.id} service={service} deviceMac={macByDeviceId[service.deviceId] ?? null} + deviceIp={ipByDeviceId[service.deviceId] ?? null} draggable={dragEnabled} isDragging={draggedId === service.id} onDragStart={handleDragStart(service.id)}
- + + MAC