require('dotenv').config(); const express = require('express'); const { createServer } = require('http'); const { Server } = require('socket.io'); const app = express(); const httpServer = createServer(app); const io = new Server(httpServer, { cors: { origin: process.env.ALLOWED_ORIGIN || 'http://localhost' } }); app.use(express.json()); // ── /emit ───────────────────────────────────────────────────────────────────── // PHP calls this after any significant action. // Body: { event, data, company_id } // app.post('/emit', (req, res) => { const secret = req.headers['x-emit-secret']; if (secret !== process.env.EMIT_SECRET) { return res.status(403).json({ ok: false, message: 'Forbidden' }); } const { event, data, company_id, target, user_id } = req.body; if (!event || !company_id) { return res.status(400).json({ ok: false, message: 'event and company_id required' }); } if (target === 'user' && user_id) { // Notify the acting user on all their tabs + all admins (excluding the acting user to avoid duplicates) io.to(`user_${user_id}`).emit(event, data); io.to(`admin_${company_id}`).except(`user_${user_id}`).emit(event, data); console.log(`[emit] company=${company_id} user=${user_id} event=${event}`, data); } else if (target === 'admin') { // Admins and owners only io.to(`admin_${company_id}`).emit(event, data); console.log(`[emit] company=${company_id} admin-only event=${event}`, data); } else { // Company-wide — stock events, GL events, scheduler alerts io.to(`company_${company_id}`).emit(event, data); console.log(`[emit] company=${company_id} event=${event}`, data); } res.json({ ok: true }); }); // ── /health ─────────────────────────────────────────────────────────────────── app.get('/health', (req, res) => { res.json({ status: 'ok', uptime: Math.floor(process.uptime()), connections: io.engine.clientsCount, memory_mb: Math.round(process.memoryUsage().rss / 1024 / 1024 * 10) / 10 }); }); // ── WebSocket connections ───────────────────────────────────────────────────── // Each browser tab connects here on page load. // It joins a room named company_ so events stay isolated per company. // io.on('connection', (socket) => { const company_id = socket.handshake.query.company_id; const user_id = socket.handshake.query.user_id; const role = socket.handshake.query.role || 'viewer'; if (!company_id) { socket.disconnect(); return; } socket.join(`company_${company_id}`); if (user_id) { socket.join(`user_${user_id}`); } if (role === 'admin' || role === 'owner') { socket.join(`admin_${company_id}`); } console.log(`[connect] socket=${socket.id} company=${company_id} user=${user_id} role=${role}`); socket.on('disconnect', () => { console.log(`[disconnect] socket=${socket.id}`); }); }); // ── Start ───────────────────────────────────────────────────────────────────── const PORT = process.env.PORT || 3000; httpServer.listen(PORT, () => { console.log(`Node.js real-time server running on port ${PORT}`); });