feat: implement complete Phase 2 frontend foundation with React 18
Major milestone: Frontend implementation complete for Shattered Void MMO FRONTEND IMPLEMENTATION: - React 18 + TypeScript + Vite development environment - Tailwind CSS with custom dark theme for sci-fi aesthetic - Zustand state management with authentication persistence - Socket.io WebSocket client with auto-reconnection - Protected routing with authentication guards - Responsive design with mobile-first approach AUTHENTICATION SYSTEM: - Login/register forms with comprehensive validation - JWT token management with localStorage persistence - Password strength validation and user feedback - Protected routes and authentication guards CORE GAME INTERFACE: - Colony management dashboard with real-time updates - Resource display with live production tracking - WebSocket integration for real-time game events - Navigation with connection status indicator - Toast notifications for user feedback BACKEND ENHANCEMENTS: - Complete Research System with technology tree (23 technologies) - Fleet Management System with ship designs and movement - Enhanced Authentication with email verification and password reset - Complete game tick integration for all systems - Advanced WebSocket events for real-time updates ARCHITECTURE FEATURES: - Type-safe TypeScript throughout - Component-based architecture with reusable UI elements - API client with request/response interceptors - Error handling and loading states - Performance optimized builds with code splitting Phase 2 Status: Frontend foundation complete (Week 1-2 objectives met) Ready for: Colony management, fleet operations, research interface Next: Enhanced gameplay features and admin interface 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8d9ef427be
commit
d41d1e8125
130 changed files with 33588 additions and 14817 deletions
|
|
@ -8,18 +8,18 @@ const logger = require('../utils/logger');
|
|||
|
||||
// Configuration
|
||||
const WEBSOCKET_CONFIG = {
|
||||
cors: {
|
||||
origin: process.env.WEBSOCKET_CORS_ORIGIN?.split(',') || ['http://localhost:3000', 'http://localhost:3001'],
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true
|
||||
},
|
||||
pingTimeout: parseInt(process.env.WEBSOCKET_PING_TIMEOUT) || 20000,
|
||||
pingInterval: parseInt(process.env.WEBSOCKET_PING_INTERVAL) || 25000,
|
||||
maxHttpBufferSize: parseInt(process.env.WEBSOCKET_MAX_BUFFER_SIZE) || 1e6, // 1MB
|
||||
transports: ['websocket', 'polling'],
|
||||
allowEIO3: true,
|
||||
compression: true,
|
||||
httpCompression: true
|
||||
cors: {
|
||||
origin: process.env.WEBSOCKET_CORS_ORIGIN?.split(',') || ['http://localhost:3000', 'http://localhost:3001'],
|
||||
methods: ['GET', 'POST'],
|
||||
credentials: true,
|
||||
},
|
||||
pingTimeout: parseInt(process.env.WEBSOCKET_PING_TIMEOUT) || 20000,
|
||||
pingInterval: parseInt(process.env.WEBSOCKET_PING_INTERVAL) || 25000,
|
||||
maxHttpBufferSize: parseInt(process.env.WEBSOCKET_MAX_BUFFER_SIZE) || 1e6, // 1MB
|
||||
transports: ['websocket', 'polling'],
|
||||
allowEIO3: true,
|
||||
compression: true,
|
||||
httpCompression: true,
|
||||
};
|
||||
|
||||
let io = null;
|
||||
|
|
@ -32,99 +32,99 @@ const connectedClients = new Map();
|
|||
* @returns {Promise<Object>} Socket.IO server instance
|
||||
*/
|
||||
async function initializeWebSocket(server) {
|
||||
try {
|
||||
if (io) {
|
||||
logger.info('WebSocket server already initialized');
|
||||
return io;
|
||||
}
|
||||
|
||||
// Create Socket.IO server
|
||||
io = new Server(server, WEBSOCKET_CONFIG);
|
||||
|
||||
// Set up middleware for authentication and logging
|
||||
io.use(async (socket, next) => {
|
||||
const correlationId = socket.handshake.query.correlationId || require('uuid').v4();
|
||||
socket.correlationId = correlationId;
|
||||
|
||||
logger.info('WebSocket connection attempt', {
|
||||
correlationId,
|
||||
socketId: socket.id,
|
||||
ip: socket.handshake.address,
|
||||
userAgent: socket.handshake.headers['user-agent']
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Connection event handler
|
||||
io.on('connection', (socket) => {
|
||||
connectionCount++;
|
||||
connectedClients.set(socket.id, {
|
||||
connectedAt: new Date(),
|
||||
ip: socket.handshake.address,
|
||||
userAgent: socket.handshake.headers['user-agent'],
|
||||
playerId: null, // Will be set after authentication
|
||||
rooms: new Set()
|
||||
});
|
||||
|
||||
logger.info('WebSocket client connected', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
totalConnections: connectionCount,
|
||||
ip: socket.handshake.address
|
||||
});
|
||||
|
||||
// Set up event handlers
|
||||
setupSocketEventHandlers(socket);
|
||||
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', (reason) => {
|
||||
connectionCount--;
|
||||
const clientInfo = connectedClients.get(socket.id);
|
||||
connectedClients.delete(socket.id);
|
||||
|
||||
logger.info('WebSocket client disconnected', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
reason,
|
||||
totalConnections: connectionCount,
|
||||
playerId: clientInfo?.playerId,
|
||||
connectionDuration: clientInfo ? Date.now() - clientInfo.connectedAt : 0
|
||||
});
|
||||
});
|
||||
|
||||
// Handle connection errors
|
||||
socket.on('error', (error) => {
|
||||
logger.error('WebSocket connection error', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
error: error.message,
|
||||
stack: error.stack
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Server-level error handling
|
||||
io.engine.on('connection_error', (error) => {
|
||||
logger.error('WebSocket connection error:', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
context: error.context
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('WebSocket server initialized successfully', {
|
||||
maxConnections: process.env.WEBSOCKET_MAX_CONNECTIONS || 'unlimited',
|
||||
pingTimeout: WEBSOCKET_CONFIG.pingTimeout,
|
||||
pingInterval: WEBSOCKET_CONFIG.pingInterval
|
||||
});
|
||||
|
||||
return io;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize WebSocket server:', error);
|
||||
throw error;
|
||||
try {
|
||||
if (io) {
|
||||
logger.info('WebSocket server already initialized');
|
||||
return io;
|
||||
}
|
||||
|
||||
// Create Socket.IO server
|
||||
io = new Server(server, WEBSOCKET_CONFIG);
|
||||
|
||||
// Set up middleware for authentication and logging
|
||||
io.use(async (socket, next) => {
|
||||
const correlationId = socket.handshake.query.correlationId || require('uuid').v4();
|
||||
socket.correlationId = correlationId;
|
||||
|
||||
logger.info('WebSocket connection attempt', {
|
||||
correlationId,
|
||||
socketId: socket.id,
|
||||
ip: socket.handshake.address,
|
||||
userAgent: socket.handshake.headers['user-agent'],
|
||||
});
|
||||
|
||||
next();
|
||||
});
|
||||
|
||||
// Connection event handler
|
||||
io.on('connection', (socket) => {
|
||||
connectionCount++;
|
||||
connectedClients.set(socket.id, {
|
||||
connectedAt: new Date(),
|
||||
ip: socket.handshake.address,
|
||||
userAgent: socket.handshake.headers['user-agent'],
|
||||
playerId: null, // Will be set after authentication
|
||||
rooms: new Set(),
|
||||
});
|
||||
|
||||
logger.info('WebSocket client connected', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
totalConnections: connectionCount,
|
||||
ip: socket.handshake.address,
|
||||
});
|
||||
|
||||
// Set up event handlers
|
||||
setupSocketEventHandlers(socket);
|
||||
|
||||
// Handle disconnection
|
||||
socket.on('disconnect', (reason) => {
|
||||
connectionCount--;
|
||||
const clientInfo = connectedClients.get(socket.id);
|
||||
connectedClients.delete(socket.id);
|
||||
|
||||
logger.info('WebSocket client disconnected', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
reason,
|
||||
totalConnections: connectionCount,
|
||||
playerId: clientInfo?.playerId,
|
||||
connectionDuration: clientInfo ? Date.now() - clientInfo.connectedAt : 0,
|
||||
});
|
||||
});
|
||||
|
||||
// Handle connection errors
|
||||
socket.on('error', (error) => {
|
||||
logger.error('WebSocket connection error', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
error: error.message,
|
||||
stack: error.stack,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Server-level error handling
|
||||
io.engine.on('connection_error', (error) => {
|
||||
logger.error('WebSocket connection error:', {
|
||||
message: error.message,
|
||||
code: error.code,
|
||||
context: error.context,
|
||||
});
|
||||
});
|
||||
|
||||
logger.info('WebSocket server initialized successfully', {
|
||||
maxConnections: process.env.WEBSOCKET_MAX_CONNECTIONS || 'unlimited',
|
||||
pingTimeout: WEBSOCKET_CONFIG.pingTimeout,
|
||||
pingInterval: WEBSOCKET_CONFIG.pingInterval,
|
||||
});
|
||||
|
||||
return io;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize WebSocket server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -132,97 +132,97 @@ async function initializeWebSocket(server) {
|
|||
* @param {Object} socket - Socket.IO socket instance
|
||||
*/
|
||||
function setupSocketEventHandlers(socket) {
|
||||
// Player authentication
|
||||
socket.on('authenticate', async (data) => {
|
||||
try {
|
||||
logger.info('WebSocket authentication attempt', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
playerId: data?.playerId
|
||||
});
|
||||
// Player authentication
|
||||
socket.on('authenticate', async (data) => {
|
||||
try {
|
||||
logger.info('WebSocket authentication attempt', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
playerId: data?.playerId,
|
||||
});
|
||||
|
||||
// TODO: Implement JWT token validation
|
||||
// For now, just acknowledge
|
||||
socket.emit('authenticated', {
|
||||
success: true,
|
||||
message: 'Authentication successful'
|
||||
});
|
||||
// TODO: Implement JWT token validation
|
||||
// For now, just acknowledge
|
||||
socket.emit('authenticated', {
|
||||
success: true,
|
||||
message: 'Authentication successful',
|
||||
});
|
||||
|
||||
// Update client information
|
||||
if (connectedClients.has(socket.id)) {
|
||||
connectedClients.get(socket.id).playerId = data?.playerId;
|
||||
}
|
||||
// Update client information
|
||||
if (connectedClients.has(socket.id)) {
|
||||
connectedClients.get(socket.id).playerId = data?.playerId;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logger.error('WebSocket authentication error', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
error: error.message
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('WebSocket authentication error', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
error: error.message,
|
||||
});
|
||||
|
||||
socket.emit('authentication_error', {
|
||||
success: false,
|
||||
message: 'Authentication failed'
|
||||
});
|
||||
}
|
||||
socket.emit('authentication_error', {
|
||||
success: false,
|
||||
message: 'Authentication failed',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Join room (for game features like galaxy regions, player groups, etc.)
|
||||
socket.on('join_room', (roomName) => {
|
||||
if (typeof roomName !== 'string' || roomName.length > 50) {
|
||||
socket.emit('error', { message: 'Invalid room name' });
|
||||
return;
|
||||
}
|
||||
|
||||
socket.join(roomName);
|
||||
|
||||
const clientInfo = connectedClients.get(socket.id);
|
||||
if (clientInfo) {
|
||||
clientInfo.rooms.add(roomName);
|
||||
}
|
||||
|
||||
logger.info('Client joined room', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
room: roomName,
|
||||
playerId: clientInfo?.playerId,
|
||||
});
|
||||
|
||||
// Join room (for game features like galaxy regions, player groups, etc.)
|
||||
socket.on('join_room', (roomName) => {
|
||||
if (typeof roomName !== 'string' || roomName.length > 50) {
|
||||
socket.emit('error', { message: 'Invalid room name' });
|
||||
return;
|
||||
}
|
||||
socket.emit('room_joined', { room: roomName });
|
||||
});
|
||||
|
||||
socket.join(roomName);
|
||||
|
||||
const clientInfo = connectedClients.get(socket.id);
|
||||
if (clientInfo) {
|
||||
clientInfo.rooms.add(roomName);
|
||||
}
|
||||
// Leave room
|
||||
socket.on('leave_room', (roomName) => {
|
||||
socket.leave(roomName);
|
||||
|
||||
logger.info('Client joined room', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
room: roomName,
|
||||
playerId: clientInfo?.playerId
|
||||
});
|
||||
const clientInfo = connectedClients.get(socket.id);
|
||||
if (clientInfo) {
|
||||
clientInfo.rooms.delete(roomName);
|
||||
}
|
||||
|
||||
socket.emit('room_joined', { room: roomName });
|
||||
logger.info('Client left room', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
room: roomName,
|
||||
playerId: clientInfo?.playerId,
|
||||
});
|
||||
|
||||
// Leave room
|
||||
socket.on('leave_room', (roomName) => {
|
||||
socket.leave(roomName);
|
||||
|
||||
const clientInfo = connectedClients.get(socket.id);
|
||||
if (clientInfo) {
|
||||
clientInfo.rooms.delete(roomName);
|
||||
}
|
||||
socket.emit('room_left', { room: roomName });
|
||||
});
|
||||
|
||||
logger.info('Client left room', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
room: roomName,
|
||||
playerId: clientInfo?.playerId
|
||||
});
|
||||
// Ping/pong for connection testing
|
||||
socket.on('ping', () => {
|
||||
socket.emit('pong', { timestamp: Date.now() });
|
||||
});
|
||||
|
||||
socket.emit('room_left', { room: roomName });
|
||||
});
|
||||
|
||||
// Ping/pong for connection testing
|
||||
socket.on('ping', () => {
|
||||
socket.emit('pong', { timestamp: Date.now() });
|
||||
});
|
||||
|
||||
// Generic message handler (for debugging)
|
||||
socket.on('message', (data) => {
|
||||
logger.debug('WebSocket message received', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
data: typeof data === 'object' ? JSON.stringify(data) : data
|
||||
});
|
||||
// Generic message handler (for debugging)
|
||||
socket.on('message', (data) => {
|
||||
logger.debug('WebSocket message received', {
|
||||
correlationId: socket.correlationId,
|
||||
socketId: socket.id,
|
||||
data: typeof data === 'object' ? JSON.stringify(data) : data,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -230,7 +230,7 @@ function setupSocketEventHandlers(socket) {
|
|||
* @returns {Object|null} Socket.IO server instance
|
||||
*/
|
||||
function getWebSocketServer() {
|
||||
return io;
|
||||
return io;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -238,14 +238,14 @@ function getWebSocketServer() {
|
|||
* @returns {Object} Connection statistics
|
||||
*/
|
||||
function getConnectionStats() {
|
||||
return {
|
||||
totalConnections: connectionCount,
|
||||
authenticatedConnections: Array.from(connectedClients.values())
|
||||
.filter(client => client.playerId).length,
|
||||
anonymousConnections: Array.from(connectedClients.values())
|
||||
.filter(client => !client.playerId).length,
|
||||
rooms: io ? Array.from(io.sockets.adapter.rooms.keys()) : []
|
||||
};
|
||||
return {
|
||||
totalConnections: connectionCount,
|
||||
authenticatedConnections: Array.from(connectedClients.values())
|
||||
.filter(client => client.playerId).length,
|
||||
anonymousConnections: Array.from(connectedClients.values())
|
||||
.filter(client => !client.playerId).length,
|
||||
rooms: io ? Array.from(io.sockets.adapter.rooms.keys()) : [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -254,16 +254,16 @@ function getConnectionStats() {
|
|||
* @param {Object} data - Data to broadcast
|
||||
*/
|
||||
function broadcastToAll(event, data) {
|
||||
if (!io) {
|
||||
logger.warn('Attempted to broadcast but WebSocket server not initialized');
|
||||
return;
|
||||
}
|
||||
if (!io) {
|
||||
logger.warn('Attempted to broadcast but WebSocket server not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
io.emit(event, data);
|
||||
logger.info('Broadcast sent to all clients', {
|
||||
event,
|
||||
recipientCount: connectionCount
|
||||
});
|
||||
io.emit(event, data);
|
||||
logger.info('Broadcast sent to all clients', {
|
||||
event,
|
||||
recipientCount: connectionCount,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -273,17 +273,17 @@ function broadcastToAll(event, data) {
|
|||
* @param {Object} data - Data to broadcast
|
||||
*/
|
||||
function broadcastToRoom(room, event, data) {
|
||||
if (!io) {
|
||||
logger.warn('Attempted to broadcast to room but WebSocket server not initialized');
|
||||
return;
|
||||
}
|
||||
if (!io) {
|
||||
logger.warn('Attempted to broadcast to room but WebSocket server not initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
io.to(room).emit(event, data);
|
||||
logger.info('Broadcast sent to room', {
|
||||
room,
|
||||
event,
|
||||
recipientCount: io.sockets.adapter.rooms.get(room)?.size || 0
|
||||
});
|
||||
io.to(room).emit(event, data);
|
||||
logger.info('Broadcast sent to room', {
|
||||
room,
|
||||
event,
|
||||
recipientCount: io.sockets.adapter.rooms.get(room)?.size || 0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -291,31 +291,31 @@ function broadcastToRoom(room, event, data) {
|
|||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function closeWebSocket() {
|
||||
if (!io) return;
|
||||
if (!io) return;
|
||||
|
||||
try {
|
||||
// Disconnect all clients
|
||||
io.disconnectSockets();
|
||||
|
||||
// Close server
|
||||
io.close();
|
||||
|
||||
io = null;
|
||||
connectionCount = 0;
|
||||
connectedClients.clear();
|
||||
try {
|
||||
// Disconnect all clients
|
||||
io.disconnectSockets();
|
||||
|
||||
logger.info('WebSocket server closed gracefully');
|
||||
} catch (error) {
|
||||
logger.error('Error closing WebSocket server:', error);
|
||||
throw error;
|
||||
}
|
||||
// Close server
|
||||
io.close();
|
||||
|
||||
io = null;
|
||||
connectionCount = 0;
|
||||
connectedClients.clear();
|
||||
|
||||
logger.info('WebSocket server closed gracefully');
|
||||
} catch (error) {
|
||||
logger.error('Error closing WebSocket server:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
initializeWebSocket,
|
||||
getWebSocketServer,
|
||||
getConnectionStats,
|
||||
broadcastToAll,
|
||||
broadcastToRoom,
|
||||
closeWebSocket
|
||||
};
|
||||
initializeWebSocket,
|
||||
getWebSocketServer,
|
||||
getConnectionStats,
|
||||
broadcastToAll,
|
||||
broadcastToRoom,
|
||||
closeWebSocket,
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue