import type { FastifyInstance } from "fastify"; import * as XLSX from "xlsx"; import * as deviceRepo from "../db/repositories/devices.js"; import * as serviceRepo from "../db/repositories/services.js"; import * as categoryRepo from "../db/repositories/categories.js"; interface TransferRow { type: "device" | "service"; displayName: string; hostname: string; ip: string; mac: string; manufacturer: string; model: string; category: string; alias: string; favorite: string; visible: string; order: number | string; port: string | number; https: string; url: string; deviceHostname: string; deviceIp: string; } function buildExportRows(): TransferRow[] { const devices = deviceRepo.listDevices(); const services = serviceRepo.listServices(); const deviceById = new Map(devices.map((d) => [d.id, d])); const deviceRows: TransferRow[] = devices.map((d) => ({ type: "device", displayName: d.hostname, hostname: d.hostname, ip: d.ip, mac: d.mac ?? "", manufacturer: d.manufacturer ?? "", model: d.model ?? "", category: "", alias: "", favorite: "", visible: "", order: "", port: "", https: "", url: "", deviceHostname: "", deviceIp: "", })); const serviceRows: TransferRow[] = services.map((s) => { const device = deviceById.get(s.deviceId); return { type: "service", displayName: s.displayName, hostname: s.hostname, ip: "", mac: "", manufacturer: "", model: "", category: s.category ?? "", alias: s.alias.join(";"), favorite: s.favorite ? "true" : "false", visible: s.visible ? "true" : "false", order: s.order, port: s.port, https: s.https ? "true" : "false", url: s.url, deviceHostname: device?.hostname ?? "", deviceIp: device?.ip ?? "", }; }); return [...deviceRows, ...serviceRows]; } export async function transferRoutes(app: FastifyInstance): Promise { app.get("/api/transfer/export", async (request, reply) => { const query = request.query as { format?: string }; const format = query.format === "xlsx" ? "xlsx" : query.format === "json" ? "json" : "csv"; const rows = buildExportRows(); if (format === "json") { reply.header("Content-Disposition", 'attachment; filename="launchpad-export.json"'); reply.type("application/json"); return rows; } const worksheet = XLSX.utils.json_to_sheet(rows); if (format === "xlsx") { const workbook = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(workbook, worksheet, "LaunchPad"); const buffer = XLSX.write(workbook, { type: "buffer", bookType: "xlsx" }) as Buffer; reply.header("Content-Disposition", 'attachment; filename="launchpad-export.xlsx"'); reply.type("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); return reply.send(buffer); } const csv = XLSX.utils.sheet_to_csv(worksheet); reply.header("Content-Disposition", 'attachment; filename="launchpad-export.csv"'); reply.type("text/csv; charset=utf-8"); return reply.send(csv); }); app.post("/api/transfer/import", async (request, reply) => { const body = request.body as { format?: string; content?: string } | undefined; if (!body?.content) { return reply.code(400).send({ error: "Kein Dateiinhalt übermittelt" }); } let rows: Record[]; try { if (body.format === "json") { const text = Buffer.from(body.content, "base64").toString("utf-8"); rows = JSON.parse(text); } else { const buffer = Buffer.from(body.content, "base64"); const workbook = XLSX.read(buffer, { type: "buffer" }); const sheetName = workbook.SheetNames[0]; rows = XLSX.utils.sheet_to_json(workbook.Sheets[sheetName], { defval: "" }); } } catch (err) { return reply.code(400).send({ error: "Datei konnte nicht gelesen werden", detail: err instanceof Error ? err.message : String(err), }); } let devicesImported = 0; let devicesSkipped = 0; let servicesImported = 0; let servicesSkipped = 0; const errors: string[] = []; // Geräte-Zeilen zuerst verarbeiten, damit Dienst-Zeilen aus derselben // Datei sie bereits über deviceHostname/deviceIp finden können. const deviceRows = rows.filter((r) => String(r.type ?? "").trim() === "device"); const serviceRows = rows.filter((r) => String(r.type ?? "").trim() === "service"); for (const row of deviceRows) { try { const hostname = String(row.hostname ?? row.displayName ?? "").trim(); const ip = String(row.ip ?? "").trim(); if (!hostname || !ip) { errors.push(`Geräte-Zeile übersprungen (hostname/ip fehlt): ${JSON.stringify(row)}`); continue; } const allDevices = deviceRepo.listDevices(); const existing = allDevices.find((d) => d.ip === ip) ?? allDevices.find((d) => d.hostname === hostname); if (existing) { devicesSkipped++; continue; } deviceRepo.createDevice({ hostname, ip, mac: row.mac ? String(row.mac).trim() : undefined, manufacturer: row.manufacturer ? String(row.manufacturer).trim() : undefined, model: row.model ? String(row.model).trim() : undefined, }); devicesImported++; } catch (err) { errors.push(err instanceof Error ? err.message : String(err)); } } for (const row of serviceRows) { try { const deviceHostname = String(row.deviceHostname ?? "").trim(); const deviceIp = String(row.deviceIp ?? "").trim(); const hostname = String(row.hostname ?? deviceHostname).trim(); const port = Number(row.port); const displayName = String(row.displayName ?? "").trim(); const url = String(row.url ?? "").trim(); if (!displayName || !hostname || !port || !url) { errors.push(`Dienst-Zeile übersprungen (Pflichtfelder fehlen): ${JSON.stringify(row)}`); continue; } const allDevices = deviceRepo.listDevices(); let device = deviceIp ? allDevices.find((d) => d.ip === deviceIp) : undefined; if (!device && deviceHostname) { device = allDevices.find((d) => d.hostname === deviceHostname); } if (!device) { device = deviceRepo.createDevice({ hostname: deviceHostname || hostname, ip: deviceIp || hostname, }); devicesImported++; } const existingService = serviceRepo .listServicesByDevice(device.id) .find((s) => s.port === port); if (existingService) { servicesSkipped++; continue; } const category = row.category ? String(row.category).trim() : undefined; serviceRepo.createService({ deviceId: device.id, displayName, hostname, url, https: String(row.https ?? "").toLowerCase() === "true", port, category: category || undefined, alias: row.alias ? String(row.alias) .split(";") .map((a) => a.trim()) .filter(Boolean) : undefined, favorite: String(row.favorite ?? "").toLowerCase() === "true", order: row.order !== undefined && row.order !== "" ? Number(row.order) : undefined, visible: row.visible !== undefined && row.visible !== "" ? String(row.visible).toLowerCase() === "true" : undefined, }); if (category) { categoryRepo.ensureCategory(category); } servicesImported++; } catch (err) { errors.push(err instanceof Error ? err.message : String(err)); } } return { devicesImported, devicesSkipped, servicesImported, servicesSkipped, errors, }; }); }