import http from 'http'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { createAiCore, UpstreamError } from './server/aiCore.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const PORT = process.env.PORT || 7860; const HOST = process.env.HOST || '0.0.0.0'; const distDir = path.join(__dirname, 'dist'); const ai = createAiCore(process.env); function sendJson(res, statusCode, payload) { res.writeHead(statusCode, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store', 'Access-Control-Allow-Origin': '*', }); res.end(JSON.stringify(payload)); } function readBody(req) { return new Promise((resolve, reject) => { let data = ''; req.on('data', (chunk) => (data += chunk)); req.on('end', () => resolve(data)); req.on('error', reject); }); } // MIME types mapping const mimeTypes = { '.html': 'text/html; charset=utf-8', '.js': 'application/javascript', '.mjs': 'application/javascript', '.css': 'text/css', '.json': 'application/json', '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.svg': 'image/svg+xml', '.woff': 'font/woff', '.woff2': 'font/woff2', '.ttf': 'font/ttf', '.eot': 'application/vnd.ms-fontobject', }; function getMimeType(filePath) { const ext = path.extname(filePath).toLowerCase(); return mimeTypes[ext] || 'application/octet-stream'; } function serveFile(res, filePath, statusCode = 200) { try { const content = fs.readFileSync(filePath); const mimeType = getMimeType(filePath); res.writeHead(statusCode, { 'Content-Type': mimeType, 'Cache-Control': 'public, max-age=3600', 'Access-Control-Allow-Origin': '*', }); res.end(content); } catch (err) { console.error(`Error serving file ${filePath}:`, err); res.writeHead(500, { 'Content-Type': 'text/plain' }); res.end('Internal Server Error'); } } const server = http.createServer((req, res) => { // Set CORS headers res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type'); if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } // Remove query string from URL const urlPath = req.url.split('?')[0]; // API routes (production) if (urlPath === '/api/ai/ping' && req.method === 'GET') { return sendJson(res, 200, ai.ping()); } if (urlPath === '/api/ai/mix' && req.method === 'POST') { return (async () => { try { const raw = await readBody(req); const parsed = raw ? JSON.parse(raw) : {}; const result = await ai.mix({ substances: parsed.substances || [], temperatureC: parsed.temperatureC, }); return sendJson(res, 200, result); } catch (err) { const triedModels = err?.triedModels || []; const lastModel = err?.lastModel || null; if (err instanceof UpstreamError) { if (err.status === 401 || err.status === 403) { return sendJson(res, 401, { error: 'API key Groq invalide ou non autorisee', triedModels, lastModel }); } if (err.status === 429) { return sendJson(res, 429, { error: 'Quota/limite Groq atteinte (429). Reessaye plus tard.', triedModels, lastModel }); } if (err.status >= 400 && err.status < 500) { return sendJson(res, err.status, { error: err.upstreamMessage || err.message, triedModels, lastModel }); } return sendJson(res, 502, { error: 'Erreur upstream Groq lors de la generation IA', details: err.message, triedModels, lastModel }); } return sendJson(res, 500, { error: 'Erreur serveur /api/ai/mix', details: err?.message || String(err), triedModels, lastModel }); } })(); } let filePath = path.join(distDir, urlPath === '/' ? 'index.html' : urlPath); // Normalize path to prevent directory traversal filePath = path.normalize(filePath); if (!filePath.startsWith(distDir)) { res.writeHead(403, { 'Content-Type': 'text/plain' }); res.end('Forbidden'); return; } // Check if file exists try { const stats = fs.statSync(filePath); if (stats.isFile()) { serveFile(res, filePath); } else if (stats.isDirectory()) { // Try to serve index.html from the directory const indexPath = path.join(filePath, 'index.html'); if (fs.existsSync(indexPath)) { serveFile(res, indexPath); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } } } catch (err) { // File not found, serve index.html for SPA routing const indexPath = path.join(distDir, 'index.html'); if (fs.existsSync(indexPath)) { serveFile(res, indexPath, 200); } else { res.writeHead(404, { 'Content-Type': 'text/plain' }); res.end('Not Found'); } } }); server.listen(PORT, HOST, () => { console.log(`\nāœ“ Server is running!`); console.log(`āœ“ Listening on http://${HOST}:${PORT}`); console.log(`āœ“ Environment: ${process.env.NODE_ENV || 'development'}`); }); process.on('SIGTERM', () => { console.log('SIGTERM received, shutting down gracefully...'); server.close(() => { console.log('Server closed'); process.exit(0); }); }); process.on('SIGINT', () => { console.log('SIGINT received, shutting down gracefully...'); server.close(() => { console.log('Server closed'); process.exit(0); }); });