Spaces:
Sleeping
Sleeping
| 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`); | |
| }); |