Bundles the prior autonomous-session output that was sitting unstaged: - Em-dash sweep across src/ + tests/ (en-dash/em-dash to hyphen, ~2280 instances) - country-flag-icons rollout (CountryFlag component, replaces emoji glyphs that never rendered on Windows; lazy-loads the 3x2 SVG index as a single chunk after the per-subpath dynamic-import approach silently failed in webpack) - Admin IA Phase 1+2: 7-domain regroup, 41 to 38 pages, /admin/berths index, redirects (ocr to ai, reports to dashboard, invitations to users), docs/admin-ia-proposal.md - Per-template email tester (registry + endpoint + UI on Email admin page) - Cancel-document mode picker (delete-from-Documenso vs keep-for-audit) - Dashboard PDF report: 25 widgets, SVG charts, date-range picker, 11 resolvers - Customize-widgets per-region sortables at xl+ (charts/rails/feed); single flat sortable below xl when the layout stacks; per-viewport saved orders - Audit doc updates capturing each shipped item - Lint fixes: react-compiler immutability in DonutChart (reduce instead of let-reassign), set-state-in-effect disables in CountryFlag and UploadForSigning preview-bytes effect, unused 'confirm' destructures in interest contract + reservation tabs, unescaped apostrophe in test-template card copy
121 lines
4.3 KiB
TypeScript
121 lines
4.3 KiB
TypeScript
/**
|
|
* Custom server entry point.
|
|
*
|
|
* Boots Next.js, attaches Socket.io to the same HTTP server, and in
|
|
* development mode also registers BullMQ recurring jobs and starts the
|
|
* workers inline (so you don't need the separate crm-worker container).
|
|
*
|
|
* In production the workers run in the crm-worker container (Dockerfile.worker
|
|
* → dist/worker.js) and this file only handles Next.js + Socket.io.
|
|
*/
|
|
|
|
import { createServer, type Server as HttpServer } from 'node:http';
|
|
|
|
import next from 'next';
|
|
|
|
import { initSocketServer, closeSocketServer } from '@/lib/socket/server';
|
|
import { logger } from '@/lib/logger';
|
|
import { env } from '@/lib/env';
|
|
import { redis } from '@/lib/redis';
|
|
|
|
const dev = env.NODE_ENV !== 'production';
|
|
|
|
async function gracefulShutdown(signal: string, httpServer: HttpServer): Promise<void> {
|
|
logger.info({ signal }, 'Shutdown signal received; closing connections');
|
|
|
|
// Order matters: close Socket.io first so it stops accepting new
|
|
// sockets and emits disconnect events while the HTTP server is still
|
|
// up to flush them. `httpServer.close` only stops new connections;
|
|
// it waits for keep-alive HTTP and long-poll websockets to drain on
|
|
// their own, so without an explicit io.close() upfront the polls hold
|
|
// the server past the compose stop_grace_period and the process gets
|
|
// SIGKILL'd mid-frame.
|
|
await closeSocketServer().catch((err) => logger.warn({ err }, 'closeSocketServer error'));
|
|
|
|
// Then drain the HTTP layer.
|
|
await new Promise<void>((resolve) => {
|
|
httpServer.close((err) => {
|
|
if (err) logger.warn({ err }, 'httpServer.close emitted error');
|
|
resolve();
|
|
});
|
|
// Hard timeout - 25s leaves headroom under a 30s compose grace
|
|
// period before SIGKILL would arrive anyway.
|
|
setTimeout(() => resolve(), 25_000).unref();
|
|
});
|
|
|
|
try {
|
|
redis.disconnect();
|
|
} catch (err) {
|
|
logger.warn({ err }, 'redis.disconnect error');
|
|
}
|
|
|
|
logger.info({ signal }, 'Shutdown complete');
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const app = next({ dev, port: env.PORT });
|
|
const handle = app.getRequestHandler();
|
|
|
|
await app.prepare();
|
|
|
|
const httpServer = createServer((req, res) => {
|
|
handle(req, res);
|
|
});
|
|
|
|
// Attach Socket.io to the HTTP server (uses Redis adapter for pub/sub)
|
|
initSocketServer(httpServer);
|
|
logger.info('Socket.io initialized');
|
|
|
|
// In development, run BullMQ workers inline so a single `pnpm dev` is enough
|
|
if (dev) {
|
|
const { registerRecurringJobs } = await import('@/lib/queue/scheduler');
|
|
const { emailWorker } = await import('@/lib/queue/workers/email');
|
|
const { documentsWorker } = await import('@/lib/queue/workers/documents');
|
|
const { notificationsWorker } = await import('@/lib/queue/workers/notifications');
|
|
const { importWorker } = await import('@/lib/queue/workers/import');
|
|
const { exportWorker } = await import('@/lib/queue/workers/export');
|
|
const { aiWorker } = await import('@/lib/queue/workers/ai');
|
|
const { bulkWorker } = await import('@/lib/queue/workers/bulk');
|
|
const { maintenanceWorker } = await import('@/lib/queue/workers/maintenance');
|
|
const { reportsWorker } = await import('@/lib/queue/workers/reports');
|
|
const { webhooksWorker } = await import('@/lib/queue/workers/webhooks');
|
|
|
|
await registerRecurringJobs();
|
|
logger.info('BullMQ recurring jobs registered (dev mode)');
|
|
|
|
// Keep a reference so workers aren't GC'd
|
|
void [
|
|
emailWorker,
|
|
documentsWorker,
|
|
notificationsWorker,
|
|
importWorker,
|
|
exportWorker,
|
|
aiWorker,
|
|
bulkWorker,
|
|
maintenanceWorker,
|
|
reportsWorker,
|
|
webhooksWorker,
|
|
];
|
|
}
|
|
|
|
httpServer.listen(env.PORT, () => {
|
|
logger.info({ port: env.PORT, env: env.NODE_ENV }, 'CRM server listening');
|
|
});
|
|
|
|
// Graceful stop on container restart / deploy. Without this, every
|
|
// `docker compose up -d` rolling restart drops in-flight uploads, EOI
|
|
// generation, Documenso requests, and Socket.io frames mid-statement.
|
|
// Match docker-compose `stop_grace_period: 30s` (or longer) so the
|
|
// 25s drain inside gracefulShutdown can complete before SIGKILL.
|
|
for (const sig of ['SIGTERM', 'SIGINT'] as const) {
|
|
process.once(sig, () => {
|
|
void gracefulShutdown(sig, httpServer).finally(() => process.exit(0));
|
|
});
|
|
}
|
|
}
|
|
|
|
main().catch((err) => {
|
|
logger.error(err, 'Failed to start server');
|
|
process.exit(1);
|
|
});
|