import { randomUUID } from "node:crypto"; import { eq } from "drizzle-orm"; import type { Service, ServiceCreateInput, ServiceUpdateInput } from "@launchpad/shared"; import { db } from "../client.js"; import { services } from "../schema.js"; function nowIso(): string { return new Date().toISOString(); } function mapRow(row: typeof services.$inferSelect): Service { return { id: row.id, deviceId: row.deviceId, displayName: row.displayName, hostname: row.hostname, url: row.url, https: row.https, port: row.port, category: row.category, icon: row.icon, favicon: row.favicon, description: row.description, favorite: row.favorite, alias: JSON.parse(row.alias) as string[], order: row.order, }; } export interface ServiceFilter { deviceId?: string; category?: string; favorite?: boolean; } export function listServices(filter: ServiceFilter = {}): Service[] { return db .select() .from(services) .all() .map(mapRow) .filter((service) => { if (filter.deviceId && service.deviceId !== filter.deviceId) return false; if (filter.category && service.category !== filter.category) return false; if (filter.favorite !== undefined && service.favorite !== filter.favorite) return false; return true; }); } export function listServicesByDevice(deviceId: string): Service[] { return listServices({ deviceId }); } export function getService(id: string): Service | null { const row = db.select().from(services).where(eq(services.id, id)).get(); return row ? mapRow(row) : null; } export function createService(input: ServiceCreateInput): Service { const id = randomUUID(); const timestamp = nowIso(); db.insert(services) .values({ id, deviceId: input.deviceId, displayName: input.displayName, hostname: input.hostname, url: input.url, https: input.https ?? false, port: input.port, category: input.category ?? null, icon: input.icon ?? null, favicon: input.favicon ?? null, description: input.description ?? null, favorite: input.favorite ?? false, alias: JSON.stringify(input.alias ?? []), order: input.order ?? 0, createdAt: timestamp, updatedAt: timestamp, }) .run(); return getService(id)!; } /** * 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. */ export function updateService(id: string, input: ServiceUpdateInput): Service | null { const existing = getService(id); if (!existing) return null; db.update(services) .set({ ...(input.displayName !== undefined && { displayName: input.displayName }), ...(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.icon !== undefined && { icon: input.icon }), ...(input.favicon !== undefined && { favicon: input.favicon }), ...(input.description !== undefined && { description: input.description }), ...(input.favorite !== undefined && { favorite: input.favorite }), ...(input.alias !== undefined && { alias: JSON.stringify(input.alias) }), ...(input.order !== undefined && { order: input.order }), updatedAt: nowIso(), }) .where(eq(services.id, id)) .run(); return getService(id); } export function deleteService(id: string): boolean { const result = db.delete(services).where(eq(services.id, id)).run(); return result.changes > 0; } export interface ServiceScanInput { deviceId: string; hostname: string; url: string; https: boolean; port: number; favicon?: string | null; description?: string | null; /** Nur relevant, wenn dabei ein NEUER Dienst angelegt wird. */ suggestedDisplayName: string; /** Nur relevant, wenn dabei ein NEUER Dienst angelegt wird. */ suggestedCategory?: string | null; } export interface ScanUpsertResult { service: Service; created: boolean; } function findByDeviceAndPort(deviceId: string, port: number): Service | null { return listServicesByDevice(deviceId).find((s) => s.port === port) ?? null; } /** * Legt einen per Scan gefundenen Dienst an oder aktualisiert die scan-eigenen * Felder eines bereits bekannten Dienstes (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. */ export function upsertServiceFromScan(input: ServiceScanInput): ScanUpsertResult { const existing = findByDeviceAndPort(input.deviceId, input.port); const timestamp = nowIso(); 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, }) .where(eq(services.id, existing.id)) .run(); return { service: getService(existing.id)!, created: false }; } const id = randomUUID(); db.insert(services) .values({ id, deviceId: input.deviceId, displayName: input.suggestedDisplayName, category: input.suggestedCategory ?? null, favorite: false, order: 0, alias: "[]", icon: null, hostname: input.hostname, url: input.url, https: input.https, port: input.port, favicon: input.favicon ?? null, description: input.description ?? null, createdAt: timestamp, updatedAt: timestamp, }) .run(); return { service: getService(id)!, created: true }; }