## 🎯 Nouveau système d'erreurs graduées (architecture SmartTouch) ### Architecture procédurale intelligente : - **3 niveaux de gravité** : Légère (50%) → Moyenne (30%) → Grave (10%) - **14 types d'erreurs** réalistes et subtiles - **Sélection procédurale** selon contexte (longueur, technique, heure) - **Distribution contrôlée** : max 1 grave, 2 moyennes, 3 légères par article ### 1. Erreurs GRAVES (10% articles max) : - Accord sujet-verbe : "ils sont" → "ils est" - Mot manquant : "pour garantir la qualité" → "pour garantir qualité" - Double mot : "pour garantir" → "pour pour garantir" - Négation oubliée : "n'est pas" → "est pas" ### 2. Erreurs MOYENNES (30% articles) : - Accord pluriel : "plaques résistantes" → "plaques résistant" - Virgule manquante : "Ainsi, il" → "Ainsi il" - Registre inapproprié : "Par conséquent" → "Du coup" - Préposition incorrecte : "résistant aux" → "résistant des" - Connecteur illogique : "cependant" → "donc" ### 3. Erreurs LÉGÈRES (50% articles) : - Double espace : "de votre" → "de votre" - Trait d'union : "c'est-à-dire" → "c'est à dire" - Espace ponctuation : "qualité ?" → "qualité?" - Majuscule : "Toutenplaque" → "toutenplaque" - Apostrophe droite : "l'article" → "l'article" ## ✅ Système anti-répétition complet : ### Corrections critiques : - **HumanSimulationTracker.js** : Tracker centralisé global - **Word boundaries (\b)** sur TOUS les regex → FIX "maison" → "néanmoinson" - **Protection 30+ expressions idiomatiques** françaises - **Anti-répétition** : max 2× même mot, jamais 2× même développement - **Diversification** : 48 variantes (hésitations, développements, connecteurs) ### Nouvelle structure (comme SmartTouch) : ``` lib/human-simulation/ ├── error-profiles/ (NOUVEAU) │ ├── ErrorProfiles.js (définitions + probabilités) │ ├── ErrorGrave.js (10% articles) │ ├── ErrorMoyenne.js (30% articles) │ ├── ErrorLegere.js (50% articles) │ └── ErrorSelector.js (sélection procédurale) ├── HumanSimulationCore.js (orchestrateur) ├── HumanSimulationTracker.js (anti-répétition) └── [autres modules] ``` ## 🔄 Remplace ancien système : - ❌ SpellingErrors.js (basique, répétitif, "et" → "." × 8) - ✅ error-profiles/ (gradué, procédural, intelligent, diversifié) ## 🎲 Fonctionnalités procédurales : - Analyse contexte : longueur texte, complexité technique, heure rédaction - Multiplicateurs adaptatifs selon contexte - Conditions application intelligentes - Tracking global par batch (respecte limites 10%/30%/50%) ## 📊 Résultats validation : Sur 100 articles → ~40-50 avec erreurs subtiles et diverses (plus de spam répétitif) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
2313 lines
76 KiB
JavaScript
2313 lines
76 KiB
JavaScript
// ========================================
|
|
// FICHIER: ManualServer.js
|
|
// RESPONSABILITÉ: Mode MANUAL - Interface Client + API + WebSocket
|
|
// FONCTIONNALITÉS: Dashboard, tests modulaires, API complète
|
|
// ========================================
|
|
|
|
// ⏱️ Timing chargement modules (logs avant/après chaque require)
|
|
const _t0 = Date.now();
|
|
console.log(`[${new Date().toISOString()}] ⏱️ [require] Début chargement ManualServer modules...`);
|
|
|
|
const _t1 = Date.now();
|
|
const express = require('express');
|
|
console.log(`[${new Date().toISOString()}] ✓ [require] express en ${Date.now() - _t1}ms`);
|
|
|
|
const _t2 = Date.now();
|
|
const cors = require('cors');
|
|
console.log(`[${new Date().toISOString()}] ✓ [require] cors en ${Date.now() - _t2}ms`);
|
|
|
|
const _t3 = Date.now();
|
|
const path = require('path');
|
|
console.log(`[${new Date().toISOString()}] ✓ [require] path en ${Date.now() - _t3}ms`);
|
|
|
|
const _t4 = Date.now();
|
|
const WebSocket = require('ws');
|
|
console.log(`[${new Date().toISOString()}] ✓ [require] ws (WebSocket) en ${Date.now() - _t4}ms`);
|
|
|
|
const _t5 = Date.now();
|
|
const { logSh } = require('../ErrorReporting');
|
|
console.log(`[${new Date().toISOString()}] ✓ [require] ErrorReporting en ${Date.now() - _t5}ms`);
|
|
|
|
const _t6 = Date.now();
|
|
const { handleModularWorkflow, benchmarkStacks } = require('../Main');
|
|
console.log(`[${new Date().toISOString()}] ✓ [require] Main en ${Date.now() - _t6}ms`);
|
|
|
|
const _t7 = Date.now();
|
|
const { APIController } = require('../APIController');
|
|
console.log(`[${new Date().toISOString()}] ✓ [require] APIController en ${Date.now() - _t7}ms`);
|
|
|
|
const _t8 = Date.now();
|
|
const { BatchController } = require('../batch/BatchController');
|
|
console.log(`[${new Date().toISOString()}] ✓ [require] BatchController en ${Date.now() - _t8}ms`);
|
|
|
|
console.log(`[${new Date().toISOString()}] ✅ [require] TOTAL ManualServer modules chargés en ${Date.now() - _t0}ms`);
|
|
|
|
/**
|
|
* 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();
|
|
this.batchController = new BatchController();
|
|
|
|
// Cache pour status LLMs (évite d'appeler trop souvent)
|
|
this.llmStatusCache = null;
|
|
this.llmStatusCacheTime = null;
|
|
}
|
|
|
|
// ========================================
|
|
// 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;
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
logSh('🎯 Démarrage ManualServer...', 'INFO');
|
|
|
|
try {
|
|
// 1. Configuration Express
|
|
logSh('⏱️ [1/7] Configuration Express...', 'INFO');
|
|
const t1 = Date.now();
|
|
await this.setupExpressApp();
|
|
logSh(`✓ Express configuré en ${Date.now() - t1}ms`, 'INFO');
|
|
|
|
// 2. Routes API
|
|
logSh('⏱️ [2/7] Configuration routes API...', 'INFO');
|
|
const t2 = Date.now();
|
|
this.setupAPIRoutes();
|
|
logSh(`✓ Routes API configurées en ${Date.now() - t2}ms`, 'INFO');
|
|
|
|
// 3. Interface Web
|
|
logSh('⏱️ [3/7] Configuration interface web...', 'INFO');
|
|
const t3 = Date.now();
|
|
this.setupWebInterface();
|
|
logSh(`✓ Interface web configurée en ${Date.now() - t3}ms`, 'INFO');
|
|
|
|
// 4. WebSocket pour logs temps réel
|
|
logSh('⏱️ [4/7] Démarrage WebSocket serveur...', 'INFO');
|
|
const t4 = Date.now();
|
|
await this.setupWebSocketServer();
|
|
|
|
// ✅ PHASE 3: Injecter WebSocket server dans APIController
|
|
if (this.wsServer) {
|
|
this.apiController.setWebSocketServer(this.wsServer);
|
|
}
|
|
|
|
logSh(`✓ WebSocket démarré en ${Date.now() - t4}ms`, 'INFO');
|
|
|
|
// 5. Démarrage serveur HTTP
|
|
logSh('⏱️ [5/7] Démarrage serveur HTTP...', 'INFO');
|
|
const t5 = Date.now();
|
|
await this.startHTTPServer();
|
|
logSh(`✓ Serveur HTTP démarré en ${Date.now() - t5}ms`, 'INFO');
|
|
|
|
// 6. Monitoring
|
|
logSh('⏱️ [6/7] Démarrage monitoring...', 'INFO');
|
|
const t6 = Date.now();
|
|
this.startMonitoring();
|
|
logSh(`✓ Monitoring démarré en ${Date.now() - t6}ms`, 'INFO');
|
|
|
|
// 7. Initialisation status LLMs au démarrage (en background)
|
|
logSh('⏱️ [7/7] Initialisation LLM status (background)...', 'INFO');
|
|
const t7 = Date.now();
|
|
this.initializeLLMStatus();
|
|
logSh(`✓ LLM init lancé en ${Date.now() - t7}ms`, 'INFO');
|
|
|
|
this.isRunning = true;
|
|
this.stats.startTime = Date.now();
|
|
|
|
logSh(`✅ ManualServer démarré sur http://localhost:${this.config.port} (total: ${Date.now() - startTime}ms)`, '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 {
|
|
// Arrêter le monitoring
|
|
if (this.monitorInterval) {
|
|
clearInterval(this.monitorInterval);
|
|
this.monitorInterval = null;
|
|
}
|
|
|
|
// Arrêter le refresh status LLMs
|
|
if (this.llmStatusInterval) {
|
|
clearInterval(this.llmStatusInterval);
|
|
this.llmStatusInterval = null;
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
|
|
// ========================================
|
|
// ENDPOINTS GESTION CONFIGURATIONS
|
|
// ========================================
|
|
|
|
// Sauvegarder une configuration
|
|
this.app.post('/api/config/save', async (req, res) => {
|
|
try {
|
|
const { name, config } = req.body;
|
|
|
|
if (!name || !config) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Nom et configuration requis'
|
|
});
|
|
}
|
|
|
|
const { ConfigManager } = require('../ConfigManager');
|
|
const configManager = new ConfigManager();
|
|
|
|
const result = await configManager.saveConfig(name, config);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `Configuration "${name}" sauvegardée`,
|
|
savedName: result.name
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur save config: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Lister les configurations
|
|
this.app.get('/api/config/list', async (req, res) => {
|
|
try {
|
|
const { ConfigManager } = require('../ConfigManager');
|
|
const configManager = new ConfigManager();
|
|
|
|
const configs = await configManager.listConfigs();
|
|
|
|
res.json({
|
|
success: true,
|
|
configs,
|
|
count: configs.length
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur list configs: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Charger une configuration
|
|
this.app.get('/api/config/:name', async (req, res) => {
|
|
try {
|
|
const { name } = req.params;
|
|
|
|
const { ConfigManager } = require('../ConfigManager');
|
|
const configManager = new ConfigManager();
|
|
|
|
const configData = await configManager.loadConfig(name);
|
|
|
|
res.json({
|
|
success: true,
|
|
config: configData
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur load config: ${error.message}`, 'ERROR');
|
|
res.status(404).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Supprimer une configuration
|
|
this.app.delete('/api/config/:name', async (req, res) => {
|
|
try {
|
|
const { name } = req.params;
|
|
|
|
const { ConfigManager } = require('../ConfigManager');
|
|
const configManager = new ConfigManager();
|
|
|
|
await configManager.deleteConfig(name);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `Configuration "${name}" supprimée`
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur delete config: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========================================
|
|
// ENDPOINTS PIPELINE MANAGEMENT
|
|
// ========================================
|
|
|
|
// Sauvegarder un pipeline
|
|
this.app.post('/api/pipeline/save', async (req, res) => {
|
|
try {
|
|
const { pipelineDefinition } = req.body;
|
|
|
|
if (!pipelineDefinition) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'pipelineDefinition requis'
|
|
});
|
|
}
|
|
|
|
const { ConfigManager } = require('../ConfigManager');
|
|
const configManager = new ConfigManager();
|
|
|
|
const result = await configManager.savePipeline(pipelineDefinition);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `Pipeline "${pipelineDefinition.name}" sauvegardé`,
|
|
savedName: result.name
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur save pipeline: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Lister les pipelines
|
|
this.app.get('/api/pipeline/list', async (req, res) => {
|
|
try {
|
|
const { ConfigManager } = require('../ConfigManager');
|
|
const configManager = new ConfigManager();
|
|
|
|
const pipelines = await configManager.listPipelines();
|
|
|
|
res.json({
|
|
success: true,
|
|
pipelines,
|
|
count: pipelines.length
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur list pipelines: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Obtenir modules disponibles (AVANT :name pour éviter conflit)
|
|
this.app.get('/api/pipeline/modules', async (req, res) => {
|
|
try {
|
|
const { PipelineDefinition, AVAILABLE_LLM_PROVIDERS } = require('../pipeline/PipelineDefinition');
|
|
|
|
const modules = PipelineDefinition.listModules();
|
|
|
|
res.json({
|
|
success: true,
|
|
modules,
|
|
llmProviders: AVAILABLE_LLM_PROVIDERS
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur get modules: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Obtenir templates prédéfinis (AVANT :name pour éviter conflit)
|
|
this.app.get('/api/pipeline/templates', async (req, res) => {
|
|
try {
|
|
const { listTemplates, getCategories } = require('../pipeline/PipelineTemplates');
|
|
|
|
const templates = listTemplates();
|
|
const categories = getCategories();
|
|
|
|
res.json({
|
|
success: true,
|
|
templates,
|
|
categories
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur get templates: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Obtenir template par nom (AVANT :name pour éviter conflit)
|
|
this.app.get('/api/pipeline/templates/:name', async (req, res) => {
|
|
try {
|
|
const { name } = req.params;
|
|
const { getTemplate } = require('../pipeline/PipelineTemplates');
|
|
|
|
const template = getTemplate(name);
|
|
|
|
if (!template) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: `Template "${name}" non trouvé`
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
template
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur get template: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Charger un pipeline (Route paramétrée APRÈS les routes spécifiques)
|
|
this.app.get('/api/pipeline/:name', async (req, res) => {
|
|
try {
|
|
const { name } = req.params;
|
|
|
|
const { ConfigManager } = require('../ConfigManager');
|
|
const configManager = new ConfigManager();
|
|
|
|
const pipeline = await configManager.loadPipeline(name);
|
|
|
|
res.json({
|
|
success: true,
|
|
pipeline
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur load pipeline: ${error.message}`, 'ERROR');
|
|
res.status(404).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Supprimer un pipeline
|
|
this.app.delete('/api/pipeline/:name', async (req, res) => {
|
|
try {
|
|
const { name } = req.params;
|
|
|
|
const { ConfigManager } = require('../ConfigManager');
|
|
const configManager = new ConfigManager();
|
|
|
|
await configManager.deletePipeline(name);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: `Pipeline "${name}" supprimé`
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur delete pipeline: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Exécuter un pipeline
|
|
this.app.post('/api/pipeline/execute', async (req, res) => {
|
|
try {
|
|
const { pipelineConfig, rowNumber, options = {} } = req.body;
|
|
|
|
if (!pipelineConfig) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'pipelineConfig requis'
|
|
});
|
|
}
|
|
|
|
if (!rowNumber || rowNumber < 2) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'rowNumber requis (minimum 2)'
|
|
});
|
|
}
|
|
|
|
logSh(`🚀 Exécution pipeline: ${pipelineConfig.name} (row ${rowNumber})`, 'INFO');
|
|
if (options.saveIntermediateSteps) {
|
|
logSh(` 💾 Sauvegarde étapes intermédiaires ACTIVÉE`, 'INFO');
|
|
}
|
|
|
|
const { handleFullWorkflow } = require('../Main');
|
|
|
|
const result = await handleFullWorkflow({
|
|
pipelineConfig,
|
|
rowNumber,
|
|
options, // ✅ Transmettre les options au workflow
|
|
source: 'pipeline_api'
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
result: {
|
|
finalContent: result.finalContent,
|
|
executionLog: result.executionLog,
|
|
versionHistory: result.versionHistory, // ✅ Inclure version history
|
|
stats: result.stats
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur execute pipeline: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Valider un pipeline
|
|
this.app.post('/api/pipeline/validate', async (req, res) => {
|
|
try {
|
|
const { pipelineDefinition } = req.body;
|
|
|
|
if (!pipelineDefinition) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'pipelineDefinition requis'
|
|
});
|
|
}
|
|
|
|
const { PipelineDefinition } = require('../pipeline/PipelineDefinition');
|
|
|
|
const validation = PipelineDefinition.validate(pipelineDefinition);
|
|
|
|
res.json({
|
|
success: validation.valid,
|
|
valid: validation.valid,
|
|
errors: validation.errors
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur validate pipeline: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// Estimer durée/coût d'un pipeline
|
|
this.app.post('/api/pipeline/estimate', async (req, res) => {
|
|
try {
|
|
const { pipelineDefinition } = req.body;
|
|
|
|
if (!pipelineDefinition) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'pipelineDefinition requis'
|
|
});
|
|
}
|
|
|
|
const { PipelineDefinition } = require('../pipeline/PipelineDefinition');
|
|
|
|
const summary = PipelineDefinition.getSummary(pipelineDefinition);
|
|
const duration = PipelineDefinition.estimateDuration(pipelineDefinition);
|
|
|
|
res.json({
|
|
success: true,
|
|
estimate: {
|
|
totalSteps: summary.totalSteps,
|
|
summary: summary.summary,
|
|
estimatedDuration: duration.formatted,
|
|
estimatedSeconds: duration.seconds
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur estimate pipeline: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========================================
|
|
// ENDPOINT PRODUCTION RUN
|
|
// ========================================
|
|
|
|
this.app.post('/api/production-run', async (req, res) => {
|
|
try {
|
|
const {
|
|
rowNumber,
|
|
selectiveStack,
|
|
adversarialMode,
|
|
humanSimulationMode,
|
|
patternBreakingMode,
|
|
saveIntermediateSteps = true
|
|
} = req.body;
|
|
|
|
if (!rowNumber) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'rowNumber requis'
|
|
});
|
|
}
|
|
|
|
logSh(`🚀 PRODUCTION RUN: Row ${rowNumber}`, 'INFO');
|
|
|
|
// Appel handleFullWorkflow depuis Main.js
|
|
const { handleFullWorkflow } = require('../Main');
|
|
|
|
const result = await handleFullWorkflow({
|
|
rowNumber,
|
|
selectiveStack: selectiveStack || 'standardEnhancement',
|
|
adversarialMode: adversarialMode || 'light',
|
|
humanSimulationMode: humanSimulationMode || 'none',
|
|
patternBreakingMode: patternBreakingMode || 'none',
|
|
saveIntermediateSteps,
|
|
source: 'production_web'
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
result: {
|
|
wordCount: result.compiledWordCount,
|
|
duration: result.totalDuration,
|
|
llmUsed: result.llmUsed,
|
|
cost: result.estimatedCost,
|
|
slug: result.slug,
|
|
gsheetsLink: `https://docs.google.com/spreadsheets/d/${process.env.GOOGLE_SHEETS_ID}`
|
|
}
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur production run: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error.message
|
|
});
|
|
}
|
|
});
|
|
|
|
// ========================================
|
|
// 🚀 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);
|
|
});
|
|
|
|
// ========================================
|
|
// 🎯 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);
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
// === LLM MONITORING API ===
|
|
this.app.get('/api/llm/status', async (req, res) => {
|
|
await this.handleLLMStatus(req, res);
|
|
});
|
|
|
|
// === PROMPT ENGINE API ===
|
|
this.app.post('/api/generate-prompt', async (req, res) => {
|
|
await this.apiController.generatePrompt(req, res);
|
|
});
|
|
this.app.get('/api/trends', async (req, res) => {
|
|
await this.apiController.getTrends(req, res);
|
|
});
|
|
this.app.post('/api/trends/:trendId', async (req, res) => {
|
|
await this.apiController.setTrend(req, res);
|
|
});
|
|
this.app.get('/api/prompt-engine/status', async (req, res) => {
|
|
await this.apiController.getPromptEngineStatus(req, res);
|
|
});
|
|
|
|
// === WORKFLOW CONFIGURATION API ===
|
|
this.app.get('/api/workflow/sequences', async (req, res) => {
|
|
await this.apiController.getWorkflowSequences(req, res);
|
|
});
|
|
this.app.post('/api/workflow/sequences', async (req, res) => {
|
|
await this.apiController.createWorkflowSequence(req, res);
|
|
});
|
|
this.app.post('/api/workflow/execute', async (req, res) => {
|
|
await this.apiController.executeConfigurableWorkflow(req, res);
|
|
});
|
|
|
|
// === VALIDATION API (PHASE 3) ===
|
|
this.app.post('/api/validation/start', async (req, res) => {
|
|
await this.apiController.startValidation(req, res);
|
|
});
|
|
this.app.get('/api/validation/status/:id', async (req, res) => {
|
|
await this.apiController.getValidationStatus(req, res);
|
|
});
|
|
this.app.post('/api/validation/stop/:id', async (req, res) => {
|
|
await this.apiController.stopValidation(req, res);
|
|
});
|
|
this.app.get('/api/validation/list', async (req, res) => {
|
|
await this.apiController.listValidations(req, res);
|
|
});
|
|
this.app.get('/api/validation/:id/report', async (req, res) => {
|
|
await this.apiController.getValidationReport(req, res);
|
|
});
|
|
this.app.get('/api/validation/:id/evaluations', async (req, res) => {
|
|
await this.apiController.getValidationEvaluations(req, res);
|
|
});
|
|
// ✅ NOUVEAU: Presets validation
|
|
this.app.get('/api/validation/presets', async (req, res) => {
|
|
await this.apiController.getValidationPresets(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()
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialise le status LLMs au démarrage (en background)
|
|
* ⚡ OPTIMISÉ: Mode rapide au démarrage, complet après 30s
|
|
*/
|
|
async initializeLLMStatus() {
|
|
logSh('🚀 Initialisation status LLMs (mode rapide)...', 'DEBUG');
|
|
|
|
// ⚡ Phase 1: Vérification RAPIDE des clés API (immédiat, sans réseau)
|
|
setImmediate(async () => {
|
|
try {
|
|
await this.refreshLLMStatus(true); // quickMode = true
|
|
logSh('✅ Status LLMs (rapide) initialisé - vérification clés API OK', 'INFO');
|
|
|
|
// ⚡ Phase 2: Test COMPLET avec appels réseau après 30 secondes
|
|
setTimeout(async () => {
|
|
try {
|
|
logSh('🔄 Démarrage vérification complète status LLMs (avec tests réseau)...', 'DEBUG');
|
|
await this.refreshLLMStatus(false); // quickMode = false
|
|
logSh('✅ Status LLMs (complet) mis à jour avec tests réseau', 'INFO');
|
|
} catch (error) {
|
|
logSh(`⚠️ Erreur vérification complète LLMs: ${error.message}`, 'WARNING');
|
|
}
|
|
}, 30000); // Attendre 30 secondes avant le test complet
|
|
|
|
} catch (error) {
|
|
logSh(`⚠️ Erreur initialisation rapide status LLMs: ${error.message}`, 'WARNING');
|
|
}
|
|
});
|
|
|
|
// Rafraîchir toutes les 30 minutes (1800000ms) en mode complet
|
|
this.llmStatusInterval = setInterval(async () => {
|
|
try {
|
|
await this.refreshLLMStatus(false); // Mode complet pour les refreshs périodiques
|
|
} catch (error) {
|
|
logSh(`⚠️ Erreur refresh status LLMs: ${error.message}`, 'WARNING');
|
|
}
|
|
}, 1800000);
|
|
}
|
|
|
|
/**
|
|
* Rafraîchit le cache du status LLMs
|
|
* ⚡ OPTIMISÉ: Test rapide sans appels LLM réels au démarrage
|
|
*/
|
|
async refreshLLMStatus(quickMode = false) {
|
|
const { getLLMProvidersList } = require('../LLMManager');
|
|
|
|
logSh(`📊 Récupération status LLMs${quickMode ? ' (mode rapide)' : ''}...`, 'DEBUG');
|
|
|
|
const providers = getLLMProvidersList();
|
|
const providersWithStatus = [];
|
|
|
|
if (quickMode) {
|
|
// ⚡ MODE RAPIDE: Vérifier juste les clés API sans appels réseau
|
|
for (const provider of providers) {
|
|
const hasApiKey = this.checkProviderApiKey(provider.id);
|
|
|
|
providersWithStatus.push({
|
|
...provider,
|
|
status: hasApiKey ? 'unknown' : 'no_key',
|
|
latency: null,
|
|
lastTest: null,
|
|
credits: 'unlimited',
|
|
calls: 0,
|
|
successRate: hasApiKey ? null : 0,
|
|
quickMode: true
|
|
});
|
|
}
|
|
|
|
logSh('⚡ Status rapide LLMs (sans appels réseau) - vérification clés API uniquement', 'DEBUG');
|
|
} else {
|
|
// MODE COMPLET: Test réseau réel
|
|
for (const provider of providers) {
|
|
const startTime = Date.now();
|
|
let status = 'offline';
|
|
let latency = null;
|
|
let lastTest = null;
|
|
|
|
try {
|
|
const { callLLM } = require('../LLMManager');
|
|
await callLLM(provider.id, 'Test ping', { maxTokens: 10 });
|
|
|
|
latency = Date.now() - startTime;
|
|
status = 'online';
|
|
lastTest = new Date().toLocaleTimeString('fr-FR');
|
|
} catch (error) {
|
|
logSh(`⚠️ Provider ${provider.id} offline: ${error.message}`, 'DEBUG');
|
|
status = 'offline';
|
|
}
|
|
|
|
providersWithStatus.push({
|
|
...provider,
|
|
status,
|
|
latency,
|
|
lastTest,
|
|
credits: 'unlimited',
|
|
calls: 0,
|
|
successRate: status === 'online' ? 100 : 0
|
|
});
|
|
}
|
|
}
|
|
|
|
// Mettre à jour le cache
|
|
this.llmStatusCache = {
|
|
success: true,
|
|
providers: providersWithStatus,
|
|
summary: {
|
|
total: providersWithStatus.length,
|
|
online: providersWithStatus.filter(p => p.status === 'online').length,
|
|
offline: providersWithStatus.filter(p => p.status === 'offline').length,
|
|
unknown: providersWithStatus.filter(p => p.status === 'unknown').length,
|
|
no_key: providersWithStatus.filter(p => p.status === 'no_key').length
|
|
},
|
|
quickMode: quickMode,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
this.llmStatusCacheTime = Date.now();
|
|
|
|
if (quickMode) {
|
|
logSh(`⚡ Status LLMs (rapide): ${this.llmStatusCache.summary.unknown} providers avec clés, ${this.llmStatusCache.summary.no_key} sans clés`, 'INFO');
|
|
} else {
|
|
logSh(`✅ Status LLMs (complet): ${this.llmStatusCache.summary.online}/${this.llmStatusCache.summary.total} online`, 'INFO');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Vérifie si un provider a une clé API configurée
|
|
*/
|
|
checkProviderApiKey(providerId) {
|
|
const keyMap = {
|
|
'claude-sonnet-4-5': 'ANTHROPIC_API_KEY',
|
|
'claude-3-5-sonnet-20241022': 'ANTHROPIC_API_KEY',
|
|
'gpt-4o': 'OPENAI_API_KEY',
|
|
'gpt-4o-mini': 'OPENAI_API_KEY',
|
|
'gemini-2-0-flash-exp': 'GOOGLE_API_KEY',
|
|
'gemini-pro': 'GOOGLE_API_KEY',
|
|
'deepseek-chat': 'DEEPSEEK_API_KEY',
|
|
'moonshot-v1-8k': 'MOONSHOT_API_KEY',
|
|
'mistral-small-latest': 'MISTRAL_API_KEY'
|
|
};
|
|
|
|
const envKey = keyMap[providerId];
|
|
if (!envKey) return false;
|
|
|
|
const apiKey = process.env[envKey];
|
|
return apiKey && apiKey.length > 10;
|
|
}
|
|
|
|
/**
|
|
* Handler pour status et monitoring des LLMs
|
|
*/
|
|
async handleLLMStatus(req, res) {
|
|
try {
|
|
// Si on a un cache, le retourner directement
|
|
if (this.llmStatusCache) {
|
|
res.json(this.llmStatusCache);
|
|
} else {
|
|
// Pas encore de cache, retourner une réponse vide
|
|
res.json({
|
|
success: true,
|
|
providers: [],
|
|
summary: {
|
|
total: 0,
|
|
online: 0,
|
|
offline: 0
|
|
},
|
|
timestamp: new Date().toISOString(),
|
|
message: 'Status LLMs en cours de chargement...'
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur status LLMs: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur récupération status LLMs',
|
|
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 = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<article>
|
|
<h1>|Titre_Principal{{${cleanKeyword}}}{Rédige un titre H1 accrocheur pour "${cleanKeyword}"}|</h1>
|
|
<intro>|Introduction{{${cleanKeyword}}}{Rédige une introduction engageante de 2-3 phrases pour "${cleanKeyword}"}|</intro>
|
|
<section1>
|
|
<h2>|Sous_Titre_1{{${cleanKeyword}}}{Rédige un sous-titre H2 pour "${cleanKeyword}"}|</h2>
|
|
<content>|Contenu_1{{${cleanKeyword}}}{Rédige un paragraphe détaillé sur "${cleanKeyword}"}|</content>
|
|
</section1>
|
|
<section2>
|
|
<h2>|Sous_Titre_2{{${cleanKeyword}}}{Rédige un autre sous-titre H2 pour "${cleanKeyword}"}|</h2>
|
|
<content>|Contenu_2{{${cleanKeyword}}}{Rédige un autre paragraphe sur "${cleanKeyword}"}|</content>
|
|
</section2>
|
|
<conclusion>|Conclusion{{${cleanKeyword}}}{Rédige une conclusion pour l'article sur "${cleanKeyword}"}|</conclusion>
|
|
</article>`;
|
|
|
|
// 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 `
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>SEO Generator - Mode MANUAL</title>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<style>
|
|
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; }
|
|
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 15px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); }
|
|
.header { text-align: center; margin-bottom: 40px; }
|
|
.header h1 { color: #2d3748; margin-bottom: 10px; }
|
|
.mode-badge { background: #48bb78; color: white; padding: 8px 16px; border-radius: 20px; font-weight: bold; }
|
|
.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 40px; }
|
|
.stat-card { background: #f7fafc; padding: 20px; border-radius: 10px; text-align: center; border: 1px solid #e2e8f0; }
|
|
.stat-number { font-size: 2em; font-weight: bold; color: #2d3748; }
|
|
.stat-label { color: #718096; margin-top: 5px; }
|
|
.section { margin: 30px 0; padding: 25px; border: 1px solid #e2e8f0; border-radius: 10px; background: #f9f9f9; }
|
|
.section h2 { color: #2d3748; margin-bottom: 15px; }
|
|
.button { display: inline-block; padding: 12px 24px; margin: 8px; background: linear-gradient(135deg, #667eea, #764ba2); color: white; text-decoration: none; border-radius: 8px; border: none; cursor: pointer; font-weight: 500; transition: all 0.2s; }
|
|
.button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); }
|
|
.button.success { background: linear-gradient(135deg, #48bb78, #38a169); }
|
|
.button.warning { background: linear-gradient(135deg, #ed8936, #dd6b20); }
|
|
.alert { padding: 15px; margin: 20px 0; border-radius: 8px; border-left: 4px solid; }
|
|
.alert.success { background: #f0fff4; border-color: #48bb78; color: #22543d; }
|
|
.alert.info { background: #ebf8ff; border-color: #4299e1; color: #2a4365; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🎯 SEO Generator Server</h1>
|
|
<span class="mode-badge">MODE MANUAL</span>
|
|
<p style="color: #718096; margin-top: 15px;">Interface Client + API + Tests Modulaires</p>
|
|
</div>
|
|
|
|
<div class="alert success">
|
|
<strong>✅ Mode MANUAL Actif</strong><br>
|
|
Interface complète disponible • WebSocket temps réel • API complète
|
|
</div>
|
|
|
|
<div class="stats">
|
|
<div class="stat-card">
|
|
<div class="stat-number">${uptime}s</div>
|
|
<div class="stat-label">Uptime</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number">${this.stats.requests}</div>
|
|
<div class="stat-label">Requêtes</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number">${this.activeClients.size}</div>
|
|
<div class="stat-label">Clients WebSocket</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-number">${this.stats.testsExecuted}</div>
|
|
<div class="stat-label">Tests Exécutés</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🧪 Interface Test Modulaire</h2>
|
|
<p>Interface avancée pour tester toutes les combinaisons modulaires avec logs temps réel.</p>
|
|
<a href="/test-modulaire.html" target="_blank" class="button">🚀 Ouvrir Interface Test</a>
|
|
<a href="/step-by-step" target="_blank" class="button">⚡ Interface Step-by-Step</a>
|
|
<a href="/api/modulaire-config" target="_blank" class="button success">📋 Configuration API</a>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>📊 Monitoring & API</h2>
|
|
<p>Endpoints disponibles en mode MANUAL.</p>
|
|
<a href="/llm-monitoring.html" target="_blank" class="button warning">🤖 LLM Monitoring</a>
|
|
<a href="/api/status" target="_blank" class="button">📊 Status API</a>
|
|
<a href="/api/stats" target="_blank" class="button">📈 Statistiques</a>
|
|
<button onclick="testConnection()" class="button success">🔍 Test Connexion</button>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>🌐 WebSocket Logs</h2>
|
|
<p>Logs temps réel sur <strong>ws://localhost:${this.config.wsPort}</strong></p>
|
|
<button onclick="startLogViewer()" class="button warning">🔍 Ouvrir Log Viewer</button>
|
|
<div id="wsStatus" style="margin-top: 10px; padding: 10px; background: #e2e8f0; border-radius: 5px;">
|
|
Status: <span id="wsStatusText">Déconnecté</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="section">
|
|
<h2>💡 Informations Mode MANUAL</h2>
|
|
<ul style="color: #4a5568; line-height: 1.6;">
|
|
<li><strong>Interface Client</strong> : Dashboard complet et interface de test</li>
|
|
<li><strong>API Complète</strong> : Tests individuels, benchmarks, configuration</li>
|
|
<li><strong>WebSocket</strong> : Logs temps réel sur port ${this.config.wsPort}</li>
|
|
<li><strong>Multi-Client</strong> : Plusieurs utilisateurs simultanés</li>
|
|
<li><strong>Pas de GSheets</strong> : Données test simulées ou fournies</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let ws = null;
|
|
|
|
function testConnection() {
|
|
fetch('/api/status')
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
alert('✅ Connexion OK: ' + (data.mode || 'Mode MANUAL actif') + ' - Uptime: ' + Math.floor(data.uptime/1000) + 's');
|
|
})
|
|
.catch(err => {
|
|
alert('❌ Erreur connexion: ' + err.message);
|
|
});
|
|
}
|
|
|
|
function startLogViewer() {
|
|
fetch('/api/start-log-viewer', { method: 'POST' })
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
alert('✅ Log Viewer lancé! WebSocket sur port ' + data.wsPort);
|
|
// Reconnecter le WebSocket pour voir les logs
|
|
connectWebSocket();
|
|
} else {
|
|
alert('❌ Erreur: ' + data.message);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
alert('❌ Erreur lancement: ' + err.message);
|
|
});
|
|
}
|
|
|
|
let reconnectAttempts = 0;
|
|
const maxReconnectDelay = 30000; // 30s max
|
|
|
|
function connectWebSocket() {
|
|
try {
|
|
ws = new WebSocket('ws://localhost:${this.config.wsPort}');
|
|
|
|
ws.onopen = () => {
|
|
reconnectAttempts = 0; // Reset compteur
|
|
document.getElementById('wsStatusText').textContent = 'Connecté ✅';
|
|
document.getElementById('wsStatus').style.background = '#c6f6d5';
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
document.getElementById('wsStatusText').textContent = 'Déconnecté ❌';
|
|
document.getElementById('wsStatus').style.background = '#fed7d7';
|
|
|
|
// Backoff exponentiel pour éviter spam
|
|
reconnectAttempts++;
|
|
const delay = Math.min(1000 * Math.pow(2, reconnectAttempts), maxReconnectDelay);
|
|
console.log('WebSocket fermé, reconnexion dans ' + (delay/1000) + 's (tentative ' + reconnectAttempts + ')');
|
|
setTimeout(connectWebSocket, delay);
|
|
};
|
|
|
|
} catch (error) {
|
|
console.warn('WebSocket non disponible:', error.message);
|
|
// Retry avec backoff en cas d'erreur de connexion
|
|
reconnectAttempts++;
|
|
const delay = Math.min(5000 * reconnectAttempts, maxReconnectDelay);
|
|
setTimeout(connectWebSocket, delay);
|
|
}
|
|
}
|
|
|
|
// Auto-connect WebSocket
|
|
connectWebSocket();
|
|
|
|
// Refresh stats every 30s
|
|
setInterval(() => {
|
|
window.location.reload();
|
|
}, 30000);
|
|
</script>
|
|
</body>
|
|
</html>
|
|
`;
|
|
}
|
|
|
|
// ========================================
|
|
// 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 }; |