feat: implement comprehensive startup system and fix authentication
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>
This commit is contained in:
parent
d41d1e8125
commit
e681c446b6
36 changed files with 7719 additions and 183 deletions
725
start-game.js
Normal file
725
start-game.js
Normal file
|
|
@ -0,0 +1,725 @@
|
|||
#!/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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue