diff --git a/apps/backend/src/db/client.ts b/apps/backend/src/db/client.ts
index 62d7e6e..92f1f36 100644
--- a/apps/backend/src/db/client.ts
+++ b/apps/backend/src/db/client.ts
@@ -55,6 +55,8 @@ export function ensureSchema(): void {
visible INTEGER NOT NULL DEFAULT 1,
display_name_edited_manually INTEGER NOT NULL DEFAULT 0,
category_edited_manually INTEGER NOT NULL DEFAULT 0,
+ last_suggested_display_name TEXT,
+ last_suggested_category TEXT,
port INTEGER NOT NULL,
favicon TEXT,
description TEXT,
@@ -130,6 +132,8 @@ export function ensureSchema(): void {
// 0 (nicht manuell bearbeitet) ist für Bestandsdaten die richtige Annahme.
ensureColumn("services", "display_name_edited_manually", "INTEGER NOT NULL DEFAULT 0");
ensureColumn("services", "category_edited_manually", "INTEGER NOT NULL DEFAULT 0");
+ ensureColumn("services", "last_suggested_display_name", "TEXT");
+ ensureColumn("services", "last_suggested_category", "TEXT");
}
function ensureColumn(table: string, column: string, definition: string): void {
diff --git a/apps/backend/src/db/repositories/services.ts b/apps/backend/src/db/repositories/services.ts
index 53e1d49..1681a2d 100644
--- a/apps/backend/src/db/repositories/services.ts
+++ b/apps/backend/src/db/repositories/services.ts
@@ -215,6 +215,12 @@ function findByDeviceAndPort(deviceId: string, port: number): Service | null {
* wird das über nameChanges zur manuellen Bestätigung zurückgegeben, statt
* entweder stillschweigend ignoriert oder automatisch übernommen zu werden.
*
+ * Ein abweichender Vorschlag wird ERST gemeldet, wenn er zwei Scans in Folge
+ * derselbe ist (siehe lastSuggestedDisplayName/lastSuggestedCategory in
+ * schema.ts) - Titel-Erkennung kann pro Scan leicht schwanken (z. B. eine
+ * SPA, deren
nur gelegentlich rechtzeitig geladen ist), ein
+ * einmaliger Ausrutscher soll nicht sofort als Änderung gemeldet werden.
+ *
* suggestedDisplayName/suggestedCategory/suggestedIcon/suggestedVisible
* bestimmen den Anfangswert NUR beim erstmaligen Anlegen.
*/
@@ -223,21 +229,27 @@ export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult
const timestamp = nowIso();
if (existing) {
- // Rohzeile inkl. der *EditedManually-Flags laden, die mapRow() bewusst
- // nicht nach außen gibt (nur intern für den Vergleich hier relevant).
+ // Rohzeile inkl. der *EditedManually-/lastSuggested*-Felder laden, die
+ // mapRow() bewusst nicht nach außen gibt (nur intern hier relevant).
const existingRow = db.select().from(services).where(eq(services.id, existing.id)).get()!;
db.update(services)
.set({
favicon: input.favicon ?? existing.favicon,
description: input.description ?? existing.description,
+ lastSuggestedDisplayName: input.suggestedDisplayName ?? existingRow.lastSuggestedDisplayName,
+ lastSuggestedCategory: input.suggestedCategory ?? existingRow.lastSuggestedCategory,
updatedAt: timestamp,
})
.where(eq(services.id, existing.id))
.run();
const nameChanges: ScanNameChange[] = [];
- if (input.suggestedDisplayName && input.suggestedDisplayName !== existing.displayName) {
+ if (
+ input.suggestedDisplayName &&
+ input.suggestedDisplayName !== existing.displayName &&
+ input.suggestedDisplayName === existingRow.lastSuggestedDisplayName
+ ) {
nameChanges.push({
field: "displayName",
current: existing.displayName,
@@ -248,7 +260,8 @@ export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult
if (
input.suggestedCategory !== undefined &&
(input.suggestedCategory ?? null) !== existing.category &&
- input.suggestedCategory
+ input.suggestedCategory &&
+ input.suggestedCategory === existingRow.lastSuggestedCategory
) {
nameChanges.push({
field: "category",
@@ -279,6 +292,8 @@ export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult
port: input.port,
favicon: input.favicon ?? null,
description: input.description ?? null,
+ lastSuggestedDisplayName: input.suggestedDisplayName,
+ lastSuggestedCategory: input.suggestedCategory ?? null,
createdAt: timestamp,
updatedAt: timestamp,
})
diff --git a/apps/backend/src/db/schema.ts b/apps/backend/src/db/schema.ts
index 7be54d4..4875218 100644
--- a/apps/backend/src/db/schema.ts
+++ b/apps/backend/src/db/schema.ts
@@ -53,6 +53,14 @@ export const services = sqliteTable("services", {
categoryEditedManually: integer("category_edited_manually", { mode: "boolean" })
.notNull()
.default(false),
+ // Merkt sich den zuletzt vom Scanner berechneten Vorschlag (unabhängig
+ // davon, ob er zu einer Änderungsmeldung führte). Dient dazu, einmalige
+ // "Ausrutscher" abzufedern (z. B. eine SPA, deren der Scanner nur
+ // gelegentlich rechtzeitig sieht): ein neuer, abweichender Vorschlag wird
+ // erst als Änderung gemeldet, wenn er beim ZWEITEN Mal in Folge wieder
+ // auftaucht - siehe upsertServiceFromScan.
+ lastSuggestedDisplayName: text("last_suggested_display_name"),
+ lastSuggestedCategory: text("last_suggested_category"),
// Bei jedem Scan aktualisierbar
port: integer("port").notNull(),
diff --git a/apps/backend/src/routes/settings.ts b/apps/backend/src/routes/settings.ts
index 4bb4fbb..33439bb 100644
--- a/apps/backend/src/routes/settings.ts
+++ b/apps/backend/src/routes/settings.ts
@@ -6,11 +6,14 @@ export async function settingsRoutes(app: FastifyInstance): Promise {
const all = settingsRepo.listSettings();
return {
recentVisitsLimit: Number(all.recentVisitsLimit ?? 5),
+ readLaterLimit: Number(all.readLaterLimit ?? 5),
};
});
app.patch("/api/settings", async (request, reply) => {
- const body = request.body as { recentVisitsLimit?: number } | undefined;
+ const body = request.body as
+ | { recentVisitsLimit?: number; readLaterLimit?: number }
+ | undefined;
if (body?.recentVisitsLimit !== undefined) {
const value = Number(body.recentVisitsLimit);
@@ -20,7 +23,18 @@ export async function settingsRoutes(app: FastifyInstance): Promise {
settingsRepo.setSetting("recentVisitsLimit", String(Math.round(value)));
}
+ if (body?.readLaterLimit !== undefined) {
+ const value = Number(body.readLaterLimit);
+ if (!Number.isFinite(value) || value < 0 || value > 50) {
+ return reply.code(400).send({ error: "readLaterLimit muss zwischen 0 und 50 liegen" });
+ }
+ settingsRepo.setSetting("readLaterLimit", String(Math.round(value)));
+ }
+
const all = settingsRepo.listSettings();
- return { recentVisitsLimit: Number(all.recentVisitsLimit ?? 5) };
+ return {
+ recentVisitsLimit: Number(all.recentVisitsLimit ?? 5),
+ readLaterLimit: Number(all.readLaterLimit ?? 5),
+ };
});
}
diff --git a/apps/frontend/src/hooks/useSettings.ts b/apps/frontend/src/hooks/useSettings.ts
index 152f46d..37c2bd6 100644
--- a/apps/frontend/src/hooks/useSettings.ts
+++ b/apps/frontend/src/hooks/useSettings.ts
@@ -2,6 +2,7 @@ import { useQuery } from "@tanstack/react-query";
export interface AppSettings {
recentVisitsLimit: number;
+ readLaterLimit: number;
}
async function fetchSettings(): Promise {
diff --git a/apps/frontend/src/router.tsx b/apps/frontend/src/router.tsx
index ba55be0..8a48ad6 100644
--- a/apps/frontend/src/router.tsx
+++ b/apps/frontend/src/router.tsx
@@ -7,6 +7,7 @@ import { ServicesPage } from "./routes/admin/ServicesPage.js";
import { BookmarksPage } from "./routes/admin/BookmarksPage.js";
import { CategoriesPage } from "./routes/admin/CategoriesPage.js";
import { ScannerPage } from "./routes/admin/ScannerPage.js";
+import { ReadLaterPage } from "./routes/admin/ReadLaterPage.js";
import { PluginsPage } from "./routes/admin/PluginsPage.js";
import { SettingsPage } from "./routes/admin/SettingsPage.js";
import { LogsPage } from "./routes/admin/LogsPage.js";
@@ -72,6 +73,12 @@ const adminScannerRoute = createRoute({
component: ScannerPage,
});
+const adminReadLaterRoute = createRoute({
+ getParentRoute: () => adminRoute,
+ path: "/read-later",
+ component: ReadLaterPage,
+});
+
const adminPluginsRoute = createRoute({
getParentRoute: () => adminRoute,
path: "/plugins",
@@ -100,6 +107,7 @@ const routeTree = rootRoute.addChildren([
adminBookmarksRoute,
adminCategoriesRoute,
adminScannerRoute,
+ adminReadLaterRoute,
adminPluginsRoute,
adminSettingsRoute,
adminLogsRoute,
diff --git a/apps/frontend/src/routes/HomePage.tsx b/apps/frontend/src/routes/HomePage.tsx
index 328a250..c68b37e 100644
--- a/apps/frontend/src/routes/HomePage.tsx
+++ b/apps/frontend/src/routes/HomePage.tsx
@@ -6,6 +6,7 @@ import { rankServices, type SearchResult } from "@launchpad/shared";
import { useServices } from "../hooks/useServices.js";
import { useBookmarks } from "../hooks/useBookmarks.js";
import { useDevices } from "../hooks/useDevices.js";
+import { useSettings } from "../hooks/useSettings.js";
import { useBackendHealth } from "../hooks/useBackendHealth.js";
import { useTheme } from "../hooks/useTheme.js";
import { useCategories } from "../hooks/useCategories.js";
@@ -94,6 +95,7 @@ export function HomePage() {
const { data: services, isLoading: servicesLoading, isError: servicesError } = useServices();
const { data: bookmarks, isLoading: bookmarksLoading } = useBookmarks();
const { data: devices } = useDevices();
+ const { data: settings } = useSettings();
const { data: categories } = useCategories();
const { data: recentVisits } = useRecentVisits();
const { data: readLaterItems } = useReadLater();
@@ -317,23 +319,27 @@ export function HomePage() {