| |
| |
| |
| |
| |
|
|
| import Fastify, { type FastifyInstance } from "fastify"; |
| import fastifyStatic from "@fastify/static"; |
| import fastifyMultipart from "@fastify/multipart"; |
| import { fileURLToPath } from "node:url"; |
| import { dirname, resolve } from "node:path"; |
| import { existsSync, mkdirSync } from "node:fs"; |
|
|
| import { loadConfig } from "./config.js"; |
| import { createLogger } from "./log.js"; |
| import { createStore } from "./queue/store-factory.js"; |
| import { registerHealthRoutes } from "./routes/health.js"; |
| import { registerAnalysisRoutes } from "./routes/analysis.js"; |
| import { registerPolishRoutes } from "./routes/polish.js"; |
| import { QueueRunner } from "./queue/runner.js"; |
|
|
| const __filename = fileURLToPath(import.meta.url); |
| const __dirname = dirname(__filename); |
|
|
| |
| |
| |
| |
| function resolveWebLegacyRoot(): string { |
| const webRoot = resolve(__dirname, "../../web-legacy/dist"); |
| if (!existsSync(resolve(webRoot, "index.html"))) { |
| throw new Error("εη«―ιζδΊ§η©δΈεε¨οΌθ―·ε
θΏθ‘ pnpm --filter @task-optimizer/web-legacy build ζ pnpm build"); |
| } |
| return webRoot; |
| } |
|
|
| async function buildServer(): Promise<{ app: FastifyInstance; runner: QueueRunner }> { |
| const config = loadConfig(); |
| const logger = createLogger(config.logLevel); |
|
|
| |
| for (const dir of [config.uploadDir, config.exportDir]) { |
| try { |
| mkdirSync(dir, { recursive: true }); |
| } catch (err) { |
| logger.error({ err, dir }, "Failed to create storage directory"); |
| throw err; |
| } |
| } |
|
|
| const store = await createStore(config); |
| const runner = new QueueRunner({ store, config, logger }); |
|
|
| |
| |
| for (const job of await store.listResumableJobs()) { |
| logger.info({ jobId: job.id }, "Recovering resumable job from previous run"); |
| await store.recoverInterruptedJob(job.id); |
| runner.scheduleJob(job.id); |
| } |
|
|
| const app = Fastify({ |
| |
| |
| loggerInstance: logger, |
| bodyLimit: config.maxUploadMB * 1024 * 1024, |
| disableRequestLogging: false, |
| trustProxy: true, |
| }); |
|
|
| await app.register(fastifyMultipart, { |
| limits: { |
| fileSize: config.maxUploadMB * 1024 * 1024, |
| files: config.maxBatchFiles, |
| }, |
| }); |
|
|
| |
| await registerHealthRoutes(app); |
| await registerPolishRoutes(app, { config }); |
| await registerAnalysisRoutes(app, { store, config, runner }); |
| app.addHook("onClose", async () => { |
| await store.close(); |
| }); |
|
|
| |
| const webRoot = resolveWebLegacyRoot(); |
| await app.register(fastifyStatic, { |
| root: webRoot, |
| prefix: "/", |
| decorateReply: false, |
| }); |
|
|
| |
| |
| app.setNotFoundHandler((req, reply) => { |
| if (req.raw.url && req.raw.url.startsWith("/api/")) { |
| reply.code(404).send({ code: 404, msg: "Not Found" }); |
| return; |
| } |
| reply.sendFile("index.html", webRoot); |
| }); |
|
|
| return { app, runner }; |
| } |
|
|
| async function main() { |
| const { app, runner } = await buildServer(); |
| const config = loadConfig(); |
|
|
| const shutdown = async (signal: string) => { |
| app.log.info({ signal }, "Shutdown signal received, closing server"); |
| runner.requestShutdown(); |
| try { |
| await app.close(); |
| } catch (err) { |
| app.log.error({ err }, "Error closing Fastify"); |
| } |
| process.exit(0); |
| }; |
| process.once("SIGINT", () => void shutdown("SIGINT")); |
| process.once("SIGTERM", () => void shutdown("SIGTERM")); |
|
|
| try { |
| await app.listen({ port: config.port, host: "0.0.0.0" }); |
| app.log.info({ port: config.port }, "Server listening"); |
| } catch (err) { |
| app.log.error({ err }, "Server failed to start"); |
| process.exit(1); |
| } |
| } |
|
|
| main().catch((err) => { |
| console.error("Fatal startup error:", err); |
| process.exit(1); |
| }); |
|
|