round14: Uebernehmen-Bug und Suggestion-Debounce fixen, Read-Later-Verwaltungsseite, konfigurierbares Read-Later-Limit

This commit is contained in:
2026-07-23 09:17:50 +02:00
parent 2772c29036
commit 70edeeed38
11 changed files with 333 additions and 37 deletions

View File

@@ -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 {

View File

@@ -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 <title> 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,
})

View File

@@ -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 <title> 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(),

View File

@@ -6,11 +6,14 @@ export async function settingsRoutes(app: FastifyInstance): Promise<void> {
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<void> {
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),
};
});
}