round13: manuelle vs. echte Aenderungen trennen, Scan-Uebersicht ueber Navigation persistieren, ausgeblendete Dienste als Tab, Suche ueber Geraetenamen

This commit is contained in:
2026-07-23 01:21:31 +02:00
parent 1f8218dff1
commit 2772c29036
10 changed files with 322 additions and 126 deletions

View File

@@ -53,6 +53,8 @@ export function ensureSchema(): void {
url TEXT NOT NULL,
https INTEGER NOT NULL DEFAULT 0,
visible INTEGER NOT NULL DEFAULT 1,
display_name_edited_manually INTEGER NOT NULL DEFAULT 0,
category_edited_manually INTEGER NOT NULL DEFAULT 0,
port INTEGER NOT NULL,
favicon TEXT,
description TEXT,
@@ -123,6 +125,11 @@ export function ensureSchema(): void {
// sich am bisherigen Verhalten nichts unerwartet ändert.
ensureColumn("services", "visible", "INTEGER NOT NULL DEFAULT 1");
ensureColumn("categories", "color", "TEXT");
// Für Datenbanken vor Einführung der Unterscheidung "manuell angepasst" vs.
// "vom Scan abweichend" bei displayName/category (siehe schema.ts). Default
// 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");
}
function ensureColumn(table: string, column: string, definition: string): void {

View File

@@ -91,6 +91,14 @@ export function createService(input: ServiceCreateInput): Service {
* Für automatische Scan-Ergebnisse siehe upsertServiceFromScan() unten die
* NIEMALS displayName, category, favorite, order, alias, icon, hostname, url,
* https oder visible eines bereits bekannten Dienstes überschreibt.
*
* Ein PATCH mit displayName/category markiert das jeweilige Feld standardmäßig
* als "manuell bearbeitet" (displayNameEditedManually/categoryEditedManually),
* damit ein späterer Scan-Vorschlag, der davon abweicht, in der Änderungs-
* übersicht klar als "weicht nur wegen deiner manuellen Anpassung ab" markiert
* werden kann statt als echte inhaltliche Änderung. Beim Übernehmen eines
* Scan-Vorschlags wird die jeweilige *EditedManually-Flag explizit auf false
* gesetzt (siehe ScannerPage/DevicesPage "Übernehmen"-Button).
*/
export function updateService(id: string, input: ServiceUpdateInput): Service | null {
const existing = getService(id);
@@ -98,12 +106,18 @@ export function updateService(id: string, input: ServiceUpdateInput): Service |
db.update(services)
.set({
...(input.displayName !== undefined && { displayName: input.displayName }),
...(input.displayName !== undefined && {
displayName: input.displayName,
displayNameEditedManually: input.displayNameEditedManually ?? true,
}),
...(input.hostname !== undefined && { hostname: input.hostname }),
...(input.url !== undefined && { url: input.url }),
...(input.https !== undefined && { https: input.https }),
...(input.port !== undefined && { port: input.port }),
...(input.category !== undefined && { category: input.category }),
...(input.category !== undefined && {
category: input.category,
categoryEditedManually: input.categoryEditedManually ?? true,
}),
...(input.icon !== undefined && { icon: input.icon }),
...(input.favicon !== undefined && { favicon: input.favicon }),
...(input.description !== undefined && { description: input.description }),
@@ -163,6 +177,14 @@ export interface ScanNameChange {
field: "displayName" | "category";
current: string | null;
suggested: string;
/**
* true, wenn der aktuell gespeicherte Wert vom Scan-Vorschlag nur deshalb
* abweicht, weil der Nutzer ihn zuvor manuell angepasst hat (siehe
* displayNameEditedManually/categoryEditedManually). Solche Änderungen sind
* erwartbar/harmlos und sollten in der UI klar von echten inhaltlichen
* Änderungen getrennt dargestellt werden.
*/
manualOverride: boolean;
}
export interface ScanUpsertResult {
@@ -201,6 +223,10 @@ 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).
const existingRow = db.select().from(services).where(eq(services.id, existing.id)).get()!;
db.update(services)
.set({
favicon: input.favicon ?? existing.favicon,
@@ -216,6 +242,7 @@ export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult
field: "displayName",
current: existing.displayName,
suggested: input.suggestedDisplayName,
manualOverride: existingRow.displayNameEditedManually,
});
}
if (
@@ -227,6 +254,7 @@ export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult
field: "category",
current: existing.category,
suggested: input.suggestedCategory,
manualOverride: existingRow.categoryEditedManually,
});
}

View File

@@ -42,6 +42,17 @@ export const services = sqliteTable("services", {
https: integer("https", { mode: "boolean" }).notNull().default(false),
// Ausgeblendete Dienste erscheinen nicht in der Suche, bleiben aber im Adminbereich.
visible: integer("visible", { mode: "boolean" }).notNull().default(true),
// Wird true, sobald displayName/category einmal manuell über die Admin-UI
// gesetzt wurden (siehe updateService). Dient dazu, bei einem erneuten Scan
// zu erkennen: weicht der Wert vom Scan-Vorschlag ab, WEIL der Nutzer ihn
// manuell angepasst hat (harmlos, nur zur Info) oder weil sich der
// tatsächlich erkannte Wert geändert hat (echte Änderung)?
displayNameEditedManually: integer("display_name_edited_manually", { mode: "boolean" })
.notNull()
.default(false),
categoryEditedManually: integer("category_edited_manually", { mode: "boolean" })
.notNull()
.default(false),
// Bei jedem Scan aktualisierbar
port: integer("port").notNull(),