diff --git a/lib/batch/BatchController.js b/lib/batch/BatchController.js
new file mode 100644
index 0000000..a3f25c5
--- /dev/null
+++ b/lib/batch/BatchController.js
@@ -0,0 +1,385 @@
+// ========================================
+// BATCH CONTROLLER - API ENDPOINTS
+// Responsabilité: Gestion API pour traitement batch avec configuration pipeline
+// ========================================
+
+const { logSh } = require('../ErrorReporting');
+const fs = require('fs').promises;
+const path = require('path');
+const { BatchProcessor } = require('./BatchProcessor');
+const { DigitalOceanTemplates } = require('./DigitalOceanTemplates');
+
+/**
+ * BATCH CONTROLLER
+ * Gestion complète de l'interface de traitement batch
+ */
+class BatchController {
+
+ constructor() {
+ this.configPath = path.join(__dirname, '../../config/batch-config.json');
+ this.statusPath = path.join(__dirname, '../../config/batch-status.json');
+
+ // Initialiser les composants Phase 2
+ this.batchProcessor = new BatchProcessor();
+ this.digitalOceanTemplates = new DigitalOceanTemplates();
+
+ // Configuration par défaut
+ this.defaultConfig = {
+ selective: 'standardEnhancement',
+ adversarial: 'light',
+ humanSimulation: 'none',
+ patternBreaking: 'none',
+ intensity: 1.0,
+ rowRange: { start: 2, end: 10 },
+ saveIntermediateSteps: false,
+ lastUpdated: new Date().toISOString()
+ };
+
+ // État par défaut
+ this.defaultStatus = {
+ status: 'idle',
+ currentRow: null,
+ totalRows: 0,
+ progress: 0,
+ startTime: null,
+ estimatedEnd: null,
+ errors: [],
+ lastResult: null,
+ config: this.defaultConfig
+ };
+
+ this.initializeFiles();
+ }
+
+ /**
+ * Initialise les fichiers de configuration
+ */
+ async initializeFiles() {
+ try {
+ // Créer le dossier config s'il n'existe pas
+ const configDir = path.dirname(this.configPath);
+ await fs.mkdir(configDir, { recursive: true });
+
+ // Créer config par défaut si inexistant
+ try {
+ await fs.access(this.configPath);
+ } catch {
+ await fs.writeFile(this.configPath, JSON.stringify(this.defaultConfig, null, 2));
+ logSh('📝 Configuration batch par défaut créée', 'DEBUG');
+ }
+
+ // Créer status par défaut si inexistant
+ try {
+ await fs.access(this.statusPath);
+ } catch {
+ await fs.writeFile(this.statusPath, JSON.stringify(this.defaultStatus, null, 2));
+ logSh('📊 Status batch par défaut créé', 'DEBUG');
+ }
+
+ } catch (error) {
+ logSh(`❌ Erreur initialisation fichiers batch: ${error.message}`, 'ERROR');
+ }
+ }
+
+ // ========================================
+ // ENDPOINTS CONFIGURATION
+ // ========================================
+
+ /**
+ * GET /api/batch/config
+ * Récupère la configuration actuelle
+ */
+ async getConfig(req, res) {
+ try {
+ // Utiliser la nouvelle API du BatchProcessor refactorisé
+ const status = this.batchProcessor.getExtendedStatus();
+
+ logSh('📋 Configuration batch récupérée', 'DEBUG');
+
+ res.json({
+ success: true,
+ config: status.config,
+ availableOptions: status.availableOptions
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur récupération config: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur récupération configuration',
+ details: error.message
+ });
+ }
+ }
+
+ /**
+ * POST /api/batch/config
+ * Sauvegarde la configuration
+ */
+ async saveConfig(req, res) {
+ try {
+ const newConfig = req.body;
+
+ // Utiliser la nouvelle API du BatchProcessor refactorisé
+ const result = await this.batchProcessor.updateConfiguration(newConfig);
+
+ res.json({
+ success: true,
+ message: 'Configuration sauvegardée avec succès',
+ config: result.config
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur sauvegarde config: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur sauvegarde configuration',
+ details: error.message
+ });
+ }
+ }
+
+ // ========================================
+ // ENDPOINTS CONTRÔLE TRAITEMENT
+ // ========================================
+
+ /**
+ * POST /api/batch/start
+ * Démarre le traitement batch
+ */
+ async startBatch(req, res) {
+ try {
+ // Démarrer le traitement via BatchProcessor
+ const status = await this.batchProcessor.start();
+
+ logSh(`🚀 Traitement batch démarré - ${status.totalRows} lignes`, 'INFO');
+
+ res.json({
+ success: true,
+ message: 'Traitement batch démarré',
+ status: status
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur démarrage batch: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur démarrage traitement',
+ details: error.message
+ });
+ }
+ }
+
+ /**
+ * POST /api/batch/stop
+ * Arrête le traitement batch
+ */
+ async stopBatch(req, res) {
+ try {
+ const status = await this.batchProcessor.stop();
+
+ logSh('🛑 Traitement batch arrêté', 'INFO');
+
+ res.json({
+ success: true,
+ message: 'Traitement batch arrêté',
+ status: status
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur arrêt batch: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur arrêt traitement',
+ details: error.message
+ });
+ }
+ }
+
+ /**
+ * POST /api/batch/pause
+ * Met en pause le traitement
+ */
+ async pauseBatch(req, res) {
+ try {
+ const status = await this.batchProcessor.pause();
+
+ logSh('⏸️ Traitement batch mis en pause', 'INFO');
+
+ res.json({
+ success: true,
+ message: 'Traitement mis en pause',
+ status: status
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur pause batch: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur pause traitement',
+ details: error.message
+ });
+ }
+ }
+
+ /**
+ * POST /api/batch/resume
+ * Reprend le traitement
+ */
+ async resumeBatch(req, res) {
+ try {
+ const status = await this.batchProcessor.resume();
+
+ logSh('▶️ Traitement batch repris', 'INFO');
+
+ res.json({
+ success: true,
+ message: 'Traitement repris',
+ status: status
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur reprise batch: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur reprise traitement',
+ details: error.message
+ });
+ }
+ }
+
+ // ========================================
+ // ENDPOINTS MONITORING
+ // ========================================
+
+ /**
+ * GET /api/batch/status
+ * Récupère l'état actuel du traitement
+ */
+ async getStatus(req, res) {
+ try {
+ const status = this.batchProcessor.getStatus();
+
+ res.json({
+ success: true,
+ status: status,
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur récupération status: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur récupération status',
+ details: error.message
+ });
+ }
+ }
+
+ /**
+ * GET /api/batch/progress
+ * Récupère la progression détaillée
+ */
+ async getProgress(req, res) {
+ try {
+ const progress = this.batchProcessor.getProgress();
+
+ res.json({
+ success: true,
+ progress: progress,
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur récupération progress: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur récupération progression',
+ details: error.message
+ });
+ }
+ }
+
+ // ========================================
+ // ENDPOINTS DIGITAL OCEAN
+ // ========================================
+
+ /**
+ * GET /api/batch/templates
+ * Liste les templates disponibles
+ */
+ async getTemplates(req, res) {
+ try {
+ const templates = await this.digitalOceanTemplates.listAvailableTemplates();
+ const stats = this.digitalOceanTemplates.getCacheStats();
+
+ res.json({
+ success: true,
+ templates: templates,
+ cacheStats: stats,
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur récupération templates: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur récupération templates',
+ details: error.message
+ });
+ }
+ }
+
+ /**
+ * GET /api/batch/templates/:filename
+ * Récupère un template spécifique
+ */
+ async getTemplate(req, res) {
+ try {
+ const { filename } = req.params;
+ const template = await this.digitalOceanTemplates.getTemplate(filename);
+
+ res.json({
+ success: true,
+ filename: filename,
+ template: template,
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur récupération template ${req.params.filename}: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur récupération template',
+ details: error.message
+ });
+ }
+ }
+
+ /**
+ * DELETE /api/batch/cache
+ * Vide le cache des templates
+ */
+ async clearCache(req, res) {
+ try {
+ await this.digitalOceanTemplates.clearCache();
+
+ res.json({
+ success: true,
+ message: 'Cache vidé avec succès',
+ timestamp: new Date().toISOString()
+ });
+
+ } catch (error) {
+ logSh(`❌ Erreur vidage cache: ${error.message}`, 'ERROR');
+ res.status(500).json({
+ success: false,
+ error: 'Erreur vidage cache',
+ details: error.message
+ });
+ }
+ }
+}
+
+// ============= EXPORTS =============
+module.exports = { BatchController };
\ No newline at end of file
diff --git a/lib/batch/BatchProcessor.js b/lib/batch/BatchProcessor.js
new file mode 100644
index 0000000..525ece1
--- /dev/null
+++ b/lib/batch/BatchProcessor.js
@@ -0,0 +1,142 @@
+// ========================================
+// BATCH PROCESSOR - REFACTORISÉ
+// Responsabilité: Traitement batch interface web avec configuration flexible
+// ========================================
+
+const { QueueProcessor } = require('../shared/QueueProcessor');
+const { logSh } = require('../ErrorReporting');
+const path = require('path');
+
+/**
+ * BATCH PROCESSOR
+ * Spécialisé pour interface web avec configuration modulaire flexible
+ */
+class BatchProcessor extends QueueProcessor {
+
+ constructor() {
+ super({
+ name: 'BatchProcessor',
+ configPath: path.join(__dirname, '../../config/batch-config.json'),
+ statusPath: path.join(__dirname, '../../config/batch-status.json'),
+ queuePath: path.join(__dirname, '../../config/batch-queue.json'),
+ config: {
+ selective: 'standardEnhancement',
+ adversarial: 'light',
+ humanSimulation: 'none',
+ patternBreaking: 'none',
+ intensity: 1.0,
+ rowRange: { start: 2, end: 10 },
+ saveIntermediateSteps: false,
+ maxRetries: 3,
+ delayBetweenItems: 1000
+ }
+ });
+
+ // Initialiser immédiatement
+ this.initialize().catch(error => {
+ logSh(`❌ Erreur initialisation BatchProcessor: ${error.message}`, 'ERROR');
+ });
+ }
+
+ /**
+ * Alias pour compatibilité - Initialise les fichiers
+ */
+ async initializeFiles() {
+ return await super.initializeFiles();
+ }
+
+ /**
+ * Alias pour compatibilité - Initialise le processeur
+ */
+ async initializeProcessor() {
+ return await this.initialize();
+ }
+
+ /**
+ * Construit la configuration spécifique BatchProcessor
+ */
+ buildRowConfig(rowNumber, data = null) {
+ return {
+ rowNumber,
+ source: 'batch_processor',
+ selectiveStack: this.config.selective,
+ adversarialMode: this.config.adversarial,
+ humanSimulationMode: this.config.humanSimulation,
+ patternBreakingMode: this.config.patternBreaking,
+ intensity: this.config.intensity,
+ saveIntermediateSteps: this.config.saveIntermediateSteps
+ };
+ }
+
+ /**
+ * API spécifique BatchProcessor - Configuration
+ */
+ async updateConfiguration(newConfig) {
+ try {
+ // Validation basique
+ const requiredFields = ['selective', 'adversarial', 'humanSimulation', 'patternBreaking', 'intensity', 'rowRange'];
+ for (const field of requiredFields) {
+ if (!(field in newConfig)) {
+ throw new Error(`Champ requis manquant: ${field}`);
+ }
+ }
+
+ // Validation intensité
+ if (newConfig.intensity < 0.5 || newConfig.intensity > 1.5) {
+ throw new Error('Intensité doit être entre 0.5 et 1.5');
+ }
+
+ // Validation rowRange
+ if (!newConfig.rowRange.start || !newConfig.rowRange.end || newConfig.rowRange.start >= newConfig.rowRange.end) {
+ throw new Error('Plage de lignes invalide');
+ }
+
+ // Mettre à jour la configuration
+ this.config = { ...this.config, ...newConfig };
+ this.config.lastUpdated = new Date().toISOString();
+
+ // Sauvegarder
+ if (this.configPath) {
+ const fs = require('fs').promises;
+ await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2));
+ }
+
+ logSh(`✅ Configuration BatchProcessor mise à jour: ${JSON.stringify(newConfig)}`, 'INFO');
+
+ return { success: true, config: this.config };
+
+ } catch (error) {
+ logSh(`❌ Erreur mise à jour configuration: ${error.message}`, 'ERROR');
+ throw error;
+ }
+ }
+
+ /**
+ * Retourne les options disponibles
+ */
+ getAvailableOptions() {
+ return {
+ selective: ['lightEnhancement', 'standardEnhancement', 'fullEnhancement', 'personalityFocus', 'fluidityFocus'],
+ adversarial: ['none', 'light', 'standard', 'heavy', 'adaptive'],
+ humanSimulation: ['none', 'lightSimulation', 'personalityFocus', 'adaptive'],
+ patternBreaking: ['none', 'syntaxFocus', 'connectorsFocus', 'adaptive'],
+ intensityRange: { min: 0.5, max: 1.5, step: 0.1 }
+ };
+ }
+
+ /**
+ * Status étendu avec options disponibles
+ */
+ getExtendedStatus() {
+ const baseStatus = this.getStatus();
+ return {
+ ...baseStatus,
+ availableOptions: this.getAvailableOptions(),
+ mode: 'BATCH_MANUAL',
+ timestamp: new Date().toISOString()
+ };
+ }
+}
+
+// ============= EXPORTS =============
+module.exports = { BatchProcessor };
\ No newline at end of file
diff --git a/lib/batch/BatchProcessor.original.js b/lib/batch/BatchProcessor.original.js
new file mode 100644
index 0000000..d9d4667
--- /dev/null
+++ b/lib/batch/BatchProcessor.original.js
@@ -0,0 +1,622 @@
+// ========================================
+// BATCH PROCESSOR - SYSTÈME DE QUEUE
+// Responsabilité: Traitement batch des lignes Google Sheets avec pipeline modulaire
+// ========================================
+
+const { logSh } = require('../ErrorReporting');
+const { tracer } = require('../trace');
+const { handleModularWorkflow } = require('../Main');
+const { readInstructionsData } = require('../BrainConfig');
+const fs = require('fs').promises;
+const path = require('path');
+
+/**
+ * BATCH PROCESSOR
+ * Système de queue pour traiter les lignes Google Sheets une par une
+ */
+class BatchProcessor {
+
+ constructor() {
+ this.statusPath = path.join(__dirname, '../../config/batch-status.json');
+ this.configPath = path.join(__dirname, '../../config/batch-config.json');
+ this.queuePath = path.join(__dirname, '../../config/batch-queue.json');
+
+ // État du processeur
+ this.isRunning = false;
+ this.isPaused = false;
+ this.currentRow = null;
+ this.queue = [];
+ this.errors = [];
+ this.results = [];
+
+ // Configuration par défaut
+ this.config = {
+ selective: 'standardEnhancement',
+ adversarial: 'light',
+ humanSimulation: 'none',
+ patternBreaking: 'none',
+ intensity: 1.0,
+ rowRange: { start: 2, end: 10 },
+ saveIntermediateSteps: false
+ };
+
+ // Métriques
+ this.startTime = null;
+ this.processedCount = 0;
+ this.errorCount = 0;
+
+ // Callbacks pour updates
+ this.onStatusUpdate = null;
+ this.onProgress = null;
+ this.onError = null;
+ this.onComplete = null;
+
+ this.initializeProcessor();
+ }
+
+ /**
+ * Initialise le processeur
+ */
+ async initializeProcessor() {
+ try {
+ // Charger la configuration
+ await this.loadConfig();
+
+ // Initialiser la queue si elle n'existe pas
+ await this.initializeQueue();
+
+ logSh('🎯 BatchProcessor initialisé', 'DEBUG');
+
+ } catch (error) {
+ logSh(`❌ Erreur initialisation BatchProcessor: ${error.message}`, 'ERROR');
+ }
+ }
+
+ /**
+ * Initialise les fichiers de configuration (alias pour compatibilité tests)
+ */
+ async initializeFiles() {
+ try {
+ // Créer le dossier config s'il n'existe pas
+ const configDir = path.dirname(this.configPath);
+ await fs.mkdir(configDir, { recursive: true });
+
+ // Créer config par défaut si inexistant
+ try {
+ await fs.access(this.configPath);
+ } catch {
+ await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2));
+ logSh('📝 Configuration batch par défaut créée', 'DEBUG');
+ }
+
+ // Créer status par défaut si inexistant
+ const defaultStatus = {
+ status: 'idle',
+ currentRow: null,
+ totalRows: 0,
+ progress: 0,
+ startTime: null,
+ estimatedEnd: null,
+ errors: [],
+ lastResult: null,
+ config: this.config
+ };
+
+ try {
+ await fs.access(this.statusPath);
+ } catch {
+ await fs.writeFile(this.statusPath, JSON.stringify(defaultStatus, null, 2));
+ logSh('📊 Status batch par défaut créé', 'DEBUG');
+ }
+
+ } catch (error) {
+ logSh(`❌ Erreur initialisation fichiers batch: ${error.message}`, 'ERROR');
+ }
+ }
+
+ /**
+ * Charge la configuration
+ */
+ async loadConfig() {
+ try {
+ const configData = await fs.readFile(this.configPath, 'utf8');
+ this.config = JSON.parse(configData);
+ logSh(`📋 Configuration chargée: ${JSON.stringify(this.config)}`, 'DEBUG');
+ } catch (error) {
+ logSh('⚠️ Configuration non trouvée, utilisation des valeurs par défaut', 'WARNING');
+ }
+ }
+
+ /**
+ * Initialise la queue
+ */
+ async initializeQueue() {
+ try {
+ // Essayer de charger la queue existante
+ try {
+ const queueData = await fs.readFile(this.queuePath, 'utf8');
+ const savedQueue = JSON.parse(queueData);
+
+ if (savedQueue.queue && Array.isArray(savedQueue.queue)) {
+ this.queue = savedQueue.queue;
+ this.processedCount = savedQueue.processedCount || 0;
+ logSh(`📊 Queue restaurée: ${this.queue.length} éléments`, 'DEBUG');
+ }
+ } catch {
+ // Queue n'existe pas, on la créera
+ }
+
+ // Si queue vide, la populer depuis la configuration
+ if (this.queue.length === 0) {
+ await this.populateQueue();
+ }
+
+ } catch (error) {
+ logSh(`❌ Erreur initialisation queue: ${error.message}`, 'ERROR');
+ }
+ }
+
+ /**
+ * Popule la queue avec les lignes à traiter
+ */
+ async populateQueue() {
+ try {
+ this.queue = [];
+
+ const { start, end } = this.config.rowRange;
+
+ for (let rowNumber = start; rowNumber <= end; rowNumber++) {
+ this.queue.push({
+ rowNumber,
+ status: 'pending',
+ attempts: 0,
+ maxAttempts: 3,
+ error: null,
+ result: null,
+ startTime: null,
+ endTime: null
+ });
+ }
+
+ await this.saveQueue();
+
+ logSh(`📋 Queue populée: ${this.queue.length} lignes (${start} à ${end})`, 'INFO');
+
+ } catch (error) {
+ logSh(`❌ Erreur population queue: ${error.message}`, 'ERROR');
+ throw error;
+ }
+ }
+
+ /**
+ * Sauvegarde la queue
+ */
+ async saveQueue() {
+ try {
+ const queueData = {
+ queue: this.queue,
+ processedCount: this.processedCount,
+ lastUpdate: new Date().toISOString()
+ };
+
+ await fs.writeFile(this.queuePath, JSON.stringify(queueData, null, 2));
+ } catch (error) {
+ logSh(`❌ Erreur sauvegarde queue: ${error.message}`, 'ERROR');
+ }
+ }
+
+ // ========================================
+ // CONTRÔLES PRINCIPAUX
+ // ========================================
+
+ /**
+ * Démarre le traitement batch
+ */
+ async start() {
+ return tracer.run('BatchProcessor.start', async () => {
+ if (this.isRunning) {
+ throw new Error('Le traitement est déjà en cours');
+ }
+
+ logSh('🚀 Démarrage traitement batch', 'INFO');
+
+ this.isRunning = true;
+ this.isPaused = false;
+ this.startTime = new Date();
+ this.processedCount = 0;
+ this.errorCount = 0;
+
+ // Charger la configuration la plus récente
+ await this.loadConfig();
+
+ // Si queue vide ou configuration changée, repopuler
+ if (this.queue.length === 0) {
+ await this.populateQueue();
+ }
+
+ // Mettre à jour le status
+ await this.updateStatus();
+
+ // Démarrer le traitement asynchrone
+ this.processQueue().catch(error => {
+ logSh(`❌ Erreur traitement queue: ${error.message}`, 'ERROR');
+ this.handleError(error);
+ });
+
+ return this.getStatus();
+ });
+ }
+
+ /**
+ * Arrête le traitement batch
+ */
+ async stop() {
+ return tracer.run('BatchProcessor.stop', async () => {
+ logSh('🛑 Arrêt traitement batch', 'INFO');
+
+ this.isRunning = false;
+ this.isPaused = false;
+ this.currentRow = null;
+
+ await this.updateStatus();
+
+ return this.getStatus();
+ });
+ }
+
+ /**
+ * Met en pause le traitement
+ */
+ async pause() {
+ return tracer.run('BatchProcessor.pause', async () => {
+ if (!this.isRunning) {
+ throw new Error('Aucun traitement en cours');
+ }
+
+ logSh('⏸️ Mise en pause traitement batch', 'INFO');
+
+ this.isPaused = true;
+
+ await this.updateStatus();
+
+ return this.getStatus();
+ });
+ }
+
+ /**
+ * Reprend le traitement
+ */
+ async resume() {
+ return tracer.run('BatchProcessor.resume', async () => {
+ if (!this.isRunning || !this.isPaused) {
+ throw new Error('Aucun traitement en pause');
+ }
+
+ logSh('▶️ Reprise traitement batch', 'INFO');
+
+ this.isPaused = false;
+
+ await this.updateStatus();
+
+ // Reprendre le traitement
+ this.processQueue().catch(error => {
+ logSh(`❌ Erreur reprise traitement: ${error.message}`, 'ERROR');
+ this.handleError(error);
+ });
+
+ return this.getStatus();
+ });
+ }
+
+ // ========================================
+ // TRAITEMENT QUEUE
+ // ========================================
+
+ /**
+ * Traite la queue élément par élément
+ */
+ async processQueue() {
+ return tracer.run('BatchProcessor.processQueue', async () => {
+ while (this.isRunning && !this.isPaused) {
+ // Chercher le prochain élément à traiter
+ const nextItem = this.queue.find(item => item.status === 'pending' ||
+ (item.status === 'error' && item.attempts < item.maxAttempts));
+
+ if (!nextItem) {
+ // Queue terminée
+ logSh('✅ Traitement queue terminé', 'INFO');
+ await this.complete();
+ break;
+ }
+
+ // Traiter l'élément
+ await this.processItem(nextItem);
+
+ // Pause entre les éléments (pour éviter rate limiting)
+ await this.sleep(1000);
+ }
+ });
+ }
+
+ /**
+ * Traite un élément de la queue
+ */
+ async processItem(item) {
+ return tracer.run('BatchProcessor.processItem', async () => {
+ logSh(`🔄 Traitement ligne ${item.rowNumber} (tentative ${item.attempts + 1}/${item.maxAttempts})`, 'INFO');
+
+ this.currentRow = item.rowNumber;
+ item.status = 'processing';
+ item.startTime = new Date().toISOString();
+ item.attempts++;
+
+ await this.updateStatus();
+ await this.saveQueue();
+
+ try {
+ // Traiter la ligne avec le pipeline modulaire
+ const result = await this.processRow(item.rowNumber);
+
+ // Succès
+ item.status = 'completed';
+ item.result = result;
+ item.endTime = new Date().toISOString();
+ item.error = null;
+
+ this.processedCount++;
+
+ logSh(`✅ Ligne ${item.rowNumber} traitée avec succès`, 'INFO');
+
+ // Callback succès
+ if (this.onProgress) {
+ this.onProgress(item, this.getProgress());
+ }
+
+ } catch (error) {
+ // Erreur
+ item.error = {
+ message: error.message,
+ stack: error.stack,
+ timestamp: new Date().toISOString()
+ };
+
+ if (item.attempts >= item.maxAttempts) {
+ item.status = 'failed';
+ this.errorCount++;
+
+ logSh(`❌ Ligne ${item.rowNumber} échouée définitivement après ${item.attempts} tentatives`, 'ERROR');
+ } else {
+ item.status = 'error';
+ logSh(`⚠️ Ligne ${item.rowNumber} échouée, retry possible`, 'WARNING');
+ }
+
+ // Callback erreur
+ if (this.onError) {
+ this.onError(item, error);
+ }
+ }
+
+ this.currentRow = null;
+ await this.updateStatus();
+ await this.saveQueue();
+ });
+ }
+
+ /**
+ * Traite une ligne spécifique
+ */
+ async processRow(rowNumber) {
+ return tracer.run('BatchProcessor.processRow', { rowNumber }, async () => {
+ // Configuration pour cette ligne
+ const rowConfig = {
+ rowNumber,
+ source: 'batch_processor',
+ selectiveStack: this.config.selective,
+ adversarialMode: this.config.adversarial,
+ humanSimulationMode: this.config.humanSimulation,
+ patternBreakingMode: this.config.patternBreaking,
+ intensity: this.config.intensity,
+ saveIntermediateSteps: this.config.saveIntermediateSteps
+ };
+
+ logSh(`🎯 Configuration ligne ${rowNumber}: ${JSON.stringify(rowConfig)}`, 'DEBUG');
+
+ // Exécuter le workflow modulaire
+ const result = await handleModularWorkflow(rowConfig);
+
+ logSh(`📊 Résultat ligne ${rowNumber}: ${result ? 'SUCCESS' : 'FAILED'}`, 'INFO');
+
+ return result;
+ });
+ }
+
+ // ========================================
+ // GESTION ÉTAT
+ // ========================================
+
+ /**
+ * Met à jour le status
+ */
+ async updateStatus() {
+ const status = this.getStatus();
+
+ try {
+ await fs.writeFile(this.statusPath, JSON.stringify(status, null, 2));
+
+ // Callback update
+ if (this.onStatusUpdate) {
+ this.onStatusUpdate(status);
+ }
+
+ } catch (error) {
+ logSh(`❌ Erreur mise à jour status: ${error.message}`, 'ERROR');
+ }
+ }
+
+ /**
+ * Retourne le status actuel
+ */
+ getStatus() {
+ const now = new Date();
+ const completedItems = this.queue.filter(item => item.status === 'completed').length;
+ const failedItems = this.queue.filter(item => item.status === 'failed').length;
+ const totalItems = this.queue.length;
+
+ const progress = totalItems > 0 ? ((completedItems + failedItems) / totalItems) * 100 : 0;
+
+ let status = 'idle';
+ if (this.isRunning && this.isPaused) {
+ status = 'paused';
+ } else if (this.isRunning) {
+ status = 'running';
+ } else if (completedItems + failedItems === totalItems && totalItems > 0) {
+ status = 'completed';
+ }
+
+ return {
+ status,
+ currentRow: this.currentRow,
+ totalRows: totalItems,
+ completedRows: completedItems,
+ failedRows: failedItems,
+ progress: Math.round(progress),
+ startTime: this.startTime ? this.startTime.toISOString() : null,
+ estimatedEnd: this.estimateCompletionTime(),
+ errors: this.queue.filter(item => item.error).map(item => ({
+ rowNumber: item.rowNumber,
+ error: item.error,
+ attempts: item.attempts
+ })),
+ lastResult: this.getLastResult(),
+ config: this.config,
+ queue: this.queue
+ };
+ }
+
+ /**
+ * Retourne la progression détaillée
+ */
+ getProgress() {
+ const status = this.getStatus();
+ const now = new Date();
+ const elapsed = this.startTime ? now - this.startTime : 0;
+
+ const avgTimePerRow = status.completedRows > 0 ? elapsed / status.completedRows : 0;
+ const remainingRows = status.totalRows - status.completedRows - status.failedRows;
+ const estimatedRemaining = avgTimePerRow * remainingRows;
+
+ return {
+ ...status,
+ metrics: {
+ elapsedTime: elapsed,
+ avgTimePerRow: avgTimePerRow,
+ estimatedRemaining: estimatedRemaining,
+ completionPercentage: status.progress,
+ throughput: status.completedRows > 0 && elapsed > 0 ? (status.completedRows / (elapsed / 1000 / 60)) : 0 // rows/minute
+ }
+ };
+ }
+
+ /**
+ * Estime l'heure de fin
+ */
+ estimateCompletionTime() {
+ if (!this.startTime || !this.isRunning || this.isPaused) {
+ return null;
+ }
+
+ const progress = this.getProgress();
+ if (progress.metrics.estimatedRemaining > 0) {
+ const endTime = new Date(Date.now() + progress.metrics.estimatedRemaining);
+ return endTime.toISOString();
+ }
+
+ return null;
+ }
+
+ /**
+ * Retourne le dernier résultat
+ */
+ getLastResult() {
+ const completedItems = this.queue.filter(item => item.status === 'completed');
+ if (completedItems.length === 0) return null;
+
+ const lastItem = completedItems[completedItems.length - 1];
+ return {
+ rowNumber: lastItem.rowNumber,
+ result: lastItem.result,
+ endTime: lastItem.endTime
+ };
+ }
+
+ /**
+ * Gère les erreurs critiques
+ */
+ async handleError(error) {
+ logSh(`💥 Erreur critique BatchProcessor: ${error.message}`, 'ERROR');
+
+ this.isRunning = false;
+ this.isPaused = false;
+
+ await this.updateStatus();
+
+ if (this.onError) {
+ this.onError(null, error);
+ }
+ }
+
+ /**
+ * Termine le traitement
+ */
+ async complete() {
+ logSh('🏁 Traitement batch terminé', 'INFO');
+
+ this.isRunning = false;
+ this.isPaused = false;
+ this.currentRow = null;
+
+ await this.updateStatus();
+
+ if (this.onComplete) {
+ this.onComplete(this.getStatus());
+ }
+ }
+
+ // ========================================
+ // UTILITAIRES
+ // ========================================
+
+ /**
+ * Pause l'exécution
+ */
+ async sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ /**
+ * Reset la queue
+ */
+ async resetQueue() {
+ logSh('🔄 Reset de la queue', 'INFO');
+
+ this.queue = [];
+ this.processedCount = 0;
+ this.errorCount = 0;
+
+ await this.populateQueue();
+ await this.updateStatus();
+ }
+
+ /**
+ * Configure les callbacks
+ */
+ setCallbacks({ onStatusUpdate, onProgress, onError, onComplete }) {
+ this.onStatusUpdate = onStatusUpdate;
+ this.onProgress = onProgress;
+ this.onError = onError;
+ this.onComplete = onComplete;
+ }
+}
+
+// ============= EXPORTS =============
+module.exports = { BatchProcessor };
\ No newline at end of file
diff --git a/lib/batch/DigitalOceanTemplates.js b/lib/batch/DigitalOceanTemplates.js
new file mode 100644
index 0000000..e55a0b0
--- /dev/null
+++ b/lib/batch/DigitalOceanTemplates.js
@@ -0,0 +1,429 @@
+// ========================================
+// DIGITAL OCEAN TEMPLATES - RÉCUPÉRATION XML
+// Responsabilité: Récupération et cache des templates XML depuis DigitalOcean Spaces
+// ========================================
+
+const { logSh } = require('../ErrorReporting');
+const { tracer } = require('../trace');
+const fs = require('fs').promises;
+const path = require('path');
+const axios = require('axios');
+
+/**
+ * DIGITAL OCEAN TEMPLATES MANAGER
+ * Gestion récupération, cache et fallback des templates XML
+ */
+class DigitalOceanTemplates {
+
+ constructor() {
+ this.cacheDir = path.join(__dirname, '../../cache/templates');
+ this.config = {
+ endpoint: process.env.DO_SPACES_ENDPOINT || 'https://fra1.digitaloceanspaces.com',
+ bucket: process.env.DO_SPACES_BUCKET || 'autocollant',
+ region: process.env.DO_SPACES_REGION || 'fra1',
+ accessKey: process.env.DO_SPACES_KEY,
+ secretKey: process.env.DO_SPACES_SECRET,
+ timeout: 10000 // 10 secondes
+ };
+
+ // Cache en mémoire
+ this.memoryCache = new Map();
+ this.cacheExpiry = 5 * 60 * 1000; // 5 minutes
+
+ // Templates par défaut
+ this.defaultTemplates = {
+ 'default.xml': this.getDefaultTemplate(),
+ 'simple.xml': this.getSimpleTemplate(),
+ 'advanced.xml': this.getAdvancedTemplate()
+ };
+
+ this.initializeTemplateManager();
+ }
+
+ /**
+ * Initialise le gestionnaire de templates
+ */
+ async initializeTemplateManager() {
+ try {
+ // Créer le dossier cache
+ await fs.mkdir(this.cacheDir, { recursive: true });
+
+ // Vérifier la configuration DO
+ this.checkConfiguration();
+
+ logSh('🌊 DigitalOceanTemplates initialisé', 'DEBUG');
+
+ } catch (error) {
+ logSh(`❌ Erreur initialisation DigitalOceanTemplates: ${error.message}`, 'ERROR');
+ }
+ }
+
+ /**
+ * Vérifie la configuration Digital Ocean
+ */
+ checkConfiguration() {
+ const hasCredentials = this.config.accessKey && this.config.secretKey;
+
+ if (!hasCredentials) {
+ logSh('⚠️ Credentials Digital Ocean manquantes, utilisation cache/fallback uniquement', 'WARNING');
+ } else {
+ logSh('✅ Configuration Digital Ocean OK', 'DEBUG');
+ }
+
+ return hasCredentials;
+ }
+
+ // ========================================
+ // RÉCUPÉRATION TEMPLATES
+ // ========================================
+
+ /**
+ * Récupère un template XML (avec cache et fallback)
+ */
+ async getTemplate(filename) {
+ return tracer.run('DigitalOceanTemplates.getTemplate', { filename }, async () => {
+ if (!filename) {
+ throw new Error('Nom de fichier template requis');
+ }
+
+ logSh(`📋 Récupération template: ${filename}`, 'DEBUG');
+
+ try {
+ // 1. Vérifier le cache mémoire
+ const memoryCached = this.getFromMemoryCache(filename);
+ if (memoryCached) {
+ logSh(`⚡ Template ${filename} trouvé en cache mémoire`, 'DEBUG');
+ return memoryCached;
+ }
+
+ // 2. Vérifier le cache fichier
+ const fileCached = await this.getFromFileCache(filename);
+ if (fileCached) {
+ logSh(`💾 Template ${filename} trouvé en cache fichier`, 'DEBUG');
+ this.setMemoryCache(filename, fileCached);
+ return fileCached;
+ }
+
+ // 3. Récupérer depuis Digital Ocean
+ if (this.checkConfiguration()) {
+ try {
+ const template = await this.fetchFromDigitalOcean(filename);
+ if (template) {
+ logSh(`🌊 Template ${filename} récupéré depuis Digital Ocean`, 'INFO');
+
+ // Sauvegarder en cache
+ await this.saveToFileCache(filename, template);
+ this.setMemoryCache(filename, template);
+
+ return template;
+ }
+ } catch (doError) {
+ logSh(`⚠️ Erreur Digital Ocean pour ${filename}: ${doError.message}`, 'WARNING');
+ }
+ }
+
+ // 4. Fallback sur template par défaut
+ const defaultTemplate = this.getDefaultTemplateForFile(filename);
+ logSh(`🔄 Utilisation template par défaut pour ${filename}`, 'WARNING');
+
+ return defaultTemplate;
+
+ } catch (error) {
+ logSh(`❌ Erreur récupération template ${filename}: ${error.message}`, 'ERROR');
+
+ // Fallback ultime
+ return this.getDefaultTemplate();
+ }
+ });
+ }
+
+ /**
+ * Récupère depuis Digital Ocean Spaces
+ */
+ async fetchFromDigitalOcean(filename) {
+ return tracer.run('DigitalOceanTemplates.fetchFromDigitalOcean', { filename }, async () => {
+ const url = `${this.config.endpoint}/${this.config.bucket}/templates/${filename}`;
+
+ logSh(`🌊 Récupération DO: ${url}`, 'DEBUG');
+
+ try {
+ // Utiliser une requête simple sans authentification S3 complexe
+ // Digital Ocean Spaces peut être configuré pour accès public aux templates
+ const response = await axios.get(url, {
+ timeout: this.config.timeout,
+ responseType: 'text',
+ headers: {
+ 'Accept': 'application/xml, text/xml, text/plain'
+ }
+ });
+
+ if (response.status === 200 && response.data) {
+ logSh(`✅ Template ${filename} récupéré (${response.data.length} chars)`, 'DEBUG');
+ return response.data;
+ }
+
+ throw new Error(`Réponse invalide: ${response.status}`);
+
+ } catch (error) {
+ if (error.response) {
+ logSh(`❌ Digital Ocean error ${error.response.status}: ${error.response.statusText}`, 'WARNING');
+ } else {
+ logSh(`❌ Digital Ocean network error: ${error.message}`, 'WARNING');
+ }
+ throw error;
+ }
+ });
+ }
+
+ // ========================================
+ // GESTION CACHE
+ // ========================================
+
+ /**
+ * Récupère depuis le cache mémoire
+ */
+ getFromMemoryCache(filename) {
+ const cached = this.memoryCache.get(filename);
+
+ if (cached && Date.now() - cached.timestamp < this.cacheExpiry) {
+ return cached.content;
+ }
+
+ if (cached) {
+ this.memoryCache.delete(filename);
+ }
+
+ return null;
+ }
+
+ /**
+ * Sauvegarde en cache mémoire
+ */
+ setMemoryCache(filename, content) {
+ this.memoryCache.set(filename, {
+ content,
+ timestamp: Date.now()
+ });
+ }
+
+ /**
+ * Récupère depuis le cache fichier
+ */
+ async getFromFileCache(filename) {
+ try {
+ const cachePath = path.join(this.cacheDir, filename);
+ const stats = await fs.stat(cachePath);
+
+ // Cache valide pendant 1 heure
+ const maxAge = 60 * 60 * 1000;
+ if (Date.now() - stats.mtime.getTime() < maxAge) {
+ const content = await fs.readFile(cachePath, 'utf8');
+ return content;
+ }
+ } catch (error) {
+ // Fichier cache n'existe pas ou erreur
+ }
+
+ return null;
+ }
+
+ /**
+ * Sauvegarde en cache fichier
+ */
+ async saveToFileCache(filename, content) {
+ try {
+ const cachePath = path.join(this.cacheDir, filename);
+ await fs.writeFile(cachePath, content, 'utf8');
+ logSh(`💾 Template ${filename} sauvé en cache`, 'DEBUG');
+ } catch (error) {
+ logSh(`⚠️ Erreur sauvegarde cache ${filename}: ${error.message}`, 'WARNING');
+ }
+ }
+
+ // ========================================
+ // TEMPLATES PAR DÉFAUT
+ // ========================================
+
+ /**
+ * Retourne le template par défaut approprié
+ */
+ getDefaultTemplateForFile(filename) {
+ const lowerFilename = filename.toLowerCase();
+
+ if (lowerFilename.includes('simple')) {
+ return this.defaultTemplates['simple.xml'];
+ } else if (lowerFilename.includes('advanced') || lowerFilename.includes('complet')) {
+ return this.defaultTemplates['advanced.xml'];
+ }
+
+ return this.defaultTemplates['default.xml'];
+ }
+
+ /**
+ * Template par défaut standard
+ */
+ getDefaultTemplate() {
+ return `
+
+ |Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|
+
+ |Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases qui présente {{MC0}} et donne envie de lire la suite. Ton {{personality.style}}}|
+
+
+ |Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|
+ |Paragraphe_1{{MC+1_1}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_1}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|
+
+
+
+ |Titre_H2_2{{MC+1_2}}{Crée un titre H2 informatif sur {{MC+1_2}}. Style {{personality.style}}}|
+ |Paragraphe_2{{MC+1_2}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_2}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|
+
+
+ |Conclusion{{MC0}}{Conclusion engageante de 2 phrases sur {{MC0}}. Appel à l'action subtil. Ton {{personality.style}}}|
+ `;
+ }
+
+ /**
+ * Template simple
+ */
+ getSimpleTemplate() {
+ return `
+
+ |Titre_H1{{T0}}{Titre principal pour {{MC0}}}|
+ |Introduction{{MC0}}{Introduction pour {{MC0}}}|
+ |Contenu_Principal{{MC0}}{Contenu principal sur {{MC0}}}|
+ |Conclusion{{MC0}}{Conclusion sur {{MC0}}}|
+ `;
+ }
+
+ /**
+ * Template avancé
+ */
+ getAdvancedTemplate() {
+ return `
+
+ |Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|
+
+ |Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases qui présente {{MC0}} et donne envie de lire la suite. Ton {{personality.style}}}|
+
+
+ |Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|
+ |Paragraphe_1{{MC+1_1}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_1}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|
+
+
+
+ |Titre_H2_2{{MC+1_2}}{Crée un titre H2 informatif sur {{MC+1_2}}. Style {{personality.style}}}|
+ |Paragraphe_2{{MC+1_2}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_2}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|
+
+
+
+ |Titre_H2_3{{MC+1_3}}{Crée un titre H2 informatif sur {{MC+1_3}}. Style {{personality.style}}}|
+ |Paragraphe_3{{MC+1_3}}{Explique en 4-5 phrases les avantages de {{MC+1_3}} pour {{MC0}}. Ton {{personality.style}}}|
+
+
+
+ |FAQ_Titre{Titre de section FAQ accrocheur sur {{MC0}}}|
+
+
+ |Faq_q_1{{MC+1_1}}{Question fréquente sur {{MC+1_1}} et {{MC0}}}|
+ |Faq_a_1{{MC+1_1}}{Réponse claire et précise. 2-3 phrases. Ton {{personality.style}}}|
+
+
+
+ |Faq_q_2{{MC+1_2}}{Question pratique sur {{MC+1_2}} en lien avec {{MC0}}}|
+ |Faq_a_2{{MC+1_2}}{Réponse détaillée et utile. 2-3 phrases explicatives. Ton {{personality.style}}}|
+
+
+
+ |Faq_q_3{{MC+1_3}}{Question sur {{MC+1_3}} que se posent les clients}|
+ |Faq_a_3{{MC+1_3}}{Réponse complète qui rassure et informe. 2-3 phrases. Ton {{personality.style}}}|
+
+
+
+ |Conclusion{{MC0}}{Conclusion engageante de 2 phrases sur {{MC0}}. Appel à l'action subtil. Ton {{personality.style}}}|
+ `;
+ }
+
+ // ========================================
+ // UTILITAIRES
+ // ========================================
+
+ /**
+ * Liste les templates disponibles
+ */
+ async listAvailableTemplates() {
+ const templates = [];
+
+ // Templates par défaut
+ Object.keys(this.defaultTemplates).forEach(name => {
+ templates.push({
+ name,
+ source: 'default',
+ cached: true
+ });
+ });
+
+ // Templates en cache
+ try {
+ const cacheFiles = await fs.readdir(this.cacheDir);
+ cacheFiles.forEach(file => {
+ if (file.endsWith('.xml')) {
+ templates.push({
+ name: file,
+ source: 'cache',
+ cached: true
+ });
+ }
+ });
+ } catch (error) {
+ // Dossier cache n'existe pas
+ }
+
+ return templates;
+ }
+
+ /**
+ * Vide le cache
+ */
+ async clearCache() {
+ try {
+ // Vider cache mémoire
+ this.memoryCache.clear();
+
+ // Vider cache fichier
+ const cacheFiles = await fs.readdir(this.cacheDir);
+ for (const file of cacheFiles) {
+ if (file.endsWith('.xml')) {
+ await fs.unlink(path.join(this.cacheDir, file));
+ }
+ }
+
+ logSh('🗑️ Cache templates vidé', 'INFO');
+
+ } catch (error) {
+ logSh(`❌ Erreur vidage cache: ${error.message}`, 'ERROR');
+ }
+ }
+
+ /**
+ * Retourne les statistiques du cache
+ */
+ getCacheStats() {
+ return {
+ memoryCache: {
+ size: this.memoryCache.size,
+ expiry: this.cacheExpiry
+ },
+ config: {
+ hasCredentials: this.checkConfiguration(),
+ endpoint: this.config.endpoint,
+ bucket: this.config.bucket,
+ timeout: this.config.timeout
+ },
+ defaultTemplates: Object.keys(this.defaultTemplates).length
+ };
+ }
+}
+
+// ============= EXPORTS =============
+module.exports = { DigitalOceanTemplates };
\ No newline at end of file
diff --git a/lib/modes/ManualServer.js b/lib/modes/ManualServer.js
index 94c9f7c..6853b78 100644
--- a/lib/modes/ManualServer.js
+++ b/lib/modes/ManualServer.js
@@ -12,6 +12,7 @@ const WebSocket = require('ws');
const { logSh } = require('../ErrorReporting');
const { handleModularWorkflow, benchmarkStacks } = require('../Main');
const { APIController } = require('../APIController');
+const { BatchController } = require('../batch/BatchController');
/**
* SERVEUR MODE MANUAL
@@ -41,6 +42,7 @@ class ManualServer {
this.isRunning = false;
this.apiController = new APIController();
+ this.batchController = new BatchController();
}
// ========================================
@@ -277,6 +279,58 @@ class ManualServer {
await this.apiController.createArticle(req, res);
});
+ // ========================================
+ // 🎯 BATCH PROCESSING API ENDPOINTS
+ // ========================================
+
+ // Configuration batch
+ this.app.get('/api/batch/config', async (req, res) => {
+ await this.batchController.getConfig(req, res);
+ });
+
+ this.app.post('/api/batch/config', async (req, res) => {
+ await this.batchController.saveConfig(req, res);
+ });
+
+ // Contrôle traitement batch
+ this.app.post('/api/batch/start', async (req, res) => {
+ await this.batchController.startBatch(req, res);
+ });
+
+ this.app.post('/api/batch/stop', async (req, res) => {
+ await this.batchController.stopBatch(req, res);
+ });
+
+ this.app.post('/api/batch/pause', async (req, res) => {
+ await this.batchController.pauseBatch(req, res);
+ });
+
+ this.app.post('/api/batch/resume', async (req, res) => {
+ await this.batchController.resumeBatch(req, res);
+ });
+
+ // Monitoring batch
+ this.app.get('/api/batch/status', async (req, res) => {
+ await this.batchController.getStatus(req, res);
+ });
+
+ this.app.get('/api/batch/progress', async (req, res) => {
+ await this.batchController.getProgress(req, res);
+ });
+
+ // Templates Digital Ocean
+ this.app.get('/api/batch/templates', async (req, res) => {
+ await this.batchController.getTemplates(req, res);
+ });
+
+ this.app.get('/api/batch/templates/:filename', async (req, res) => {
+ await this.batchController.getTemplate(req, res);
+ });
+
+ this.app.delete('/api/batch/cache', async (req, res) => {
+ await this.batchController.clearCache(req, res);
+ });
+
// === GESTION PROJETS ===
this.app.get('/api/projects', async (req, res) => {
await this.apiController.getProjects(req, res);
diff --git a/lib/shared/QueueProcessor.js b/lib/shared/QueueProcessor.js
new file mode 100644
index 0000000..e00aa88
--- /dev/null
+++ b/lib/shared/QueueProcessor.js
@@ -0,0 +1,759 @@
+// ========================================
+// QUEUE PROCESSOR - CLASSE COMMUNE
+// Responsabilité: Logique partagée de queue, retry, persistance
+// ========================================
+
+const { logSh } = require('../ErrorReporting');
+const { tracer } = require('../trace');
+const { handleModularWorkflow } = require('../Main');
+const { readInstructionsData } = require('../BrainConfig');
+const fs = require('fs').promises;
+const path = require('path');
+
+/**
+ * QUEUE PROCESSOR BASE
+ * Classe commune pour la gestion de queue avec retry logic et persistance
+ */
+class QueueProcessor {
+
+ constructor(options = {}) {
+ this.name = options.name || 'QueueProcessor';
+ this.configPath = options.configPath;
+ this.statusPath = options.statusPath;
+ this.queuePath = options.queuePath;
+
+ // Configuration par défaut
+ this.config = {
+ selective: 'standardEnhancement',
+ adversarial: 'light',
+ humanSimulation: 'none',
+ patternBreaking: 'none',
+ intensity: 1.0,
+ rowRange: { start: 2, end: 10 },
+ saveIntermediateSteps: false,
+ maxRetries: 3,
+ delayBetweenItems: 1000,
+ batchSize: 1,
+ ...options.config
+ };
+
+ // État du processeur
+ this.isRunning = false;
+ this.isPaused = false;
+ this.currentRow = null;
+ this.queue = [];
+ this.processedItems = [];
+ this.failedItems = [];
+
+ // Métriques
+ this.startTime = null;
+ this.processedCount = 0;
+ this.errorCount = 0;
+
+ // Stats détaillées
+ this.stats = {
+ itemsQueued: 0,
+ itemsProcessed: 0,
+ itemsFailed: 0,
+ averageProcessingTime: 0,
+ totalProcessingTime: 0,
+ startTime: Date.now(),
+ lastProcessedAt: null
+ };
+
+ // Callbacks optionnels
+ this.onStatusUpdate = null;
+ this.onProgress = null;
+ this.onError = null;
+ this.onComplete = null;
+ this.onItemProcessed = null;
+ }
+
+ // ========================================
+ // INITIALISATION
+ // ========================================
+
+ /**
+ * Initialise le processeur
+ */
+ async initialize() {
+ try {
+ await this.loadConfig();
+ await this.initializeQueue();
+ logSh(`🎯 ${this.name} initialisé`, 'DEBUG');
+ } catch (error) {
+ logSh(`❌ Erreur initialisation ${this.name}: ${error.message}`, 'ERROR');
+ throw error;
+ }
+ }
+
+ /**
+ * Charge la configuration
+ */
+ async loadConfig() {
+ if (!this.configPath) return;
+
+ try {
+ const configData = await fs.readFile(this.configPath, 'utf8');
+ this.config = { ...this.config, ...JSON.parse(configData) };
+ logSh(`📋 Configuration ${this.name} chargée`, 'DEBUG');
+ } catch (error) {
+ logSh(`⚠️ Configuration non trouvée pour ${this.name}, utilisation valeurs par défaut`, 'WARNING');
+ }
+ }
+
+ /**
+ * Initialise les fichiers de configuration
+ */
+ async initializeFiles() {
+ if (!this.configPath) return;
+
+ try {
+ const configDir = path.dirname(this.configPath);
+ await fs.mkdir(configDir, { recursive: true });
+
+ // Créer config par défaut si inexistant
+ try {
+ await fs.access(this.configPath);
+ } catch {
+ await fs.writeFile(this.configPath, JSON.stringify(this.config, null, 2));
+ logSh(`📝 Configuration ${this.name} par défaut créée`, 'DEBUG');
+ }
+
+ // Créer status par défaut si inexistant
+ if (this.statusPath) {
+ const defaultStatus = this.getDefaultStatus();
+ try {
+ await fs.access(this.statusPath);
+ } catch {
+ await fs.writeFile(this.statusPath, JSON.stringify(defaultStatus, null, 2));
+ logSh(`📊 Status ${this.name} par défaut créé`, 'DEBUG');
+ }
+ }
+
+ } catch (error) {
+ logSh(`❌ Erreur initialisation fichiers ${this.name}: ${error.message}`, 'ERROR');
+ }
+ }
+
+ // ========================================
+ // GESTION QUEUE
+ // ========================================
+
+ /**
+ * Initialise la queue
+ */
+ async initializeQueue() {
+ try {
+ // Essayer de charger la queue existante
+ if (this.queuePath) {
+ try {
+ const queueData = await fs.readFile(this.queuePath, 'utf8');
+ const savedQueue = JSON.parse(queueData);
+
+ if (savedQueue.queue && Array.isArray(savedQueue.queue)) {
+ this.queue = savedQueue.queue;
+ this.processedCount = savedQueue.processedCount || 0;
+ logSh(`📊 Queue ${this.name} restaurée: ${this.queue.length} éléments`, 'DEBUG');
+ }
+ } catch {
+ // Queue n'existe pas, on la créera
+ }
+ }
+
+ // Si queue vide, la populer
+ if (this.queue.length === 0) {
+ await this.populateQueue();
+ }
+
+ } catch (error) {
+ logSh(`❌ Erreur initialisation queue ${this.name}: ${error.message}`, 'ERROR');
+ }
+ }
+
+ /**
+ * Popule la queue avec les lignes à traiter
+ */
+ async populateQueue() {
+ try {
+ this.queue = [];
+ const { start, end } = this.config.rowRange;
+
+ for (let rowNumber = start; rowNumber <= end; rowNumber++) {
+ this.queue.push({
+ rowNumber,
+ status: 'pending',
+ attempts: 0,
+ maxAttempts: this.config.maxRetries,
+ error: null,
+ result: null,
+ startTime: null,
+ endTime: null,
+ addedAt: Date.now()
+ });
+ }
+
+ await this.saveQueue();
+ this.stats.itemsQueued = this.queue.length;
+
+ logSh(`📋 Queue ${this.name} populée: ${this.queue.length} lignes (${start} à ${end})`, 'INFO');
+
+ } catch (error) {
+ logSh(`❌ Erreur population queue ${this.name}: ${error.message}`, 'ERROR');
+ throw error;
+ }
+ }
+
+ /**
+ * Popule la queue depuis Google Sheets (version avancée)
+ */
+ async populateQueueFromSheets() {
+ try {
+ this.queue = [];
+ let currentRow = this.config.startRow || 2;
+ let consecutiveEmptyRows = 0;
+ const maxEmptyRows = 5;
+
+ while (currentRow <= (this.config.endRow || 50)) {
+ if (this.config.endRow && currentRow > this.config.endRow) {
+ break;
+ }
+
+ try {
+ const csvData = await readInstructionsData(currentRow);
+
+ if (!csvData || !csvData.mc0) {
+ consecutiveEmptyRows++;
+ if (consecutiveEmptyRows >= maxEmptyRows) {
+ logSh(`🛑 Arrêt scan après ${maxEmptyRows} lignes vides consécutives`, 'INFO');
+ break;
+ }
+ } else {
+ consecutiveEmptyRows = 0;
+
+ this.queue.push({
+ rowNumber: currentRow,
+ data: csvData,
+ status: 'pending',
+ attempts: 0,
+ maxAttempts: this.config.maxRetries,
+ error: null,
+ result: null,
+ startTime: null,
+ endTime: null,
+ addedAt: Date.now()
+ });
+ }
+ } catch (error) {
+ consecutiveEmptyRows++;
+ if (consecutiveEmptyRows >= maxEmptyRows) {
+ break;
+ }
+ }
+
+ currentRow++;
+ }
+
+ await this.saveQueue();
+ this.stats.itemsQueued = this.queue.length;
+
+ logSh(`📊 Queue ${this.name} chargée depuis Sheets: ${this.stats.itemsQueued} éléments`, 'INFO');
+
+ } catch (error) {
+ logSh(`❌ Erreur chargement queue depuis Sheets: ${error.message}`, 'ERROR');
+ throw error;
+ }
+ }
+
+ /**
+ * Sauvegarde la queue
+ */
+ async saveQueue() {
+ if (!this.queuePath) return;
+
+ try {
+ const queueData = {
+ queue: this.queue,
+ processedCount: this.processedCount,
+ lastUpdate: new Date().toISOString()
+ };
+
+ await fs.writeFile(this.queuePath, JSON.stringify(queueData, null, 2));
+ } catch (error) {
+ logSh(`❌ Erreur sauvegarde queue ${this.name}: ${error.message}`, 'ERROR');
+ }
+ }
+
+ // ========================================
+ // CONTRÔLES PRINCIPAUX
+ // ========================================
+
+ /**
+ * Démarre le traitement
+ */
+ async start() {
+ return tracer.run(`${this.name}.start`, async () => {
+ if (this.isRunning) {
+ throw new Error(`${this.name} est déjà en cours`);
+ }
+
+ logSh(`🚀 Démarrage ${this.name}`, 'INFO');
+
+ this.isRunning = true;
+ this.isPaused = false;
+ this.startTime = new Date();
+ this.processedCount = 0;
+ this.errorCount = 0;
+
+ await this.loadConfig();
+
+ if (this.queue.length === 0) {
+ await this.populateQueue();
+ }
+
+ await this.updateStatus();
+
+ // Démarrer le traitement asynchrone
+ this.processQueue().catch(error => {
+ logSh(`❌ Erreur traitement queue ${this.name}: ${error.message}`, 'ERROR');
+ this.handleError(error);
+ });
+
+ return this.getStatus();
+ });
+ }
+
+ /**
+ * Arrête le traitement
+ */
+ async stop() {
+ return tracer.run(`${this.name}.stop`, async () => {
+ logSh(`🛑 Arrêt ${this.name}`, 'INFO');
+
+ this.isRunning = false;
+ this.isPaused = false;
+ this.currentRow = null;
+
+ await this.updateStatus();
+ return this.getStatus();
+ });
+ }
+
+ /**
+ * Met en pause le traitement
+ */
+ async pause() {
+ return tracer.run(`${this.name}.pause`, async () => {
+ if (!this.isRunning) {
+ throw new Error(`Aucun traitement ${this.name} en cours`);
+ }
+
+ logSh(`⏸️ Mise en pause ${this.name}`, 'INFO');
+ this.isPaused = true;
+
+ await this.updateStatus();
+ return this.getStatus();
+ });
+ }
+
+ /**
+ * Reprend le traitement
+ */
+ async resume() {
+ return tracer.run(`${this.name}.resume`, async () => {
+ if (!this.isRunning || !this.isPaused) {
+ throw new Error(`Aucun traitement ${this.name} en pause`);
+ }
+
+ logSh(`▶️ Reprise ${this.name}`, 'INFO');
+ this.isPaused = false;
+
+ await this.updateStatus();
+
+ // Reprendre le traitement
+ this.processQueue().catch(error => {
+ logSh(`❌ Erreur reprise traitement ${this.name}: ${error.message}`, 'ERROR');
+ this.handleError(error);
+ });
+
+ return this.getStatus();
+ });
+ }
+
+ // ========================================
+ // TRAITEMENT QUEUE
+ // ========================================
+
+ /**
+ * Traite la queue
+ */
+ async processQueue() {
+ return tracer.run(`${this.name}.processQueue`, async () => {
+ while (this.isRunning && !this.isPaused) {
+ const nextItem = this.queue.find(item => item.status === 'pending' ||
+ (item.status === 'error' && item.attempts < item.maxAttempts));
+
+ if (!nextItem) {
+ logSh(`✅ Traitement ${this.name} terminé`, 'INFO');
+ await this.complete();
+ break;
+ }
+
+ await this.processItem(nextItem);
+
+ if (this.config.delayBetweenItems > 0) {
+ await this.sleep(this.config.delayBetweenItems);
+ }
+ }
+ });
+ }
+
+ /**
+ * Traite un élément de la queue
+ */
+ async processItem(item) {
+ return tracer.run(`${this.name}.processItem`, async () => {
+ logSh(`🔄 Traitement ${this.name} ligne ${item.rowNumber} (tentative ${item.attempts + 1}/${item.maxAttempts})`, 'INFO');
+
+ this.currentRow = item.rowNumber;
+ item.status = 'processing';
+ item.startTime = new Date().toISOString();
+ item.attempts++;
+
+ await this.updateStatus();
+ await this.saveQueue();
+
+ try {
+ const result = await this.processRow(item.rowNumber, item.data);
+
+ // Succès
+ item.status = 'completed';
+ item.result = result;
+ item.endTime = new Date().toISOString();
+ item.error = null;
+
+ this.processedCount++;
+ this.processedItems.push(item);
+
+ const duration = Date.now() - new Date(item.startTime).getTime();
+ this.stats.itemsProcessed++;
+ this.stats.totalProcessingTime += duration;
+ this.stats.averageProcessingTime = Math.round(this.stats.totalProcessingTime / this.stats.itemsProcessed);
+ this.stats.lastProcessedAt = Date.now();
+
+ logSh(`✅ ${this.name} ligne ${item.rowNumber} traitée avec succès (${duration}ms)`, 'INFO');
+
+ if (this.onItemProcessed) {
+ this.onItemProcessed(item, result);
+ }
+
+ if (this.onProgress) {
+ this.onProgress(item, this.getProgress());
+ }
+
+ } catch (error) {
+ item.error = {
+ message: error.message,
+ stack: error.stack,
+ timestamp: new Date().toISOString()
+ };
+
+ if (item.attempts >= item.maxAttempts) {
+ item.status = 'failed';
+ this.errorCount++;
+ this.failedItems.push(item);
+
+ logSh(`❌ ${this.name} ligne ${item.rowNumber} échouée définitivement après ${item.attempts} tentatives`, 'ERROR');
+ } else {
+ item.status = 'error';
+ logSh(`⚠️ ${this.name} ligne ${item.rowNumber} échouée, retry possible`, 'WARNING');
+ }
+
+ if (this.onError) {
+ this.onError(item, error);
+ }
+ }
+
+ this.currentRow = null;
+ await this.updateStatus();
+ await this.saveQueue();
+ });
+ }
+
+ /**
+ * Traite une ligne spécifique - à surcharger dans les classes enfants
+ */
+ async processRow(rowNumber, data = null) {
+ const rowConfig = this.buildRowConfig(rowNumber, data);
+ logSh(`🎯 Configuration ${this.name} ligne ${rowNumber}: ${JSON.stringify(rowConfig)}`, 'DEBUG');
+
+ const result = await handleModularWorkflow(rowConfig);
+ logSh(`📊 Résultat ${this.name} ligne ${rowNumber}: ${result ? 'SUCCESS' : 'FAILED'}`, 'INFO');
+
+ return result;
+ }
+
+ /**
+ * Construit la configuration pour une ligne - à surcharger si nécessaire
+ */
+ buildRowConfig(rowNumber, data = null) {
+ return {
+ rowNumber,
+ source: `${this.name.toLowerCase()}_row_${rowNumber}`,
+ selectiveStack: this.config.selective,
+ adversarialMode: this.config.adversarial,
+ humanSimulationMode: this.config.humanSimulation,
+ patternBreakingMode: this.config.patternBreaking,
+ intensity: this.config.intensity,
+ saveIntermediateSteps: this.config.saveIntermediateSteps,
+ data
+ };
+ }
+
+ // ========================================
+ // GESTION ÉTAT
+ // ========================================
+
+ /**
+ * Met à jour le status
+ */
+ async updateStatus() {
+ const status = this.getStatus();
+
+ if (this.statusPath) {
+ try {
+ await fs.writeFile(this.statusPath, JSON.stringify(status, null, 2));
+ } catch (error) {
+ logSh(`❌ Erreur mise à jour status ${this.name}: ${error.message}`, 'ERROR');
+ }
+ }
+
+ if (this.onStatusUpdate) {
+ this.onStatusUpdate(status);
+ }
+ }
+
+ /**
+ * Retourne le status actuel
+ */
+ getStatus() {
+ const now = new Date();
+ const completedItems = this.queue.filter(item => item.status === 'completed').length;
+ const failedItems = this.queue.filter(item => item.status === 'failed').length;
+ const totalItems = this.queue.length;
+
+ const progress = totalItems > 0 ? ((completedItems + failedItems) / totalItems) * 100 : 0;
+
+ let status = 'idle';
+ if (this.isRunning && this.isPaused) {
+ status = 'paused';
+ } else if (this.isRunning) {
+ status = 'running';
+ } else if (completedItems + failedItems === totalItems && totalItems > 0) {
+ status = 'completed';
+ }
+
+ return {
+ status,
+ currentRow: this.currentRow,
+ totalRows: totalItems,
+ completedRows: completedItems,
+ failedRows: failedItems,
+ progress: Math.round(progress),
+ startTime: this.startTime ? this.startTime.toISOString() : null,
+ estimatedEnd: this.estimateCompletionTime(),
+ errors: this.queue.filter(item => item.error).map(item => ({
+ rowNumber: item.rowNumber,
+ error: item.error,
+ attempts: item.attempts
+ })),
+ lastResult: this.getLastResult(),
+ config: this.config,
+ queue: this.queue,
+ stats: this.stats
+ };
+ }
+
+ /**
+ * Retourne la progression détaillée
+ */
+ getProgress() {
+ // Calcul direct des métriques sans appeler getStatus() pour éviter la récursion
+ const now = new Date();
+ const elapsed = this.startTime ? now - this.startTime : 0;
+
+ const completedRows = this.processedItems.length;
+ const failedRows = this.failedItems.length;
+ const totalRows = this.queue.length + completedRows + failedRows;
+
+ const avgTimePerRow = completedRows > 0 ? elapsed / completedRows : 0;
+ const remainingRows = totalRows - completedRows - failedRows;
+ const estimatedRemaining = avgTimePerRow * remainingRows;
+
+ return {
+ status: this.status,
+ currentRow: this.currentItem ? this.currentItem.rowNumber : null,
+ totalRows: totalRows,
+ completedRows: completedRows,
+ failedRows: failedRows,
+ progress: totalRows > 0 ? Math.round((completedRows / totalRows) * 100) : 0,
+ startTime: this.startTime ? this.startTime.toISOString() : null,
+ estimatedEnd: null, // Calculé séparément pour éviter récursion
+ errors: this.failedItems.map(item => ({ row: item.rowNumber, error: item.error })),
+ lastResult: this.processedItems.length > 0 ? this.processedItems[this.processedItems.length - 1].result : null,
+ config: this.config,
+ queue: this.queue,
+ stats: {
+ itemsQueued: this.queue.length,
+ itemsProcessed: completedRows,
+ itemsFailed: failedRows,
+ averageProcessingTime: avgTimePerRow,
+ totalProcessingTime: elapsed,
+ startTime: this.startTime ? this.startTime.getTime() : null,
+ lastProcessedAt: this.processedItems.length > 0 ? this.processedItems[this.processedItems.length - 1].endTime : null
+ },
+ metrics: {
+ elapsedTime: elapsed,
+ avgTimePerRow: avgTimePerRow,
+ estimatedRemaining: estimatedRemaining,
+ completionPercentage: totalRows > 0 ? (completedRows / totalRows) * 100 : 0,
+ throughput: completedRows > 0 && elapsed > 0 ? (completedRows / (elapsed / 1000 / 60)) : 0
+ }
+ };
+ }
+
+ /**
+ * Estime l'heure de fin
+ */
+ estimateCompletionTime() {
+ if (!this.startTime || !this.isRunning || this.isPaused) {
+ return null;
+ }
+
+ // Calcul direct sans appeler getProgress() pour éviter la récursion
+ const now = new Date();
+ const elapsed = now - this.startTime;
+ const completedRows = this.processedItems.length;
+
+ if (completedRows > 0) {
+ const avgTimePerRow = elapsed / completedRows;
+ const remainingRows = this.queue.length;
+ const estimatedRemaining = avgTimePerRow * remainingRows;
+
+ if (estimatedRemaining > 0) {
+ const endTime = new Date(Date.now() + estimatedRemaining);
+ return endTime.toISOString();
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Retourne le dernier résultat
+ */
+ getLastResult() {
+ const completedItems = this.queue.filter(item => item.status === 'completed');
+ if (completedItems.length === 0) return null;
+
+ const lastItem = completedItems[completedItems.length - 1];
+ return {
+ rowNumber: lastItem.rowNumber,
+ result: lastItem.result,
+ endTime: lastItem.endTime
+ };
+ }
+
+ /**
+ * Status par défaut
+ */
+ getDefaultStatus() {
+ return {
+ status: 'idle',
+ currentRow: null,
+ totalRows: 0,
+ progress: 0,
+ startTime: null,
+ estimatedEnd: null,
+ errors: [],
+ lastResult: null,
+ config: this.config
+ };
+ }
+
+ // ========================================
+ // GESTION ERREURS
+ // ========================================
+
+ /**
+ * Gère les erreurs critiques
+ */
+ async handleError(error) {
+ logSh(`💥 Erreur critique ${this.name}: ${error.message}`, 'ERROR');
+
+ this.isRunning = false;
+ this.isPaused = false;
+
+ await this.updateStatus();
+
+ if (this.onError) {
+ this.onError(null, error);
+ }
+ }
+
+ /**
+ * Termine le traitement
+ */
+ async complete() {
+ logSh(`🏁 Traitement ${this.name} terminé`, 'INFO');
+
+ this.isRunning = false;
+ this.isPaused = false;
+ this.currentRow = null;
+
+ await this.updateStatus();
+
+ if (this.onComplete) {
+ this.onComplete(this.getStatus());
+ }
+ }
+
+ // ========================================
+ // UTILITAIRES
+ // ========================================
+
+ /**
+ * Pause l'exécution
+ */
+ async sleep(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ /**
+ * Reset la queue
+ */
+ async resetQueue() {
+ logSh(`🔄 Reset de la queue ${this.name}`, 'INFO');
+
+ this.queue = [];
+ this.processedCount = 0;
+ this.errorCount = 0;
+
+ await this.populateQueue();
+ await this.updateStatus();
+ }
+
+ /**
+ * Configure les callbacks
+ */
+ setCallbacks({ onStatusUpdate, onProgress, onError, onComplete, onItemProcessed }) {
+ this.onStatusUpdate = onStatusUpdate;
+ this.onProgress = onProgress;
+ this.onError = onError;
+ this.onComplete = onComplete;
+ this.onItemProcessed = onItemProcessed;
+ }
+}
+
+// ============= EXPORTS =============
+module.exports = { QueueProcessor };
\ No newline at end of file
diff --git a/public/batch-dashboard.html b/public/batch-dashboard.html
new file mode 100644
index 0000000..a340a19
--- /dev/null
+++ b/public/batch-dashboard.html
@@ -0,0 +1,1045 @@
+
+
+
+
+
+ Dashboard Batch Processing - SEO Generator
+
+
+
+
+
+
+
+
+
+
+
+
⚙️ Configuration Pipeline
+
+
+
+
+
+
+ 💾 Sauvegarder Config
+ 📋 Charger Config
+
+
+
+
+
+
🎮 Contrôles de Traitement
+
+
+
+
+
+
+ ▶️ Démarrer
+ ⏸️ Pause
+ ⏯️ Reprendre
+ ⏹️ Arrêter
+
+
+
+
+
+
+
+
+
📋 État de la Queue
+
+
+
+
+
+
+
+
+
+
🌊 Templates Digital Ocean
+
+
+ 🔄 Actualiser
+ 🗑️ Vider Cache
+
+
+
+
+
+
+
+ Cache stats will appear here
+
+
+
+
+
+
📝 Logs Temps Réel
+
+
+ 🗑️ Vider Logs
+ 📜 Auto-Scroll
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/public/batch-interface.html b/public/batch-interface.html
new file mode 100644
index 0000000..5477e75
--- /dev/null
+++ b/public/batch-interface.html
@@ -0,0 +1,625 @@
+
+
+
+
+
+ Interface Traitement Batch - SEO Generator
+
+
+
+
+
+
+
+
+
+
⚙️ Configuration Pipeline
+
+
+ Selective Enhancement
+
+ Light Enhancement
+ Standard Enhancement
+ Full Enhancement
+ Personality Focus
+ Fluidity Focus
+
+
+
+
+ Adversarial Mode
+
+ None
+ Light
+ Standard
+ Heavy
+ Adaptive
+
+
+
+
+ Human Simulation
+
+ None
+ Light Simulation
+ Personality Focus
+ Adaptive
+
+
+
+
+ Pattern Breaking
+
+ None
+ Syntax Focus
+ Connectors Focus
+ Adaptive
+
+
+
+
+ Intensité (0.5 - 1.5)
+
+ 1.0
+
+
+
+
+
+
+
+ Sauvegarder étapes intermédiaires
+
+
+
+
+ 💾 Sauvegarder Configuration
+
+
+
+
+
+
+
🎮 Contrôles
+
+
+ ▶️ Démarrer
+
+
+ ⏸️ Pause
+
+
+ ⏯️ Reprendre
+
+
+ ⏹️ Arrêter
+
+
+
+
+
+ 🔄 Recharger Config
+
+
+ 📊 Actualiser Status
+
+
+
+
+
+
+
📊 État du Traitement
+
+
+
+
+
+
📝 Logs en Temps Réel
+
+
+ [06:50:00]
+ [INFO]
+ Interface de traitement batch prête
+
+
+
+
+
+
+
+
+
\ No newline at end of file