// ======================================== // FICHIER: lib/error-reporting.js - CONVERTI POUR NODE.JS // Description: Système de validation et rapport d'erreur // ======================================== // Lazy loading des modules externes let nodemailer; const fs = require('fs').promises; const path = require('path'); const pino = require('pino'); const pretty = require('pino-pretty'); const { PassThrough } = require('stream'); const WebSocket = require('ws'); // Configuration (Google Sheets logging removed) // WebSocket server for real-time logs let wsServer; const wsClients = new Set(); // Enhanced Pino logger configuration with real-time streaming and dated files const now = new Date(); const timestamp = now.toISOString().slice(0, 10) + '_' + now.toLocaleTimeString('fr-FR').replace(/:/g, '-'); const logFile = path.join(__dirname, '..', 'logs', `seo-generator-${timestamp}.log`); const prettyStream = pretty({ colorize: true, translateTime: 'HH:MM:ss.l', ignore: 'pid,hostname', }); const tee = new PassThrough(); // Lazy loading des pipes console (évite blocage à l'import) let consolePipeInitialized = false; // File destination with dated filename - FORCE DEBUG LEVEL const fileDest = pino.destination({ dest: logFile, mkdir: true, sync: false, minLength: 0 // Force immediate write even for small logs }); tee.pipe(fileDest); // Custom levels for Pino to include TRACE, PROMPT, and LLM const customLevels = { trace: 5, // Below debug (10) debug: 10, info: 20, prompt: 25, // New level for prompts (between info and warn) llm: 26, // New level for LLM interactions (between prompt and warn) warn: 30, error: 40, fatal: 50 }; // Pino logger instance with enhanced configuration and custom levels const logger = pino( { level: 'debug', // FORCE DEBUG LEVEL for file logging base: undefined, timestamp: pino.stdTimeFunctions.isoTime, customLevels: customLevels, useOnlyCustomLevels: true }, tee ); // Initialize WebSocket server (only when explicitly requested) function initWebSocketServer() { if (!wsServer && process.env.ENABLE_LOG_WS === 'true') { try { const logPort = process.env.LOG_WS_PORT || 8082; wsServer = new WebSocket.Server({ port: logPort }); wsServer.on('connection', (ws) => { wsClients.add(ws); logger.info('Client connected to log WebSocket'); ws.on('close', () => { wsClients.delete(ws); logger.info('Client disconnected from log WebSocket'); }); ws.on('error', (error) => { logger.error('WebSocket error:', error.message); wsClients.delete(ws); }); }); wsServer.on('error', (error) => { if (error.code === 'EADDRINUSE') { logger.warn(`WebSocket port ${logPort} already in use`); wsServer = null; } else { logger.error('WebSocket server error:', error.message); } }); logger.info(`Log WebSocket server started on port ${logPort}`); } catch (error) { logger.warn(`Failed to start WebSocket server: ${error.message}`); wsServer = null; } } } // Broadcast log to WebSocket clients function broadcastLog(message, level) { const logData = { timestamp: new Date().toISOString(), level: level.toUpperCase(), message: message }; wsClients.forEach(ws => { if (ws.readyState === WebSocket.OPEN) { try { ws.send(JSON.stringify(logData)); } catch (error) { logger.error('Failed to send log to WebSocket client:', error.message); wsClients.delete(ws); } } }); } // 🔄 NODE.JS : Google Sheets API setup (remplace SpreadsheetApp) // Google Sheets integration removed for export async function logSh(message, level = 'INFO') { // Initialize WebSocket server if not already done if (!wsServer) { initWebSocketServer(); } // Initialize console pipe if needed (lazy loading) if (!consolePipeInitialized && process.env.ENABLE_CONSOLE_LOG === 'true') { tee.pipe(prettyStream).pipe(process.stdout); consolePipeInitialized = true; } // Convert level to lowercase for Pino const pinoLevel = level.toLowerCase(); // Enhanced trace metadata for hierarchical logging const traceData = {}; if (message.includes('▶') || message.includes('✔') || message.includes('✖') || message.includes('•')) { traceData.trace = true; traceData.evt = message.includes('▶') ? 'span.start' : message.includes('✔') ? 'span.end' : message.includes('✖') ? 'span.error' : 'span.event'; } // Log with Pino (handles console output with pretty formatting and file logging) switch (pinoLevel) { case 'error': logger.error(traceData, message); break; case 'warning': case 'warn': logger.warn(traceData, message); break; case 'debug': logger.debug(traceData, message); break; case 'trace': logger.trace(traceData, message); break; case 'prompt': logger.prompt(traceData, message); break; case 'llm': logger.llm(traceData, message); break; default: logger.info(traceData, message); } // Broadcast to WebSocket clients for real-time viewing broadcastLog(message, level); // Force immediate flush to ensure real-time display and prevent log loss logger.flush(); // Google Sheets logging removed for export } // Fonction pour déterminer si on doit logger en console function shouldLogToConsole(messageLevel, configLevel) { const levels = { DEBUG: 0, INFO: 1, WARNING: 2, ERROR: 3 }; return levels[messageLevel] >= levels[configLevel]; } // Log to file is now handled by Pino transport // This function is kept for compatibility but does nothing async function logToFile(message, level) { // Pino handles file logging via transport configuration // This function is deprecated and kept for compatibility only } // 🔄 NODE.JS : Log vers Google Sheets (version async) // Google Sheets logging functions removed for export // 🔄 NODE.JS : Version simplifiée cleanLogSheet async function cleanLogSheet() { try { logSh('🧹 Nettoyage logs...', 'INFO'); // 1. Nettoyer fichiers logs locaux (garder 7 derniers jours) await cleanLocalLogs(); logSh('✅ Logs nettoyés', 'INFO'); } catch (error) { logSh('Erreur nettoyage logs: ' + error.message, 'ERROR'); } } async function cleanLocalLogs() { try { // Note: With Pino, log files are managed differently // This function is kept for compatibility with Google Sheets logs cleanup // Pino log rotation should be handled by external tools like logrotate // For now, we keep the basic cleanup for any remaining old log files const logsDir = path.join(__dirname, '../logs'); try { const files = await fs.readdir(logsDir); const cutoffDate = new Date(); cutoffDate.setDate(cutoffDate.getDate() - 7); // Garder 7 jours for (const file of files) { if (file.endsWith('.log')) { const filePath = path.join(logsDir, file); const stats = await fs.stat(filePath); if (stats.mtime < cutoffDate) { await fs.unlink(filePath); logSh(`🗑️ Supprimé log ancien: ${file}`, 'INFO'); } } } } catch (error) { // Directory might not exist, that's fine } } catch (error) { // Silent fail } } // cleanGoogleSheetsLogs function removed for export // ============= VALIDATION PRINCIPALE - IDENTIQUE ============= function validateWorkflowIntegrity(elements, generatedContent, finalXML, csvData) { logSh('🔍 >>> VALIDATION INTÉGRITÉ WORKFLOW <<<', 'INFO'); // Using logSh instead of console.log const errors = []; const warnings = []; const stats = { elementsExtracted: elements.length, contentGenerated: Object.keys(generatedContent).length, tagsReplaced: 0, tagsRemaining: 0 }; // TEST 1: Détection tags dupliqués const duplicateCheck = detectDuplicateTags(elements); if (duplicateCheck.hasDuplicates) { errors.push({ type: 'DUPLICATE_TAGS', severity: 'HIGH', message: `Tags dupliqués détectés: ${duplicateCheck.duplicates.join(', ')}`, impact: 'Certains contenus ne seront pas remplacés dans le XML final', suggestion: 'Vérifier le template XML pour corriger la structure' }); } // TEST 2: Cohérence éléments extraits vs générés const missingGeneration = elements.filter(el => !generatedContent[el.originalTag]); if (missingGeneration.length > 0) { errors.push({ type: 'MISSING_GENERATION', severity: 'HIGH', message: `${missingGeneration.length} éléments extraits mais non générés`, details: missingGeneration.map(el => el.originalTag), impact: 'Contenu incomplet dans le XML final' }); } // TEST 3: Tags non remplacés dans XML final const remainingTags = (finalXML.match(/\|[^|]*\|/g) || []); stats.tagsRemaining = remainingTags.length; if (remainingTags.length > 0) { errors.push({ type: 'UNREPLACED_TAGS', severity: 'HIGH', message: `${remainingTags.length} tags non remplacés dans le XML final`, details: remainingTags.slice(0, 5), impact: 'XML final contient des placeholders non remplacés' }); } // TEST 4: Variables CSV manquantes const missingVars = detectMissingCSVVariables(csvData); if (missingVars.length > 0) { warnings.push({ type: 'MISSING_CSV_VARIABLES', severity: 'MEDIUM', message: `Variables CSV manquantes: ${missingVars.join(', ')}`, impact: 'Système de génération de mots-clés automatique activé' }); } // TEST 5: Qualité génération IA const generationQuality = assessGenerationQuality(generatedContent); if (generationQuality.errorRate > 0.1) { warnings.push({ type: 'GENERATION_QUALITY', severity: 'MEDIUM', message: `${(generationQuality.errorRate * 100).toFixed(1)}% d'erreurs de génération IA`, impact: 'Qualité du contenu potentiellement dégradée' }); } // CALCUL STATS FINALES stats.tagsReplaced = elements.length - remainingTags.length; stats.successRate = stats.elementsExtracted > 0 ? ((stats.tagsReplaced / elements.length) * 100).toFixed(1) : '100'; const report = { timestamp: new Date().toISOString(), csvData: { mc0: csvData.mc0, t0: csvData.t0 }, stats: stats, errors: errors, warnings: warnings, status: errors.length === 0 ? 'SUCCESS' : 'ERROR' }; const logLevel = report.status === 'SUCCESS' ? 'INFO' : 'ERROR'; logSh(`✅ Validation terminée: ${report.status} (${errors.length} erreurs, ${warnings.length} warnings)`, 'INFO'); // Using logSh instead of console.log // ENVOYER RAPPORT SI ERREURS (async en arrière-plan) if (errors.length > 0 || warnings.length > 2) { sendErrorReport(report).catch(err => { logSh('Erreur envoi rapport: ' + err.message, 'ERROR'); // Using logSh instead of console.error }); } return report; } // ============= HELPERS - IDENTIQUES ============= function detectDuplicateTags(elements) { const tagCounts = {}; const duplicates = []; elements.forEach(element => { const tag = element.originalTag; tagCounts[tag] = (tagCounts[tag] || 0) + 1; if (tagCounts[tag] === 2) { duplicates.push(tag); logSh(`❌ DUPLICATE détecté: ${tag}`, 'ERROR'); // Using logSh instead of console.error } }); return { hasDuplicates: duplicates.length > 0, duplicates: duplicates, counts: tagCounts }; } function detectMissingCSVVariables(csvData) { const missing = []; if (!csvData.mcPlus1 || csvData.mcPlus1.split(',').length < 4) { missing.push('MC+1 (insuffisant)'); } if (!csvData.tPlus1 || csvData.tPlus1.split(',').length < 4) { missing.push('T+1 (insuffisant)'); } if (!csvData.lPlus1 || csvData.lPlus1.split(',').length < 4) { missing.push('L+1 (insuffisant)'); } return missing; } function assessGenerationQuality(generatedContent) { let errorCount = 0; let totalCount = Object.keys(generatedContent).length; Object.values(generatedContent).forEach(content => { if (content && ( content.includes('[ERREUR') || content.includes('ERROR') || content.length < 10 )) { errorCount++; } }); return { errorRate: totalCount > 0 ? errorCount / totalCount : 0, totalGenerated: totalCount, errorsFound: errorCount }; } // 🔄 NODE.JS : Email avec nodemailer (remplace MailApp) async function sendErrorReport(report) { try { logSh('📧 Envoi rapport d\'erreur par email...', 'INFO'); // Using logSh instead of console.log // Lazy load nodemailer seulement quand nécessaire if (!nodemailer) { nodemailer = require('nodemailer'); } // Configuration nodemailer (Gmail par exemple) const transporter = nodemailer.createTransport({ service: 'gmail', auth: { user: process.env.EMAIL_USER, // 'your-email@gmail.com' pass: process.env.EMAIL_APP_PASSWORD // App password Google } }); const subject = `Erreur Workflow SEO Node.js - ${report.status} - ${report.csvData.mc0}`; const htmlBody = createHTMLReport(report); const mailOptions = { from: process.env.EMAIL_USER, to: 'alexistrouve.pro@gmail.com', subject: subject, html: htmlBody, attachments: [{ filename: `error-report-${Date.now()}.json`, content: JSON.stringify(report, null, 2), contentType: 'application/json' }] }; await transporter.sendMail(mailOptions); logSh('✅ Rapport d\'erreur envoyé par email', 'INFO'); // Using logSh instead of console.log } catch (error) { logSh(`❌ Échec envoi email: ${error.message}`, 'ERROR'); // Using logSh instead of console.error } } // ============= HTML REPORT - IDENTIQUE ============= function createHTMLReport(report) { const statusColor = report.status === 'SUCCESS' ? '#28a745' : '#dc3545'; let html = `
Statut: ${report.status}
Article: ${report.csvData.t0}
Mot-clé: ${report.csvData.mc0}
Taux de réussite: ${report.stats.successRate}%
Timestamp: ${report.timestamp}
Plateforme: Node.js Server
Message: ${error.message}
Impact: ${error.impact}
${error.suggestion ? `Solution: ${error.suggestion}
` : ''}${warning.message}