seo-generator-server/public/batch-interface.html
StillHammer a2ffe7fec5 Refactor batch processing system with shared QueueProcessor base class
• 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>
2025-09-19 02:04:48 +08:00

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>