import express from "express"; import { globalDeviceRouter } from "./routes/global-device.routes.js"; import { projectDeviceRouter } from "./routes/project-device.routes.js"; import { documentBrandingRouter } from "./routes/document-branding.routes.js"; import { projectRouter } from "./routes/project.routes.js"; import { errorMiddleware } from "./middleware/error.middleware.js"; import { createLogger, toErrorMeta } from "../shared/logging/logger.js"; import { closeDefaultDatabase } from "../db/client.js"; const logger = createLogger("api"); const app = express(); const port = Number(process.env.PORT || 3000); const heartbeatIntervalMs = 5 * 60 * 1000; const shutdownTimeoutMs = 15 * 1000; app.use(express.json({ limit: "25mb" })); app.use((req, res, next) => { if (req.path === "/health") { next(); return; } const startedAt = Date.now(); logger.debug("request started", { method: req.method, path: req.originalUrl }); res.on("finish", () => { const meta = { method: req.method, path: req.originalUrl, status: res.statusCode, durationMs: Date.now() - startedAt, }; if (res.statusCode >= 500) logger.error("request completed", meta); else if (res.statusCode >= 400) logger.warn("request completed", meta); else logger.info("request completed", meta); }); res.on("close", () => { if (!res.writableEnded) { logger.warn("request aborted before response finished", { method: req.method, path: req.originalUrl, durationMs: Date.now() - startedAt, }); } }); next(); }); app.get("/health", (_req, res) => { res.json({ ok: true }); }); app.use("/api/projects", projectRouter); app.use("/api/global-devices", globalDeviceRouter); app.use("/api/project-devices", projectDeviceRouter); app.use("/api/document-branding", documentBrandingRouter); app.use(errorMiddleware); process.on("uncaughtException", (error) => { logger.error("uncaught exception, exiting", toErrorMeta(error)); process.exit(1); }); process.on("unhandledRejection", (reason) => { logger.error("unhandled rejection, exiting", toErrorMeta(reason)); process.exit(1); }); setInterval(() => { const memory = process.memoryUsage(); logger.verbose("heartbeat", { uptimeSeconds: Math.round(process.uptime()), rssMb: Math.round(memory.rss / 1024 / 1024), heapUsedMb: Math.round(memory.heapUsed / 1024 / 1024), }); }, heartbeatIntervalMs).unref(); const server = app.listen(port, () => { logger.info("server started", { port }); }); let shuttingDown = false; function shutdown(signal: NodeJS.Signals): void { if (shuttingDown) return; shuttingDown = true; logger.info("shutting down", { signal }); // Falls ein Request haengt oder ein Socket nicht freigegeben wird, soll der // Prozess trotzdem enden, statt auf das SIGKILL des Containers zu warten. const forceExit = setTimeout(() => { logger.error("shutdown timed out, forcing exit"); process.exit(1); }, shutdownTimeoutMs); forceExit.unref(); server.close((error) => { if (error) logger.error("http server close failed", toErrorMeta(error)); try { closeDefaultDatabase(); } catch (closeError) { logger.error("database close failed", toErrorMeta(closeError)); } logger.info("shutdown complete"); process.exit(error ? 1 : 0); }); // Ohne das warten offene Keep-Alive-Verbindungen (z. B. die des // Next.js-Servers) den Timeout oben aus; laufende Requests laufen weiter. server.closeIdleConnections(); } process.on("SIGTERM", shutdown); process.on("SIGINT", shutdown);