import http from 'http'; import net from 'net'; import dotenv from 'dotenv'; import path from 'path'; // Load environment variables from .env file FIRST dotenv.config({ path: path.resolve(__dirname, '../.env') }); // Stop queue metrics collection on shutdown // Note: This is imported statically but doesn't trigger database/queue activity until called import { stopQueueMetrics } from './utils/queueMetrics'; const PORT: number = parseInt(process.env.PORT || '5000', 10); const isDev = process.env.NODE_ENV !== 'production'; const MAX_PORT_ATTEMPTS = isDev ? 6 : 1; // In dev try PORT..PORT+5 if in use /** * Check if a port is free (no one listening). */ function isPortFree(port: number): Promise { return new Promise((resolve) => { const socket = new net.Socket(); const onError = () => { socket.destroy(); resolve(true); // Error (e.g. ECONNREFUSED) means nothing listening → port free }; socket.setTimeout(200); socket.once('error', onError); socket.once('timeout', () => { socket.destroy(); resolve(true); }); socket.connect(port, '127.0.0.1', () => { socket.destroy(); resolve(false); // Connected → something is listening → port in use }); }); } /** * Find first free port in [startPort, startPort + maxAttempts). */ async function findFreePort(startPort: number, maxAttempts: number): Promise { for (let i = 0; i < maxAttempts; i++) { const port = startPort + i; if (await isPortFree(port)) return port; } return startPort; // Fallback to original; listen will then fail with EADDRINUSE } // Start server const startServer = async (): Promise => { try { // Initialize Google Secret Manager before starting server // This will merge secrets from GCS into process.env if enabled const { initializeGoogleSecretManager } = require('./services/googleSecretManager.service'); console.log('🔐 Initializing secrets...'); await initializeGoogleSecretManager(); const { default: app, initializeAppDatabase } = require('./app'); const { initSocket } = require('./realtime/socket'); // Initialize database connection explicitly after secrets are loaded await initializeAppDatabase(); require('./queues/tatWorker'); // Initialize TAT worker const { logTatConfig } = require('./config/tat.config'); const { logSystemConfig } = require('./config/system.config'); const { initializeHolidaysCache } = require('./utils/tatTimeUtils'); const { seedDefaultConfigurations } = require('./services/configSeed.service'); const { startPauseResumeJob } = require('./jobs/pauseResumeJob'); require('./queues/pauseResumeWorker'); // Initialize pause resume worker const { initializeQueueMetrics } = require('./utils/queueMetrics'); const { emailService } = require('./services/email.service'); // Initialize email service after secrets are loaded try { await emailService.initialize(); console.log('📧 Email service initialized'); } catch (error) { console.warn('⚠️ Email service initialization warning (will use test account if SMTP not configured):', error); } const server = http.createServer(app); initSocket(server); // Seed default configurations if table is empty try { await seedDefaultConfigurations(); } catch (error) { console.error('⚠️ Configuration seeding error:', error); } // Seed default activity types if table is empty const { seedDefaultActivityTypes } = require('./services/activityTypeSeed.service'); try { await seedDefaultActivityTypes(); } catch (error) { console.error('⚠️ Activity type seeding error:', error); } // Ensure demo admin user exists (admin@example.com / Admin@123) const { ensureDemoAdminUser } = require('./scripts/seed-admin-user'); try { await ensureDemoAdminUser(); } catch (error) { console.warn('⚠️ Demo admin user setup warning:', error); } // Initialize holidays cache for TAT calculations try { await initializeHolidaysCache(); } catch (error) { // Silently fall back to weekends-only TAT calculation } // Start scheduled jobs startPauseResumeJob(); const { startForm16NotificationJobs } = require('./jobs/form16NotificationJob'); startForm16NotificationJobs(); const { startForm16ArchiveJob } = require('./services/form16Archive.service'); startForm16ArchiveJob(); // Initialize queue metrics collection for Prometheus initializeQueueMetrics(); // In development, if default port is in use (e.g. previous run or another app), try next ports let portToUse = PORT; if (isDev) { const freePort = await findFreePort(PORT, MAX_PORT_ATTEMPTS); if (freePort !== PORT) { console.warn(`⚠️ Port ${PORT} is in use. Using port ${freePort} instead.`); console.warn(` Update frontend .env VITE_API_BASE_URL to http://localhost:${freePort}/api/v1 if needed.`); portToUse = freePort; } } server.once('error', (err: NodeJS.ErrnoException) => { if (err.code === 'EADDRINUSE') { console.error(''); console.error('❌ Port ' + portToUse + ' is already in use.'); console.error(' Another process (often a previous backend instance) is using it.'); console.error(' Windows: netstat -ano | findstr :' + portToUse); console.error(' Then: taskkill /PID /F'); console.error(' Or run in a single terminal and avoid starting backend twice.'); console.error(''); } else { console.error('❌ Server error:', err); } process.exit(1); }); server.listen(portToUse, () => { console.log(`🚀 Server running on port ${portToUse} | ${process.env.NODE_ENV || 'development'}`); console.log(` API base: http://localhost:${portToUse}/api/v1 (ensure frontend uses this and CORS allows your origin)`); }); } catch (error) { console.error('❌ Unable to start server:', error); process.exit(1); } }; // Graceful shutdown process.on('SIGTERM', () => { console.log('🛑 SIGTERM signal received: closing HTTP server'); stopQueueMetrics(); process.exit(0); }); process.on('SIGINT', () => { console.log('🛑 SIGINT signal received: closing HTTP server'); stopQueueMetrics(); process.exit(0); }); startServer();