// ======================================== // FICHIER: ManualServer.js // RESPONSABILITÉ: Mode MANUAL - Interface Client + API + WebSocket // FONCTIONNALITÉS: Dashboard, tests modulaires, API complète // ======================================== const express = require('express'); const cors = require('cors'); const path = require('path'); const WebSocket = require('ws'); const { logSh } = require('../ErrorReporting'); const { handleModularWorkflow, benchmarkStacks } = require('../Main'); const { APIController } = require('../APIController'); /** * SERVEUR MODE MANUAL * Interface client complète avec API, WebSocket et dashboard */ class ManualServer { constructor(options = {}) { this.config = { port: options.port || process.env.MANUAL_PORT || 3000, wsPort: options.wsPort || process.env.WS_PORT || 8081, host: options.host || '0.0.0.0', ...options }; this.app = null; this.server = null; this.wsServer = null; this.activeClients = new Set(); this.stats = { sessions: 0, requests: 0, testsExecuted: 0, startTime: Date.now(), lastActivity: null }; this.isRunning = false; this.apiController = new APIController(); } // ======================================== // DÉMARRAGE ET ARRÊT // ======================================== /** * Démarre le serveur MANUAL complet */ async start() { if (this.isRunning) { logSh('⚠️ ManualServer déjà en cours d\'exécution', 'WARNING'); return; } logSh('🎯 Démarrage ManualServer...', 'INFO'); try { // 1. Configuration Express await this.setupExpressApp(); // 2. Routes API this.setupAPIRoutes(); // 3. Interface Web this.setupWebInterface(); // 4. WebSocket pour logs temps réel await this.setupWebSocketServer(); // 5. Démarrage serveur HTTP await this.startHTTPServer(); // 6. Monitoring this.startMonitoring(); this.isRunning = true; this.stats.startTime = Date.now(); logSh(`✅ ManualServer démarré sur http://localhost:${this.config.port}`, 'INFO'); logSh(`📡 WebSocket logs sur ws://localhost:${this.config.wsPort}`, 'INFO'); } catch (error) { logSh(`❌ Erreur démarrage ManualServer: ${error.message}`, 'ERROR'); await this.stop(); throw error; } } /** * Arrête le serveur MANUAL */ async stop() { if (!this.isRunning) return; logSh('🛑 Arrêt ManualServer...', 'INFO'); try { // Déconnecter tous les clients WebSocket this.disconnectAllClients(); // Arrêter WebSocket server if (this.wsServer) { this.wsServer.close(); this.wsServer = null; } // Arrêter serveur HTTP if (this.server) { await new Promise((resolve) => { this.server.close(() => resolve()); }); this.server = null; } this.isRunning = false; logSh('✅ ManualServer arrêté', 'INFO'); } catch (error) { logSh(`⚠️ Erreur arrêt ManualServer: ${error.message}`, 'WARNING'); } } // ======================================== // CONFIGURATION EXPRESS // ======================================== /** * Configure l'application Express */ async setupExpressApp() { this.app = express(); // Middleware de base this.app.use(express.json({ limit: '10mb' })); this.app.use(express.urlencoded({ extended: true })); this.app.use(cors()); // Middleware de logs des requêtes this.app.use((req, res, next) => { this.stats.requests++; this.stats.lastActivity = Date.now(); logSh(`📥 ${req.method} ${req.path} - ${req.ip}`, 'TRACE'); next(); }); // Fichiers statiques this.app.use(express.static(path.join(__dirname, '../../public'))); // Route spécifique pour l'interface step-by-step this.app.get('/step-by-step', (req, res) => { res.sendFile(path.join(__dirname, '../../public/step-by-step.html')); }); logSh('⚙️ Express configuré', 'DEBUG'); } /** * Configure les routes API */ setupAPIRoutes() { // Route de status this.app.get('/api/status', (req, res) => { res.json({ success: true, mode: 'MANUAL', status: 'running', uptime: Date.now() - this.stats.startTime, stats: { ...this.stats }, clients: this.activeClients.size, timestamp: new Date().toISOString() }); }); // Test modulaire individuel this.app.post('/api/test-modulaire', async (req, res) => { await this.handleTestModulaire(req, res); }); // 🆕 Workflow modulaire avec sauvegarde par étapes this.app.post('/api/workflow-modulaire', async (req, res) => { await this.handleWorkflowModulaire(req, res); }); // Benchmark modulaire complet this.app.post('/api/benchmark-modulaire', async (req, res) => { await this.handleBenchmarkModulaire(req, res); }); // Configuration modulaire disponible this.app.get('/api/modulaire-config', (req, res) => { this.handleModulaireConfig(req, res); }); // Stats détaillées this.app.get('/api/stats', (req, res) => { res.json({ success: true, stats: { ...this.stats, uptime: Date.now() - this.stats.startTime, activeClients: this.activeClients.size, memory: process.memoryUsage(), timestamp: new Date().toISOString() } }); }); // Lancer le log viewer avec WebSocket this.app.post('/api/start-log-viewer', (req, res) => { this.handleStartLogViewer(req, res); }); // ======================================== // APIs STEP-BY-STEP // ======================================== // Initialiser une session step-by-step this.app.post('/api/step-by-step/init', async (req, res) => { await this.handleStepByStepInit(req, res); }); // Exécuter une étape this.app.post('/api/step-by-step/execute', async (req, res) => { await this.handleStepByStepExecute(req, res); }); // Status d'une session this.app.get('/api/step-by-step/status/:sessionId', (req, res) => { this.handleStepByStepStatus(req, res); }); // Reset une session this.app.post('/api/step-by-step/reset', (req, res) => { this.handleStepByStepReset(req, res); }); // Export résultats this.app.get('/api/step-by-step/export/:sessionId', (req, res) => { this.handleStepByStepExport(req, res); }); // Liste des sessions actives this.app.get('/api/step-by-step/sessions', (req, res) => { this.handleStepByStepSessions(req, res); }); // API pour récupérer les personnalités this.app.get('/api/personalities', async (req, res) => { await this.handleGetPersonalities(req, res); }); // 🆕 API simple pour générer un article avec mot-clé this.app.post('/api/generate-simple', async (req, res) => { await this.handleGenerateSimple(req, res); }); // ======================================== // 🚀 NOUVEAUX ENDPOINTS API RESTful // ======================================== // === GESTION ARTICLES === this.app.get('/api/articles', async (req, res) => { await this.apiController.getArticles(req, res); }); this.app.get('/api/articles/:id', async (req, res) => { await this.apiController.getArticle(req, res); }); this.app.post('/api/articles', async (req, res) => { await this.apiController.createArticle(req, res); }); // === GESTION PROJETS === this.app.get('/api/projects', async (req, res) => { await this.apiController.getProjects(req, res); }); this.app.post('/api/projects', async (req, res) => { await this.apiController.createProject(req, res); }); // === GESTION TEMPLATES === this.app.get('/api/templates', async (req, res) => { await this.apiController.getTemplates(req, res); }); this.app.post('/api/templates', async (req, res) => { await this.apiController.createTemplate(req, res); }); // === CONFIGURATION === this.app.get('/api/config/personalities', async (req, res) => { await this.apiController.getPersonalitiesConfig(req, res); }); // === MONITORING === this.app.get('/api/health', async (req, res) => { await this.apiController.getHealth(req, res); }); this.app.get('/api/metrics', async (req, res) => { await this.apiController.getMetrics(req, res); }); // Gestion d'erreurs API this.app.use('/api/*', (error, req, res, next) => { logSh(`❌ Erreur API ${req.path}: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur serveur interne', message: error.message, timestamp: new Date().toISOString() }); }); logSh('🛠️ Routes API configurées', 'DEBUG'); } // ======================================== // HANDLERS API // ======================================== /** * Gère les tests modulaires individuels */ async handleTestModulaire(req, res) { try { const config = req.body; this.stats.testsExecuted++; logSh(`🧪 Test modulaire: ${config.selectiveStack} + ${config.adversarialMode} + ${config.humanSimulationMode} + ${config.patternBreakingMode}`, 'INFO'); // Validation des paramètres if (!config.rowNumber || config.rowNumber < 2) { return res.status(400).json({ success: false, error: 'Numéro de ligne invalide (minimum 2)' }); } // Exécution du test const result = await handleModularWorkflow({ ...config, source: 'manual_server_api' }); logSh(`✅ Test modulaire terminé: ${result.stats.totalDuration}ms`, 'INFO'); res.json({ success: true, message: 'Test modulaire terminé avec succès', stats: result.stats, config: config, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur test modulaire: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message, config: req.body, timestamp: new Date().toISOString() }); } } /** * Gère les benchmarks modulaires */ async handleBenchmarkModulaire(req, res) { try { const { rowNumber = 2 } = req.body; logSh(`📊 Benchmark modulaire ligne ${rowNumber}...`, 'INFO'); if (rowNumber < 2) { return res.status(400).json({ success: false, error: 'Numéro de ligne invalide (minimum 2)' }); } const benchResults = await benchmarkStacks(rowNumber); const successfulTests = benchResults.filter(r => r.success); const avgDuration = successfulTests.length > 0 ? successfulTests.reduce((sum, r) => sum + r.duration, 0) / successfulTests.length : 0; this.stats.testsExecuted += benchResults.length; logSh(`📊 Benchmark terminé: ${successfulTests.length}/${benchResults.length} tests réussis`, 'INFO'); res.json({ success: true, message: `Benchmark terminé: ${successfulTests.length}/${benchResults.length} tests réussis`, summary: { totalTests: benchResults.length, successfulTests: successfulTests.length, failedTests: benchResults.length - successfulTests.length, averageDuration: Math.round(avgDuration), rowNumber: rowNumber }, results: benchResults, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur benchmark modulaire: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message, timestamp: new Date().toISOString() }); } } /** * 🆕 Gère les workflows modulaires avec sauvegarde par étapes */ async handleWorkflowModulaire(req, res) { try { const config = req.body; this.stats.testsExecuted++; // Configuration par défaut avec sauvegarde activée const workflowConfig = { rowNumber: config.rowNumber || 2, selectiveStack: config.selectiveStack || 'standardEnhancement', adversarialMode: config.adversarialMode || 'light', humanSimulationMode: config.humanSimulationMode || 'none', patternBreakingMode: config.patternBreakingMode || 'none', saveIntermediateSteps: config.saveIntermediateSteps !== false, // Par défaut true source: 'api_manual_server' }; logSh(`🔗 Workflow modulaire avec étapes: ligne ${workflowConfig.rowNumber}`, 'INFO'); logSh(` 📋 Config: ${workflowConfig.selectiveStack} + ${workflowConfig.adversarialMode} + ${workflowConfig.humanSimulationMode} + ${workflowConfig.patternBreakingMode}`, 'DEBUG'); logSh(` 💾 Sauvegarde étapes: ${workflowConfig.saveIntermediateSteps ? 'ACTIVÉE' : 'DÉSACTIVÉE'}`, 'INFO'); // Validation des paramètres if (workflowConfig.rowNumber < 2) { return res.status(400).json({ success: false, error: 'Numéro de ligne invalide (minimum 2)' }); } // Exécution du workflow complet const startTime = Date.now(); const result = await handleModularWorkflow(workflowConfig); const duration = Date.now() - startTime; // Statistiques finales const finalStats = { duration, success: result.success, versionsCreated: result.stats?.versionHistory?.length || 1, parentArticleId: result.stats?.parentArticleId, finalArticleId: result.storageResult?.articleId, totalModifications: { selective: result.stats?.selectiveEnhancements || 0, adversarial: result.stats?.adversarialModifications || 0, human: result.stats?.humanSimulationModifications || 0, pattern: result.stats?.patternBreakingModifications || 0 }, finalLength: result.stats?.finalLength || 0 }; logSh(`✅ Workflow modulaire terminé: ${finalStats.versionsCreated} versions créées en ${duration}ms`, 'INFO'); res.json({ success: true, message: `Workflow modulaire terminé avec succès (${finalStats.versionsCreated} versions sauvegardées)`, config: workflowConfig, stats: finalStats, versionHistory: result.stats?.versionHistory, result: { parentArticleId: finalStats.parentArticleId, finalArticleId: finalStats.finalArticleId, duration: finalStats.duration, modificationsCount: Object.values(finalStats.totalModifications).reduce((sum, val) => sum + val, 0), finalWordCount: result.storageResult?.wordCount, personality: result.stats?.personality }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur workflow modulaire: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message, timestamp: new Date().toISOString() }); } } /** * Retourne la configuration modulaire */ handleModulaireConfig(req, res) { try { const config = { selectiveStacks: [ { value: 'lightEnhancement', name: 'Light Enhancement', description: 'Améliorations légères' }, { value: 'standardEnhancement', name: 'Standard Enhancement', description: 'Améliorations standard' }, { value: 'fullEnhancement', name: 'Full Enhancement', description: 'Améliorations complètes' }, { value: 'personalityFocus', name: 'Personality Focus', description: 'Focus personnalité' }, { value: 'fluidityFocus', name: 'Fluidity Focus', description: 'Focus fluidité' }, { value: 'adaptive', name: 'Adaptive', description: 'Adaptation automatique' } ], adversarialModes: [ { value: 'none', name: 'None', description: 'Aucune technique adversariale' }, { value: 'light', name: 'Light', description: 'Techniques adversariales légères' }, { value: 'standard', name: 'Standard', description: 'Techniques adversariales standard' }, { value: 'heavy', name: 'Heavy', description: 'Techniques adversariales intensives' }, { value: 'adaptive', name: 'Adaptive', description: 'Adaptation automatique' } ], humanSimulationModes: [ { value: 'none', name: 'None', description: 'Aucune simulation humaine' }, { value: 'lightSimulation', name: 'Light Simulation', description: 'Simulation légère' }, { value: 'standardSimulation', name: 'Standard Simulation', description: 'Simulation standard' }, { value: 'heavySimulation', name: 'Heavy Simulation', description: 'Simulation intensive' }, { value: 'adaptiveSimulation', name: 'Adaptive Simulation', description: 'Simulation adaptative' }, { value: 'personalityFocus', name: 'Personality Focus', description: 'Focus personnalité' }, { value: 'temporalFocus', name: 'Temporal Focus', description: 'Focus temporel' } ], patternBreakingModes: [ { value: 'none', name: 'None', description: 'Aucun pattern breaking' }, { value: 'lightPatternBreaking', name: 'Light Pattern Breaking', description: 'Pattern breaking léger' }, { value: 'standardPatternBreaking', name: 'Standard Pattern Breaking', description: 'Pattern breaking standard' }, { value: 'heavyPatternBreaking', name: 'Heavy Pattern Breaking', description: 'Pattern breaking intensif' }, { value: 'adaptivePatternBreaking', name: 'Adaptive Pattern Breaking', description: 'Pattern breaking adaptatif' }, { value: 'syntaxFocus', name: 'Syntax Focus', description: 'Focus syntaxe uniquement' }, { value: 'connectorsFocus', name: 'Connectors Focus', description: 'Focus connecteurs uniquement' } ] }; res.json({ success: true, config: config, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur config modulaire: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: error.message }); } } /** * Lance le log viewer avec WebSocket */ handleStartLogViewer(req, res) { try { const { spawn } = require('child_process'); const path = require('path'); const os = require('os'); // Démarrer le WebSocket pour logs process.env.ENABLE_LOG_WS = 'true'; const { initWebSocketServer } = require('../ErrorReporting'); initWebSocketServer(); // Servir le log viewer via une route HTTP au lieu d'un fichier local const logViewerUrl = `http://localhost:${this.config.port}/logs-viewer.html`; // Ouvrir dans le navigateur selon l'OS let command, args; switch (os.platform()) { case 'darwin': // macOS command = 'open'; args = [logViewerUrl]; break; case 'win32': // Windows command = 'cmd'; args = ['/c', 'start', logViewerUrl]; break; default: // Linux et WSL // Pour WSL, utiliser explorer.exe de Windows if (process.env.WSL_DISTRO_NAME) { command = '/mnt/c/Windows/System32/cmd.exe'; args = ['/c', 'start', logViewerUrl]; } else { command = 'xdg-open'; args = [logViewerUrl]; } break; } spawn(command, args, { detached: true, stdio: 'ignore' }); const logPort = process.env.LOG_WS_PORT || 8082; logSh(`🌐 Log viewer lancé avec WebSocket sur port ${logPort}`, 'INFO'); res.json({ success: true, message: 'Log viewer lancé', wsPort: logPort, viewerUrl: logViewerUrl, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur lancement log viewer: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lancement log viewer', message: error.message, timestamp: new Date().toISOString() }); } } // ======================================== // HANDLERS STEP-BY-STEP // ======================================== /** * Initialise une nouvelle session step-by-step */ async handleStepByStepInit(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const inputData = req.body; logSh(`🎯 Initialisation session step-by-step`, 'INFO'); logSh(` Input: ${JSON.stringify(inputData)}`, 'DEBUG'); const session = sessionManager.createSession(inputData); res.json({ success: true, sessionId: session.id, steps: session.steps.map(step => ({ id: step.id, system: step.system, name: step.name, description: step.description, status: step.status })), inputData: session.inputData, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur init step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur initialisation session', message: error.message, timestamp: new Date().toISOString() }); } } /** * Exécute une étape */ async handleStepByStepExecute(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const { StepExecutor } = require('../StepExecutor'); const { sessionId, stepId, options = {} } = req.body; if (!sessionId || !stepId) { return res.status(400).json({ success: false, error: 'sessionId et stepId requis', timestamp: new Date().toISOString() }); } logSh(`🚀 Exécution étape ${stepId} pour session ${sessionId}`, 'INFO'); // Récupérer la session const session = sessionManager.getSession(sessionId); // Trouver l'étape const step = session.steps.find(s => s.id === stepId); if (!step) { return res.status(400).json({ success: false, error: `Étape ${stepId} introuvable`, timestamp: new Date().toISOString() }); } // Marquer l'étape comme en cours step.status = 'executing'; // Créer l'exécuteur et lancer l'étape const executor = new StepExecutor(); logSh(`🚀 Execution step ${step.system} avec données: ${JSON.stringify(session.inputData)}`, 'DEBUG'); // Récupérer le contenu de l'étape précédente pour chaînage let inputContent = null; if (stepId > 1) { const previousResult = session.results.find(r => r.stepId === stepId - 1); logSh(`🔍 DEBUG Chaînage: previousResult=${!!previousResult}`, 'DEBUG'); if (previousResult) { logSh(`🔍 DEBUG Chaînage: previousResult.result=${!!previousResult.result}`, 'DEBUG'); if (previousResult.result) { // StepExecutor retourne un objet avec une propriété 'content' if (previousResult.result.content) { inputContent = previousResult.result.content; logSh(`🔄 Chaînage: utilisation contenu.content étape ${stepId - 1}`, 'DEBUG'); } else { // Fallback si c'est juste le contenu directement inputContent = previousResult.result; logSh(`🔄 Chaînage: utilisation contenu direct étape ${stepId - 1}`, 'DEBUG'); } logSh(`🔍 DEBUG: inputContent type=${typeof inputContent}, keys=${Object.keys(inputContent || {})}`, 'DEBUG'); } else { logSh(`🚨 DEBUG: previousResult.result est vide ou null !`, 'ERROR'); } } else { logSh(`🚨 DEBUG: Pas de previousResult trouvé pour stepId=${stepId - 1}`, 'ERROR'); } } // Ajouter le contenu d'entrée aux options si disponible const executionOptions = { ...options, inputContent: inputContent }; const result = await executor.executeStep(step.system, session.inputData, executionOptions); logSh(`📊 Résultat step ${step.system}: success=${result.success}, content=${Object.keys(result.content || {}).length} éléments, duration=${result.stats?.duration}ms`, 'INFO'); // Si pas d'erreur mais temps < 100ms, forcer une erreur pour debug if (result.success && result.stats?.duration < 100) { logSh(`⚠️ WARN: Step trop rapide (${result.stats?.duration}ms), probablement pas d'appel LLM réel`, 'WARN'); result.debugWarning = `⚠️ Exécution suspecte: ${result.stats?.duration}ms (probablement pas d'appel LLM)`; } // Ajouter le résultat à la session sessionManager.addStepResult(sessionId, stepId, result); // Déterminer la prochaine étape const nextStep = session.steps.find(s => s.id === stepId + 1); res.json({ success: true, stepId: stepId, system: step.system, name: step.name, result: { success: result.success, content: result.result, formatted: result.formatted, xmlFormatted: result.xmlFormatted, error: result.error, debugWarning: result.debugWarning }, stats: result.stats, nextStep: nextStep ? nextStep.id : null, sessionStatus: session.status, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur exécution step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur exécution étape', message: error.message, timestamp: new Date().toISOString() }); } } /** * Récupère le status d'une session */ handleStepByStepStatus(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const { sessionId } = req.params; const session = sessionManager.getSession(sessionId); res.json({ success: true, session: { id: session.id, status: session.status, createdAt: new Date(session.createdAt).toISOString(), currentStep: session.currentStep, completedSteps: session.completedSteps, totalSteps: session.steps.length, inputData: session.inputData, steps: session.steps, globalStats: session.globalStats, results: session.results.map(r => ({ stepId: r.stepId, system: r.system, success: r.success, timestamp: new Date(r.timestamp).toISOString(), stats: r.stats })) }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur status step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération status', message: error.message, timestamp: new Date().toISOString() }); } } /** * Reset une session */ handleStepByStepReset(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const { sessionId } = req.body; if (!sessionId) { return res.status(400).json({ success: false, error: 'sessionId requis', timestamp: new Date().toISOString() }); } const session = sessionManager.resetSession(sessionId); res.json({ success: true, sessionId: session.id, message: 'Session reset avec succès', steps: session.steps, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur reset step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur reset session', message: error.message, timestamp: new Date().toISOString() }); } } /** * Export les résultats d'une session */ handleStepByStepExport(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const { sessionId } = req.params; const exportData = sessionManager.exportSession(sessionId); res.setHeader('Content-Type', 'application/json'); res.setHeader('Content-Disposition', `attachment; filename="step-by-step-${sessionId}.json"`); res.json(exportData); } catch (error) { logSh(`❌ Erreur export step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur export session', message: error.message, timestamp: new Date().toISOString() }); } } /** * Liste les sessions actives */ handleStepByStepSessions(req, res) { try { const { sessionManager } = require('../StepByStepSessionManager'); const sessions = sessionManager.listSessions(); res.json({ success: true, sessions: sessions, total: sessions.length, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur list sessions step-by-step: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération sessions', message: error.message, timestamp: new Date().toISOString() }); } } /** * Handler pour récupérer les personnalités disponibles */ async handleGetPersonalities(req, res) { try { const { getPersonalities } = require('../BrainConfig'); const personalities = await getPersonalities(); res.json({ success: true, personalities: personalities || [], total: (personalities || []).length, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération personnalités: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur récupération personnalités', message: error.message, timestamp: new Date().toISOString() }); } } /** * 🆕 Handler pour génération simple d'article avec mot-clé */ async handleGenerateSimple(req, res) { try { const { keyword } = req.body; // Validation basique if (!keyword || typeof keyword !== 'string' || keyword.trim().length === 0) { return res.status(400).json({ success: false, error: 'Mot-clé requis', message: 'Le paramètre "keyword" est obligatoire et doit être une chaîne non vide' }); } const cleanKeyword = keyword.trim(); logSh(`🎯 Génération simple pour mot-clé: "${cleanKeyword}"`, 'INFO'); // Créer un template XML simple basé sur le mot-clé const simpleTemplate = `

