// server.js const http = require('http'); const express = require('express'); const { createClient } = require('redis'); const { Server } = require('socket.io'); const fetch = require('node-fetch'); // npm i node-fetch@2 const fs = require('fs'); const yaml = require('js-yaml'); // npm i js-yaml const path = require('path'); const app = express(); const server = http.createServer(app); const io = new Server(server); /* --------------------------------------------------------------------- */ /* ---------- 1. Load the YAML configuration file ---------------------- */ /* --------------------------------------------------------------------- */ let config = {}; try { const filePath = '/app/cosmostat_settings.yaml'; const file = fs.readFileSync(filePath, 'utf8'); config = yaml.load(file); } catch (e) { console.error('Failed to load config.yaml:', e); process.exit(1); } const API_PORT = config.custom_api_port || 5000; // fallback to 5000 const API_HOST = config.api_bind_ip || '192.168.37.1'; // fallback IP const API_BASE = `http://${API_HOST}:${API_PORT}`; console.log('API URL:', API_BASE); /* --------------------------------------------------------------------- */ /* ---------- 2. Socket.io -------------------------------------------- */ /* --------------------------------------------------------------------- */ io.on('connection', async socket => { console.log('client connected:', socket.id); /* ------------- send cached client_summary ------------- */ if (clientSummaryCache.last) { socket.emit('client_summary', clientSummaryCache.last); console.log('sent cached client_summary to', socket.id); } /* ----------------------------------------------------------------- */ /* Call the external API every time a client connects */ /* ----------------------------------------------------------------- */ try { const resp = await fetch(`${API_BASE}/start_timer`, { method: 'GET' }); const data = await resp.json(); console.log('API responded to connect:', data); } catch (err) { console.error('Failed to hit start_timer endpoint:', err); } /* ----------------------------------------------------------------- */ /* Listen for tableRendered event from the client */ /* ----------------------------------------------------------------- */ socket.on('tableRendered', async () => { console.log('Client reported table rendered - starting timer'); try { const resp = await fetch(`${API_BASE}/start_timer`, { method: 'GET' }); const text = await resp.text(); console.log('Timer endpoint responded:', text); } catch (err) { console.error('Failed to hit start_timer:', err); } }); }); /* --------------------------------------------------------------------- */ /* ---------- 3. Serve static files ----------------------------------- */ /* --------------------------------------------------------------------- */ app.use(express.static('public')); /* --------------------------------------------------------------------- */ /* ---------- 4. Redis subscriber ------------------------------------- */ /* --------------------------------------------------------------------- */ const redisClient = createClient({ url: 'redis://0.0.0.0:6379', socket: { keepAlive: 60000, // 60 s TCP keep-alive reconnectStrategy: attempts => Math.min(attempts * 100, 3000) } // back-off }); redisClient.on('error', err => console.error('Redis error', err)); /* --- local cache for client_summary -------------------------------- */ const clientSummaryCache = {}; // { last: } /* --------------------------------------------------------------------- */ (async () => { await redisClient.connect(); const sub = redisClient.duplicate(); await sub.connect(); /* --------------------------------------------------------------------- */ /* Helper that re-subscribes to a channel (and re-sends the handler) */ /* --------------------------------------------------------------------- */ async function safeSubscribe(channel, handler) { try { await sub.subscribe(channel, handler); console.log(`Subscribed to ${channel}`); } catch (e) { console.error(`Failed to subscribe to ${channel}`, e); } } /* --------------------------------------------------------------------- */ /* Subscribe to all required channels */ /* --------------------------------------------------------------------- */ await safeSubscribe('host_metrics', (msg) => forward('host_metrics', msg)); await safeSubscribe('client_summary', (msg) => forward('client_summary', msg)); /* --------------------------------------------------------------------- */ /* Forward messages to Socket.io */ /* --------------------------------------------------------------------- */ function forward(channel, message) { try { const payload = JSON.parse(message); /* ----- update cache on client_summary ----- */ if (channel === 'client_summary') { clientSummaryCache.last = payload; } io.emit(channel, payload); } catch (e) { console.error(`Failed to parse message from ${channel}`, e); } } /* --------------------------------------------------------------------- */ /* Re-subscribe automatically when the Redis connection reconnects */ /* --------------------------------------------------------------------- */ sub.on('reconnecting', () => console.log('Redis reconnecting…')); sub.on('ready', async () => { console.log('Redis ready - re-subscribing to all channels'); await safeSubscribe('host_metrics', (msg) => forward('host_metrics', msg)); await safeSubscribe('client_summary', (msg) => forward('client_summary', msg)); }); /* Optional: if the connection ends for any reason, close the process */ sub.on('end', () => { console.error('Redis connection closed - exiting'); process.exit(1); }); })(); /* --------------------------------------------------------------------- */ /* ---------- 5. Start the HTTP server --------------------------------- */ /* --------------------------------------------------------------------- */ const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`Server listening on http://localhost:${PORT}`); });