Major improvements: - Created startup orchestration system with health monitoring and graceful shutdown - Fixed user registration and login with simplified authentication flow - Rebuilt authentication forms from scratch with direct API integration - Implemented comprehensive debugging and error handling - Added Redis fallback functionality for disabled environments - Fixed CORS configuration for cross-origin frontend requests - Simplified password validation to 6+ characters (removed complexity requirements) - Added toast notifications at app level for better UX feedback - Created comprehensive startup/shutdown scripts with OODA methodology - Fixed database validation and connection issues - Implemented TokenService memory fallback when Redis is disabled Technical details: - New SimpleLoginForm.tsx and SimpleRegisterForm.tsx components - Enhanced CORS middleware with additional allowed origins - Simplified auth validators and removed strict password requirements - Added extensive logging and diagnostic capabilities - Fixed authentication middleware token validation - Implemented graceful Redis error handling throughout the stack - Created modular startup system with configurable health checks 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
725 lines
No EOL
22 KiB
JavaScript
725 lines
No EOL
22 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Shattered Void MMO - Comprehensive Startup Orchestrator
|
|
*
|
|
* This script provides a complete startup solution for the Shattered Void MMO,
|
|
* handling all aspects of system initialization, validation, and monitoring.
|
|
*
|
|
* Features:
|
|
* - Pre-flight system checks
|
|
* - Database connectivity and migration validation
|
|
* - Redis connectivity with fallback handling
|
|
* - Backend and frontend server startup
|
|
* - Health monitoring and service validation
|
|
* - Graceful error handling and recovery
|
|
* - Performance metrics and logging
|
|
*/
|
|
|
|
const path = require('path');
|
|
const { spawn, exec } = require('child_process');
|
|
const fs = require('fs').promises;
|
|
const http = require('http');
|
|
const express = require('express');
|
|
|
|
// Load environment variables
|
|
require('dotenv').config();
|
|
|
|
// Import our custom modules
|
|
const StartupChecks = require('./scripts/startup-checks');
|
|
const HealthMonitor = require('./scripts/health-monitor');
|
|
const DatabaseValidator = require('./scripts/database-validator');
|
|
|
|
// Node.js version compatibility checking
|
|
function getNodeVersion() {
|
|
const version = process.version;
|
|
const match = version.match(/^v(\d+)\.(\d+)\.(\d+)/);
|
|
if (!match) {
|
|
throw new Error(`Unable to parse Node.js version: ${version}`);
|
|
}
|
|
return {
|
|
major: parseInt(match[1], 10),
|
|
minor: parseInt(match[2], 10),
|
|
patch: parseInt(match[3], 10),
|
|
full: version
|
|
};
|
|
}
|
|
|
|
function isViteCompatible() {
|
|
const nodeVersion = getNodeVersion();
|
|
// Vite 7.x requires Node.js 20+ for crypto.hash() support
|
|
return nodeVersion.major >= 20;
|
|
}
|
|
|
|
// Configuration
|
|
const config = {
|
|
backend: {
|
|
port: process.env.PORT || 3000,
|
|
host: process.env.HOST || '0.0.0.0',
|
|
script: 'src/server.js',
|
|
startupTimeout: 30000
|
|
},
|
|
frontend: {
|
|
port: process.env.FRONTEND_PORT || 5173,
|
|
host: process.env.FRONTEND_HOST || '0.0.0.0',
|
|
directory: './frontend',
|
|
buildDirectory: './frontend/dist',
|
|
startupTimeout: 20000
|
|
},
|
|
database: {
|
|
checkTimeout: 10000,
|
|
migrationTimeout: 30000
|
|
},
|
|
redis: {
|
|
checkTimeout: 5000,
|
|
optional: true
|
|
},
|
|
startup: {
|
|
mode: process.env.NODE_ENV || 'development',
|
|
enableFrontend: process.env.ENABLE_FRONTEND !== 'false',
|
|
enableHealthMonitoring: process.env.ENABLE_HEALTH_MONITORING !== 'false',
|
|
healthCheckInterval: 30000,
|
|
maxRetries: 3,
|
|
retryDelay: 2000,
|
|
frontendFallback: process.env.FRONTEND_FALLBACK !== 'false'
|
|
}
|
|
};
|
|
|
|
// Color codes for console output
|
|
const colors = {
|
|
reset: '\x1b[0m',
|
|
bright: '\x1b[1m',
|
|
red: '\x1b[31m',
|
|
green: '\x1b[32m',
|
|
yellow: '\x1b[33m',
|
|
blue: '\x1b[34m',
|
|
magenta: '\x1b[35m',
|
|
cyan: '\x1b[36m',
|
|
white: '\x1b[37m'
|
|
};
|
|
|
|
// Process tracking
|
|
const processes = {
|
|
backend: null,
|
|
frontend: null
|
|
};
|
|
|
|
// Startup state
|
|
const startupState = {
|
|
startTime: Date.now(),
|
|
phase: 'initialization',
|
|
services: {},
|
|
metrics: {}
|
|
};
|
|
|
|
/**
|
|
* Enhanced logging with colors and timestamps
|
|
*/
|
|
function log(level, message, data = null) {
|
|
const timestamp = new Date().toISOString();
|
|
const pid = process.pid;
|
|
|
|
let colorCode = colors.white;
|
|
let prefix = 'INFO';
|
|
|
|
switch (level) {
|
|
case 'error':
|
|
colorCode = colors.red;
|
|
prefix = 'ERROR';
|
|
break;
|
|
case 'warn':
|
|
colorCode = colors.yellow;
|
|
prefix = 'WARN';
|
|
break;
|
|
case 'success':
|
|
colorCode = colors.green;
|
|
prefix = 'SUCCESS';
|
|
break;
|
|
case 'info':
|
|
colorCode = colors.cyan;
|
|
prefix = 'INFO';
|
|
break;
|
|
case 'debug':
|
|
colorCode = colors.magenta;
|
|
prefix = 'DEBUG';
|
|
break;
|
|
}
|
|
|
|
const logMessage = `${colors.bright}[${timestamp}] [PID:${pid}] [${prefix}]${colors.reset} ${colorCode}${message}${colors.reset}`;
|
|
console.log(logMessage);
|
|
|
|
if (data) {
|
|
console.log(`${colors.blue}${JSON.stringify(data, null, 2)}${colors.reset}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display startup banner
|
|
*/
|
|
function displayBanner() {
|
|
const banner = `
|
|
${colors.cyan}╔═══════════════════════════════════════════════════════════════╗
|
|
║ ║
|
|
║ ${colors.bright}SHATTERED VOID MMO STARTUP${colors.reset}${colors.cyan} ║
|
|
║ ${colors.white}Post-Collapse Galaxy Strategy Game${colors.reset}${colors.cyan} ║
|
|
║ ║
|
|
║ ${colors.yellow}Mode:${colors.reset} ${colors.white}${config.startup.mode.toUpperCase()}${colors.reset}${colors.cyan} ║
|
|
║ ${colors.yellow}Backend:${colors.reset} ${colors.white}${config.backend.host}:${config.backend.port}${colors.reset}${colors.cyan} ║
|
|
║ ${colors.yellow}Frontend:${colors.reset} ${colors.white}${config.startup.enableFrontend ? `${config.frontend.host}:${config.frontend.port}` : 'Disabled'}${colors.reset}${colors.cyan} ║
|
|
║ ║
|
|
╚═══════════════════════════════════════════════════════════════╝${colors.reset}
|
|
`;
|
|
console.log(banner);
|
|
}
|
|
|
|
/**
|
|
* Update startup phase
|
|
*/
|
|
function updatePhase(phase, details = null) {
|
|
startupState.phase = phase;
|
|
log('info', `Starting phase: ${phase}`, details);
|
|
}
|
|
|
|
/**
|
|
* Measure execution time
|
|
*/
|
|
function measureTime(startTime) {
|
|
return Date.now() - startTime;
|
|
}
|
|
|
|
/**
|
|
* Check if a port is available
|
|
*/
|
|
function checkPort(port, host = 'localhost') {
|
|
return new Promise((resolve) => {
|
|
const server = require('net').createServer();
|
|
|
|
server.listen(port, host, () => {
|
|
server.once('close', () => resolve(true));
|
|
server.close();
|
|
});
|
|
|
|
server.on('error', () => resolve(false));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wait for a service to become available
|
|
*/
|
|
function waitForService(host, port, timeout = 10000, retries = 10) {
|
|
return new Promise((resolve, reject) => {
|
|
let attempts = 0;
|
|
const interval = timeout / retries;
|
|
|
|
const check = () => {
|
|
attempts++;
|
|
|
|
const req = http.request({
|
|
hostname: host,
|
|
port: port,
|
|
path: '/health',
|
|
method: 'GET',
|
|
timeout: 2000
|
|
}, (res) => {
|
|
if (res.statusCode === 200) {
|
|
resolve(true);
|
|
} else if (attempts < retries) {
|
|
setTimeout(check, interval);
|
|
} else {
|
|
reject(new Error(`Service not ready after ${attempts} attempts`));
|
|
}
|
|
});
|
|
|
|
req.on('error', () => {
|
|
if (attempts < retries) {
|
|
setTimeout(check, interval);
|
|
} else {
|
|
reject(new Error(`Service not reachable after ${attempts} attempts`));
|
|
}
|
|
});
|
|
|
|
req.end();
|
|
};
|
|
|
|
check();
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Spawn a process with enhanced monitoring
|
|
*/
|
|
function spawnProcess(command, args, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
...options
|
|
});
|
|
|
|
child.stdout.on('data', (data) => {
|
|
const output = data.toString().trim();
|
|
if (output) {
|
|
log('debug', `[${command}] ${output}`);
|
|
}
|
|
});
|
|
|
|
child.stderr.on('data', (data) => {
|
|
const output = data.toString().trim();
|
|
if (output && !output.includes('ExperimentalWarning')) {
|
|
log('warn', `[${command}] ${output}`);
|
|
}
|
|
});
|
|
|
|
child.on('error', (error) => {
|
|
log('error', `Process error for ${command}:`, error);
|
|
reject(error);
|
|
});
|
|
|
|
child.on('exit', (code, signal) => {
|
|
if (code !== 0 && signal !== 'SIGTERM') {
|
|
const error = new Error(`Process ${command} exited with code ${code}`);
|
|
log('error', error.message);
|
|
reject(error);
|
|
}
|
|
});
|
|
|
|
// Consider the process started if it doesn't exit within a second
|
|
setTimeout(() => {
|
|
if (!child.killed) {
|
|
resolve(child);
|
|
}
|
|
}, 1000);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Pre-flight system checks
|
|
*/
|
|
async function runPreflightChecks() {
|
|
updatePhase('Pre-flight Checks');
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
const checks = new StartupChecks();
|
|
const results = await checks.runAllChecks();
|
|
|
|
const duration = measureTime(startTime);
|
|
startupState.metrics.preflightDuration = duration;
|
|
|
|
if (results.success) {
|
|
log('success', `Pre-flight checks completed in ${duration}ms`);
|
|
startupState.services.preflight = { status: 'healthy', checks: results.checks };
|
|
} else {
|
|
log('error', 'Pre-flight checks failed:', results.failures);
|
|
throw new Error('Pre-flight validation failed');
|
|
}
|
|
} catch (error) {
|
|
log('error', 'Pre-flight checks error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validate database connectivity and run migrations
|
|
*/
|
|
async function validateDatabase() {
|
|
updatePhase('Database Validation');
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
const validator = new DatabaseValidator();
|
|
const results = await validator.validateDatabase();
|
|
|
|
const duration = measureTime(startTime);
|
|
startupState.metrics.databaseDuration = duration;
|
|
|
|
if (results.success) {
|
|
log('success', `Database validation completed in ${duration}ms`);
|
|
startupState.services.database = { status: 'healthy', ...results };
|
|
} else {
|
|
// Detailed error logging for database validation failures
|
|
const errorDetails = {
|
|
general: results.error,
|
|
connectivity: results.connectivity?.error || null,
|
|
migrations: results.migrations?.error || null,
|
|
schema: results.schema?.error || null,
|
|
missingTables: results.schema?.missingTables || [],
|
|
seeds: results.seeds?.error || null,
|
|
integrity: results.integrity?.error || null
|
|
};
|
|
|
|
log("error", "Database validation failed:", errorDetails);
|
|
|
|
if (results.schema && !results.schema.success) {
|
|
log("error", `Schema validation failed - Missing tables: ${results.schema.missingTables.join(", ")}`);
|
|
log("info", `Current coverage: ${results.schema.coverage}`);
|
|
if (results.schema.troubleshooting) {
|
|
log("info", "Troubleshooting suggestions:");
|
|
results.schema.troubleshooting.forEach(tip => log("info", ` - ${tip}`));
|
|
}
|
|
}
|
|
|
|
throw new Error(`Database validation failed: ${JSON.stringify(errorDetails, null, 2)}`);
|
|
}
|
|
} catch (error) {
|
|
log('error', 'Database validation error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start the backend server
|
|
*/
|
|
async function startBackendServer() {
|
|
updatePhase('Backend Server Startup');
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
// Check if port is available
|
|
const portAvailable = await checkPort(config.backend.port, config.backend.host);
|
|
if (!portAvailable) {
|
|
throw new Error(`Backend port ${config.backend.port} is already in use`);
|
|
}
|
|
|
|
// Start the backend process
|
|
log('info', `Starting backend server on ${config.backend.host}:${config.backend.port}`);
|
|
const backendProcess = await spawnProcess('node', [config.backend.script], {
|
|
env: { ...process.env, NODE_ENV: config.startup.mode }
|
|
});
|
|
|
|
processes.backend = backendProcess;
|
|
|
|
// Wait for the server to be ready
|
|
await waitForService(config.backend.host, config.backend.port, config.backend.startupTimeout);
|
|
|
|
const duration = measureTime(startTime);
|
|
startupState.metrics.backendDuration = duration;
|
|
|
|
log('success', `Backend server started in ${duration}ms`);
|
|
startupState.services.backend = {
|
|
status: 'healthy',
|
|
port: config.backend.port,
|
|
pid: backendProcess.pid
|
|
};
|
|
} catch (error) {
|
|
log('error', 'Backend server startup failed:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Serve built frontend using Express static server
|
|
*/
|
|
async function serveBuildFrontend() {
|
|
log('info', 'Starting built frontend static server...');
|
|
|
|
try {
|
|
// Check if built frontend exists
|
|
await fs.access(config.frontend.buildDirectory);
|
|
|
|
// Create Express app for serving static files
|
|
const app = express();
|
|
|
|
// Serve static files from build directory
|
|
app.use(express.static(config.frontend.buildDirectory));
|
|
|
|
// Handle SPA routing - serve index.html for all non-file requests
|
|
app.get('*', (req, res) => {
|
|
res.sendFile(path.join(process.cwd(), config.frontend.buildDirectory, 'index.html'));
|
|
});
|
|
|
|
// Start the static server
|
|
const server = app.listen(config.frontend.port, config.frontend.host, () => {
|
|
log('success', `Built frontend served on ${config.frontend.host}:${config.frontend.port}`);
|
|
});
|
|
|
|
// Store server reference for cleanup
|
|
processes.frontend = {
|
|
kill: (signal) => {
|
|
server.close();
|
|
},
|
|
pid: process.pid
|
|
};
|
|
|
|
return server;
|
|
} catch (error) {
|
|
log('error', 'Failed to serve built frontend:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build and start the frontend server
|
|
*/
|
|
async function startFrontendServer() {
|
|
if (!config.startup.enableFrontend) {
|
|
log('info', 'Frontend disabled by configuration');
|
|
return;
|
|
}
|
|
|
|
updatePhase('Frontend Server Startup');
|
|
const startTime = Date.now();
|
|
|
|
// Check Node.js version compatibility with Vite
|
|
const nodeVersion = getNodeVersion();
|
|
const viteCompatible = isViteCompatible();
|
|
|
|
log('info', `Node.js version: ${nodeVersion.full}`);
|
|
|
|
if (!viteCompatible) {
|
|
log('warn', `Node.js ${nodeVersion.full} is not compatible with Vite 7.x (requires Node.js 20+)`);
|
|
log('warn', 'crypto.hash() function is not available in this Node.js version');
|
|
|
|
if (config.startup.frontendFallback) {
|
|
log('info', 'Attempting to serve built frontend as fallback...');
|
|
try {
|
|
await serveBuildFrontend();
|
|
|
|
const duration = measureTime(startTime);
|
|
startupState.metrics.frontendDuration = duration;
|
|
|
|
log('success', `Built frontend fallback started in ${duration}ms`);
|
|
startupState.services.frontend = {
|
|
status: 'healthy',
|
|
port: config.frontend.port,
|
|
mode: 'static',
|
|
nodeCompatibility: 'fallback'
|
|
};
|
|
return;
|
|
} catch (fallbackError) {
|
|
log('error', 'Frontend fallback also failed:', fallbackError);
|
|
throw new Error(`Both Vite dev server and static fallback failed: ${fallbackError.message}`);
|
|
}
|
|
} else {
|
|
throw new Error(`Node.js ${nodeVersion.full} is incompatible with Vite 7.x. Upgrade to Node.js 20+ or enable fallback mode.`);
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Check if frontend directory exists
|
|
await fs.access(config.frontend.directory);
|
|
|
|
// Check if port is available
|
|
const portAvailable = await checkPort(config.frontend.port, config.frontend.host);
|
|
if (!portAvailable) {
|
|
throw new Error(`Frontend port ${config.frontend.port} is already in use`);
|
|
}
|
|
|
|
log('info', `Starting Vite development server on ${config.frontend.host}:${config.frontend.port}`);
|
|
|
|
// Start the frontend development server
|
|
const frontendProcess = await spawnProcess('npm', ['run', 'dev'], {
|
|
cwd: config.frontend.directory,
|
|
env: {
|
|
...process.env,
|
|
PORT: config.frontend.port,
|
|
HOST: config.frontend.host
|
|
}
|
|
});
|
|
|
|
processes.frontend = frontendProcess;
|
|
|
|
// Wait for the server to be ready
|
|
await waitForService(config.frontend.host, config.frontend.port, config.frontend.startupTimeout);
|
|
|
|
const duration = measureTime(startTime);
|
|
startupState.metrics.frontendDuration = duration;
|
|
|
|
log('success', `Vite development server started in ${duration}ms`);
|
|
startupState.services.frontend = {
|
|
status: 'healthy',
|
|
port: config.frontend.port,
|
|
pid: frontendProcess.pid,
|
|
mode: 'development',
|
|
nodeCompatibility: 'compatible'
|
|
};
|
|
} catch (error) {
|
|
log('error', 'Vite development server startup failed:', error);
|
|
|
|
// Try fallback to built frontend if enabled and we haven't tried it yet
|
|
if (config.startup.frontendFallback && viteCompatible) {
|
|
log('warn', 'Attempting to serve built frontend as fallback...');
|
|
try {
|
|
await serveBuildFrontend();
|
|
|
|
const duration = measureTime(startTime);
|
|
startupState.metrics.frontendDuration = duration;
|
|
|
|
log('success', `Built frontend fallback started in ${duration}ms`);
|
|
startupState.services.frontend = {
|
|
status: 'healthy',
|
|
port: config.frontend.port,
|
|
mode: 'static',
|
|
nodeCompatibility: 'fallback'
|
|
};
|
|
return;
|
|
} catch (fallbackError) {
|
|
log('error', 'Frontend fallback also failed:', fallbackError);
|
|
}
|
|
}
|
|
|
|
// Frontend failure is not critical if we're running in production mode
|
|
if (config.startup.mode === 'production') {
|
|
log('warn', 'Continuing without frontend in production mode');
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Start health monitoring
|
|
*/
|
|
async function startHealthMonitoring() {
|
|
if (!config.startup.enableHealthMonitoring) {
|
|
log('info', 'Health monitoring disabled by configuration');
|
|
return;
|
|
}
|
|
|
|
updatePhase('Health Monitoring Initialization');
|
|
|
|
try {
|
|
const monitor = new HealthMonitor({
|
|
services: startupState.services,
|
|
interval: config.startup.healthCheckInterval,
|
|
onHealthChange: (service, status) => {
|
|
log(status === 'healthy' ? 'success' : 'error',
|
|
`Service ${service} status: ${status}`);
|
|
}
|
|
});
|
|
|
|
await monitor.start();
|
|
|
|
log('success', 'Health monitoring started');
|
|
startupState.services.healthMonitor = { status: 'healthy' };
|
|
} catch (error) {
|
|
log('error', 'Health monitoring startup failed:', error);
|
|
// Health monitoring failure is not critical
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display startup summary
|
|
*/
|
|
function displayStartupSummary() {
|
|
const totalDuration = measureTime(startupState.startTime);
|
|
|
|
log('success', `🚀 Shattered Void MMO startup completed in ${totalDuration}ms`);
|
|
|
|
const summary = `
|
|
${colors.green}╔═══════════════════════════════════════════════════════════════╗
|
|
║ STARTUP SUMMARY ║
|
|
╠═══════════════════════════════════════════════════════════════╣${colors.reset}
|
|
${colors.white}║ Total Duration: ${totalDuration}ms${' '.repeat(47 - totalDuration.toString().length)}║
|
|
║ ║${colors.reset}
|
|
${colors.cyan}║ Services Status: ║${colors.reset}`;
|
|
|
|
console.log(summary);
|
|
|
|
Object.entries(startupState.services).forEach(([service, info]) => {
|
|
const status = info.status === 'healthy' ? '✅' : '❌';
|
|
const serviceName = service.charAt(0).toUpperCase() + service.slice(1);
|
|
const port = info.port ? `:${info.port}` : '';
|
|
let extraInfo = '';
|
|
|
|
// Add extra info for frontend service
|
|
if (service === 'frontend' && info.mode) {
|
|
extraInfo = ` (${info.mode})`;
|
|
}
|
|
|
|
const totalLength = serviceName.length + port.length + extraInfo.length;
|
|
const line = `${colors.white}║ ${status} ${serviceName}${port}${extraInfo}${' '.repeat(55 - totalLength)}║${colors.reset}`;
|
|
console.log(line);
|
|
});
|
|
|
|
console.log(`${colors.green}║ ║
|
|
╚═══════════════════════════════════════════════════════════════╝${colors.reset}`);
|
|
|
|
if (config.startup.enableFrontend && startupState.services.frontend) {
|
|
log('info', `🌐 Game URL: http://${config.frontend.host}:${config.frontend.port}`);
|
|
}
|
|
|
|
log('info', `📊 API URL: http://${config.backend.host}:${config.backend.port}`);
|
|
log('info', `📋 Press Ctrl+C to stop all services`);
|
|
}
|
|
|
|
/**
|
|
* Graceful shutdown handler
|
|
*/
|
|
function setupGracefulShutdown() {
|
|
const shutdown = async (signal) => {
|
|
log('warn', `Received ${signal}. Starting graceful shutdown...`);
|
|
|
|
try {
|
|
// Stop processes
|
|
if (processes.frontend) {
|
|
log('info', 'Stopping frontend server...');
|
|
processes.frontend.kill('SIGTERM');
|
|
}
|
|
|
|
if (processes.backend) {
|
|
log('info', 'Stopping backend server...');
|
|
processes.backend.kill('SIGTERM');
|
|
}
|
|
|
|
// Wait a moment for graceful shutdown
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
log('success', 'All services stopped successfully');
|
|
process.exit(0);
|
|
} catch (error) {
|
|
log('error', 'Error during shutdown:', error);
|
|
process.exit(1);
|
|
}
|
|
};
|
|
|
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
|
|
process.on('unhandledRejection', (reason, promise) => {
|
|
log('error', 'Unhandled Promise Rejection:', { reason, promise: promise.toString() });
|
|
});
|
|
|
|
process.on('uncaughtException', (error) => {
|
|
log('error', 'Uncaught Exception:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Main startup function
|
|
*/
|
|
async function startGame() {
|
|
try {
|
|
displayBanner();
|
|
setupGracefulShutdown();
|
|
|
|
// Run startup sequence
|
|
await runPreflightChecks();
|
|
await validateDatabase();
|
|
await startBackendServer();
|
|
await startFrontendServer();
|
|
await startHealthMonitoring();
|
|
|
|
displayStartupSummary();
|
|
|
|
} catch (error) {
|
|
log('error', '💥 Startup failed:', error);
|
|
|
|
// Cleanup any started processes
|
|
if (processes.backend) processes.backend.kill('SIGTERM');
|
|
if (processes.frontend) processes.frontend.kill('SIGTERM');
|
|
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
// Start the game if this file is run directly
|
|
if (require.main === module) {
|
|
startGame();
|
|
}
|
|
|
|
module.exports = {
|
|
startGame,
|
|
config,
|
|
startupState
|
|
}; |