// Load environment variables from .env file require('dotenv').config(); const express = require('express'); const http = require('http'); const WebSocket = require('ws'); const path = require('path'); // Create Express app and HTTP server const app = express(); const server = http.createServer(app); const PORT = 8090; // Middleware to parse JSON requests app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Serve static files from public directory app.use(express.static(path.join(__dirname, '../public'))); // Basic route for the main page app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../public/index.html')); }); // Route for overlay page with username parameter app.get('/overlay/:username', (req, res) => { res.sendFile(path.join(__dirname, '../public/overlay.html')); }); // API Routes const apiRoutes = require('./routes/api'); app.use('/api', apiRoutes); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'OK', message: 'RetroPulse server is running', timestamp: new Date().toISOString() }); }); // WebSocket Setup const retroClient = require('./retroClient'); // Create WebSocket server const wss = new WebSocket.Server({ server, path: '/ws' }); // Store active connections by username const userConnections = new Map(); wss.on('connection', async (ws, req) => { // Extract username from query parameters const url = new URL(req.url, `http://localhost:${PORT}`); const username = url.searchParams.get('user'); if (!username) { ws.send(JSON.stringify({ error: 'Username parameter is required (?user=yourname)' })); ws.close(); return; } console.log(`WebSocket connected for user: ${username}`); // Store the connection userConnections.set(username, ws); // Send initial data immediately try { const initialData = await retroClient.getOverlayData(username, 5); ws.send(JSON.stringify({ type: 'initial', data: initialData })); } catch (error) { ws.send(JSON.stringify({ type: 'error', message: `Failed to load initial data: ${error.message}` })); } // Set up polling for updates every 10 seconds const pollInterval = setInterval(async () => { if (ws.readyState !== WebSocket.OPEN) { clearInterval(pollInterval); userConnections.delete(username); return; } try { const updatedData = await retroClient.getOverlayData(username, 5); ws.send(JSON.stringify({ type: 'update', data: updatedData })); } catch (error) { console.error(`Error fetching updates for ${username}:`, error.message); } }, 10000); // Poll every 10 seconds // Handle client disconnect ws.on('close', () => { clearInterval(pollInterval); userConnections.delete(username); console.log(`WebSocket disconnected for user: ${username}`); }); ws.on('error', (error) => { console.error(`WebSocket error for ${username}:`, error.message); }); }); // Error handling middleware app.use((err, req, res, next) => { console.error('Server error:', err); res.status(500).json({ error: 'Internal server error', message: process.env.NODE_ENV === 'development' ? err.message : 'Something went wrong' }); }); // Handle 404 for unmatched routes app.use((req, res) => { res.status(404).json({ error: 'Not found', message: `Route ${req.method} ${req.path} not found` }); }); // Start the server (HTTP and WebSocket together) server.listen(PORT, () => { console.log(`RetroPulse server is running on http://localhost:${PORT}`); console.log(`Health check: http://localhost:${PORT}/health`); console.log(`Main page: http://localhost:${PORT}/`); console.log(`Overlay example: http://localhost:${PORT}/overlay/your-username`); console.log(`WebSocket endpoint: ws://localhost:${PORT}/ws?user=your-username`); }); // Graceful shutdown handling process.on('SIGTERM', () => { console.log('Received SIGTERM, shutting down gracefully'); server.close(() => { console.log('Server closed'); process.exit(0); }); }); process.on('SIGINT', () => { console.log('Received SIGINT, shutting down gracefully'); server.close(() => { console.log('Server closed'); process.exit(0); }); }); // Export server for potential testing module.exports = server;