import type { FastifyInstance } from "fastify"; import { ServiceCreateSchema, ServiceReorderSchema, ServiceUpdateSchema } from "@launchpad/shared"; import * as deviceRepo from "../db/repositories/devices.js"; import * as serviceRepo from "../db/repositories/services.js"; import { suggestIconsForServiceName } from "../scanner/iconDb.js"; interface ServiceListQuery { deviceId?: string; category?: string; favorite?: string; } export async function serviceRoutes(app: FastifyInstance): Promise { app.get("/api/services", async (request) => { const query = request.query as ServiceListQuery; return serviceRepo.listServices({ deviceId: query.deviceId, category: query.category, favorite: query.favorite === undefined ? undefined : query.favorite === "true", }); }); app.get("/api/services/:id", async (request, reply) => { const { id } = request.params as { id: string }; const service = serviceRepo.getService(id); if (!service) { return reply.code(404).send({ error: "Dienst nicht gefunden" }); } return service; }); app.post("/api/services", async (request, reply) => { const parsed = ServiceCreateSchema.safeParse(request.body); if (!parsed.success) { return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues }); } const device = deviceRepo.getDevice(parsed.data.deviceId); if (!device) { return reply.code(400).send({ error: `Gerät ${parsed.data.deviceId} existiert nicht` }); } const service = serviceRepo.createService(parsed.data); return reply.code(201).send(service); }); // Muss vor der /:id-Route stehen, damit "reorder" nicht als ID interpretiert wird. app.patch("/api/services/reorder", async (request, reply) => { const parsed = ServiceReorderSchema.safeParse(request.body); if (!parsed.success) { return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues }); } return serviceRepo.reorderServices(parsed.data); }); app.patch("/api/services/:id", async (request, reply) => { const { id } = request.params as { id: string }; const parsed = ServiceUpdateSchema.safeParse(request.body); if (!parsed.success) { return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues }); } const service = serviceRepo.updateService(id, parsed.data); if (!service) { return reply.code(404).send({ error: "Dienst nicht gefunden" }); } return service; }); app.delete("/api/services/:id", async (request, reply) => { const { id } = request.params as { id: string }; const deleted = serviceRepo.deleteService(id); if (!deleted) { return reply.code(404).send({ error: "Dienst nicht gefunden" }); } return reply.code(204).send(); }); // Liefert für jeden Dienst OHNE Favicon eine Liste möglicher Treffer aus // der externen Icon-Datenbank (siehe scanner/iconDb.ts), damit der Nutzer // selbst auswählen kann statt dass automatisch geraten wird (mehrdeutige // Namen wie "LXC Adguard" könnten sonst falsch zugeordnet werden). app.get("/api/services/favicon-suggestions", async () => { const services = serviceRepo.listServices().filter((s) => !s.favicon); const suggestions = []; for (const service of services) { const candidates = await suggestIconsForServiceName(service.displayName); if (candidates.length > 0) { suggestions.push({ serviceId: service.id, serviceName: service.displayName, candidates }); } } return { checked: services.length, suggestions }; }); }