• 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>
1045 lines
35 KiB
HTML
1045 lines
35 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dashboard Batch Processing - 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, #1e3c72 0%, #2a5298 100%);
|
|
min-height: 100vh;
|
|
color: #333;
|
|
}
|
|
|
|
.dashboard {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
padding: 20px;
|
|
}
|
|
|
|
.header {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
border-radius: 20px;
|
|
padding: 30px;
|
|
margin-bottom: 30px;
|
|
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
|
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 2.5rem;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.header p {
|
|
color: #666;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 30px;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.grid-full {
|
|
grid-column: span 2;
|
|
}
|
|
|
|
.card {
|
|
background: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
border-radius: 20px;
|
|
padding: 30px;
|
|
box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
|
|
border: 1px solid rgba(255, 255, 255, 0.18);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.card:hover {
|
|
transform: translateY(-5px);
|
|
}
|
|
|
|
.card h2 {
|
|
font-size: 1.5rem;
|
|
margin-bottom: 20px;
|
|
color: #4f46e5;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.card-icon {
|
|
font-size: 1.8rem;
|
|
}
|
|
|
|
/* Configuration Form */
|
|
.form-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.form-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-group label {
|
|
font-weight: 600;
|
|
margin-bottom: 8px;
|
|
color: #374151;
|
|
}
|
|
|
|
.form-group select,
|
|
.form-group input {
|
|
padding: 12px 15px;
|
|
border: 2px solid #e5e7eb;
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
transition: border-color 0.3s ease;
|
|
}
|
|
|
|
.form-group select:focus,
|
|
.form-group input:focus {
|
|
outline: none;
|
|
border-color: #4f46e5;
|
|
}
|
|
|
|
/* Buttons */
|
|
.btn {
|
|
padding: 12px 24px;
|
|
border: none;
|
|
border-radius: 10px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: all 0.3s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.btn-primary {
|
|
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
|
color: white;
|
|
}
|
|
|
|
.btn-secondary {
|
|
background: #6b7280;
|
|
color: white;
|
|
}
|
|
|
|
.btn-success {
|
|
background: #10b981;
|
|
color: white;
|
|
}
|
|
|
|
.btn-warning {
|
|
background: #f59e0b;
|
|
color: white;
|
|
}
|
|
|
|
.btn-danger {
|
|
background: #ef4444;
|
|
color: white;
|
|
}
|
|
|
|
.btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
|
}
|
|
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none !important;
|
|
}
|
|
|
|
.btn-row {
|
|
display: flex;
|
|
gap: 15px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
/* Status Display */
|
|
.status-badge {
|
|
display: inline-block;
|
|
padding: 6px 12px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-idle { background: #e5e7eb; color: #6b7280; }
|
|
.status-running { background: #dbeafe; color: #1d4ed8; }
|
|
.status-paused { background: #fef3c7; color: #d97706; }
|
|
.status-completed { background: #d1fae5; color: #047857; }
|
|
.status-error { background: #fee2e2; color: #dc2626; }
|
|
|
|
/* Progress Bar */
|
|
.progress-container {
|
|
background: #f3f4f6;
|
|
border-radius: 10px;
|
|
height: 20px;
|
|
overflow: hidden;
|
|
margin: 15px 0;
|
|
}
|
|
|
|
.progress-bar {
|
|
background: linear-gradient(90deg, #4f46e5 0%, #7c3aed 100%);
|
|
height: 100%;
|
|
transition: width 0.5s ease;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Metrics Grid */
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 15px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.metric {
|
|
text-align: center;
|
|
padding: 20px;
|
|
background: rgba(79, 70, 229, 0.1);
|
|
border-radius: 15px;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
color: #4f46e5;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.metric-label {
|
|
font-size: 0.9rem;
|
|
color: #6b7280;
|
|
text-transform: uppercase;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Queue List */
|
|
.queue-list {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.queue-item {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
padding: 15px;
|
|
border-bottom: 1px solid #e5e7eb;
|
|
transition: background-color 0.2s ease;
|
|
}
|
|
|
|
.queue-item:hover {
|
|
background: #f9fafb;
|
|
}
|
|
|
|
.queue-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.queue-row {
|
|
font-weight: 600;
|
|
color: #4f46e5;
|
|
min-width: 60px;
|
|
}
|
|
|
|
.queue-status {
|
|
flex: 1;
|
|
margin-left: 15px;
|
|
}
|
|
|
|
.queue-time {
|
|
font-size: 0.9rem;
|
|
color: #6b7280;
|
|
}
|
|
|
|
/* Log Display */
|
|
.log-container {
|
|
background: #1f2937;
|
|
border-radius: 15px;
|
|
padding: 20px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 13px;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.log-entry {
|
|
margin-bottom: 8px;
|
|
padding: 5px 0;
|
|
}
|
|
|
|
.log-timestamp {
|
|
color: #9ca3af;
|
|
}
|
|
|
|
.log-level-info {
|
|
color: #60a5fa;
|
|
}
|
|
|
|
.log-level-warning {
|
|
color: #fbbf24;
|
|
}
|
|
|
|
.log-level-error {
|
|
color: #f87171;
|
|
}
|
|
|
|
.log-level-success {
|
|
color: #34d399;
|
|
}
|
|
|
|
.log-message {
|
|
color: #f9fafb;
|
|
}
|
|
|
|
/* Templates Section */
|
|
.template-list {
|
|
display: grid;
|
|
gap: 15px;
|
|
margin: 20px 0;
|
|
}
|
|
|
|
.template-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 15px;
|
|
background: rgba(79, 70, 229, 0.05);
|
|
border-radius: 10px;
|
|
border-left: 4px solid #4f46e5;
|
|
}
|
|
|
|
.template-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.template-name {
|
|
font-weight: 600;
|
|
color: #374151;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
.template-source {
|
|
font-size: 0.9rem;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.template-actions {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
/* Responsive */
|
|
@media (max-width: 768px) {
|
|
.grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.form-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.metrics-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.btn-row {
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
/* Animation */
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.5; }
|
|
}
|
|
|
|
.pulse {
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
/* Notification */
|
|
.notification {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
padding: 15px 20px;
|
|
background: #10b981;
|
|
color: white;
|
|
border-radius: 10px;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
|
|
z-index: 1000;
|
|
transform: translateX(400px);
|
|
transition: transform 0.3s ease;
|
|
}
|
|
|
|
.notification.show {
|
|
transform: translateX(0);
|
|
}
|
|
|
|
.notification.error {
|
|
background: #ef4444;
|
|
}
|
|
|
|
.notification.warning {
|
|
background: #f59e0b;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="dashboard">
|
|
<!-- Header -->
|
|
<div class="header">
|
|
<h1>🚀 Dashboard Batch Processing</h1>
|
|
<p>Interface complète de traitement batch avec pipeline modulaire</p>
|
|
</div>
|
|
|
|
<!-- Main Grid -->
|
|
<div class="grid">
|
|
<!-- Configuration Panel -->
|
|
<div class="card">
|
|
<h2>⚙️ Configuration Pipeline</h2>
|
|
|
|
<div class="form-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é</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 for="saveSteps">Sauvegarde Étapes</label>
|
|
<input type="checkbox" id="saveSteps">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-grid">
|
|
<div class="form-group">
|
|
<label for="rowStart">Ligne Début</label>
|
|
<input type="number" id="rowStart" value="2" min="1">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="rowEnd">Ligne Fin</label>
|
|
<input type="number" id="rowEnd" value="10" min="1">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<button class="btn btn-primary" onclick="saveConfig()">💾 Sauvegarder Config</button>
|
|
<button class="btn btn-secondary" onclick="loadConfig()">📋 Charger Config</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Control Panel -->
|
|
<div class="card">
|
|
<h2>🎮 Contrôles de Traitement</h2>
|
|
|
|
<div id="statusDisplay" class="metrics-grid">
|
|
<div class="metric">
|
|
<div class="metric-value" id="currentStatus">IDLE</div>
|
|
<div class="metric-label">Status</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="currentRow">-</div>
|
|
<div class="metric-label">Ligne Actuelle</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="progressPercent">0%</div>
|
|
<div class="metric-label">Progression</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="completedRows">0</div>
|
|
<div class="metric-label">Terminées</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="progress-container">
|
|
<div class="progress-bar" id="progressBar" style="width: 0%">0%</div>
|
|
</div>
|
|
|
|
<div class="btn-row">
|
|
<button class="btn btn-success" id="startBtn" onclick="startBatch()">▶️ Démarrer</button>
|
|
<button class="btn btn-warning" id="pauseBtn" onclick="pauseBatch()" disabled>⏸️ Pause</button>
|
|
<button class="btn btn-primary" id="resumeBtn" onclick="resumeBatch()" disabled>⏯️ Reprendre</button>
|
|
<button class="btn btn-danger" id="stopBtn" onclick="stopBatch()" disabled>⏹️ Arrêter</button>
|
|
</div>
|
|
|
|
<div class="metrics-grid" style="margin-top: 20px;">
|
|
<div class="metric">
|
|
<div class="metric-value" id="elapsedTime">0s</div>
|
|
<div class="metric-label">Temps Écoulé</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="estimatedEnd">-</div>
|
|
<div class="metric-label">Fin Estimée</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="throughput">0</div>
|
|
<div class="metric-label">Lignes/min</div>
|
|
</div>
|
|
<div class="metric">
|
|
<div class="metric-value" id="errorCount">0</div>
|
|
<div class="metric-label">Erreurs</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Queue Status -->
|
|
<div class="card grid-full">
|
|
<h2>📋 État de la Queue</h2>
|
|
<div class="queue-list" id="queueList">
|
|
<!-- Queue items will be populated here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Templates & Logs Grid -->
|
|
<div class="grid">
|
|
<!-- Digital Ocean Templates -->
|
|
<div class="card">
|
|
<h2>🌊 Templates Digital Ocean</h2>
|
|
|
|
<div class="btn-row" style="margin-bottom: 20px;">
|
|
<button class="btn btn-primary" onclick="refreshTemplates()">🔄 Actualiser</button>
|
|
<button class="btn btn-secondary" onclick="clearTemplateCache()">🗑️ Vider Cache</button>
|
|
</div>
|
|
|
|
<div class="template-list" id="templateList">
|
|
<!-- Templates will be populated here -->
|
|
</div>
|
|
|
|
<div id="cacheStats" style="margin-top: 15px; padding: 15px; background: #f8fafc; border-radius: 10px; font-size: 0.9rem; color: #6b7280;">
|
|
Cache stats will appear here
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Real-time Logs -->
|
|
<div class="card">
|
|
<h2>📝 Logs Temps Réel</h2>
|
|
|
|
<div class="btn-row" style="margin-bottom: 20px;">
|
|
<button class="btn btn-secondary" onclick="clearLogs()">🗑️ Vider Logs</button>
|
|
<button class="btn btn-primary" onclick="toggleAutoScroll()">📜 Auto-Scroll</button>
|
|
</div>
|
|
|
|
<div class="log-container" id="logContainer">
|
|
<!-- Logs will appear here -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notification -->
|
|
<div id="notification" class="notification"></div>
|
|
|
|
<script>
|
|
// Global state
|
|
let currentConfig = {};
|
|
let batchStatus = {};
|
|
let autoScroll = true;
|
|
let updateInterval;
|
|
|
|
// Initialize dashboard
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadConfig();
|
|
refreshTemplates();
|
|
startStatusUpdates();
|
|
setupEventListeners();
|
|
|
|
showNotification('Dashboard initialisé', 'success');
|
|
});
|
|
|
|
// Event Listeners
|
|
function setupEventListeners() {
|
|
// Intensity slider
|
|
document.getElementById('intensity').addEventListener('input', function() {
|
|
document.getElementById('intensityValue').textContent = this.value;
|
|
});
|
|
}
|
|
|
|
// Configuration Management
|
|
async function loadConfig() {
|
|
try {
|
|
const response = await fetch('/api/batch/config');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
currentConfig = data.config;
|
|
populateConfigForm(data.config);
|
|
showNotification('Configuration chargée', 'success');
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
showNotification('Erreur chargement config: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function saveConfig() {
|
|
try {
|
|
const config = getConfigFromForm();
|
|
|
|
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) {
|
|
currentConfig = data.config;
|
|
showNotification('Configuration sauvegardée', 'success');
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
showNotification('Erreur sauvegarde: ' + 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('saveSteps').checked = config.saveIntermediateSteps || false;
|
|
document.getElementById('rowStart').value = config.rowRange?.start || 2;
|
|
document.getElementById('rowEnd').value = config.rowRange?.end || 10;
|
|
}
|
|
|
|
function getConfigFromForm() {
|
|
return {
|
|
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),
|
|
saveIntermediateSteps: document.getElementById('saveSteps').checked,
|
|
rowRange: {
|
|
start: parseInt(document.getElementById('rowStart').value),
|
|
end: parseInt(document.getElementById('rowEnd').value)
|
|
}
|
|
};
|
|
}
|
|
|
|
// Batch Control
|
|
async function startBatch() {
|
|
try {
|
|
const response = await fetch('/api/batch/start', { method: 'POST' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showNotification('Traitement démarré', 'success');
|
|
updateControlButtons('running');
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
showNotification('Erreur démarrage: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function pauseBatch() {
|
|
try {
|
|
const response = await fetch('/api/batch/pause', { method: 'POST' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showNotification('Traitement mis en pause', 'warning');
|
|
updateControlButtons('paused');
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
showNotification('Erreur pause: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function resumeBatch() {
|
|
try {
|
|
const response = await fetch('/api/batch/resume', { method: 'POST' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showNotification('Traitement repris', 'success');
|
|
updateControlButtons('running');
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
showNotification('Erreur reprise: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function stopBatch() {
|
|
try {
|
|
const response = await fetch('/api/batch/stop', { method: 'POST' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showNotification('Traitement arrêté', 'warning');
|
|
updateControlButtons('idle');
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
showNotification('Erreur arrêt: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Status Updates
|
|
function startStatusUpdates() {
|
|
updateInterval = setInterval(async () => {
|
|
await updateBatchStatus();
|
|
await updateProgress();
|
|
}, 2000); // Update every 2 seconds
|
|
}
|
|
|
|
async function updateBatchStatus() {
|
|
try {
|
|
const response = await fetch('/api/batch/status');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
batchStatus = data.status;
|
|
displayBatchStatus(data.status);
|
|
updateQueueDisplay(data.status.queue);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating status:', error);
|
|
}
|
|
}
|
|
|
|
async function updateProgress() {
|
|
try {
|
|
const response = await fetch('/api/batch/progress');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
displayProgress(data.progress);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error updating progress:', error);
|
|
}
|
|
}
|
|
|
|
function displayBatchStatus(status) {
|
|
document.getElementById('currentStatus').textContent = status.status.toUpperCase();
|
|
document.getElementById('currentRow').textContent = status.currentRow || '-';
|
|
document.getElementById('progressPercent').textContent = status.progress + '%';
|
|
document.getElementById('completedRows').textContent = status.completedRows;
|
|
document.getElementById('errorCount').textContent = status.failedRows;
|
|
|
|
// Update progress bar
|
|
const progressBar = document.getElementById('progressBar');
|
|
progressBar.style.width = status.progress + '%';
|
|
progressBar.textContent = status.progress + '%';
|
|
|
|
// Update control buttons based on status
|
|
updateControlButtons(status.status);
|
|
}
|
|
|
|
function displayProgress(progress) {
|
|
if (progress.metrics) {
|
|
const elapsed = Math.round(progress.metrics.elapsedTime / 1000);
|
|
document.getElementById('elapsedTime').textContent = formatTime(elapsed);
|
|
|
|
const throughput = Math.round(progress.metrics.throughput || 0);
|
|
document.getElementById('throughput').textContent = throughput;
|
|
|
|
if (progress.estimatedEnd) {
|
|
const endTime = new Date(progress.estimatedEnd);
|
|
document.getElementById('estimatedEnd').textContent = endTime.toLocaleTimeString();
|
|
} else {
|
|
document.getElementById('estimatedEnd').textContent = '-';
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateControlButtons(status) {
|
|
const startBtn = document.getElementById('startBtn');
|
|
const pauseBtn = document.getElementById('pauseBtn');
|
|
const resumeBtn = document.getElementById('resumeBtn');
|
|
const stopBtn = document.getElementById('stopBtn');
|
|
|
|
// Reset all buttons
|
|
[startBtn, pauseBtn, resumeBtn, stopBtn].forEach(btn => {
|
|
btn.disabled = false;
|
|
btn.classList.remove('pulse');
|
|
});
|
|
|
|
switch (status) {
|
|
case 'idle':
|
|
pauseBtn.disabled = true;
|
|
resumeBtn.disabled = true;
|
|
stopBtn.disabled = true;
|
|
break;
|
|
case 'running':
|
|
startBtn.disabled = true;
|
|
resumeBtn.disabled = true;
|
|
startBtn.classList.add('pulse');
|
|
break;
|
|
case 'paused':
|
|
startBtn.disabled = true;
|
|
pauseBtn.disabled = true;
|
|
break;
|
|
case 'completed':
|
|
pauseBtn.disabled = true;
|
|
resumeBtn.disabled = true;
|
|
stopBtn.disabled = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
function updateQueueDisplay(queue) {
|
|
const queueList = document.getElementById('queueList');
|
|
|
|
if (!queue || queue.length === 0) {
|
|
queueList.innerHTML = '<div style="text-align: center; padding: 40px; color: #9ca3af;">Aucune tâche dans la queue</div>';
|
|
return;
|
|
}
|
|
|
|
queueList.innerHTML = queue.map(item => `
|
|
<div class="queue-item">
|
|
<div class="queue-row">Ligne ${item.rowNumber}</div>
|
|
<div class="queue-status">
|
|
<span class="status-badge status-${item.status}">${item.status}</span>
|
|
${item.attempts > 0 ? `<span style="margin-left: 10px; font-size: 0.9rem; color: #6b7280;">Tentative ${item.attempts}/${item.maxAttempts}</span>` : ''}
|
|
</div>
|
|
<div class="queue-time">
|
|
${item.startTime ? new Date(item.startTime).toLocaleTimeString() : ''}
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
// Templates Management
|
|
async function refreshTemplates() {
|
|
try {
|
|
const response = await fetch('/api/batch/templates');
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
displayTemplates(data.templates);
|
|
displayCacheStats(data.cacheStats);
|
|
showNotification('Templates actualisés', 'success');
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
showNotification('Erreur templates: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
async function clearTemplateCache() {
|
|
try {
|
|
const response = await fetch('/api/batch/cache', { method: 'DELETE' });
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
showNotification('Cache vidé', 'success');
|
|
await refreshTemplates();
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
showNotification('Erreur vidage cache: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function displayTemplates(templates) {
|
|
const templateList = document.getElementById('templateList');
|
|
|
|
if (!templates || templates.length === 0) {
|
|
templateList.innerHTML = '<div style="text-align: center; padding: 20px; color: #9ca3af;">Aucun template disponible</div>';
|
|
return;
|
|
}
|
|
|
|
templateList.innerHTML = templates.map(template => `
|
|
<div class="template-item">
|
|
<div class="template-info">
|
|
<div class="template-name">${template.name}</div>
|
|
<div class="template-source">Source: ${template.source}</div>
|
|
</div>
|
|
<div class="template-actions">
|
|
<button class="btn btn-secondary" onclick="viewTemplate('${template.name}')" style="padding: 6px 12px; font-size: 12px;">👁️ Voir</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function displayCacheStats(stats) {
|
|
const cacheStatsDiv = document.getElementById('cacheStats');
|
|
cacheStatsDiv.innerHTML = `
|
|
<strong>Cache:</strong> ${stats.memoryCache.size} en mémoire |
|
|
<strong>Endpoint:</strong> ${stats.config.endpoint}/${stats.config.bucket} |
|
|
<strong>Templates par défaut:</strong> ${stats.defaultTemplates}
|
|
`;
|
|
}
|
|
|
|
async function viewTemplate(filename) {
|
|
try {
|
|
const response = await fetch(`/api/batch/templates/${filename}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
// Create a modal or new window to display template
|
|
const newWindow = window.open('', '_blank', 'width=800,height=600');
|
|
newWindow.document.write(`
|
|
<html>
|
|
<head><title>Template: ${filename}</title></head>
|
|
<body style="font-family: monospace; padding: 20px; background: #f8fafc;">
|
|
<h2>Template: ${filename}</h2>
|
|
<pre style="background: white; padding: 20px; border-radius: 10px; border: 1px solid #e5e7eb;">${data.template}</pre>
|
|
</body>
|
|
</html>
|
|
`);
|
|
} else {
|
|
throw new Error(data.error);
|
|
}
|
|
} catch (error) {
|
|
showNotification('Erreur lecture template: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
// Logs Management
|
|
function clearLogs() {
|
|
document.getElementById('logContainer').innerHTML = '';
|
|
showNotification('Logs vidés', 'success');
|
|
}
|
|
|
|
function toggleAutoScroll() {
|
|
autoScroll = !autoScroll;
|
|
const btn = event.target;
|
|
btn.textContent = autoScroll ? '📜 Auto-Scroll' : '📜 Manuel';
|
|
btn.classList.toggle('btn-primary');
|
|
btn.classList.toggle('btn-secondary');
|
|
|
|
showNotification(`Auto-scroll ${autoScroll ? 'activé' : 'désactivé'}`, 'success');
|
|
}
|
|
|
|
function addLogEntry(level, message) {
|
|
const logContainer = document.getElementById('logContainer');
|
|
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.toUpperCase()}]</span>
|
|
<span class="log-message">${message}</span>
|
|
`;
|
|
|
|
logContainer.appendChild(logEntry);
|
|
|
|
if (autoScroll) {
|
|
logContainer.scrollTop = logContainer.scrollHeight;
|
|
}
|
|
}
|
|
|
|
// Utility Functions
|
|
function formatTime(seconds) {
|
|
if (seconds < 60) return seconds + 's';
|
|
const minutes = Math.floor(seconds / 60);
|
|
const secs = seconds % 60;
|
|
return `${minutes}m ${secs}s`;
|
|
}
|
|
|
|
function showNotification(message, type = 'success') {
|
|
const notification = document.getElementById('notification');
|
|
notification.textContent = message;
|
|
notification.className = `notification ${type}`;
|
|
notification.classList.add('show');
|
|
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
}, 3000);
|
|
}
|
|
|
|
// Cleanup
|
|
window.addEventListener('beforeunload', function() {
|
|
if (updateInterval) {
|
|
clearInterval(updateInterval);
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |