/** * Express server with WebSocket proxy for TASTE Voice Bot frontend * * Architecture: * - Browser connects to this server on port 3000 (HTTP + WebSocket) * - Server serves static files from current directory * - WebSocket requests to /ws/* are proxied internally to backend * - Browser never directly connects to backend port */ const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const path = require('path'); // Read backend configuration from environment variables const BACKEND_HOST = process.env.BACKEND_HOST || 'localhost'; const BACKEND_PORT = process.env.BACKEND_PORT || '8000'; const BACKEND_WS_PATH = process.env.BACKEND_WS_PATH || '/ws/orchestrator'; const FRONTEND_PORT = process.env.FRONTEND_PORT || '3000'; // Parse BACKEND_HOST to extract protocol and hostname let backendUrl; try { // If BACKEND_HOST includes protocol (http:// or https://), parse it if (BACKEND_HOST.includes('://')) { const url = new URL(BACKEND_HOST); const protocol = url.protocol === 'https:' ? 'https' : 'http'; const wsProtocol = url.protocol === 'https:' ? 'wss' : 'ws'; const host = url.hostname; const port = url.port || BACKEND_PORT; backendUrl = { http: `${protocol}://${host}:${port}`, ws: `${wsProtocol}://${host}:${port}`, display: `${protocol}://${host}:${port}` }; } else { // BACKEND_HOST is just hostname backendUrl = { http: `http://${BACKEND_HOST}:${BACKEND_PORT}`, ws: `ws://${BACKEND_HOST}:${BACKEND_PORT}`, display: `http://${BACKEND_HOST}:${BACKEND_PORT}` }; } } catch (e) { // Fallback to simple hostname backendUrl = { http: `http://${BACKEND_HOST}:${BACKEND_PORT}`, ws: `ws://${BACKEND_HOST}:${BACKEND_PORT}`, display: `http://${BACKEND_HOST}:${BACKEND_PORT}` }; } const app = express(); // Serve static files from current directory app.use(express.static(__dirname)); // WebSocket proxy middleware const wsProxy = createProxyMiddleware({ target: backendUrl.http, changeOrigin: true, ws: true, // Enable WebSocket proxying logLevel: 'info', onError: (err, req, res) => { console.error('Proxy error:', err); if (res.writeHead) { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end('Bad Gateway: Could not connect to backend'); } }, onProxyReqWs: (proxyReq, req, socket, options, head) => { console.log(`[WS Proxy] ${req.url} -> ${backendUrl.ws}${req.url}`); } }); // Apply WebSocket proxy to /ws/* paths app.use('/ws', wsProxy); // Start server const server = app.listen(FRONTEND_PORT, () => { console.log('================================='); console.log('TASTE Voice Bot Frontend Server'); console.log('================================='); console.log(`Frontend: http://localhost:${FRONTEND_PORT}`); console.log(`Backend: ${backendUrl.display}`); console.log(`WS Proxy: /ws/* -> ${backendUrl.ws}/ws/*`); console.log('================================='); }); // Enable WebSocket upgrade handling server.on('upgrade', (req, socket, head) => { if (req.url.startsWith('/ws')) { wsProxy.upgrade(req, socket, head); } else { socket.destroy(); } }); // Graceful shutdown process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully...'); server.close(() => { console.log('Server closed'); process.exit(0); }); }); process.on('SIGINT', () => { console.log('SIGINT received, shutting down gracefully...'); server.close(() => { console.log('Server closed'); process.exit(0); }); });