require('dotenv').config({ path: require('path').join(__dirname, '../../secrets/.env') }); const express = require('express'); const cors = require('cors'); const rateLimit = require('express-rate-limit'); const { WebSocketServer } = require('ws'); const http = require('http'); const path = require('path'); const fs = require('fs'); const { v4: uuidv4 } = require('uuid'); const PQueue = require('p-queue').default; const os = require('os'); const winston = require('winston'); const settings = JSON.parse(fs.readFileSync(path.join(__dirname, '../settings.json'), 'utf8')); // Logger configuration const logger = winston.createLogger({ level: 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), transports: [ new winston.transports.Console(), new winston.transports.File({ filename: path.join(__dirname, '../logs/central.log') }) ] }); // Metrics storage const metrics = { requests: [], // { timestamp, ip, duration, sourceLang, targetLang, inputText, outputText } cpuHistory: [], ramHistory: [], workers: new Map() // workerId -> { id, ws, cpu, ram, lastHeartbeat, jobsActive } }; // Job queue (local processing + worker dispatch) const localQueue = new PQueue({ concurrency: settings.maxLocalJobs }); const workerQueue = new PQueue({ concurrency: 10 }); // dispatch to workers // Express app const app = express(); const server = http.createServer(app); // WebSocket server for workers const wss = new WebSocketServer({ server }); // Middleware app.use(cors()); app.use(express.json({ limit: '10mb' })); app.use(express.static(path.join(__dirname, '../../public'))); // Rate limiting const limiter = rateLimit({ windowMs: settings.rateLimit.windowMs, max: settings.rateLimit.maxRequestsPerMinutePerIP, message: { error: 'Trop de requêtes, veuillez réessayer plus tard.' }, standardHeaders: true, legacyHeaders: false, keyGenerator: (req) => req.ip }); app.use('/translate', limiter); // Admin authentication middleware const adminAuth = (req, res, next) => { const adminCode = process.env.ADMIN_ACCESS_CODE; const providedCode = req.headers['x-admin-code'] || req.query.admin_code; if (!adminCode || providedCode !== adminCode) { return res.status(401).json({ error: 'Code admin invalide' }); } next(); }; // Load translation function const translate = require('@vitalets/google-translate-api'); // Metrics collection let prevCpu = process.cpuUsage(); let prevTime = process.hrtime.bigint(); setInterval(() => { const cpuUsage = process.cpuUsage(prevCpu); const currentTime = process.hrtime.bigint(); const elapsedNs = Number(currentTime - prevTime); const elapsedUs = elapsedNs / 1000; const cpuPercent = Math.min(100, ((cpuUsage.user + cpuUsage.system) / elapsedUs) * 100); const memUsage = process.memoryUsage(); metrics.cpuHistory.push({ timestamp: Date.now(), cpu: Math.round(cpuPercent * 10) / 10 }); metrics.ramHistory.push({ timestamp: Date.now(), used: memUsage.heapUsed, total: memUsage.heapTotal }); prevCpu = process.cpuUsage(); prevTime = currentTime; // Keep only last metricsWindowSeconds const cutoff = Date.now() - settings.metricsWindowSeconds * 1000; metrics.cpuHistory = metrics.cpuHistory.filter(m => m.timestamp > cutoff); metrics.ramHistory = metrics.ramHistory.filter(m => m.timestamp > cutoff); metrics.requests = metrics.requests.filter(m => m.timestamp > cutoff); }, 1000); // Clean up inactive workers setInterval(() => { const now = Date.now(); for (const [id, worker] of metrics.workers) { if (now - worker.lastHeartbeat > 15000) { logger.info(`Worker ${id} disconnected (timeout)`); metrics.workers.delete(id); } } }, 5000); // WebSocket handling for workers wss.on('connection', (ws, req) => { const workerId = uuidv4(); logger.info(`Worker connected: ${workerId}`); const worker = { id: workerId, ws, cpu: 0, ram: 0, lastHeartbeat: Date.now(), jobsActive: 0, maxJobs: 2 }; metrics.workers.set(workerId, worker); ws.on('message', (data) => { try { const msg = JSON.parse(data); handleWorkerMessage(workerId, msg); } catch (e) { logger.error('Invalid worker message', e); } }); ws.on('close', () => { logger.info(`Worker disconnected: ${workerId}`); metrics.workers.delete(workerId); }); ws.on('error', (err) => { logger.error(`Worker ${workerId} error`, err); }); // Send welcome message with config ws.send(JSON.stringify({ type: 'welcome', workerId, maxConcurrentJobs: 2 })); }); function handleWorkerMessage(workerId, msg) { const worker = metrics.workers.get(workerId); if (!worker) return; switch (msg.type) { case 'heartbeat': worker.lastHeartbeat = Date.now(); worker.cpu = msg.cpu || 0; worker.ram = msg.ram || 0; worker.jobsActive = msg.jobsActive || 0; break; case 'result': // Job completed by worker if (msg.jobId) { // Find and resolve the waiting promise const pending = pendingJobs.get(msg.jobId); if (pending) { pending.resolve(msg.result); pendingJobs.delete(msg.jobId); } } worker.jobsActive = Math.max(0, worker.jobsActive - 1); break; case 'log': logger.info(`Worker ${workerId} log: ${msg.message}`); break; } } // Pending jobs waiting for worker results const pendingJobs = new Map(); // API Routes // Health check app.get('/api/health', (req, res) => { res.json({ status: 'ok', uptime: process.uptime() }); }); // Translate endpoint app.post('/translate', async (req, res) => { const startTime = Date.now(); const { text, source = 'auto', target = 'fr' } = req.body; const ip = req.ip; if (!text || typeof text !== 'string') { return res.status(400).json({ error: 'Texte requis' }); } if (text.length > 5000) { return res.status(400).json({ error: 'Texte trop long (max 5000 caractères)' }); } try { // Try to dispatch to available worker first let result; const availableWorker = findAvailableWorker(); if (availableWorker) { result = await dispatchToWorker(availableWorker, { text, source, target }); } else { // Process locally result = await localQueue.add(() => translateText(text, source, target)); } const duration = Date.now() - startTime; // Extract translated text from worker result object if needed const translatedText = result.translatedText || result; // Log request metrics.requests.push({ timestamp: Date.now(), ip, duration, sourceLang: source, targetLang: target, inputText: text.substring(0, 100), outputText: translatedText.substring(0, 100) }); res.json({ translatedText, source, target, duration }); } catch (error) { logger.error('Translation error', error); res.status(500).json({ error: 'Erreur de traduction' }); } }); function translateText(text, source, target) { return translate(text, { from: source, to: target }).then(res => res.text); } function findAvailableWorker() { for (const [id, worker] of metrics.workers) { if (worker.jobsActive < worker.maxJobs && worker.ws.readyState === 1) { return worker; } } return null; } function dispatchToWorker(worker, job) { return new Promise((resolve, reject) => { const jobId = uuidv4(); pendingJobs.set(jobId, { resolve, reject, timeout: setTimeout(() => { pendingJobs.delete(jobId); reject(new Error('Worker timeout')); }, 30000) }); worker.ws.send(JSON.stringify({ type: 'translate', jobId, ...job })); worker.jobsActive++; }); } // Admin dashboard page (auth handled client-side via modal) app.get('/admin', (req, res) => { res.sendFile(path.join(__dirname, '../../public/admin.html')); }); app.get('/api/admin/metrics', adminAuth, (req, res) => { const workers = []; for (const [id, w] of metrics.workers) { workers.push({ id, cpu: w.cpu, ram: w.ram, jobsActive: w.jobsActive, maxJobs: w.maxJobs, lastHeartbeat: w.lastHeartbeat }); } res.json({ workers, cpuHistory: metrics.cpuHistory, ramHistory: metrics.ramHistory, requests: metrics.requests.slice(-200), // Last 200 requests requestRate: calculateRequestRate() }); }); app.get('/api/admin/logs', adminAuth, (req, res) => { // Return recent logs from metrics.requests const logs = metrics.requests.slice(-100).map(r => ({ timestamp: r.timestamp, ip: r.ip, duration: r.duration, sourceLang: r.sourceLang, targetLang: r.targetLang, inputText: r.inputText, outputText: r.outputText })); res.json({ logs }); }); function calculateRequestRate() { const now = Date.now(); const windowMs = 120000; // 2 minutes const recent = metrics.requests.filter(r => r.timestamp > now - windowMs); return recent.length; } // Main page app.get('/', (req, res) => { res.sendFile(path.join(__dirname, '../../public/index.html')); }); // Wiki page app.get('/wiki', (req, res) => { res.sendFile(path.join(__dirname, '../../public/wiki.html')); }); // Start server const PORT = process.env.PORT || settings.port; server.listen(PORT, '0.0.0.0', () => { logger.info(`Central server running on port ${PORT}`); logger.info(`Admin panel: http://localhost:${PORT}/admin`); logger.info(`API: http://localhost:${PORT}/translate`); });