- Add comprehensive documentation (IMPLEMENTATION_COMPLETE, ProductionReady, QUICK_START, STARTUP_ANALYSIS) - Add startup scripts (start-server.sh, start-server.bat, check-setup.sh) - Add configs directory structure with README - Add ValidationGuards and Main.js backup - Add LLM monitoring HTML interface - Add cache templates and XML files - Add technical report (rapport_technique.md) - Add bundled code.js 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
47 KiB
🚀 Production Ready - Interface Configuration & Runner
Date: 2025-10-08 Version: 1.0.0 Objectif: Créer une interface complète pour configurer, sauvegarder et exécuter des workflows modulaires de production
📋 Table des Matières
- Vue d'Ensemble
- Architecture Proposée
- Architecture Modulaire Actuelle
- Composants à Créer
- Backend - Nouveaux Endpoints
- Schéma des Données
- Design System
- JavaScript - Fonctions Clés
- Plan d'Exécution Séquentiel
- Tests à Effectuer
- Notes Importantes
🎯 Vue d'Ensemble
Problématique
Actuellement, l'interface test-modulaire.html permet de tester des configurations modulaires, mais :
- ❌ Pas de sauvegarde des configurations testées
- ❌ Pas de réutilisation facile des configs
- ❌ Pas d'interface dédiée pour la production (Google Sheets)
Solution Proposée
Créer 3 pages web interconnectées :
index.html- Page d'accueil avec navigationconfig-editor.html- Éditeur de configuration avec Save/Load/Deleteproduction-runner.html- Runner de production avec logs temps réel
🏗️ Architecture Proposée
┌─────────────────────────────────────────────────────────┐
│ INDEX.HTML │
│ (Page d'accueil) │
│ │
│ ┌──────────────────────┐ ┌────────────────────────┐ │
│ │ 🔧 Configuration │ │ 🚀 Production Run │ │
│ │ Editor │ │ Runner │ │
│ └──────────────────────┘ └────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│ │
▼ ▼
┌────────────────────┐ ┌──────────────────────┐
│ config-editor.html │ │ production-runner.html│
│ │ │ │
│ - Éditer config │ │ - Load config │
│ - Save/Load │ │ - Select GSheet row │
│ - Test live │ │ - Run production │
│ - Delete │ │ - View results │
└────────────────────┘ └──────────────────────┘
Flux de Travail
-
Création Config →
config-editor.html- Éditer les 4 couches modulaires
- Tester en direct
- Sauvegarder avec nom
-
Exécution Production →
production-runner.html- Charger config sauvegardée
- Sélectionner ligne Google Sheets
- Lancer workflow complet
- Voir résultats + lien GSheets
📐 Architecture Modulaire Actuelle
4 Couches Modulaires
| Couche | Stacks Disponibles | Total |
|---|---|---|
| Selective Enhancement | lightEnhancement, standardEnhancement, fullEnhancement, personalityFocus, fluidityFocus, adaptive |
6 |
| Adversarial Generation | none, light, standard, heavy, adaptive |
5 |
| Human Simulation | none, lightSimulation, standardSimulation, heavySimulation, adaptiveSimulation, personalityFocus, temporalFocus |
7 |
| Pattern Breaking | none, lightPatternBreaking, standardPatternBreaking, heavyPatternBreaking, adaptivePatternBreaking, syntaxFocus, connectorsFocus |
7 |
Configuration Standard Actuelle
{
rowNumber: 2,
selectiveStack: 'standardEnhancement',
adversarialMode: 'light',
humanSimulationMode: 'none',
patternBreakingMode: 'none',
saveIntermediateSteps: true,
source: 'web_interface'
}
Fichiers Modulaires Existants
lib/selective-enhancement/SelectiveLayers.js- 6 stacks prédéfinislib/adversarial-generation/AdversarialLayers.js- 5 modes défenselib/human-simulation/HumanSimulationLayers.js- 6 modes simulationlib/pattern-breaking/PatternBreakingLayers.js- 7 modes cassage patterns
🔧 Composants à Créer
1. Page d'Accueil : public/index.html
Fonctionnalités :
- Design simple et clean avec 2 grandes cards cliquables
- Navigation vers config-editor.html ou production-runner.html
- Affichage du status serveur (API
/api/status) - Statistiques rapides (nombre de configs sauvegardées, dernière exécution)
Structure HTML :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SEO Generator - Accueil</title>
<style>
/* Design system (voir section Design System) */
</style>
</head>
<body>
<header>
<h1>🎯 SEO Generator - Dashboard</h1>
<div class="server-status"></div>
</header>
<main>
<div class="card-container">
<!-- Card 1: Configuration Editor -->
<div class="card" onclick="navigateTo('config-editor.html')">
<div class="card-icon">🔧</div>
<h2>Éditeur de Configuration</h2>
<p>Créer, éditer et sauvegarder des configurations modulaires</p>
<ul>
<li>4 couches configurables</li>
<li>Save/Load configs</li>
<li>Test en direct</li>
</ul>
</div>
<!-- Card 2: Production Runner -->
<div class="card" onclick="navigateTo('production-runner.html')">
<div class="card-icon">🚀</div>
<h2>Runner de Production</h2>
<p>Exécuter un workflow complet sur Google Sheets</p>
<ul>
<li>Load config sauvegardée</li>
<li>Sélection ligne GSheet</li>
<li>Logs temps réel</li>
</ul>
</div>
</div>
<div class="stats-panel">
<h3>📊 Statistiques</h3>
<div class="stats-grid">
<div class="stat-item">
<span class="stat-value" id="configCount">-</span>
<span class="stat-label">Configs sauvegardées</span>
</div>
<div class="stat-item">
<span class="stat-value" id="lastRun">-</span>
<span class="stat-label">Dernière exécution</span>
</div>
</div>
</div>
</main>
<script>
function navigateTo(page) {
window.location.href = page;
}
async function loadStats() {
try {
const statusResponse = await fetch('/api/status');
const statusData = await statusResponse.json();
const configResponse = await fetch('/api/config/list');
const configData = await configResponse.json();
document.getElementById('configCount').textContent = configData.count || 0;
const uptime = Math.floor(statusData.uptime / 1000);
document.getElementById('lastRun').textContent = `${uptime}s ago`;
} catch (error) {
console.error('Erreur chargement stats:', error);
}
}
window.onload = loadStats;
</script>
</body>
</html>
API Calls :
GET /api/status: Status serveur + uptimeGET /api/config/list: Nombre de configs sauvegardées
2. Éditeur de Config : public/config-editor.html
Fonctionnalités :
- Formulaire complet : 4 couches modulaires (selects inspirés de test-modulaire.html)
- Ligne GSheet : Input pour rowNumber
- Actions :
- 💾 Save Config : Sauvegarder avec nom personnalisé
- 📂 Load Config : Charger depuis liste déroulante
- 🗑️ Delete Config : Supprimer une config existante
- 🚀 Test Live : Tester la config immédiatement (comme test-modulaire.html)
- Logs Temps Réel : Container avec WebSocket (comme test-modulaire.html)
- Preview Config : Afficher JSON de la config actuelle
Structure HTML Clé :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Éditeur de Configuration</title>
</head>
<body>
<header>
<h1>🔧 Éditeur de Configuration Modulaire</h1>
<a href="index.html" class="btn-back">← Retour Accueil</a>
</header>
<div class="editor-container">
<!-- Section Configuration -->
<div class="config-section">
<h2>⚙️ Configuration Modulaire</h2>
<!-- Row Number -->
<div class="form-group">
<label>Ligne Google Sheet</label>
<input type="number" id="rowNumber" value="2" min="2">
</div>
<!-- Selective Enhancement -->
<div class="module-group">
<h3>🔧 Selective Enhancement</h3>
<select id="selectiveStack">
<option value="lightEnhancement">Light Enhancement</option>
<option value="standardEnhancement" selected>Standard Enhancement</option>
<option value="fullEnhancement">Full Enhancement</option>
<option value="personalityFocus">Personality Focus</option>
<option value="fluidityFocus">Fluidity Focus</option>
<option value="adaptive">Adaptive</option>
</select>
<div class="option-description">Base d'amélioration du contenu généré</div>
</div>
<!-- Adversarial Mode -->
<div class="module-group">
<h3>🎯 Adversarial Generation</h3>
<select id="adversarialMode">
<option value="none">None</option>
<option value="light" selected>Light</option>
<option value="standard">Standard</option>
<option value="heavy">Heavy</option>
<option value="adaptive">Adaptive</option>
</select>
<div class="option-description">Techniques adversariales anti-détection</div>
</div>
<!-- Human Simulation -->
<div class="module-group">
<h3>🧠 Human Simulation</h3>
<select id="humanSimulationMode">
<option value="none" selected>None</option>
<option value="lightSimulation">Light Simulation</option>
<option value="standardSimulation">Standard Simulation</option>
<option value="heavySimulation">Heavy Simulation</option>
<option value="adaptiveSimulation">Adaptive Simulation</option>
<option value="personalityFocus">Personality Focus</option>
<option value="temporalFocus">Temporal Focus</option>
</select>
<div class="option-description">Simulation comportement humain (fatigue, erreurs)</div>
</div>
<!-- Pattern Breaking -->
<div class="module-group">
<h3>🔧 Pattern Breaking</h3>
<select id="patternBreakingMode">
<option value="none" selected>None</option>
<option value="lightPatternBreaking">Light Pattern Breaking</option>
<option value="standardPatternBreaking">Standard Pattern Breaking</option>
<option value="heavyPatternBreaking">Heavy Pattern Breaking</option>
<option value="adaptivePatternBreaking">Adaptive Pattern Breaking</option>
<option value="syntaxFocus">Syntax Focus</option>
<option value="connectorsFocus">Connectors Focus</option>
</select>
<div class="option-description">Cassage patterns syntaxiques LLM</div>
</div>
</div>
<!-- Section Save/Load -->
<div class="actions-section">
<h2>💾 Gestion Configurations</h2>
<!-- Save -->
<div class="save-group">
<input type="text" id="configName" placeholder="Nom configuration...">
<button onclick="saveConfig()" class="btn-primary">💾 Save Config</button>
</div>
<!-- Load -->
<div class="load-group">
<select id="savedConfigs">
<option value="">-- Charger une config --</option>
</select>
<button onclick="loadConfig()" class="btn-secondary">📂 Load</button>
<button onclick="deleteConfig()" class="btn-danger">🗑️ Delete</button>
</div>
<!-- Test Live -->
<div class="test-group">
<button onclick="testLive()" class="btn-primary">🚀 Test Live</button>
</div>
</div>
<!-- Preview JSON -->
<div class="preview-section">
<h3>📄 Preview Configuration</h3>
<pre id="configPreview"></pre>
</div>
<!-- Logs Temps Réel -->
<div class="logs-section">
<h3>🔍 Logs Temps Réel</h3>
<button onclick="clearLogs()" class="btn-clear">Clear</button>
<div class="logs-container" id="logsContainer"></div>
</div>
</div>
<script src="config-editor.js"></script>
</body>
</html>
API Calls :
POST /api/config/save: Sauvegarder config avec nomGET /api/config/list: Lister toutes les configsGET /api/config/:name: Charger une config par nomDELETE /api/config/:name: Supprimer une configPOST /api/test-modulaire: Tester la config (existant)
3. Runner de Production : public/production-runner.html
Fonctionnalités :
- Load Config : Dropdown pour sélectionner une config sauvegardée
- Row Selection : Input pour choisir la ligne Google Sheets
- Run Button : Lancer le workflow complet avec sauvegarde versionnée
- Progress Tracking : Barre de progression + étapes
- Results Display :
- Statistiques finales (word count, LLM utilisés, coût, durée)
- Lien vers Google Sheets
- Logs détaillés
- WebSocket Logs : Logs temps réel pendant l'exécution
Structure HTML Clé :
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Production Runner</title>
</head>
<body>
<div class="runner-container">
<!-- Header -->
<header>
<h1>🚀 Production Runner</h1>
<a href="index.html" class="btn-back">← Retour Accueil</a>
</header>
<!-- Configuration Selection -->
<div class="config-selection">
<h2>⚙️ Sélection Configuration</h2>
<div class="select-group">
<label>Configuration :</label>
<select id="configSelect" onchange="loadSelectedConfig()">
<option value="">-- Sélectionner une config --</option>
</select>
<button onclick="refreshConfigs()">🔄 Refresh</button>
</div>
<div class="config-display" id="configDisplay">
<!-- Affichage config sélectionnée -->
</div>
</div>
<!-- Row Selection -->
<div class="row-selection">
<h2>📊 Google Sheets</h2>
<div class="form-group">
<label>Ligne à traiter :</label>
<input type="number" id="rowNumber" value="2" min="2" max="1000">
<span class="help-text">Ligne du Google Sheet "Instructions"</span>
</div>
</div>
<!-- Run Controls -->
<div class="run-controls">
<button onclick="runProduction()" class="btn-run" id="btnRun">
🚀 Lancer Production
</button>
<button onclick="cancelRun()" class="btn-cancel" id="btnCancel" disabled>
🛑 Annuler
</button>
</div>
<!-- Progress Tracking -->
<div class="progress-section" id="progressSection" style="display: none;">
<h2>⏳ Progression</h2>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<div class="progress-steps" id="progressSteps">
<!-- Étapes dynamiques -->
</div>
</div>
<!-- Results -->
<div class="results-section" id="resultsSection" style="display: none;">
<h2>📊 Résultats</h2>
<div class="stats-grid">
<div class="stat-card">
<span class="stat-label">Mots Générés</span>
<span class="stat-value" id="wordCount">-</span>
</div>
<div class="stat-card">
<span class="stat-label">Durée Totale</span>
<span class="stat-value" id="duration">-</span>
</div>
<div class="stat-card">
<span class="stat-label">LLM Utilisés</span>
<span class="stat-value" id="llmUsed">-</span>
</div>
<div class="stat-card">
<span class="stat-label">Coût Estimé</span>
<span class="stat-value" id="cost">-</span>
</div>
</div>
<div class="links-section">
<a href="#" id="gsheetsLink" target="_blank" class="btn-link">
📊 Voir dans Google Sheets
</a>
</div>
</div>
<!-- Logs Temps Réel -->
<div class="logs-section">
<h3>🔍 Logs Temps Réel</h3>
<button onclick="clearLogs()" class="btn-clear">Clear</button>
<div class="logs-container" id="logsContainer"></div>
</div>
</div>
<script src="production-runner.js"></script>
</body>
</html>
API Calls :
GET /api/config/list: Lister configs disponiblesGET /api/config/:name: Charger config sélectionnéePOST /api/production-run: Lancer workflow complet (nouveau endpoint)
🔧 Backend - Nouveaux Endpoints
1. Système de Stockage des Configurations
Fichier : lib/ConfigManager.js
// ========================================
// FICHIER: ConfigManager.js
// RESPONSABILITÉ: Gestion CRUD des configurations modulaires
// STOCKAGE: Fichiers JSON dans configs/
// ========================================
const fs = require('fs').promises;
const path = require('path');
const { logSh } = require('./ErrorReporting');
class ConfigManager {
constructor() {
this.configDir = path.join(__dirname, '../configs');
this.ensureConfigDir();
}
async ensureConfigDir() {
try {
await fs.mkdir(this.configDir, { recursive: true });
} catch (error) {
logSh(`⚠️ Erreur création dossier configs: ${error.message}`, 'WARNING');
}
}
/**
* Sauvegarder une configuration
*/
async saveConfig(name, config) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.configDir, `${sanitizedName}.json`);
const configData = {
name: sanitizedName,
displayName: name,
config,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
await fs.writeFile(filePath, JSON.stringify(configData, null, 2), 'utf-8');
logSh(`💾 Config sauvegardée: ${name}`, 'INFO');
return { success: true, name: sanitizedName };
}
/**
* Charger une configuration
*/
async loadConfig(name) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.configDir, `${sanitizedName}.json`);
try {
const data = await fs.readFile(filePath, 'utf-8');
const configData = JSON.parse(data);
logSh(`📂 Config chargée: ${name}`, 'DEBUG');
return configData;
} catch (error) {
logSh(`❌ Config non trouvée: ${name}`, 'ERROR');
throw new Error(`Configuration "${name}" non trouvée`);
}
}
/**
* Lister toutes les configurations
*/
async listConfigs() {
try {
const files = await fs.readdir(this.configDir);
const jsonFiles = files.filter(f => f.endsWith('.json'));
const configs = await Promise.all(
jsonFiles.map(async (file) => {
const filePath = path.join(this.configDir, file);
const data = await fs.readFile(filePath, 'utf-8');
const configData = JSON.parse(data);
return {
name: configData.name,
displayName: configData.displayName || configData.name,
createdAt: configData.createdAt,
updatedAt: configData.updatedAt
};
})
);
return configs.sort((a, b) =>
new Date(b.updatedAt) - new Date(a.updatedAt)
);
} catch (error) {
logSh(`⚠️ Erreur listing configs: ${error.message}`, 'WARNING');
return [];
}
}
/**
* Supprimer une configuration
*/
async deleteConfig(name) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.configDir, `${sanitizedName}.json`);
await fs.unlink(filePath);
logSh(`🗑️ Config supprimée: ${name}`, 'INFO');
return { success: true };
}
}
module.exports = { ConfigManager };
2. Nouveaux Endpoints dans ManualServer.js
Ajouter dans setupAPIRoutes() :
// ========================================
// 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
});
}
});
// ========================================
// 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
});
}
});
📊 Schéma des Données
Format Config Sauvegardée
Fichier : configs/nom-config.json
{
"name": "config_standard_heavy",
"displayName": "Config Standard Heavy",
"config": {
"rowNumber": 2,
"selectiveStack": "standardEnhancement",
"adversarialMode": "heavy",
"humanSimulationMode": "standardSimulation",
"patternBreakingMode": "standardPatternBreaking",
"saveIntermediateSteps": true,
"source": "web_interface"
},
"createdAt": "2025-10-08T14:30:00.000Z",
"updatedAt": "2025-10-08T14:30:00.000Z"
}
API Responses
GET /api/config/list
{
"success": true,
"configs": [
{
"name": "config_standard_heavy",
"displayName": "Config Standard Heavy",
"createdAt": "2025-10-08T14:30:00.000Z",
"updatedAt": "2025-10-08T14:30:00.000Z"
}
],
"count": 1
}
POST /api/production-run
{
"success": true,
"result": {
"wordCount": 2151,
"duration": 45320,
"llmUsed": "claude,openai,mistral",
"cost": 0.15,
"slug": "plaque-personnalisee",
"gsheetsLink": "https://docs.google.com/spreadsheets/d/xxx"
}
}
🎨 Design System
Palette de Couleurs
:root {
/* Primary Colors */
--primary: #667eea;
--secondary: #764ba2;
/* Semantic Colors */
--success: #48bb78;
--warning: #ed8936;
--error: #f56565;
--info: #4299e1;
/* Backgrounds */
--bg-light: #f7fafc;
--bg-dark: #1a202c;
--bg-card: #ffffff;
/* Text */
--text-dark: #2d3748;
--text-light: #a0aec0;
--text-muted: #718096;
/* Borders */
--border-light: #e2e8f0;
--border-dark: #2d3748;
}
Typography
body {
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-size: 14px;
line-height: 1.6;
color: var(--text-dark);
}
h1 {
font-size: 1.8em;
font-weight: 700;
margin-bottom: 25px;
}
h2 {
font-size: 1.4em;
font-weight: 600;
margin: 20px 0 15px 0;
}
h3 {
font-size: 1.2em;
font-weight: 600;
margin: 15px 0 10px 0;
}
Components
/* Buttons */
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--secondary));
color: white;
padding: 12px 24px;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
/* Cards */
.card {
background: var(--bg-card);
border-radius: 15px;
padding: 25px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
cursor: pointer;
transition: all 0.2s;
}
.card:hover {
transform: translateY(-5px);
box-shadow: 0 15px 40px rgba(0,0,0,0.15);
}
/* Form Groups */
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: var(--text-dark);
}
.form-group input,
.form-group select {
width: 100%;
padding: 12px;
border: 2px solid var(--border-light);
border-radius: 8px;
font-size: 14px;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary);
}
/* Logs Container */
.logs-container {
background: var(--bg-dark);
color: #e2e8f0;
border-radius: 8px;
padding: 15px;
font-family: 'Fira Code', monospace;
font-size: 12px;
height: 300px;
overflow-y: auto;
border: 1px solid var(--border-dark);
}
.log-entry {
margin-bottom: 4px;
padding: 2px 0;
}
.log-debug { color: #63b3ed; }
.log-info { color: #68d391; }
.log-warn { color: #f6e05e; }
.log-error { color: #fc8181; }
.log-trace { color: #a0aec0; }
💻 JavaScript - Fonctions Clés
config-editor.js
// ========================================
// GESTION CONFIGURATION EDITOR
// ========================================
let ws = null;
// WebSocket connection
function connectWebSocket() {
ws = new WebSocket('ws://localhost:8081');
ws.onopen = () => console.log('WebSocket connected');
ws.onmessage = (event) => {
try {
const logData = JSON.parse(event.data);
displayLog(logData);
} catch (e) {
displayLog({ level: 'info', msg: event.data });
}
};
ws.onclose = () => {
console.log('WebSocket disconnected');
setTimeout(connectWebSocket, 3000);
};
}
// Display log entry
function displayLog(logData) {
const logsDiv = document.getElementById('logsContainer');
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
let levelClass = 'log-info';
if (logData.level <= 10) levelClass = 'log-trace';
else if (logData.level === 20) levelClass = 'log-debug';
else if (logData.level === 30) levelClass = 'log-info';
else if (logData.level === 40) levelClass = 'log-warn';
else if (logData.level >= 50) levelClass = 'log-error';
logEntry.innerHTML = `<span class="${levelClass}">${logData.msg}</span>`;
logsDiv.appendChild(logEntry);
logsDiv.scrollTop = logsDiv.scrollHeight;
}
// Sauvegarder configuration
async function saveConfig() {
const configName = document.getElementById('configName').value;
if (!configName) {
showError('Veuillez entrer un nom de configuration');
return;
}
const config = {
rowNumber: parseInt(document.getElementById('rowNumber').value),
selectiveStack: document.getElementById('selectiveStack').value,
adversarialMode: document.getElementById('adversarialMode').value,
humanSimulationMode: document.getElementById('humanSimulationMode').value,
patternBreakingMode: document.getElementById('patternBreakingMode').value,
saveIntermediateSteps: true,
source: 'web_interface'
};
try {
const response = await fetch('/api/config/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: configName, config })
});
const result = await response.json();
if (result.success) {
showSuccess(`Configuration "${configName}" sauvegardée !`);
await refreshConfigsList();
document.getElementById('configName').value = '';
} else {
showError(result.error);
}
} catch (error) {
showError(`Erreur sauvegarde: ${error.message}`);
}
}
// Charger configuration
async function loadConfig() {
const configName = document.getElementById('savedConfigs').value;
if (!configName) return;
try {
const response = await fetch(`/api/config/${configName}`);
const result = await response.json();
if (result.success) {
const config = result.config.config;
// Remplir les champs
document.getElementById('rowNumber').value = config.rowNumber;
document.getElementById('selectiveStack').value = config.selectiveStack;
document.getElementById('adversarialMode').value = config.adversarialMode;
document.getElementById('humanSimulationMode').value = config.humanSimulationMode;
document.getElementById('patternBreakingMode').value = config.patternBreakingMode;
showSuccess(`Configuration "${configName}" chargée !`);
updateConfigPreview();
} else {
showError(result.error);
}
} catch (error) {
showError(`Erreur chargement: ${error.message}`);
}
}
// Supprimer configuration
async function deleteConfig() {
const configName = document.getElementById('savedConfigs').value;
if (!configName) return;
if (!confirm(`Supprimer la configuration "${configName}" ?`)) return;
try {
const response = await fetch(`/api/config/${configName}`, {
method: 'DELETE'
});
const result = await response.json();
if (result.success) {
showSuccess(`Configuration "${configName}" supprimée !`);
await refreshConfigsList();
} else {
showError(result.error);
}
} catch (error) {
showError(`Erreur suppression: ${error.message}`);
}
}
// Refresh configs list
async function refreshConfigsList() {
try {
const response = await fetch('/api/config/list');
const result = await response.json();
const select = document.getElementById('savedConfigs');
select.innerHTML = '<option value="">-- Charger une config --</option>';
result.configs.forEach(config => {
const option = document.createElement('option');
option.value = config.name;
option.textContent = config.displayName;
select.appendChild(option);
});
} catch (error) {
console.error('Erreur refresh configs:', error);
}
}
// Update config preview
function updateConfigPreview() {
const config = {
rowNumber: parseInt(document.getElementById('rowNumber').value),
selectiveStack: document.getElementById('selectiveStack').value,
adversarialMode: document.getElementById('adversarialMode').value,
humanSimulationMode: document.getElementById('humanSimulationMode').value,
patternBreakingMode: document.getElementById('patternBreakingMode').value
};
document.getElementById('configPreview').textContent = JSON.stringify(config, null, 2);
}
// Test live
async function testLive() {
const config = {
rowNumber: parseInt(document.getElementById('rowNumber').value),
selectiveStack: document.getElementById('selectiveStack').value,
adversarialMode: document.getElementById('adversarialMode').value,
humanSimulationMode: document.getElementById('humanSimulationMode').value,
patternBreakingMode: document.getElementById('patternBreakingMode').value,
source: 'web_interface'
};
showStatus('🚀 Test en cours...', 'loading');
clearLogs();
try {
const response = await fetch('/api/test-modulaire', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const result = await response.json();
if (result.success) {
showStatus('✅ Test terminé avec succès!', 'success');
} else {
showStatus('❌ Erreur: ' + (result.error || 'Erreur inconnue'), 'error');
}
} catch (error) {
showStatus('❌ Erreur: ' + error.message, 'error');
}
}
// Clear logs
function clearLogs() {
document.getElementById('logsContainer').innerHTML = '';
}
// Show status
function showStatus(message, type) {
// Implementation
}
// Show success
function showSuccess(message) {
showStatus(message, 'success');
}
// Show error
function showError(message) {
showStatus(message, 'error');
}
// Initialize
window.onload = async () => {
connectWebSocket();
await refreshConfigsList();
updateConfigPreview();
// Auto-update preview on change
['rowNumber', 'selectiveStack', 'adversarialMode', 'humanSimulationMode', 'patternBreakingMode'].forEach(id => {
document.getElementById(id).addEventListener('change', updateConfigPreview);
});
};
production-runner.js
// ========================================
// GESTION PRODUCTION RUNNER
// ========================================
let ws = null;
let currentConfig = null;
// WebSocket connection (same as config-editor.js)
function connectWebSocket() {
// Same implementation
}
// Load selected config
async function loadSelectedConfig() {
const configName = document.getElementById('configSelect').value;
if (!configName) {
document.getElementById('configDisplay').innerHTML = '';
currentConfig = null;
return;
}
try {
const response = await fetch(`/api/config/${configName}`);
const result = await response.json();
if (result.success) {
currentConfig = result.config.config;
displayConfigDetails(currentConfig);
}
} catch (error) {
showError(`Erreur chargement config: ${error.message}`);
}
}
// Display config details
function displayConfigDetails(config) {
const html = `
<div class="config-details">
<h3>Configuration Chargée</h3>
<ul>
<li><strong>Selective:</strong> ${config.selectiveStack}</li>
<li><strong>Adversarial:</strong> ${config.adversarialMode}</li>
<li><strong>Human Simulation:</strong> ${config.humanSimulationMode}</li>
<li><strong>Pattern Breaking:</strong> ${config.patternBreakingMode}</li>
</ul>
</div>
`;
document.getElementById('configDisplay').innerHTML = html;
}
// Refresh configs dropdown
async function refreshConfigs() {
try {
const response = await fetch('/api/config/list');
const result = await response.json();
const select = document.getElementById('configSelect');
select.innerHTML = '<option value="">-- Sélectionner une config --</option>';
result.configs.forEach(config => {
const option = document.createElement('option');
option.value = config.name;
option.textContent = config.displayName;
select.appendChild(option);
});
showSuccess('Configs rafraîchies');
} catch (error) {
showError(`Erreur refresh: ${error.message}`);
}
}
// Run production workflow
async function runProduction() {
const configName = document.getElementById('configSelect').value;
if (!configName) {
showError('Veuillez sélectionner une configuration');
return;
}
if (!currentConfig) {
showError('Configuration non chargée');
return;
}
// Override rowNumber from input
const config = {
...currentConfig,
rowNumber: parseInt(document.getElementById('rowNumber').value)
};
// UI Updates
document.getElementById('btnRun').disabled = true;
document.getElementById('btnCancel').disabled = false;
showProgressSection();
clearLogs();
try {
const response = await fetch('/api/production-run', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const result = await response.json();
if (result.success) {
hideProgressSection();
displayResults(result.result);
showSuccess('✅ Workflow terminé avec succès!');
} else {
hideProgressSection();
showError(result.error);
}
} catch (error) {
hideProgressSection();
showError(`Erreur production: ${error.message}`);
} finally {
document.getElementById('btnRun').disabled = false;
document.getElementById('btnCancel').disabled = true;
}
}
// Display results
function displayResults(result) {
document.getElementById('resultsSection').style.display = 'block';
document.getElementById('wordCount').textContent = result.wordCount || '-';
document.getElementById('duration').textContent = `${(result.duration / 1000).toFixed(1)}s`;
document.getElementById('llmUsed').textContent = result.llmUsed || '-';
document.getElementById('cost').textContent = `$${result.cost?.toFixed(4) || '0.00'}`;
document.getElementById('gsheetsLink').href = result.gsheetsLink;
}
// Show/hide progress
function showProgressSection() {
document.getElementById('progressSection').style.display = 'block';
updateProgress(10);
}
function hideProgressSection() {
document.getElementById('progressSection').style.display = 'none';
}
function updateProgress(percentage) {
document.getElementById('progressFill').style.width = percentage + '%';
}
// Cancel run
function cancelRun() {
// TODO: Implement cancel logic
showError('Annulation non implémentée');
}
// Clear logs
function clearLogs() {
document.getElementById('logsContainer').innerHTML = '';
}
// Initialize
window.onload = async () => {
connectWebSocket();
await refreshConfigs();
};
📅 Plan d'Exécution Séquentiel
| Étape | Tâche | Fichiers | Durée Estimée |
|---|---|---|---|
| 1 | Créer ConfigManager.js |
lib/ConfigManager.js |
30 min |
| 2 | Ajouter endpoints backend | lib/modes/ManualServer.js |
45 min |
| 3 | Créer page d'accueil | public/index.html + CSS |
1h |
| 4 | Créer éditeur de config | public/config-editor.html + config-editor.js |
2h |
| 5 | Créer runner de production | public/production-runner.html + production-runner.js |
2h |
| 6 | Tests end-to-end | Tous les fichiers | 1h |
| TOTAL | ~7h |
Ordre d'Implémentation Recommandé
-
Backend First (Étapes 1-2) :
- Créer ConfigManager.js
- Ajouter endpoints dans ManualServer.js
- Tester avec curl/Postman
-
Frontend Simple → Complexe (Étapes 3-5) :
- Page d'accueil (simple navigation)
- Éditeur de config (formulaires + CRUD)
- Runner de production (intégration complète)
-
Tests & Polish (Étape 6) :
- Tests fonctionnels
- Corrections bugs
- Amélioration UX
🧪 Tests à Effectuer
1. Tests Backend (ConfigManager)
- ✅ Save Config : Sauvegarder avec nom normal
- ✅ Save Config : Nom avec espaces/accents → sanitization
- ✅ Load Config : Charger config existante
- ✅ Load Config : Config inexistante → erreur 404
- ✅ List Configs : Retourne tableau vide si aucune config
- ✅ List Configs : Retourne configs triées par date
- ✅ Delete Config : Suppression réussie
- ✅ Delete Config : Config inexistante → erreur
2. Tests Frontend (Config Editor)
- ✅ Save : Sauvegarde + apparaît dans dropdown
- ✅ Load : Charge tous les champs correctement
- ✅ Delete : Supprime + met à jour dropdown
- ✅ Test Live : Exécute test modulaire
- ✅ Preview : JSON mis à jour en temps réel
- ✅ WebSocket : Logs temps réel reçus
3. Tests Frontend (Production Runner)
- ✅ Load Config : Affiche détails config
- ✅ Run Production : Lance workflow complet
- ✅ Run Production : Override rowNumber correct
- ✅ Results : Statistiques affichées correctement
- ✅ Results : Lien Google Sheets fonctionnel
- ✅ WebSocket : Logs temps réel pendant run
- ✅ Progress : Barre de progression visible
4. Tests End-to-End
- ✅ Flow Complet :
- Accueil → Config Editor
- Créer config + sauvegarder
- Retour Accueil → Production Runner
- Charger config + run production
- Vérifier résultats Google Sheets
📝 Notes Importantes
Compatibilité Existante
- ✅ Ne modifie PAS
test-modulaire.html(reste intact) - ✅ Réutilise les endpoints existants (
/api/test-modulaire) - ✅ Compatible avec architecture MANUAL/AUTO modes
- ✅ Utilise le WebSocket existant (port 8081)
- ✅ Respecte l'architecture modulaire (4 couches)
Structure Fichiers Finale
seo-generator-server/
├── configs/ # 🆕 Nouveau dossier
│ ├── config_standard.json
│ ├── config_heavy.json
│ └── config_light.json
│
├── lib/
│ ├── ConfigManager.js # 🆕 Nouveau fichier
│ └── modes/
│ └── ManualServer.js # ✏️ Modifié (nouveaux endpoints)
│
├── public/
│ ├── index.html # 🆕 Nouveau
│ ├── config-editor.html # 🆕 Nouveau
│ ├── config-editor.js # 🆕 Nouveau
│ ├── production-runner.html # 🆕 Nouveau
│ ├── production-runner.js # 🆕 Nouveau
│ └── test-modulaire.html # ✅ Existant (non modifié)
│
└── ProductionReady.md # 📋 Ce fichier
Endpoints Backend Créés
| Méthode | Endpoint | Description |
|---|---|---|
POST |
/api/config/save |
Sauvegarder configuration |
GET |
/api/config/list |
Lister configurations |
GET |
/api/config/:name |
Charger configuration |
DELETE |
/api/config/:name |
Supprimer configuration |
POST |
/api/production-run |
Lancer workflow production |
Endpoints Existants Réutilisés
| Méthode | Endpoint | Description |
|---|---|---|
GET |
/api/status |
Status serveur |
POST |
/api/test-modulaire |
Test modulaire (existant) |
Dépendances
Aucune nouvelle dépendance npm requise. Utilise uniquement :
- Express (existant)
- WebSocket (existant)
- fs/promises (Node.js built-in)
- path (Node.js built-in)
🚀 Prêt à Implémenter
Checklist Avant Démarrage
- Backup du code actuel
- Créer branche Git
feature/config-interface - Vérifier que
test-modulaire.htmlfonctionne - Vérifier que le serveur démarre en mode MANUAL
Commandes de Démarrage
# 1. Créer branche Git
git checkout -b feature/config-interface
# 2. Créer dossier configs
mkdir -p configs
# 3. Démarrer serveur en mode MANUAL
npm start
# 4. Vérifier que le serveur tourne
curl http://localhost:3000/api/status
Validation Finale
Après implémentation, vérifier :
- ✅ Les 3 pages sont accessibles et navigables
- ✅ Save/Load/Delete configs fonctionne
- ✅ Test Live fonctionne (depuis config-editor)
- ✅ Production Run fonctionne avec sauvegarde GSheets
- ✅ WebSocket logs s'affichent en temps réel
- ✅
test-modulaire.htmlfonctionne toujours (non cassé)
📞 Support
Pour toute question ou problème durant l'implémentation, se référer à :
- CLAUDE.md : Documentation complète du projet
- lib/modes/ManualServer.js : Implémentation serveur MANUAL
- public/test-modulaire.html : Exemple d'interface modulaire fonctionnelle
Dernière mise à jour : 2025-10-08 Auteur : Claude Code Status : 📋 Prêt pour implémentation