generated from Dicken/dickendock
round16: Geraete-Erkennung per ID statt IP fixen, Schwelle konfigurierbar, Icons auf Font Awesome umgestellt
This commit is contained in:
@@ -3,6 +3,7 @@ import * as deviceRepo from "../db/repositories/devices.js";
|
||||
import * as serviceRepo from "../db/repositories/services.js";
|
||||
import * as categoryRepo from "../db/repositories/categories.js";
|
||||
import * as logRepo from "../db/repositories/logs.js";
|
||||
import * as settingsRepo from "../db/repositories/settings.js";
|
||||
import { scanDeviceServices } from "../scanner/networkScanner.js";
|
||||
import { fetchFritzBoxHosts, type FritzBoxHost } from "../scanner/fritzbox.js";
|
||||
|
||||
@@ -208,20 +209,29 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
);
|
||||
|
||||
// Geräte, die die FritzBox früher gemeldet hatte, diesmal aber nicht
|
||||
// mehr in der Liste sind – werden erst nach mindestens 7 Tagen
|
||||
// Abwesenheit zur Durchsicht vorgeschlagen (nicht schon beim ersten
|
||||
// verpassten Scan), damit ein kurzzeitig offline/im Standby befindliches
|
||||
// Gerät (Reboot, WLAN-Aussetzer, Nacht-Standby) nicht sofort zum
|
||||
// Löschen vorgeschlagen wird. lastScan wird bei jedem Fund aktualisiert
|
||||
// (siehe upsertDeviceFromScan) - bleibt es stehen, war das Gerät seither
|
||||
// nicht mehr da.
|
||||
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
// mehr in der Liste sind – werden erst nach mindestens
|
||||
// staleDeviceThresholdDays Tagen Abwesenheit zur Durchsicht
|
||||
// vorgeschlagen (nicht schon beim ersten verpassten Scan), damit ein
|
||||
// kurzzeitig offline/im Standby befindliches Gerät (Reboot,
|
||||
// WLAN-Aussetzer, Nacht-Standby) nicht sofort zum Löschen
|
||||
// vorgeschlagen wird. lastScan wird bei jedem Fund aktualisiert (siehe
|
||||
// upsertDeviceFromScan) - bleibt es stehen, war das Gerät seither nicht
|
||||
// mehr da.
|
||||
//
|
||||
// "Gefunden" wird über die ID der in DIESEM Scan tatsächlich
|
||||
// upgeserteten Geräte geprüft (nicht über einen IP-Abgleich!): ändert
|
||||
// sich die IP eines Geräts (z. B. DHCP-Neuvergabe), wird es trotzdem
|
||||
// korrekt über seine MAC wiedererkannt und upgeserted - ein reiner
|
||||
// IP-Vergleich hätte es fälschlich als "nicht mehr gefunden" gemeldet,
|
||||
// obwohl es im selben Scan noch da war, nur unter neuer IP.
|
||||
const foundDeviceIds = new Set(devices.map((d) => d.id));
|
||||
const staleDeviceThresholdDays = Number(settingsRepo.listSettings().staleDeviceThresholdDays ?? 7);
|
||||
const staleThresholdMs = staleDeviceThresholdDays * 24 * 60 * 60 * 1000;
|
||||
const now = Date.now();
|
||||
const foundIps = new Set(hosts.map((h) => h.ip));
|
||||
const staleDevices = devicesBeforeScan.filter((d) => {
|
||||
if (foundIps.has(d.ip)) return false;
|
||||
if (foundDeviceIds.has(d.id)) return false;
|
||||
const lastSeen = d.lastScan ? new Date(d.lastScan).getTime() : 0;
|
||||
return now - lastSeen >= SEVEN_DAYS_MS;
|
||||
return now - lastSeen >= staleThresholdMs;
|
||||
});
|
||||
|
||||
logRepo.logScan({
|
||||
|
||||
@@ -7,12 +7,13 @@ export async function settingsRoutes(app: FastifyInstance): Promise<void> {
|
||||
return {
|
||||
recentVisitsLimit: Number(all.recentVisitsLimit ?? 5),
|
||||
readLaterLimit: Number(all.readLaterLimit ?? 5),
|
||||
staleDeviceThresholdDays: Number(all.staleDeviceThresholdDays ?? 7),
|
||||
};
|
||||
});
|
||||
|
||||
app.patch("/api/settings", async (request, reply) => {
|
||||
const body = request.body as
|
||||
| { recentVisitsLimit?: number; readLaterLimit?: number }
|
||||
| { recentVisitsLimit?: number; readLaterLimit?: number; staleDeviceThresholdDays?: number }
|
||||
| undefined;
|
||||
|
||||
if (body?.recentVisitsLimit !== undefined) {
|
||||
@@ -31,10 +32,19 @@ export async function settingsRoutes(app: FastifyInstance): Promise<void> {
|
||||
settingsRepo.setSetting("readLaterLimit", String(Math.round(value)));
|
||||
}
|
||||
|
||||
if (body?.staleDeviceThresholdDays !== undefined) {
|
||||
const value = Number(body.staleDeviceThresholdDays);
|
||||
if (!Number.isFinite(value) || value < 0 || value > 90) {
|
||||
return reply.code(400).send({ error: "staleDeviceThresholdDays muss zwischen 0 und 90 liegen" });
|
||||
}
|
||||
settingsRepo.setSetting("staleDeviceThresholdDays", String(Math.round(value)));
|
||||
}
|
||||
|
||||
const all = settingsRepo.listSettings();
|
||||
return {
|
||||
recentVisitsLimit: Number(all.recentVisitsLimit ?? 5),
|
||||
readLaterLimit: Number(all.readLaterLimit ?? 5),
|
||||
staleDeviceThresholdDays: Number(all.staleDeviceThresholdDays ?? 7),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.3.1",
|
||||
"@fortawesome/free-regular-svg-icons": "^7.3.1",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.3.1",
|
||||
"@fortawesome/react-fontawesome": "^3.5.0",
|
||||
"@launchpad/shared": "workspace:*",
|
||||
"@launchpad/ui": "workspace:*",
|
||||
"@tanstack/react-query": "^5.51.23",
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
export interface AppSettings {
|
||||
recentVisitsLimit: number;
|
||||
readLaterLimit: number;
|
||||
staleDeviceThresholdDays: number;
|
||||
}
|
||||
|
||||
async function fetchSettings(): Promise<AppSettings> {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMemo, useState, type DragEvent, type FormEvent } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFloppyDisk, faPen, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button, Favicon } from "@launchpad/ui";
|
||||
import type { Bookmark } from "@launchpad/shared";
|
||||
@@ -260,8 +262,8 @@ function EditForm({ bookmark, onDone }: { bookmark: Bookmark; onDone: () => void
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
<Button size="icon" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending} title="Speichern" aria-label="Speichern"><FontAwesomeIcon icon={faFloppyDisk} /></Button>
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Abbrechen" aria-label="Abbrechen"><FontAwesomeIcon icon={faXmark} /></Button>
|
||||
{mutation.isError ? (
|
||||
<span className="text-xs text-red-500">{(mutation.error as Error).message}</span>
|
||||
) : null}
|
||||
@@ -354,8 +356,8 @@ function BookmarkRow({
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<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>
|
||||
<Button size="icon" onClick={() => setEditing(true)} title="Bearbeiten" aria-label="Bearbeiten"><FontAwesomeIcon icon={faPen} /></Button>
|
||||
<Button size="icon" variant="danger" onClick={() => deleteMutation.mutate()} disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen"><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState, type DragEvent, type FormEvent } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFloppyDisk, faPen, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button } from "@launchpad/ui";
|
||||
import type { Category } from "@launchpad/shared";
|
||||
@@ -125,18 +127,14 @@ 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="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>
|
||||
<Button size="icon" variant="primary" onClick={() => renameMutation.mutate()} disabled={renameMutation.isPending} title="Speichern" aria-label="Speichern"><FontAwesomeIcon icon={faFloppyDisk} /></Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => setEditing(false)} title="Abbrechen" aria-label="Abbrechen"><FontAwesomeIcon icon={faXmark} /></Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="flex-1 text-sm font-medium text-black dark:text-white">{category.name}</span>
|
||||
<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>
|
||||
<Button size="icon" onClick={() => setEditing(true)} title="Umbenennen" aria-label="Umbenennen"><FontAwesomeIcon icon={faPen} /></Button>
|
||||
<Button size="icon" variant="danger" onClick={() => deleteMutation.mutate()} disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen"><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMemo, useState, type FormEvent } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBan, faCheck, faFloppyDisk, faPen, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button, Favicon } from "@launchpad/ui";
|
||||
import type { Service } from "@launchpad/shared";
|
||||
@@ -162,8 +164,8 @@ function EditDeviceForm({ device, onDone }: { device: DeviceWithServices; onDone
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<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>
|
||||
<Button size="icon" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending} title="Speichern" aria-label="Speichern"><FontAwesomeIcon icon={faFloppyDisk} /></Button>
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Abbrechen" aria-label="Abbrechen"><FontAwesomeIcon icon={faXmark} /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
@@ -218,12 +220,12 @@ function NameChangesReview({
|
||||
onClick={() => setHandled((prev) => new Set(prev).add(`${c.serviceId}:${c.field}`))}
|
||||
title="Behalten (Vorschlag verwerfen)"
|
||||
aria-label="Behalten (Vorschlag verwerfen)"
|
||||
>✕</Button>
|
||||
><FontAwesomeIcon icon={faBan} /></Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="primary"
|
||||
onClick={() => applyMutation.mutate(c)}
|
||||
disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen">✓</Button>
|
||||
disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen"><FontAwesomeIcon icon={faCheck} /></Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -261,8 +263,8 @@ function DeviceNameSuggestionBanner({
|
||||
Per Reverse-DNS gefunden: „{suggestion}" als Gerätename?
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<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>
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)"><FontAwesomeIcon icon={faBan} /></Button>
|
||||
<Button size="icon" variant="primary" onClick={() => applyMutation.mutate()} disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen"><FontAwesomeIcon icon={faCheck} /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,12 +298,12 @@ function StaleServicesReview({ staleServices, onDone }: { staleServices: Service
|
||||
{s.displayName} ({s.hostname}:{s.port})
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<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="icon" variant="ghost" onClick={() => setHandled((prev) => new Set(prev).add(s.id))} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)"><FontAwesomeIcon icon={faBan} /></Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="danger"
|
||||
onClick={() => deleteMutation.mutate(s.id)}
|
||||
disabled={deleteMutation.isPending} title="Entfernen" aria-label="Entfernen">🗑️</Button>
|
||||
disabled={deleteMutation.isPending} title="Entfernen" aria-label="Entfernen"><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -388,7 +390,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="icon" onClick={() => setEditing(true)} title="Bearbeiten" aria-label="Bearbeiten">✏️</Button>
|
||||
<Button size="icon" onClick={() => setEditing(true)} title="Bearbeiten" aria-label="Bearbeiten"><FontAwesomeIcon icon={faPen} /></Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
@@ -403,7 +405,7 @@ function DeviceRow({ device }: { device: DeviceWithServices }) {
|
||||
size="icon"
|
||||
variant="danger"
|
||||
onClick={() => deleteMutation.mutate()}
|
||||
disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen">🗑️</Button>
|
||||
disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen"><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBookmark, faPen, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button, Favicon } from "@launchpad/ui";
|
||||
import { useReadLater, type ReadLaterItem } from "../../hooks/useReadLater.js";
|
||||
@@ -77,13 +79,13 @@ function EditRow({ item, onDone }: { item: ReadLaterItem; onDone: () => void })
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Abbrechen" aria-label="Abbrechen">✕</Button>
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Abbrechen" aria-label="Abbrechen"><FontAwesomeIcon icon={faXmark} /></Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="primary"
|
||||
onClick={() => mutation.mutate()}
|
||||
disabled={mutation.isPending || !displayName.trim() || !url.trim()}
|
||||
>💾</Button>
|
||||
><FontAwesomeIcon icon={faXmark} /></Button>
|
||||
</div>
|
||||
</div>
|
||||
{mutation.isError ? (
|
||||
@@ -136,15 +138,16 @@ 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="icon" variant="ghost" onClick={() => setEditing(true)} title="Bearbeiten" aria-label="Bearbeiten">✏️</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => setEditing(true)} title="Bearbeiten" aria-label="Bearbeiten"><FontAwesomeIcon icon={faPen} /></Button>
|
||||
<Button
|
||||
size="sm"
|
||||
size="icon"
|
||||
variant="secondary"
|
||||
onClick={() => promoteMutation.mutate()}
|
||||
disabled={promoteMutation.isPending}
|
||||
title="Zu Lesezeichen (Favorit) machen und aus dieser Liste entfernen"
|
||||
aria-label="Zu Lesezeichen machen"
|
||||
>
|
||||
🔖 Zu Lesezeichen
|
||||
<FontAwesomeIcon icon={faBookmark} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
@@ -153,7 +156,7 @@ function ReadLaterRow({ item }: { item: ReadLaterItem }) {
|
||||
disabled={deleteMutation.isPending}
|
||||
title="Löschen"
|
||||
aria-label="Löschen"
|
||||
>🗑️</Button>
|
||||
><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faBan, faCheck, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { Button } from "@launchpad/ui";
|
||||
import type { Device, Service } from "@launchpad/shared";
|
||||
import { useDevices } from "../../hooks/useDevices.js";
|
||||
@@ -141,12 +143,12 @@ function NameChangesReview({
|
||||
{c.suggested}"
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(nameChangeKey(c))} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)">✕</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(nameChangeKey(c))} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)"><FontAwesomeIcon icon={faBan} /></Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="primary"
|
||||
onClick={() => applyMutation.mutate(c)}
|
||||
disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen">✓</Button>
|
||||
disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen"><FontAwesomeIcon icon={faCheck} /></Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -187,12 +189,12 @@ function StaleServicesReview({
|
||||
{s.displayName} ({s.hostname}:{s.port})
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(s.id)} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)">✕</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(s.id)} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)"><FontAwesomeIcon icon={faBan} /></Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="danger"
|
||||
onClick={() => deleteMutation.mutate(s.id)}
|
||||
disabled={deleteMutation.isPending} title="Entfernen" aria-label="Entfernen">🗑️</Button>
|
||||
disabled={deleteMutation.isPending} title="Entfernen" aria-label="Entfernen"><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -234,12 +236,12 @@ function DeviceChangesReview({
|
||||
{c.deviceHostname} – {fieldLabel(c.field)}: „{c.current}" → „{c.suggested}"
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(`${c.deviceId}:${c.field}`)} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)">✕</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(`${c.deviceId}:${c.field}`)} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)"><FontAwesomeIcon icon={faBan} /></Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="primary"
|
||||
onClick={() => applyMutation.mutate(c)}
|
||||
disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen">✓</Button>
|
||||
disabled={applyMutation.isPending} title="Übernehmen" aria-label="Übernehmen"><FontAwesomeIcon icon={faCheck} /></Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
@@ -280,12 +282,12 @@ function StaleDevicesReview({
|
||||
{d.hostname} ({d.ip})
|
||||
</span>
|
||||
<div className="flex gap-1">
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(d.id)} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)">✕</Button>
|
||||
<Button size="icon" variant="ghost" onClick={() => onResolve(d.id)} title="Behalten (Vorschlag verwerfen)" aria-label="Behalten (Vorschlag verwerfen)"><FontAwesomeIcon icon={faBan} /></Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="danger"
|
||||
onClick={() => deleteMutation.mutate(d.id)}
|
||||
disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen">🗑️</Button>
|
||||
disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen"><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMemo, useState, type DragEvent } from "react";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faFloppyDisk, faPen, faTrash, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { Button, Favicon } from "@launchpad/ui";
|
||||
import type { Service } from "@launchpad/shared";
|
||||
@@ -229,8 +231,8 @@ function EditForm({ service, onDone }: { service: Service; onDone: () => void })
|
||||
</div>
|
||||
|
||||
<div className="flex w-full items-center gap-2 pt-1">
|
||||
<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>
|
||||
<Button size="icon" variant="primary" onClick={() => mutation.mutate()} disabled={mutation.isPending} title="Speichern" aria-label="Speichern"><FontAwesomeIcon icon={faFloppyDisk} /></Button>
|
||||
<Button size="icon" variant="ghost" onClick={onDone} title="Abbrechen" aria-label="Abbrechen"><FontAwesomeIcon icon={faXmark} /></Button>
|
||||
{mutation.isError ? (
|
||||
<span className="text-xs text-red-500">{(mutation.error as Error).message}</span>
|
||||
) : null}
|
||||
@@ -394,8 +396,8 @@ function ServiceRow({
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<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>
|
||||
<Button size="icon" onClick={() => setEditing(true)} title="Bearbeiten" aria-label="Bearbeiten"><FontAwesomeIcon icon={faPen} /></Button>
|
||||
<Button size="icon" variant="danger" onClick={() => deleteMutation.mutate()} disabled={deleteMutation.isPending} title="Löschen" aria-label="Löschen"><FontAwesomeIcon icon={faTrash} /></Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -20,11 +20,13 @@ function LimitSetting({
|
||||
hint,
|
||||
settingKey,
|
||||
value: currentValue,
|
||||
max = 50,
|
||||
}: {
|
||||
label: string;
|
||||
hint: string;
|
||||
settingKey: "recentVisitsLimit" | "readLaterLimit";
|
||||
settingKey: "recentVisitsLimit" | "readLaterLimit" | "staleDeviceThresholdDays";
|
||||
value: number | undefined;
|
||||
max?: number;
|
||||
}) {
|
||||
const queryClient = useQueryClient();
|
||||
const [value, setValue] = useState<string | null>(null);
|
||||
@@ -54,7 +56,7 @@ function LimitSetting({
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={50}
|
||||
max={max}
|
||||
value={displayed}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
onBlur={() => {
|
||||
@@ -323,6 +325,17 @@ export function SettingsPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-black/10 p-5 dark:border-white/10">
|
||||
<h2 className="mb-2 font-medium text-black dark:text-white">Scan</h2>
|
||||
<LimitSetting
|
||||
label="Nicht mehr gemeldete Geräte anzeigen nach (Tagen)"
|
||||
hint="Ein Gerät, das die FritzBox nicht mehr meldet, wird erst nach so vielen Tagen im Scan-Ergebnis zur Durchsicht vorgeschlagen. 0 = sofort beim ersten verpassten Scan."
|
||||
settingKey="staleDeviceThresholdDays"
|
||||
value={settings?.staleDeviceThresholdDays}
|
||||
max={90}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-black/10 p-5 dark:border-white/10">
|
||||
<h2 className="mb-2 font-medium text-black dark:text-white">HTTPS</h2>
|
||||
<p className="mb-3 text-sm text-black/50 dark:text-white/50">
|
||||
|
||||
Reference in New Issue
Block a user