/** * Contrôleur API RESTful pour SEO Generator * Centralise toute la logique API métier */ const { logSh } = require('./ErrorReporting'); const { handleFullWorkflow } = require('./Main'); const { getPersonalities, readInstructionsData } = require('./BrainConfig'); const { getStoredArticle, getRecentArticles } = require('./ArticleStorage'); const { DynamicPromptEngine } = require('./prompt-engine/DynamicPromptEngine'); const { TrendManager } = require('./trend-prompts/TrendManager'); const { WorkflowEngine } = require('./workflow-configuration/WorkflowEngine'); const { ValidatorCore } = require('./validation/ValidatorCore'); const fs = require('fs').promises; const path = require('path'); class APIController { constructor() { this.articles = new Map(); // Cache articles en mémoire this.projects = new Map(); // Cache projets this.templates = new Map(); // Cache templates // Initialize prompt engine components this.promptEngine = new DynamicPromptEngine(); this.trendManager = new TrendManager(); this.workflowEngine = new WorkflowEngine(); // ✅ PHASE 3: Validation tracking this.activeValidations = new Map(); // Track running validations this.validationHistory = []; // Store completed validations this.wsServer = null; // WebSocket server reference (injected by ManualServer) } /** * ✅ PHASE 3: Injecte le serveur WebSocket pour broadcasting */ setWebSocketServer(wsServer) { this.wsServer = wsServer; logSh('📡 WebSocket server injecté dans APIController', 'DEBUG'); } /** * ✅ PHASE 3: Broadcast un message aux clients WebSocket */ broadcastToClients(data) { if (!this.wsServer || !this.wsServer.clients) { return; } const message = JSON.stringify(data); let sent = 0; this.wsServer.clients.forEach(client => { if (client.readyState === 1) { // OPEN state try { client.send(message); sent++; } catch (error) { logSh(`⚠️ Erreur envoi WebSocket: ${error.message}`, 'WARN'); } } }); if (sent > 0) { logSh(`📡 Message broadcast à ${sent} clients`, 'TRACE'); } } // ======================================== // GESTION ARTICLES // ======================================== /** * GET /api/articles - Liste tous les articles */ async getArticles(req, res) { try { const { limit = 50, offset = 0, project, status } = req.query; logSh(`📋 Récupération articles: limit=${limit}, offset=${offset}`, 'DEBUG'); // Récupération depuis Google Sheets const articles = await getRecentArticles(parseInt(limit)); // Filtrage optionnel let filteredArticles = articles; if (project) { filteredArticles = articles.filter(a => a.project === project); } if (status) { filteredArticles = filteredArticles.filter(a => a.status === status); } res.json({ success: true, data: { articles: filteredArticles.slice(offset, offset + limit), total: filteredArticles.length, limit: parseInt(limit), offset: parseInt(offset) }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération articles: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des articles', message: error.message }); } } /** * GET /api/articles/:id - Récupère un article spécifique */ async getArticle(req, res) { try { const { id } = req.params; const { format = 'json' } = req.query || {}; logSh(`📄 Récupération article ID: ${id}`, 'DEBUG'); const article = await getStoredArticle(id); if (!article) { return res.status(404).json({ success: false, error: 'Article non trouvé', id }); } // Format de réponse if (format === 'html') { res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.send(article.htmlContent || article.content); } else if (format === 'text') { res.setHeader('Content-Type', 'text/plain; charset=utf-8'); res.send(article.textContent || article.content); } else { res.json({ success: true, data: article, timestamp: new Date().toISOString() }); } } catch (error) { logSh(`❌ Erreur récupération article ${req.params.id}: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération de l\'article', message: error.message }); } } /** * POST /api/articles - Créer un nouvel article */ async createArticle(req, res) { try { const { keyword, rowNumber, project = 'api', config = {}, template, personalityPreference } = req.body; // Validation if (!keyword && !rowNumber) { return res.status(400).json({ success: false, error: 'Mot-clé ou numéro de ligne requis' }); } logSh(`✨ Création article: ${keyword || `ligne ${rowNumber}`}`, 'INFO'); // Configuration par défaut const workflowConfig = { rowNumber: rowNumber || 2, source: 'api', project, selectiveStack: config.selectiveStack || 'standardEnhancement', adversarialMode: config.adversarialMode || 'light', humanSimulationMode: config.humanSimulationMode || 'none', patternBreakingMode: config.patternBreakingMode || 'none', personalityPreference, template, ...config }; // Si mot-clé fourni, créer données temporaires if (keyword && !rowNumber) { workflowConfig.csvData = { mc0: keyword, t0: `Guide complet ${keyword}`, personality: personalityPreference || { nom: 'Marc', style: 'professionnel' } }; } // Exécution du workflow const result = await handleFullWorkflow(workflowConfig); res.status(201).json({ success: true, data: { id: result.id || result.slug, article: result, config: workflowConfig }, message: 'Article créé avec succès', timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur création article: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la création de l\'article', message: error.message }); } } // ======================================== // GESTION PROJETS // ======================================== /** * GET /api/projects - Liste tous les projets */ async getProjects(req, res) { try { const projects = Array.from(this.projects.values()); res.json({ success: true, data: { projects, total: projects.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération projets: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des projets', message: error.message }); } } /** * POST /api/projects - Créer un nouveau projet */ async createProject(req, res) { try { // Validation body null/undefined if (!req.body) { return res.status(400).json({ success: false, error: 'Corps de requête requis' }); } const { name, description, config = {} } = req.body; if (!name) { return res.status(400).json({ success: false, error: 'Nom du projet requis' }); } const project = { id: `project_${Date.now()}`, name, description, config, createdAt: new Date().toISOString(), articlesCount: 0 }; this.projects.set(project.id, project); logSh(`📁 Projet créé: ${name}`, 'INFO'); res.status(201).json({ success: true, data: project, message: 'Projet créé avec succès' }); } catch (error) { logSh(`❌ Erreur création projet: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la création du projet', message: error.message }); } } // ======================================== // GESTION TEMPLATES // ======================================== /** * GET /api/templates - Liste tous les templates */ async getTemplates(req, res) { try { const templates = Array.from(this.templates.values()); res.json({ success: true, data: { templates, total: templates.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération templates: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des templates', message: error.message }); } } /** * POST /api/templates - Créer un nouveau template */ async createTemplate(req, res) { try { const { name, content, description, category = 'custom' } = req.body; if (!name || !content) { return res.status(400).json({ success: false, error: 'Nom et contenu du template requis' }); } const template = { id: `template_${Date.now()}`, name, content, description, category, createdAt: new Date().toISOString() }; this.templates.set(template.id, template); logSh(`📋 Template créé: ${name}`, 'INFO'); res.status(201).json({ success: true, data: template, message: 'Template créé avec succès' }); } catch (error) { logSh(`❌ Erreur création template: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la création du template', message: error.message }); } } // ======================================== // CONFIGURATION & MONITORING // ======================================== /** * GET /api/config/personalities - Configuration personnalités */ async getPersonalitiesConfig(req, res) { try { const personalities = await getPersonalities(); res.json({ success: true, data: { personalities, total: personalities.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur config personnalités: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des personnalités', message: error.message }); } } /** * GET /api/health - Health check */ async getHealth(req, res) { try { const health = { status: 'healthy', timestamp: new Date().toISOString(), version: '1.0.0', uptime: process.uptime(), memory: process.memoryUsage(), environment: process.env.NODE_ENV || 'development' }; res.json({ success: true, data: health }); } catch (error) { res.status(500).json({ success: false, error: 'Health check failed', message: error.message }); } } /** * GET /api/metrics - Métriques système */ async getMetrics(req, res) { try { const metrics = { articles: { total: this.articles.size, recent: Array.from(this.articles.values()).filter( a => new Date(a.createdAt) > new Date(Date.now() - 24 * 60 * 60 * 1000) ).length }, projects: { total: this.projects.size }, templates: { total: this.templates.size }, system: { uptime: process.uptime(), memory: process.memoryUsage(), platform: process.platform, nodeVersion: process.version } }; res.json({ success: true, data: metrics, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur métriques: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des métriques', message: error.message }); } } // ======================================== // PROMPT ENGINE API // ======================================== /** * POST /api/generate-prompt - Génère un prompt adaptatif */ async generatePrompt(req, res) { try { const { templateType = 'technical', content = {}, csvData = null, trend = null, layerConfig = {}, customVariables = {} } = req.body; logSh(`🧠 Génération prompt: template=${templateType}, trend=${trend}`, 'INFO'); // Apply trend if specified if (trend) { await this.trendManager.setTrend(trend); } // Generate adaptive prompt const result = await this.promptEngine.generateAdaptivePrompt({ templateType, content, csvData, trend: this.trendManager.getCurrentTrend(), layerConfig, customVariables }); res.json({ success: true, prompt: result.prompt, metadata: result.metadata, timestamp: new Date().toISOString() }); logSh(`✅ Prompt généré: ${result.prompt.length} caractères`, 'DEBUG'); } catch (error) { logSh(`❌ Erreur génération prompt: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la génération du prompt', message: error.message }); } } /** * GET /api/trends - Liste toutes les tendances disponibles */ async getTrends(req, res) { try { const trends = this.trendManager.getAvailableTrends(); const currentTrend = this.trendManager.getCurrentTrend(); res.json({ success: true, data: { trends, currentTrend, total: trends.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération tendances: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des tendances', message: error.message }); } } /** * POST /api/trends/:trendId - Applique une tendance */ async setTrend(req, res) { try { const { trendId } = req.params; const { customConfig = null } = req.body; logSh(`🎯 Application tendance: ${trendId}`, 'INFO'); const trend = await this.trendManager.setTrend(trendId, customConfig); res.json({ success: true, data: { trend, applied: true }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur application tendance ${req.params.trendId}: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de l\'application de la tendance', message: error.message }); } } /** * GET /api/prompt-engine/status - Status du moteur de prompts */ async getPromptEngineStatus(req, res) { try { const engineStatus = this.promptEngine.getEngineStatus(); const trendStatus = this.trendManager.getStatus(); const workflowStatus = this.workflowEngine.getEngineStatus(); res.json({ success: true, data: { engine: engineStatus, trends: trendStatus, workflow: workflowStatus, health: 'operational' }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur status prompt engine: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération du status', message: error.message }); } } /** * GET /api/workflow/sequences - Liste toutes les séquences de workflow */ async getWorkflowSequences(req, res) { try { const sequences = this.workflowEngine.getAvailableSequences(); res.json({ success: true, data: { sequences, total: sequences.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération séquences workflow: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des séquences workflow', message: error.message }); } } /** * POST /api/workflow/sequences - Crée une séquence de workflow personnalisée */ async createWorkflowSequence(req, res) { try { const { name, sequence } = req.body; if (!name || !sequence) { return res.status(400).json({ success: false, error: 'Nom et séquence requis' }); } if (!this.workflowEngine.validateSequence(sequence)) { return res.status(400).json({ success: false, error: 'Séquence invalide' }); } const createdSequence = this.workflowEngine.createCustomSequence(name, sequence); res.json({ success: true, data: { name, sequence: createdSequence }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur création séquence workflow: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la création de la séquence workflow', message: error.message }); } } /** * POST /api/workflow/execute - Exécute un workflow configurable */ async executeConfigurableWorkflow(req, res) { try { const { content, sequenceName = 'default', customSequence = null, selectiveConfig = {}, adversarialConfig = {}, humanConfig = {}, patternConfig = {}, csvData = {}, personalities = {} } = req.body; if (!content || typeof content !== 'object') { return res.status(400).json({ success: false, error: 'Contenu requis (objet)' }); } logSh(`🔄 Exécution workflow configurable: ${customSequence ? 'custom' : sequenceName}`, 'INFO'); const result = await this.workflowEngine.executeConfigurableWorkflow(content, { sequenceName, customSequence, selectiveConfig, adversarialConfig, humanConfig, patternConfig, csvData, personalities }); res.json({ success: result.success, data: { content: result.content, stats: result.stats }, error: result.error || null, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur exécution workflow configurable: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de l\'exécution du workflow configurable', message: error.message }); } } // ======================================== // VALIDATION API (PHASE 3) // ======================================== /** * POST /api/validation/start - Démarre une nouvelle validation */ async startValidation(req, res) { try { const { pipelineConfig, rowNumber = 2, config = {} } = req.body; // Validation if (!pipelineConfig) { return res.status(400).json({ success: false, error: 'Configuration pipeline requise' }); } logSh(`🚀 Démarrage validation: ${pipelineConfig.name || 'Sans nom'}`, 'INFO'); // ✅ PHASE 3: Créer nouvelle instance ValidatorCore avec broadcast callback const validator = new ValidatorCore({ broadcastCallback: (data) => this.broadcastToClients(data) }); // Démarrer validation en arrière-plan const validationPromise = validator.runValidation(config, pipelineConfig, rowNumber); // Stocker la validation active this.activeValidations.set(validator.validationId, { validator, promise: validationPromise, startTime: Date.now(), pipelineConfig, rowNumber, status: 'running' }); // Gérer la completion en arrière-plan validationPromise.then(result => { const validation = this.activeValidations.get(validator.validationId); if (validation) { validation.status = result.success ? 'completed' : 'error'; validation.result = result; validation.endTime = Date.now(); // Déplacer vers historique après 5min setTimeout(() => { this.validationHistory.push(validation); this.activeValidations.delete(validator.validationId); }, 5 * 60 * 1000); } }).catch(error => { const validation = this.activeValidations.get(validator.validationId); if (validation) { validation.status = 'error'; validation.error = error.message; validation.endTime = Date.now(); } }); // Répondre immédiatement avec validation ID res.status(202).json({ success: true, data: { validationId: validator.validationId, status: 'running', message: 'Validation démarrée en arrière-plan' }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur démarrage validation: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors du démarrage de la validation', message: error.message }); } } /** * GET /api/validation/status/:id - Récupère le statut d'une validation */ async getValidationStatus(req, res) { try { const { id } = req.params; logSh(`📊 Récupération statut validation: ${id}`, 'DEBUG'); // Chercher dans validations actives const activeValidation = this.activeValidations.get(id); if (activeValidation) { const status = activeValidation.validator.getStatus(); return res.json({ success: true, data: { validationId: id, status: activeValidation.status, progress: status.progress, startTime: activeValidation.startTime, duration: Date.now() - activeValidation.startTime, pipelineName: activeValidation.pipelineConfig.name, result: activeValidation.result || null }, timestamp: new Date().toISOString() }); } // Chercher dans historique const historicalValidation = this.validationHistory.find(v => v.validator.validationId === id); if (historicalValidation) { return res.json({ success: true, data: { validationId: id, status: historicalValidation.status, startTime: historicalValidation.startTime, endTime: historicalValidation.endTime, duration: historicalValidation.endTime - historicalValidation.startTime, pipelineName: historicalValidation.pipelineConfig.name, result: historicalValidation.result }, timestamp: new Date().toISOString() }); } // Non trouvé return res.status(404).json({ success: false, error: 'Validation non trouvée', validationId: id }); } catch (error) { logSh(`❌ Erreur récupération statut validation: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération du statut', message: error.message }); } } /** * POST /api/validation/stop/:id - Arrête une validation en cours */ async stopValidation(req, res) { try { const { id } = req.params; logSh(`🛑 Arrêt validation: ${id}`, 'INFO'); const validation = this.activeValidations.get(id); if (!validation) { return res.status(404).json({ success: false, error: 'Validation non trouvée ou déjà terminée', validationId: id }); } // Note: Pour l'instant, on ne peut pas vraiment interrompre une validation en cours // On marque juste le statut comme "stopped" validation.status = 'stopped'; validation.endTime = Date.now(); logSh(`⚠️ Validation ${id} marquée comme arrêtée (le processus continue en arrière-plan)`, 'WARN'); res.json({ success: true, data: { validationId: id, status: 'stopped', message: 'Validation marquée comme arrêtée' }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur arrêt validation: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de l\'arrêt de la validation', message: error.message }); } } /** * GET /api/validation/list - Liste toutes les validations */ async listValidations(req, res) { try { const { status, limit = 50 } = req.query; logSh(`📋 Récupération liste validations`, 'DEBUG'); // Collecter validations actives const activeList = Array.from(this.activeValidations.values()).map(v => ({ validationId: v.validator.validationId, status: v.status, startTime: v.startTime, duration: Date.now() - v.startTime, pipelineName: v.pipelineConfig.name, progress: v.validator.getStatus().progress })); // Collecter historique const historyList = this.validationHistory.map(v => ({ validationId: v.validator.validationId, status: v.status, startTime: v.startTime, endTime: v.endTime, duration: v.endTime - v.startTime, pipelineName: v.pipelineConfig.name })); // Combiner et filtrer let allValidations = [...activeList, ...historyList]; if (status) { allValidations = allValidations.filter(v => v.status === status); } // Limiter résultats const limitedValidations = allValidations.slice(0, parseInt(limit)); res.json({ success: true, data: { validations: limitedValidations, total: allValidations.length, active: activeList.length, historical: historyList.length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur liste validations: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération de la liste', message: error.message }); } } /** * GET /api/validation/:id/report - Récupère le rapport complet d'une validation */ async getValidationReport(req, res) { try { const { id } = req.params; logSh(`📊 Récupération rapport validation: ${id}`, 'DEBUG'); // Chercher le dossier de validation const validationDir = path.join(process.cwd(), 'validations', id); const reportPath = path.join(validationDir, 'report.json'); try { const reportContent = await fs.readFile(reportPath, 'utf8'); const report = JSON.parse(reportContent); res.json({ success: true, data: report, timestamp: new Date().toISOString() }); } catch (fileError) { return res.status(404).json({ success: false, error: 'Rapport de validation non trouvé', validationId: id, message: fileError.message }); } } catch (error) { logSh(`❌ Erreur récupération rapport: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération du rapport', message: error.message }); } } /** * GET /api/validation/:id/evaluations - Récupère les évaluations détaillées */ async getValidationEvaluations(req, res) { try { const { id } = req.params; logSh(`📊 Récupération évaluations validation: ${id}`, 'DEBUG'); const validationDir = path.join(process.cwd(), 'validations', id); const evaluationsPath = path.join(validationDir, 'results', 'evaluations.json'); try { const evaluationsContent = await fs.readFile(evaluationsPath, 'utf8'); const evaluations = JSON.parse(evaluationsContent); res.json({ success: true, data: evaluations, timestamp: new Date().toISOString() }); } catch (fileError) { return res.status(404).json({ success: false, error: 'Évaluations non trouvées', validationId: id, message: fileError.message }); } } catch (error) { logSh(`❌ Erreur récupération évaluations: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des évaluations', message: error.message }); } } /** * ✅ NOUVEAU: GET /api/validation/presets - Récupère les presets disponibles */ async getValidationPresets(req, res) { try { const { VALIDATION_PRESETS } = require('./validation/ValidatorCore'); logSh(`📋 Récupération presets validation`, 'DEBUG'); res.json({ success: true, data: { presets: VALIDATION_PRESETS, count: Object.keys(VALIDATION_PRESETS).length }, timestamp: new Date().toISOString() }); } catch (error) { logSh(`❌ Erreur récupération presets: ${error.message}`, 'ERROR'); res.status(500).json({ success: false, error: 'Erreur lors de la récupération des presets', message: error.message }); } } } module.exports = { APIController };