generated from Dicken/dickendock
Fix: visible-Flag für Scan-Müll, editierbare Hostname/Port/HTTPS, Sidebar fixiert, Suche nur bei Eingabe
This commit is contained in:
@@ -52,6 +52,7 @@ export function ensureSchema(): void {
|
||||
hostname TEXT NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
https INTEGER NOT NULL DEFAULT 0,
|
||||
visible INTEGER NOT NULL DEFAULT 1,
|
||||
port INTEGER NOT NULL,
|
||||
favicon TEXT,
|
||||
description TEXT,
|
||||
@@ -76,4 +77,22 @@ export function ensureSchema(): void {
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
|
||||
// Leichte Migration für Datenbanken, die vor Einführung von "visible"
|
||||
// angelegt wurden: CREATE TABLE IF NOT EXISTS rüstet bei bereits
|
||||
// existierenden Tabellen keine neuen Spalten nach, das übernehmen wir hier
|
||||
// manuell. Bestehende Dienste werden dabei auf sichtbar (1) gesetzt, damit
|
||||
// sich am bisherigen Verhalten nichts unerwartet ändert.
|
||||
ensureColumn("services", "visible", "INTEGER NOT NULL DEFAULT 1");
|
||||
}
|
||||
|
||||
function ensureColumn(table: string, column: string, definition: string): void {
|
||||
const existingColumns = sqlite.prepare(`PRAGMA table_info(${table})`).all() as Array<{
|
||||
name: string;
|
||||
}>;
|
||||
const hasColumn = existingColumns.some((col) => col.name === column);
|
||||
|
||||
if (!hasColumn) {
|
||||
sqlite.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ function mapRow(row: typeof services.$inferSelect): Service {
|
||||
favorite: row.favorite,
|
||||
alias: JSON.parse(row.alias) as string[],
|
||||
order: row.order,
|
||||
visible: row.visible,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -76,6 +77,7 @@ export function createService(input: ServiceCreateInput): Service {
|
||||
favorite: input.favorite ?? false,
|
||||
alias: JSON.stringify(input.alias ?? []),
|
||||
order: input.order ?? 0,
|
||||
visible: input.visible ?? true,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
})
|
||||
@@ -87,7 +89,8 @@ export function createService(input: ServiceCreateInput): Service {
|
||||
/**
|
||||
* Aktualisiert einen Dienst anhand von Benutzereingaben (z. B. über die Admin-UI).
|
||||
* Für automatische Scan-Ergebnisse siehe upsertServiceFromScan() unten – die
|
||||
* NIEMALS displayName, category, favorite, order, alias oder icon überschreibt.
|
||||
* NIEMALS displayName, category, favorite, order, alias, icon, hostname, url,
|
||||
* https oder visible eines bereits bekannten Dienstes überschreibt.
|
||||
*/
|
||||
export function updateService(id: string, input: ServiceUpdateInput): Service | null {
|
||||
const existing = getService(id);
|
||||
@@ -107,6 +110,7 @@ export function updateService(id: string, input: ServiceUpdateInput): Service |
|
||||
...(input.favorite !== undefined && { favorite: input.favorite }),
|
||||
...(input.alias !== undefined && { alias: JSON.stringify(input.alias) }),
|
||||
...(input.order !== undefined && { order: input.order }),
|
||||
...(input.visible !== undefined && { visible: input.visible }),
|
||||
updatedAt: nowIso(),
|
||||
})
|
||||
.where(eq(services.id, id))
|
||||
@@ -134,6 +138,8 @@ export interface ServiceScanInput {
|
||||
suggestedCategory?: string | null;
|
||||
/** Nur relevant, wenn dabei ein NEUER Dienst angelegt wird (z. B. von einem Plugin bereitgestellt). */
|
||||
suggestedIcon?: string | null;
|
||||
/** Nur relevant, wenn dabei ein NEUER Dienst angelegt wird (z. B. Fehlerseite -> versteckt). */
|
||||
suggestedVisible?: boolean;
|
||||
}
|
||||
|
||||
export interface ScanUpsertResult {
|
||||
@@ -146,12 +152,16 @@ function findByDeviceAndPort(deviceId: string, port: number): Service | null {
|
||||
}
|
||||
|
||||
/**
|
||||
* Legt einen per Scan gefundenen Dienst an oder aktualisiert die scan-eigenen
|
||||
* Felder eines bereits bekannten Dienstes (abgeglichen über deviceId + port).
|
||||
* Legt einen per Scan gefundenen Dienst an oder aktualisiert einen bereits
|
||||
* bekannten Dienst (abgeglichen über deviceId + port).
|
||||
*
|
||||
* displayName, category, favorite, order, alias und icon werden bei einem
|
||||
* bestehenden Dienst NIEMALS verändert – nur beim erstmaligen Anlegen dienen
|
||||
* suggestedDisplayName/suggestedCategory als sinnvoller Startwert.
|
||||
* Bei einem BESTEHENDEN Dienst werden nur favicon/description aufgefrischt.
|
||||
* Alles andere – auch hostname/url/https, die früher bei jedem Scan
|
||||
* überschrieben wurden – bleibt unangetastet, damit manuelle Anpassungen
|
||||
* (z. B. IP/Port/Protokoll korrigieren) einen erneuten Scan überleben.
|
||||
*
|
||||
* suggestedDisplayName/suggestedCategory/suggestedIcon/suggestedVisible
|
||||
* wirken NUR beim erstmaligen Anlegen.
|
||||
*/
|
||||
export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult {
|
||||
const existing = findByDeviceAndPort(input.deviceId, input.port);
|
||||
@@ -160,9 +170,6 @@ export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult
|
||||
if (existing) {
|
||||
db.update(services)
|
||||
.set({
|
||||
hostname: input.hostname,
|
||||
url: input.url,
|
||||
https: input.https,
|
||||
favicon: input.favicon ?? existing.favicon,
|
||||
description: input.description ?? existing.description,
|
||||
updatedAt: timestamp,
|
||||
@@ -186,6 +193,7 @@ export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult
|
||||
hostname: input.hostname,
|
||||
url: input.url,
|
||||
https: input.https,
|
||||
visible: input.suggestedVisible ?? true,
|
||||
port: input.port,
|
||||
favicon: input.favicon ?? null,
|
||||
description: input.description ?? null,
|
||||
|
||||
@@ -19,8 +19,10 @@ export const devices = sqliteTable("devices", {
|
||||
|
||||
/**
|
||||
* Ein Dienst, der auf einem Gerät läuft (z. B. Frigate, Portainer, Gitea ...).
|
||||
* Benutzeränderungen (displayName, category, favorite, order, alias, icon)
|
||||
* dürfen von Scans NIEMALS überschrieben werden – siehe README/Spezifikation.
|
||||
* Benutzeränderungen (displayName, category, favorite, order, alias, icon,
|
||||
* hostname, url, https, visible) dürfen von einem erneuten Scan eines bereits
|
||||
* bekannten Dienstes NIEMALS überschrieben werden – siehe README/Spezifikation.
|
||||
* Nur favicon/description werden bei jedem Scan aufgefrischt.
|
||||
*/
|
||||
export const services = sqliteTable("services", {
|
||||
id: text("id").primaryKey(),
|
||||
@@ -28,18 +30,20 @@ export const services = sqliteTable("services", {
|
||||
.notNull()
|
||||
.references(() => devices.id, { onDelete: "cascade" }),
|
||||
|
||||
// vom Benutzer gepflegt, wird von Scans nicht überschrieben
|
||||
// vom Benutzer gepflegt bzw. nur beim erstmaligen Anlegen durch einen Scan gesetzt
|
||||
displayName: text("display_name").notNull(),
|
||||
category: text("category"),
|
||||
favorite: integer("favorite", { mode: "boolean" }).notNull().default(false),
|
||||
order: real("order").notNull().default(0),
|
||||
alias: text("alias").notNull().default("[]"), // JSON-Array als String
|
||||
icon: text("icon"),
|
||||
|
||||
// von Scans aktualisierbar
|
||||
hostname: text("hostname").notNull(),
|
||||
url: text("url").notNull(),
|
||||
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),
|
||||
|
||||
// Bei jedem Scan aktualisierbar
|
||||
port: integer("port").notNull(),
|
||||
favicon: text("favicon"),
|
||||
description: text("description"),
|
||||
|
||||
@@ -34,6 +34,7 @@ export async function scanRoutes(app: FastifyInstance): Promise<void> {
|
||||
suggestedDisplayName: found.suggestedDisplayName,
|
||||
suggestedCategory: found.category,
|
||||
suggestedIcon: found.icon,
|
||||
suggestedVisible: found.visible,
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -18,6 +18,23 @@ export interface DiscoveredService {
|
||||
suggestedDisplayName: string;
|
||||
category?: string;
|
||||
icon?: string;
|
||||
/** Startwert für neu angelegte Dienste: false bei Fehlerseiten/generischen Ladetiteln. */
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Erkennt generische/nichtssagende Titel, die keine sinnvolle Dienst-
|
||||
* Bezeichnung abgeben: HTTP-Statuszeilen ("404 Not Found", "302 Found") und
|
||||
* Lade-/Weiterleitungsplatzhalter (der Scanner führt kein JavaScript aus,
|
||||
* sieht bei manchen Web-UIs also nur den Ladebildschirm statt des echten Titels).
|
||||
*/
|
||||
function isLikelyJunkTitle(title: string | undefined): boolean {
|
||||
if (!title) return true;
|
||||
const trimmed = title.trim();
|
||||
if (trimmed.length === 0) return true;
|
||||
if (/^\d{3}\b/.test(trimmed)) return true; // "404 Not Found", "400 ...", "302 Found"
|
||||
if (/^(loading|lädt|please wait|redirecting|weiterleitung)/i.test(trimmed)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,6 +68,14 @@ export async function scanDeviceServices(
|
||||
port,
|
||||
});
|
||||
|
||||
const titleIsJunk = isLikelyJunkTitle(probe.title);
|
||||
const fallbackName = `${address}:${port}`;
|
||||
|
||||
// Nur sichtbar, wenn Software erkannt wurde oder eine erfolgreiche Antwort
|
||||
// (Status < 400) mit einem brauchbaren Titel vorliegt. Fehlerseiten und
|
||||
// Ladebildschirme landen als ausgeblendeter Dienst im Adminbereich.
|
||||
const visible = !!software || (probe.ok && !titleIsJunk);
|
||||
|
||||
found.push({
|
||||
hostname: address,
|
||||
url: baseUrl,
|
||||
@@ -58,9 +83,10 @@ export async function scanDeviceServices(
|
||||
port,
|
||||
favicon: probe.faviconUrl,
|
||||
description: software ? `${software.name} (automatisch erkannt)` : probe.title,
|
||||
suggestedDisplayName: software?.name ?? probe.title ?? `${address}:${port}`,
|
||||
suggestedDisplayName: software?.name ?? (titleIsJunk ? fallbackName : probe.title ?? fallbackName),
|
||||
category: software?.category,
|
||||
icon: software?.icon,
|
||||
visible,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user