• Created QueueProcessor base class for shared queue management, retry logic, and persistence • Refactored BatchProcessor to extend QueueProcessor (385→142 lines, 63% reduction) • Created BatchController with comprehensive API endpoints for batch operations • Added Digital Ocean templates integration with caching • Integrated batch endpoints into ManualServer with proper routing • Fixed infinite recursion bug in queue status calculations • Eliminated ~400 lines of duplicate code across processors • Maintained backward compatibility with existing test interfaces Architecture benefits: - Single source of truth for queue processing logic - Simplified maintenance and bug fixes - Clear separation between AutoProcessor (production) and BatchProcessor (R&D) - Extensible design for future processor types 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
625 lines
22 KiB
HTML
625 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Interface Traitement Batch - SEO Generator</title>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
|
|
.container {
|
|
max-width: 1200px;
|
|
margin: 0 auto;
|
|
background: white;
|
|
border-radius: 15px;
|
|
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.header {
|
|
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
color: white;
|
|
padding: 30px;
|
|
text-align: center;
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2.5rem;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.header p {
|
|
opacity: 0.9;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.main-content {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 30px;
|
|
padding: 30px;
|
|
}
|
|
|
|
.section {
|
|
background: #f8fafc;
|
|
border-radius: 12px;
|
|
padding: 25px;
|
|
border: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.section h2 {
|
|
color: #1e293b;
|
|
margin-bottom: 20px;
|
|
font-size: 1.3rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.config-grid {
|
|
display: grid;
|
|
gap: 15px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
}
|
|
|
|
.form-group label {
|
|
font-weight: 600;
|
|
color: #374151;
|
|
}
|
|
|
|
.form-group select,
|
|
.form-group input {
|
|
padding: 12px;
|
|
border: 2px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
font-size: 1rem;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.form-group select:focus,
|
|
.form-group input:focus {
|
|
outline: none;
|
|
border-color: #4f46e5;
|
|
}
|
|
|
|
.range-group {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
}
|
|
|
|
.controls {
|
|
display: flex;
|
|
gap: 10px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary:hover {
|
|
background: #059669;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #ef4444;
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger:hover {
|
|
background: #dc2626;
|
|
}
|
|
|
|
.btn-warning {
|
|
background: #f59e0b;
|
|
color: white;
|
|
}
|
|
|
|
.btn-warning:hover {
|
|
background: #d97706;
|
|
}
|
|
|
|
.status-section {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.status-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
|
|
.status-card {
|
|
background: white;
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
border: 2px solid #e5e7eb;
|
|
text-align: center;
|
|
}
|
|
|
|
.status-card h3 {
|
|
color: #6b7280;
|
|
font-size: 0.9rem;
|
|
text-transform: uppercase;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.status-card .value {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #1f2937;
|
|
}
|
|
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 8px;
|
|
background: #e5e7eb;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #10b981, #059669);
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.logs-section {
|
|
grid-column: 1 / -1;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
background: #1f2937;
|
|
color: #f9fafb;
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.log-entry {
|
|
margin-bottom: 5px;
|
|
padding: 2px 0;
|
|
}
|
|
|
|
.log-timestamp {
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.log-level-INFO { color: #60a5fa; }
|
|
.log-level-WARN { color: #fbbf24; }
|
|
.log-level-ERROR { color: #f87171; }
|
|
.log-level-SUCCESS { color: #34d399; }
|
|
|
|
.status-idle { color: #6b7280; }
|
|
.status-running { color: #10b981; }
|
|
.status-paused { color: #f59e0b; }
|
|
.status-error { color: #ef4444; }
|
|
|
|
@media (max-width: 768px) {
|
|
.main-content {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.controls {
|
|
justify-content: center;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>🎯 Interface Traitement Batch</h1>
|
|
<p>Configuration et monitoring du pipeline modulaire SEO</p>
|
|
</div>
|
|
|
|
<div class="main-content">
|
|
<!-- Configuration Section -->
|
|
<div class="section">
|
|
<h2>⚙️ Configuration Pipeline</h2>
|
|
<div class="config-grid">
|
|
<div class="form-group">
|
|
<label for="selective">Selective Enhancement</label>
|
|
<select id="selective">
|
|
<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>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="adversarial">Adversarial Mode</label>
|
|
<select id="adversarial">
|
|
<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>
|
|
|
|
<div class="form-group">
|
|
<label for="humanSimulation">Human Simulation</label>
|
|
<select id="humanSimulation">
|
|
<option value="none" selected>None</option>
|
|
<option value="lightSimulation">Light Simulation</option>
|
|
<option value="personalityFocus">Personality Focus</option>
|
|
<option value="adaptive">Adaptive</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="patternBreaking">Pattern Breaking</label>
|
|
<select id="patternBreaking">
|
|
<option value="none" selected>None</option>
|
|
<option value="syntaxFocus">Syntax Focus</option>
|
|
<option value="connectorsFocus">Connectors Focus</option>
|
|
<option value="adaptive">Adaptive</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="intensity">Intensité (0.5 - 1.5)</label>
|
|
<input type="range" id="intensity" min="0.5" max="1.5" step="0.1" value="1.0">
|
|
<span id="intensityValue">1.0</span>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Plage de lignes Google Sheets</label>
|
|
<div class="range-group">
|
|
<input type="number" id="rowStart" placeholder="Début" value="2" min="1">
|
|
<input type="number" id="rowEnd" placeholder="Fin" value="10" min="1">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>
|
|
<input type="checkbox" id="saveIntermediateSteps">
|
|
Sauvegarder étapes intermédiaires
|
|
</label>
|
|
</div>
|
|
|
|
<button class="btn btn-primary" onclick="saveConfiguration()">
|
|
💾 Sauvegarder Configuration
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Controls Section -->
|
|
<div class="section">
|
|
<h2>🎮 Contrôles</h2>
|
|
<div class="controls">
|
|
<button class="btn btn-primary" onclick="startBatch()">
|
|
▶️ Démarrer
|
|
</button>
|
|
<button class="btn btn-warning" onclick="pauseBatch()">
|
|
⏸️ Pause
|
|
</button>
|
|
<button class="btn btn-primary" onclick="resumeBatch()">
|
|
⏯️ Reprendre
|
|
</button>
|
|
<button class="btn btn-danger" onclick="stopBatch()">
|
|
⏹️ Arrêter
|
|
</button>
|
|
</div>
|
|
|
|
<div style="margin-top: 20px;">
|
|
<button class="btn" onclick="loadConfiguration()" style="background: #6366f1; color: white;">
|
|
🔄 Recharger Config
|
|
</button>
|
|
<button class="btn" onclick="refreshStatus()" style="background: #8b5cf6; color: white;">
|
|
📊 Actualiser Status
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Status Section -->
|
|
<div class="section status-section">
|
|
<h2>📊 État du Traitement</h2>
|
|
<div class="status-grid">
|
|
<div class="status-card">
|
|
<h3>Statut</h3>
|
|
<div class="value" id="currentStatus">Idle</div>
|
|
</div>
|
|
<div class="status-card">
|
|
<h3>Ligne Actuelle</h3>
|
|
<div class="value" id="currentRow">-</div>
|
|
</div>
|
|
<div class="status-card">
|
|
<h3>Progression</h3>
|
|
<div class="value" id="progressPercent">0%</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
<div class="status-card">
|
|
<h3>Temps Écoulé</h3>
|
|
<div class="value" id="elapsedTime">-</div>
|
|
</div>
|
|
<div class="status-card">
|
|
<h3>ETA</h3>
|
|
<div class="value" id="estimatedEnd">-</div>
|
|
</div>
|
|
<div class="status-card">
|
|
<h3>Erreurs</h3>
|
|
<div class="value" id="errorCount">0</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Logs Section -->
|
|
<div class="section status-section">
|
|
<h2>📝 Logs en Temps Réel</h2>
|
|
<div class="logs-section" id="logsContainer">
|
|
<div class="log-entry">
|
|
<span class="log-timestamp">[06:50:00]</span>
|
|
<span class="log-level-INFO">[INFO]</span>
|
|
Interface de traitement batch prête
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// État global
|
|
let currentConfig = {};
|
|
let currentStatus = {};
|
|
let statusInterval = null;
|
|
|
|
// Initialisation
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadConfiguration();
|
|
startStatusPolling();
|
|
|
|
// Slider intensité
|
|
document.getElementById('intensity').addEventListener('input', function() {
|
|
document.getElementById('intensityValue').textContent = this.value;
|
|
});
|
|
});
|
|
|
|
// Gestion configuration
|
|
async function loadConfiguration() {
|
|
try {
|
|
const response = await fetch('/api/batch/config');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
currentConfig = data.config;
|
|
populateConfigForm(data.config);
|
|
logMessage('Configuration chargée', 'INFO');
|
|
} else {
|
|
logMessage('Erreur chargement config: ' + data.error, 'ERROR');
|
|
}
|
|
} catch (error) {
|
|
logMessage('Erreur réseau: ' + error.message, 'ERROR');
|
|
}
|
|
}
|
|
|
|
function populateConfigForm(config) {
|
|
document.getElementById('selective').value = config.selective || 'standardEnhancement';
|
|
document.getElementById('adversarial').value = config.adversarial || 'light';
|
|
document.getElementById('humanSimulation').value = config.humanSimulation || 'none';
|
|
document.getElementById('patternBreaking').value = config.patternBreaking || 'none';
|
|
document.getElementById('intensity').value = config.intensity || 1.0;
|
|
document.getElementById('intensityValue').textContent = config.intensity || 1.0;
|
|
document.getElementById('rowStart').value = config.rowRange?.start || 2;
|
|
document.getElementById('rowEnd').value = config.rowRange?.end || 10;
|
|
document.getElementById('saveIntermediateSteps').checked = config.saveIntermediateSteps || false;
|
|
}
|
|
|
|
async function saveConfiguration() {
|
|
const config = {
|
|
selective: document.getElementById('selective').value,
|
|
adversarial: document.getElementById('adversarial').value,
|
|
humanSimulation: document.getElementById('humanSimulation').value,
|
|
patternBreaking: document.getElementById('patternBreaking').value,
|
|
intensity: parseFloat(document.getElementById('intensity').value),
|
|
rowRange: {
|
|
start: parseInt(document.getElementById('rowStart').value),
|
|
end: parseInt(document.getElementById('rowEnd').value)
|
|
},
|
|
saveIntermediateSteps: document.getElementById('saveIntermediateSteps').checked
|
|
};
|
|
|
|
try {
|
|
const response = await fetch('/api/batch/config', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(config)
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
logMessage('Configuration sauvegardée avec succès', 'SUCCESS');
|
|
currentConfig = config;
|
|
} else {
|
|
logMessage('Erreur sauvegarde: ' + data.error, 'ERROR');
|
|
}
|
|
} catch (error) {
|
|
logMessage('Erreur réseau: ' + error.message, 'ERROR');
|
|
}
|
|
}
|
|
|
|
// Contrôles traitement
|
|
async function startBatch() {
|
|
try {
|
|
const response = await fetch('/api/batch/start', { method: 'POST' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
logMessage('Traitement batch démarré', 'SUCCESS');
|
|
refreshStatus();
|
|
} else {
|
|
logMessage('Erreur démarrage: ' + data.error, 'ERROR');
|
|
}
|
|
} catch (error) {
|
|
logMessage('Erreur réseau: ' + error.message, 'ERROR');
|
|
}
|
|
}
|
|
|
|
async function stopBatch() {
|
|
try {
|
|
const response = await fetch('/api/batch/stop', { method: 'POST' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
logMessage('Traitement batch arrêté', 'SUCCESS');
|
|
refreshStatus();
|
|
} else {
|
|
logMessage('Erreur arrêt: ' + data.error, 'ERROR');
|
|
}
|
|
} catch (error) {
|
|
logMessage('Erreur réseau: ' + error.message, 'ERROR');
|
|
}
|
|
}
|
|
|
|
async function pauseBatch() {
|
|
try {
|
|
const response = await fetch('/api/batch/pause', { method: 'POST' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
logMessage('Traitement mis en pause', 'SUCCESS');
|
|
refreshStatus();
|
|
} else {
|
|
logMessage('Erreur pause: ' + data.error, 'ERROR');
|
|
}
|
|
} catch (error) {
|
|
logMessage('Erreur réseau: ' + error.message, 'ERROR');
|
|
}
|
|
}
|
|
|
|
async function resumeBatch() {
|
|
try {
|
|
const response = await fetch('/api/batch/resume', { method: 'POST' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
logMessage('Traitement repris', 'SUCCESS');
|
|
refreshStatus();
|
|
} else {
|
|
logMessage('Erreur reprise: ' + data.error, 'ERROR');
|
|
}
|
|
} catch (error) {
|
|
logMessage('Erreur réseau: ' + error.message, 'ERROR');
|
|
}
|
|
}
|
|
|
|
// Monitoring
|
|
async function refreshStatus() {
|
|
try {
|
|
const response = await fetch('/api/batch/progress');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
currentStatus = data.progress;
|
|
updateStatusDisplay(data.progress);
|
|
}
|
|
} catch (error) {
|
|
logMessage('Erreur récupération status: ' + error.message, 'ERROR');
|
|
}
|
|
}
|
|
|
|
function updateStatusDisplay(status) {
|
|
document.getElementById('currentStatus').textContent = status.status || 'idle';
|
|
document.getElementById('currentStatus').className = 'value status-' + (status.status || 'idle');
|
|
|
|
document.getElementById('currentRow').textContent = status.currentRow || '-';
|
|
|
|
const progress = status.metrics?.completionPercentage || 0;
|
|
document.getElementById('progressPercent').textContent = Math.round(progress) + '%';
|
|
document.getElementById('progressFill').style.width = progress + '%';
|
|
|
|
const elapsed = status.metrics?.elapsedTime || 0;
|
|
document.getElementById('elapsedTime').textContent = formatDuration(elapsed);
|
|
|
|
const remaining = status.metrics?.estimatedRemaining || 0;
|
|
document.getElementById('estimatedEnd').textContent = remaining > 0 ? formatDuration(remaining) : '-';
|
|
|
|
document.getElementById('errorCount').textContent = status.errors?.length || 0;
|
|
}
|
|
|
|
function startStatusPolling() {
|
|
statusInterval = setInterval(refreshStatus, 2000); // Toutes les 2 secondes
|
|
}
|
|
|
|
function stopStatusPolling() {
|
|
if (statusInterval) {
|
|
clearInterval(statusInterval);
|
|
statusInterval = null;
|
|
}
|
|
}
|
|
|
|
// Utilitaires
|
|
function formatDuration(ms) {
|
|
if (!ms || ms <= 0) return '-';
|
|
|
|
const seconds = Math.floor(ms / 1000);
|
|
const minutes = Math.floor(seconds / 60);
|
|
const hours = Math.floor(minutes / 60);
|
|
|
|
if (hours > 0) {
|
|
return `${hours}h ${minutes % 60}m`;
|
|
} else if (minutes > 0) {
|
|
return `${minutes}m ${seconds % 60}s`;
|
|
} else {
|
|
return `${seconds}s`;
|
|
}
|
|
}
|
|
|
|
function logMessage(message, level = 'INFO') {
|
|
const container = document.getElementById('logsContainer');
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
|
|
const logEntry = document.createElement('div');
|
|
logEntry.className = 'log-entry';
|
|
logEntry.innerHTML = `
|
|
<span class="log-timestamp">[${timestamp}]</span>
|
|
<span class="log-level-${level}">[${level}]</span>
|
|
${message}
|
|
`;
|
|
|
|
container.appendChild(logEntry);
|
|
container.scrollTop = container.scrollHeight;
|
|
|
|
// Garder seulement les 100 derniers logs
|
|
while (container.children.length > 100) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html> |