|Titre_Principal{{${cleanKeyword}}}{Rédige un titre H1 accrocheur pour "${cleanKeyword}"}|

|Introduction{{${cleanKeyword}}}{Rédige une introduction engageante de 2-3 phrases pour "${cleanKeyword}"}|

|Sous_Titre_1{{${cleanKeyword}}}{Rédige un sous-titre H2 pour "${cleanKeyword}"}|

|Contenu_1{{${cleanKeyword}}}{Rédige un paragraphe détaillé sur "${cleanKeyword}"}|

|Sous_Titre_2{{${cleanKeyword}}}{Rédige un autre sous-titre H2 pour "${cleanKeyword}"}|

|Contenu_2{{${cleanKeyword}}}{Rédige un autre paragraphe sur "${cleanKeyword}"}|
|Conclusion{{${cleanKeyword}}}{Rédige une conclusion pour l'article sur "${cleanKeyword}"}|
`; // Préparer les données pour le workflow const workflowData = { csvData: { mc0: cleanKeyword, t0: `Guide complet sur ${cleanKeyword}`, personality: { nom: 'Marc', style: 'professionnel' }, tMinus1: cleanKeyword, mcPlus1: `${cleanKeyword},guide ${cleanKeyword},tout savoir ${cleanKeyword}`, tPlus1: `Guide ${cleanKeyword},Conseils ${cleanKeyword},${cleanKeyword} pratique` }, xmlTemplate: Buffer.from(simpleTemplate).toString('base64'), source: 'api_generate_simple' }; logSh(`📝 Template créé pour "${cleanKeyword}"`, 'DEBUG'); // Utiliser le workflow modulaire simple (juste génération de base) const { handleModularWorkflow } = require('../Main'); const config = { selectiveStack: 'lightEnhancement', adversarialMode: 'none', humanSimulationMode: 'none', patternBreakingMode: 'none', saveVersions: false, source: 'api_generate_simple' }; logSh(`🚀 Démarrage génération modulaire pour "${cleanKeyword}"`, 'INFO'); const result = await handleModularWorkflow(workflowData, config); logSh(`✅ Génération terminée pour "${cleanKeyword}"`, 'INFO'); // Réponse simplifiée res.json({ success: true, keyword: cleanKeyword, article: { content: result.compiledText || result.generatedTexts || 'Contenu généré', title: result.generatedTexts?.Titre_Principal || `Article sur ${cleanKeyword}`, meta: { processing_time: result.processingTime || 'N/A', personality: result.personality?.nom || 'Marc', version: result.version || 'v1.0' } }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur génération simple: ${error.message}`, 'ERROR'); logSh(`Stack: ${error.stack}`, 'DEBUG'); res.status(500).json({ success: false, error: 'Erreur lors de la génération', message: error.message, timestamp: new Date().toISOString() }); } } // ======================================== // INTERFACE WEB // ======================================== /** * Configure l'interface web */ setupWebInterface() { // Page d'accueil - Dashboard MANUAL this.app.get('/', (req, res) => { res.send(this.generateManualDashboard()); }); // Route pour le log viewer this.app.get('/logs-viewer.html', (req, res) => { const fs = require('fs'); const logViewerPath = path.join(__dirname, '../../tools/logs-viewer.html'); try { const content = fs.readFileSync(logViewerPath, 'utf-8'); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(content); } catch (error) { logSh(`❌ Erreur lecture log viewer: ${error.message}`, 'ERROR'); res.status(500).send(`Erreur: ${error.message}`); } }); // Route 404 this.app.use('*', (req, res) => { res.status(404).json({ success: false, error: 'Route non trouvée', path: req.originalUrl, mode: 'MANUAL', message: 'Cette route n\'existe pas en mode MANUAL' }); }); logSh('🌐 Interface web configurée', 'DEBUG'); } /** * Génère le dashboard HTML du mode MANUAL */ generateManualDashboard() { const uptime = Math.floor((Date.now() - this.stats.startTime) / 1000); return ` SEO Generator - Mode MANUAL

