refactor(server): porta singola, /display e /controller come percorsi

Unifica i due server Express (display :3000, controller :3001) in un
unico processo su PORT (default 3000). Le route /display e /controller
servono rispettivamente index.html e controller.html.

In sviluppo elimina il server proxy su :3001; il plugin Vite riscrive
/display → / e /controller → /controller.html internamente.
printServerInfo aggiornata alla firma a porta singola.
This commit is contained in:
2026-05-12 12:22:43 +02:00
parent b3d114c108
commit 2fe1808fc9
4 changed files with 35 additions and 181 deletions
+13 -52
View File
@@ -9,32 +9,26 @@ import { printServerInfo } from './src/server-utils.js'
const __filename = fileURLToPath(import.meta.url) const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename) const __dirname = dirname(__filename)
// --- Configurazione del server --- const PORT = process.env.PORT || 3000
const distDir = join(__dirname, 'dist')
const DISPLAY_PORT = process.env.PORT || 3000 const app = express()
const CONTROLLER_PORT = process.env.CONTROLLER_PORT || 3001
// ======================================== app.use(express.static(distDir, { index: false }))
// Server Display (porta principale)
// ========================================
const displayApp = express() app.get(['/', '/display', '/display/*splat'], (_req, res) => {
res.sendFile(join(distDir, 'index.html'))
// Espone i file generati dalla build di Vite.
displayApp.use(express.static(join(__dirname, 'dist')))
// Fallback per SPA: restituisce `index.html` per tutte le route.
displayApp.get(/.*/, (_req, res) => {
res.sendFile(join(__dirname, 'dist', 'index.html'))
}) })
const displayServer = createServer(displayApp) app.get(['/controller', '/controller/*splat'], (_req, res) => {
res.sendFile(join(distDir, 'controller.html'))
})
// Inizializza il server WebSocket condiviso. const server = createServer(app)
const wss = new WebSocketServer({ noServer: true }) const wss = new WebSocketServer({ noServer: true })
setupWebSocketHandler(wss) setupWebSocketHandler(wss)
displayServer.on('upgrade', (request, socket, head) => { server.on('upgrade', (request, socket, head) => {
const pathname = new URL(request.url, `http://${request.headers.host}`).pathname const pathname = new URL(request.url, `http://${request.headers.host}`).pathname
if (pathname === '/ws') { if (pathname === '/ws') {
wss.handleUpgrade(request, socket, head, (ws) => { wss.handleUpgrade(request, socket, head, (ws) => {
@@ -45,39 +39,6 @@ displayServer.on('upgrade', (request, socket, head) => {
} }
}) })
displayServer.listen(DISPLAY_PORT, '0.0.0.0', () => { server.listen(PORT, '0.0.0.0', () => {
console.log(`[Display] Server running on port ${DISPLAY_PORT}`) printServerInfo(PORT)
})
// ========================================
// Server Controller (porta separata)
// ========================================
const controllerApp = express()
// Espone gli stessi file statici della build.
// IMPORTANTE: { index: false } impedisce di servire index.html (il display) sulla root.
controllerApp.use(express.static(join(__dirname, 'dist'), { index: false }))
// Fallback: restituisce `controller.html` per tutte le route.
controllerApp.get(/.*/, (_req, res) => {
res.sendFile(join(__dirname, 'dist', 'controller.html'))
})
const controllerServer = createServer(controllerApp)
// Gestisce l'upgrade WebSocket anche sulla porta del controller.
controllerServer.on('upgrade', (request, socket, head) => {
const pathname = new URL(request.url, `http://${request.headers.host}`).pathname
if (pathname === '/ws') {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request)
})
} else {
socket.destroy()
}
})
controllerServer.listen(CONTROLLER_PORT, '0.0.0.0', () => {
printServerInfo(DISPLAY_PORT, CONTROLLER_PORT)
}) })
+4 -14
View File
@@ -1,16 +1,11 @@
import { networkInterfaces } from 'os' import { networkInterfaces } from 'os'
/**
* Restituisce gli indirizzi IP di rete del sistema, escludendo loopback e bridge Docker.
* @returns {string[]} Elenco degli indirizzi IP disponibili.
*/
export function getNetworkIPs() { export function getNetworkIPs() {
const nets = networkInterfaces() const nets = networkInterfaces()
const networkIPs = [] const networkIPs = []
for (const name of Object.keys(nets)) { for (const name of Object.keys(nets)) {
for (const net of nets[name]) { for (const net of nets[name]) {
// Esclude loopback (127.0.0.1), indirizzi non IPv4 e bridge Docker (172.17.x.x, 172.18.x.x).
if (net.family === 'IPv4' && if (net.family === 'IPv4' &&
!net.internal && !net.internal &&
!net.address.startsWith('172.17.') && !net.address.startsWith('172.17.') &&
@@ -23,22 +18,17 @@ export function getNetworkIPs() {
return networkIPs return networkIPs
} }
/** export function printServerInfo(port = 3000) {
* Stampa il riepilogo di avvio del server con gli URL di accesso.
* @param {number} displayPort - Porta del display.
* @param {number} controllerPort - Porta del controller.
*/
export function printServerInfo(displayPort = 5173, controllerPort = 3001) {
const networkIPs = getNetworkIPs() const networkIPs = getNetworkIPs()
console.log(`\nSegnapunti Server`) console.log(`\nSegnapunti Server`)
console.log(` Display: http://127.0.0.1:${displayPort}/`) console.log(` Display: http://127.0.0.1:${port}/display`)
console.log(` Controller: http://127.0.0.1:${controllerPort}/`) console.log(` Controller: http://127.0.0.1:${port}/controller`)
if (networkIPs.length > 0) { if (networkIPs.length > 0) {
console.log(`\n Controller da dispositivi remoti:`) console.log(`\n Controller da dispositivi remoti:`)
networkIPs.forEach(ip => { networkIPs.forEach(ip => {
console.log(` http://${ip}:${controllerPort}/`) console.log(` http://${ip}:${port}/controller`)
}) })
} }
+9 -9
View File
@@ -102,23 +102,23 @@ describe('Server Utils', () => {
// printServerInfo // printServerInfo
// ============================================= // =============================================
describe('printServerInfo', () => { describe('printServerInfo', () => {
it('dovrebbe stampare le porte corrette (default)', () => { it('dovrebbe stampare la porta di default (3000)', () => {
os.networkInterfaces.mockReturnValue({}) os.networkInterfaces.mockReturnValue({})
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
printServerInfo() printServerInfo()
const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n') const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n')
expect(allLogs).toContain('5173') expect(allLogs).toContain('3000')
expect(allLogs).toContain('3001') expect(allLogs).toContain('/display')
expect(allLogs).toContain('/controller')
consoleSpy.mockRestore() consoleSpy.mockRestore()
}) })
it('dovrebbe stampare le porte personalizzate', () => { it('dovrebbe stampare la porta personalizzata', () => {
os.networkInterfaces.mockReturnValue({}) os.networkInterfaces.mockReturnValue({})
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
printServerInfo(3000, 4000) printServerInfo(8080)
const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n') const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n')
expect(allLogs).toContain('3000') expect(allLogs).toContain('8080')
expect(allLogs).toContain('4000')
consoleSpy.mockRestore() consoleSpy.mockRestore()
}) })
@@ -129,7 +129,7 @@ describe('Server Utils', () => {
] ]
}) })
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
printServerInfo(3000, 3001) printServerInfo(3000)
const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n') const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n')
expect(allLogs).toContain('192.168.1.50') expect(allLogs).toContain('192.168.1.50')
expect(allLogs).toContain('remoti') expect(allLogs).toContain('remoti')
@@ -139,7 +139,7 @@ describe('Server Utils', () => {
it('non dovrebbe mostrare sezione remoti se nessun IP di rete', () => { it('non dovrebbe mostrare sezione remoti se nessun IP di rete', () => {
os.networkInterfaces.mockReturnValue({}) os.networkInterfaces.mockReturnValue({})
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}) const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
printServerInfo(3000, 3001) printServerInfo(3000)
const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n') const allLogs = consoleSpy.mock.calls.map(c => c[0]).join('\n')
expect(allLogs).not.toContain('remoti') expect(allLogs).not.toContain('remoti')
consoleSpy.mockRestore() consoleSpy.mockRestore()
+9 -106
View File
@@ -1,30 +1,23 @@
import { WebSocketServer } from 'ws' import { WebSocketServer } from 'ws'
import { createServer as createHttpServer, request as httpRequest } from 'http'
import { setupWebSocketHandler } from './src/websocket-handler.js' import { setupWebSocketHandler } from './src/websocket-handler.js'
import { printServerInfo } from './src/server-utils.js' import { printServerInfo } from './src/server-utils.js'
const CONTROLLER_PORT = 3001
const DEV_PROXY_HOST = process.env.DEV_PROXY_HOST || '127.0.0.1'
/**
* Plugin Vite che integra un server WebSocket per la gestione dello stato di gioco
* e un server separato sulla porta 3001 per il controller.
* @returns {import('vite').Plugin}
*/
export default function websocketPlugin() { export default function websocketPlugin() {
return { return {
name: 'vite-plugin-websocket', name: 'vite-plugin-websocket',
configureServer(server) { configureServer(server) {
// Inizializza un server WebSocket collegato al server HTTP di Vite.
const wss = new WebSocketServer({ noServer: true }) const wss = new WebSocketServer({ noServer: true })
// Registra i gestori WebSocket con la logica di gioco.
setupWebSocketHandler(wss) setupWebSocketHandler(wss)
// Intercetta le richieste di upgrade WebSocket solo sul path /ws. // Rewrite /display → / (index.html) e /controller → /controller.html
server.middlewares.use((req, _res, next) => {
if (req.url === '/display' || req.url === '/display/') req.url = '/'
else if (req.url === '/controller' || req.url === '/controller/') req.url = '/controller.html'
next()
})
server.httpServer.on('upgrade', (request, socket, head) => { server.httpServer.on('upgrade', (request, socket, head) => {
const pathname = new URL(request.url, `http://${request.headers.host}`).pathname const pathname = new URL(request.url, `http://${request.headers.host}`).pathname
if (pathname === '/ws') { if (pathname === '/ws') {
wss.handleUpgrade(request, socket, head, (ws) => { wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request) wss.emit('connection', ws, request)
@@ -32,100 +25,10 @@ export default function websocketPlugin() {
} }
}) })
// Avvia un server separato per il controller sulla porta 3001.
server.httpServer.once('listening', () => { server.httpServer.once('listening', () => {
const viteAddr = server.httpServer.address() const { port } = server.httpServer.address()
const vitePort = viteAddr.port setTimeout(() => printServerInfo(port), 100)
startControllerDevServer(vitePort, wss)
setTimeout(() => printServerInfo(vitePort, CONTROLLER_PORT), 100)
}) })
} }
} }
} }
/**
* Avvia il server di sviluppo per il controller.
* Fa da proxy verso il dev server di Vite per moduli ES, HMR, e asset.
*/
function startControllerDevServer(vitePort, wss) {
const controllerServer = createHttpServer((req, res) => {
// Se richiesta alla root, riscrive verso controller.html
let targetPath = req.url
if (targetPath === '/' || targetPath === '') {
targetPath = '/controller.html'
}
// Proxy verso il dev server di Vite
const proxyReq = httpRequest(
{
hostname: DEV_PROXY_HOST,
port: vitePort,
path: targetPath,
method: req.method,
headers: {
...req.headers,
host: `${DEV_PROXY_HOST}:${vitePort}`,
},
},
(proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers)
proxyRes.pipe(res, { end: true })
}
)
proxyReq.on('error', (err) => {
console.error('[Controller Proxy] Error:', err.message)
if (!res.headersSent) {
res.writeHead(502)
res.end('Proxy error')
}
})
req.pipe(proxyReq, { end: true })
})
// Gestisce l'upgrade WebSocket anche sulla porta del controller
controllerServer.on('upgrade', (request, socket, head) => {
const pathname = new URL(request.url, `http://${request.headers.host}`).pathname
if (pathname === '/ws') {
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request)
})
} else {
// Per l'HMR di Vite, proxare l'upgrade WebSocket verso Vite
const proxyReq = httpRequest({
hostname: DEV_PROXY_HOST,
port: vitePort,
path: request.url,
method: 'GET',
headers: request.headers,
})
proxyReq.on('upgrade', (proxyRes, proxySocket, proxyHead) => {
socket.write(
`HTTP/1.1 101 Switching Protocols\r\n` +
Object.entries(proxyRes.headers)
.map(([k, v]) => `${k}: ${v}`)
.join('\r\n') +
'\r\n\r\n'
)
proxySocket.pipe(socket)
socket.pipe(proxySocket)
})
proxyReq.on('error', (err) => {
console.error('[Controller Proxy] WS upgrade error:', err.message)
socket.destroy()
})
proxyReq.end()
}
})
controllerServer.listen(CONTROLLER_PORT, '0.0.0.0', () => {
console.log(`[Controller] Dev server running on port ${CONTROLLER_PORT}`)
})
}