forked from jappel/leistungsbilanz-ts
- Seitenansicht (/projects/:projectId/print): tabular cover page, board overview with supply structure and power sums, one plan per board with a repeating title block; PDF via the browser print dialog with named @page rules (A4 portrait cover, A4/A3 landscape plans, page x of y). - Distribution-board info card (board number, location, fed-from, remark) in the board edit dialog, persisted through the additively extended distribution-board.update command (migration 0008, snapshot schema 6, legacy subtree commands normalized). - Global document logo upload (PNG/JPEG/SVG, max 2 MB) outside project revisions and snapshots. - fed-from stays free text; the overview hierarchy and cumulated sums are preliminary until the feed link is modeled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
114 lines
3.5 KiB
TypeScript
114 lines
3.5 KiB
TypeScript
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);
|