🎯 SEO Generator Server

MODE MANUAL

Interface Client + API + Tests Modulaires

✅ Mode MANUAL Actif
Interface complète disponible • WebSocket temps réel • API complète
${uptime}s
Uptime
${this.stats.requests}
Requêtes
${this.activeClients.size}
Clients WebSocket
${this.stats.testsExecuted}
Tests Exécutés

🧪 Interface Test Modulaire

Interface avancée pour tester toutes les combinaisons modulaires avec logs temps réel.

🚀 Ouvrir Interface Test ⚡ Interface Step-by-Step 📋 Configuration API

📊 Monitoring & API

Endpoints disponibles en mode MANUAL.

📊 Status API 📈 Statistiques

🌐 WebSocket Logs

Logs temps réel sur ws://localhost:${this.config.wsPort}

Status: Déconnecté

💡 Informations Mode MANUAL

`; } // ======================================== // WEBSOCKET SERVER // ======================================== /** * Configure le serveur WebSocket pour logs temps réel */ async setupWebSocketServer() { try { this.wsServer = new WebSocket.Server({ port: this.config.wsPort, host: this.config.host }); this.wsServer.on('connection', (ws, req) => { this.handleWebSocketConnection(ws, req); }); this.wsServer.on('error', (error) => { logSh(`❌ Erreur WebSocket: ${error.message}`, 'ERROR'); }); logSh(`📡 WebSocket Server démarré sur ws://${this.config.host}:${this.config.wsPort}`, 'DEBUG'); } catch (error) { logSh(`⚠️ Impossible de démarrer WebSocket: ${error.message}`, 'WARNING'); // Continue sans WebSocket si erreur } } /** * Gère les nouvelles connexions WebSocket */ handleWebSocketConnection(ws, req) { const clientId = `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; const clientIP = req.socket.remoteAddress; const clientData = { id: clientId, ws, ip: clientIP, connectedAt: Date.now() }; this.activeClients.add(clientData); this.stats.sessions++; logSh(`📡 Nouveau client WebSocket: ${clientId} (${clientIP})`, 'TRACE'); // Message de bienvenue ws.send(JSON.stringify({ type: 'welcome', message: 'Connecté aux logs temps réel SEO Generator (Mode MANUAL)', clientId: clientId, timestamp: new Date().toISOString() })); // Gestion fermeture ws.on('close', () => { this.activeClients.delete(clientData); logSh(`📡 Client WebSocket déconnecté: ${clientId}`, 'TRACE'); }); // Gestion erreurs ws.on('error', (error) => { this.activeClients.delete(clientData); logSh(`⚠️ Erreur client WebSocket ${clientId}: ${error.message}`, 'WARNING'); }); } /** * Diffuse un message à tous les clients WebSocket */ broadcastToClients(logData) { if (this.activeClients.size === 0) return; const message = JSON.stringify({ type: 'log', ...logData, timestamp: new Date().toISOString() }); this.activeClients.forEach(client => { if (client.ws.readyState === WebSocket.OPEN) { try { client.ws.send(message); } catch (error) { // Client déconnecté, le supprimer this.activeClients.delete(client); } } }); } /** * Déconnecte tous les clients WebSocket */ disconnectAllClients() { this.activeClients.forEach(client => { try { client.ws.close(); } catch (error) { // Ignore les erreurs de fermeture } }); this.activeClients.clear(); logSh('📡 Tous les clients WebSocket déconnectés', 'DEBUG'); } // ======================================== // SERVEUR HTTP // ======================================== /** * Démarre le serveur HTTP */ async startHTTPServer() { return new Promise((resolve, reject) => { try { this.server = this.app.listen(this.config.port, this.config.host, () => { resolve(); }); this.server.on('error', (error) => { reject(error); }); } catch (error) { reject(error); } }); } // ======================================== // MONITORING // ======================================== /** * Démarre le monitoring du serveur */ startMonitoring() { const MONITOR_INTERVAL = 30000; // 30 secondes this.monitorInterval = setInterval(() => { this.performMonitoring(); }, MONITOR_INTERVAL); logSh('💓 Monitoring ManualServer démarré', 'DEBUG'); } /** * Effectue le monitoring périodique */ performMonitoring() { const memUsage = process.memoryUsage(); const uptime = Date.now() - this.stats.startTime; logSh(`💓 ManualServer Health - Clients: ${this.activeClients.size} | Requêtes: ${this.stats.requests} | RAM: ${Math.round(memUsage.rss / 1024 / 1024)}MB`, 'TRACE'); // Nettoyage clients WebSocket morts this.cleanupDeadClients(); } /** * Nettoie les clients WebSocket déconnectés */ cleanupDeadClients() { let cleaned = 0; const deadClients = []; this.activeClients.forEach(client => { if (client.ws.readyState !== WebSocket.OPEN) { deadClients.push(client); cleaned++; } }); // Supprimer les clients morts deadClients.forEach(client => { this.activeClients.delete(client); }); if (cleaned > 0) { logSh(`🧹 ${cleaned} clients WebSocket morts nettoyés`, 'TRACE'); } } // ======================================== // ÉTAT ET CONTRÔLES // ======================================== /** * Vérifie s'il y a des clients actifs */ hasActiveClients() { return this.activeClients.size > 0; } /** * Retourne l'état du serveur MANUAL */ getStatus() { return { isRunning: this.isRunning, config: { ...this.config }, stats: { ...this.stats, uptime: Date.now() - this.stats.startTime }, activeClients: this.activeClients.size, urls: { dashboard: `http://localhost:${this.config.port}`, testInterface: `http://localhost:${this.config.port}/test-modulaire.html`, apiStatus: `http://localhost:${this.config.port}/api/status`, websocket: `ws://localhost:${this.config.wsPort}` } }; } } // ============= EXPORTS ============= module.exports = { ManualServer };