Shatteredvoid/start-game.js
MegaProxy e681c446b6 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>
2025-08-03 12:53:25 +00:00

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
};