/** * FavMasToKey - Mode Filou (script de gestion des tokens multiples) */ // Attendre que le DOM soit chargé document.addEventListener('DOMContentLoaded', function() { // Éléments DOM const uploadForm = document.getElementById('upload-form'); const jsonFileInput = document.getElementById('json-file'); const fileSummary = document.getElementById('file-summary'); const startMigration = document.getElementById('start-migration'); const pauseMigration = document.getElementById('pause-migration'); const cancelMigration = document.getElementById('cancel-migration'); const globalProgress = document.getElementById('global-progress'); const successProgress = document.getElementById('success-progress'); const skipProgress = document.getElementById('skip-progress'); const warnProgress = document.getElementById('warn-progress'); const errorProgress = document.getElementById('error-progress'); const operationLog = document.getElementById('operation-log'); const clearLog = document.getElementById('clear-log'); const statsDisplay = document.getElementById('stats-display'); // Sliders de configuration const delayBetweenTokens = document.getElementById('delay-between-tokens'); const tokenCooldown = document.getElementById('token-cooldown'); const delayValue = document.getElementById('delay-value'); const cooldownValue = document.getElementById('cooldown-value'); const autoAdjust = document.getElementById('auto-adjust'); // Variables globales let favoritesList = []; let currentIndex = 0; let totalItems = 0; let isProcessing = false; let isPaused = false; let successCount = 0; let errorCount = 0; let skippedCount = 0; let warningCount = 0; // Variables pour la gestion des tokens let tokens = []; let tokensQueue = []; let currentTokenIndex = 0; let tokenTimers = {}; let activeRequests = 0; let maxConcurrentRequests = 1; // Pour éviter les problèmes de race condition // Options de migration let migration = { status: 'not_started', startTime: null, lastUpdateTime: null, progress: { current: 0, total: 0, percentage: 0 }, stats: { success: 0, error: 0, skipped: 0, warning: 0 }, options: { delayBetweenTokens: 15, // secondes tokenCooldown: 150, // secondes autoAdjust: true } }; // ---- Gestionnaire d'événements ---- // Gérer les changements sur les sliders if (delayBetweenTokens && delayValue) { delayBetweenTokens.addEventListener('input', function() { delayValue.textContent = this.value + 's'; migration.options.delayBetweenTokens = parseInt(this.value); updateLocalStorage(); }); } if (tokenCooldown && cooldownValue) { tokenCooldown.addEventListener('input', function() { cooldownValue.textContent = this.value + 's'; migration.options.tokenCooldown = parseInt(this.value); updateLocalStorage(); }); } if (autoAdjust) { autoAdjust.addEventListener('change', function() { migration.options.autoAdjust = this.checked; updateLocalStorage(); }); } // Gérer le téléchargement et l'analyse du fichier JSON if (uploadForm) { uploadForm.addEventListener('submit', function(e) { e.preventDefault(); const file = jsonFileInput.files[0]; if (!file) { alert('Veuillez sélectionner un fichier JSON.'); return; } // Vérifier l'extension du fichier if (!file.name.endsWith('.json')) { alert('Le fichier doit être au format JSON.'); return; } // Lire le fichier const reader = new FileReader(); reader.onload = function(event) { try { const json = JSON.parse(event.target.result); // Vérifier la structure du fichier if (!json['@context'] || !json.type || !json.orderedItems) { alert('Le format du fichier JSON n\'est pas celui attendu pour un export de favoris Mastodon.'); return; } favoritesList = json.orderedItems; totalItems = favoritesList.length; // Afficher un résumé fileSummary.classList.remove('d-none'); fileSummary.innerHTML = ` <strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon. Le transfert sera effectué avec les jetons configurés. `; // Stocker les données dans localStorage pour les conserver localStorage.setItem('favmastokey_favorites', JSON.stringify(favoritesList)); // Initialiser les données de migration migration = { status: 'not_started', startTime: null, lastUpdateTime: null, progress: { current: 0, total: totalItems, percentage: 0 }, stats: { success: 0, error: 0, skipped: 0, warning: 0 }, options: { delayBetweenTokens: parseInt(delayBetweenTokens ? delayBetweenTokens.value : 15), tokenCooldown: parseInt(tokenCooldown ? tokenCooldown.value : 150), autoAdjust: autoAdjust ? autoAdjust.checked : true } }; // Sauvegarder les données de migration updateLocalStorage(); // Scroll jusqu'à la section de migration document.getElementById('step3').scrollIntoView({ behavior: 'smooth' }); } catch (error) { alert('Erreur lors de l\'analyse du fichier JSON: ' + error.message); } }; reader.onerror = function() { alert('Erreur lors de la lecture du fichier.'); }; reader.readAsText(file); }); } // Effacer le journal des opérations if (clearLog) { clearLog.addEventListener('click', function() { operationLog.innerHTML = ''; }); } // Démarrer la migration if (startMigration) { startMigration.addEventListener('click', function() { if (isProcessing) return; // Récupérer les favoris du localStorage si nécessaire if (favoritesList.length === 0 && localStorage.getItem('favmastokey_favorites')) { favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites')); totalItems = favoritesList.length; } if (favoritesList.length === 0) { addLogEntry('Aucun favori à migrer. Veuillez d\'abord télécharger votre fichier JSON.', 'error'); return; } // Vérifier s'il y a une migration en cours à reprendre if (localStorage.getItem('favmastokey_multitoken_migration')) { const savedMigration = JSON.parse(localStorage.getItem('favmastokey_multitoken_migration')); // Si la migration était en cours ou en pause, proposer de la reprendre if (savedMigration.status === 'in_progress' || savedMigration.status === 'paused') { const resumeConfirm = confirm(`Une migration précédente a été trouvée (${savedMigration.progress.percentage.toFixed(1)}% terminée). Voulez-vous la reprendre?`); if (resumeConfirm) { // Restaurer l'état de la migration migration = savedMigration; currentIndex = migration.progress.current; // Restaurer les statistiques successCount = migration.stats.success || 0; errorCount = migration.stats.error || 0; skippedCount = migration.stats.skipped || 0; warningCount = migration.stats.warning || 0; // Mettre à jour l'interface updateProgress(migration.progress.percentage); updateStatistics(); } else { // Réinitialiser la migration resetMigration(); } } else { // Réinitialiser la migration resetMigration(); } } else { // Initialiser une nouvelle migration resetMigration(); } // Initialiser les tokens initializeTokens(); if (tokens.length === 0) { addLogEntry('Aucun jeton d\'accès disponible. Veuillez configurer au moins un jeton.', 'error'); return; } // Démarrer la migration isProcessing = true; isPaused = false; startMigration.classList.add('d-none'); pauseMigration.classList.remove('d-none'); // Initialiser le temps de démarrage si c'est une nouvelle migration if (migration.status === 'not_started' || migration.startTime === null) { migration.startTime = Date.now(); } // Mettre à jour le statut de la migration migration.status = 'in_progress'; updateLocalStorage(); // Afficher un message de démarrage const tokenCountMsg = tokens.length === 1 ? '1 jeton' : `${tokens.length} jetons`; addLogEntry(`Démarrage de la migration avec ${tokenCountMsg}. Délai entre jetons: ${migration.options.delayBetweenTokens}s, Délai de récupération: ${migration.options.tokenCooldown}s`, 'info'); // Lancer le processus de migration startTokenRotation(); }); } // Gérer la pause de la migration if (pauseMigration) { pauseMigration.addEventListener('click', function() { if (!isProcessing) return; isPaused = !isPaused; if (isPaused) { pauseMigration.textContent = 'Reprendre'; addLogEntry('Migration en pause.', 'warning'); // Mettre à jour le statut de la migration migration.status = 'paused'; updateLocalStorage(); } else { pauseMigration.textContent = 'Pause'; addLogEntry('Reprise de la migration...', 'info'); // Mettre à jour le statut de la migration migration.status = 'in_progress'; updateLocalStorage(); // Reprendre le traitement startTokenRotation(); } }); } // Gérer l'annulation de la migration if (cancelMigration) { cancelMigration.addEventListener('click', function() { if (!isProcessing && currentIndex === 0) return; const confirmCancel = confirm('Êtes-vous sûr de vouloir annuler la migration en cours ?'); if (confirmCancel) { isProcessing = false; isPaused = false; addLogEntry('Migration annulée.', 'error'); // Nettoyer les timers clearAllTimers(); // Réinitialiser l'interface startMigration.classList.remove('d-none'); pauseMigration.classList.add('d-none'); pauseMigration.textContent = 'Pause'; // Réinitialiser les tokens resetTokensStatus(); // Réinitialiser les données de migration resetMigration(); } }); } // Vérifier si nous avons des données à restaurer au chargement function initOnLoad() { // Récupérer les favoris du localStorage if (localStorage.getItem('favmastokey_favorites')) { favoritesList = JSON.parse(localStorage.getItem('favmastokey_favorites')); totalItems = favoritesList.length; if (fileSummary) { fileSummary.classList.remove('d-none'); fileSummary.innerHTML = ` <strong>${totalItems}</strong> favoris trouvés dans votre fichier Mastodon. Le transfert sera effectué avec les jetons configurés. `; } } // Récupérer les données de migration if (localStorage.getItem('favmastokey_multitoken_migration')) { migration = JSON.parse(localStorage.getItem('favmastokey_multitoken_migration')); currentIndex = migration.progress.current; successCount = migration.stats.success || 0; errorCount = migration.stats.error || 0; skippedCount = migration.stats.skipped || 0; warningCount = migration.stats.warning || 0; // Mettre à jour l'interface updateProgress(migration.progress.percentage); updateStatistics(); // Restaurer les options if (migration.options) { if (delayBetweenTokens && migration.options.delayBetweenTokens) { delayBetweenTokens.value = migration.options.delayBetweenTokens; if (delayValue) { delayValue.textContent = migration.options.delayBetweenTokens + 's'; } } if (tokenCooldown && migration.options.tokenCooldown) { tokenCooldown.value = migration.options.tokenCooldown; if (cooldownValue) { cooldownValue.textContent = migration.options.tokenCooldown + 's'; } } if (autoAdjust && migration.options.autoAdjust !== undefined) { autoAdjust.checked = migration.options.autoAdjust; } } // Si la migration était en cours, proposer de la reprendre if ((migration.status === 'in_progress' || migration.status === 'paused') && migration.progress.current < migration.progress.total) { const timeAgo = getTimeAgoString(new Date(migration.lastUpdateTime)); // Ajouter une entrée au journal addLogEntry(`Une migration précédente a été trouvée (${migration.progress.percentage.toFixed(1)}% terminée, ${timeAgo}). Utilisez le bouton "Démarrer la migration" pour la reprendre.`, 'info'); } } } // Initialiser les tokens à partir des éléments du DOM function initializeTokens() { tokens = []; tokensQueue = []; // Ajouter le token principal tokens.push({ id: 'primary', name: 'Token principal', status: 'ready', lastUsed: null, cooldownUntil: null, errorCount: 0, element: document.querySelector('.token-card:first-child') }); // Ajouter les tokens supplémentaires const tokenCards = document.querySelectorAll('.token-card[data-token-id]'); tokenCards.forEach(card => { const tokenId = card.getAttribute('data-token-id'); tokens.push({ id: tokenId, name: card.querySelector('strong').textContent, status: 'ready', lastUsed: null, cooldownUntil: null, errorCount: 0, element: card }); }); // Initialiser la file d'attente avec tous les tokens resetTokensQueue(); } // Réinitialiser la file d'attente des tokens function resetTokensQueue() { tokensQueue = [...tokens]; // Copie de tous les tokens // Mélanger pour éviter d'utiliser toujours le même ordre shuffleArray(tokensQueue); } // Mélanger un tableau (algorithme de Fisher-Yates) function shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } // Démarrer la rotation des tokens function startTokenRotation() { if (!isProcessing || isPaused) { return; } // Vérifier si nous avons terminé if (currentIndex >= totalItems) { finishMigration(); return; } // Si la file d'attente est vide, la réinitialiser if (tokensQueue.length === 0) { resetTokensQueue(); } // Vérifier si on a un token disponible const availableToken = findAvailableToken(); if (availableToken) { // Traiter le prochain favori avec ce token processWithToken(availableToken); } else { // Aucun token disponible actuellement, attendre et réessayer addLogEntry('Tous les jetons sont en cours d\'utilisation ou en récupération. Attente...', 'info'); setTimeout(startTokenRotation, 5000); } } // Trouver un token disponible function findAvailableToken() { const now = Date.now(); // Vérifier d'abord la file d'attente for (let i = 0; i < tokensQueue.length; i++) { const token = tokensQueue[i]; // Vérifier si le token est prêt if (token.status === 'ready' && (!token.cooldownUntil || now >= token.cooldownUntil)) { // Retirer ce token de la file d'attente tokensQueue.splice(i, 1); return token; } } // Si aucun token n'est disponible dans la file d'attente, // vérifier tous les tokens (au cas où un token aurait fini son cooldown) for (const token of tokens) { if (token.status === 'ready' && (!token.cooldownUntil || now >= token.cooldownUntil)) { // Retirer ce token de la file d'attente s'il y est encore const queueIndex = tokensQueue.findIndex(t => t.id === token.id); if (queueIndex !== -1) { tokensQueue.splice(queueIndex, 1); } return token; } } // Aucun token disponible return null; } // Traiter un favori avec un token spécifique function processWithToken(token) { if (!isProcessing || isPaused) { return; } // Mettre à jour le statut du token updateTokenStatus(token, 'active'); // URL à traiter const url = favoritesList[currentIndex]; // Ajouter une entrée dans le journal addLogEntry(`[${token.name}] Traitement de: ${getTruncatedUrl(url)}`, 'info'); // Envoyer la requête au serveur fetch('process_multitoken.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: url, token_id: token.id, current_index: currentIndex, total_items: totalItems }) }) .then(response => response.json()) .then(data => { // Mettre à jour les statistiques en fonction du résultat handleProcessResult(data, token, url); // Mettre à jour l'index currentIndex++; // Mettre à jour la progression updateProgress(); // Mettre le token en cooldown const cooldownPeriod = migration.options.tokenCooldown * 1000; // Convertir en millisecondes const cooldownUntil = Date.now() + cooldownPeriod; token.lastUsed = Date.now(); token.cooldownUntil = cooldownUntil; updateTokenStatus(token, 'cooldown'); // Programmer la réactivation du token après le cooldown tokenTimers[token.id] = setTimeout(() => { // Remettre le token en état "prêt" updateTokenStatus(token, 'ready'); // Ajouter le token à la fin de la file d'attente tokensQueue.push(token); }, cooldownPeriod); // Démarrer le traitement du prochain favori après un délai setTimeout(startTokenRotation, migration.options.delayBetweenTokens * 1000); }) .catch(error => { console.error('Erreur lors du traitement:', error); // Ajouter l'erreur au journal addLogEntry(`[${token.name}] Erreur lors du traitement: ${error.message}`, 'error'); // Incrémenter le compteur d'erreurs du token token.errorCount = (token.errorCount || 0) + 1; // Si trop d'erreurs avec ce token, le mettre en erreur if (token.errorCount >= 3) { updateTokenStatus(token, 'error'); addLogEntry(`[${token.name}] Ce jeton rencontre des problèmes répétés et a été désactivé.`, 'error'); } else { // Sinon, mettre le token en cooldown plus court const cooldownPeriod = 30 * 1000; // 30 secondes const cooldownUntil = Date.now() + cooldownPeriod; token.lastUsed = Date.now(); token.cooldownUntil = cooldownUntil; updateTokenStatus(token, 'cooldown'); // Programmer la réactivation du token après le cooldown tokenTimers[token.id] = setTimeout(() => { updateTokenStatus(token, 'ready'); tokensQueue.push(token); }, cooldownPeriod); } // Augmenter le compteur d'erreurs errorCount++; updateStatistics(); // Continuer avec le prochain token après un délai setTimeout(startTokenRotation, migration.options.delayBetweenTokens * 1000); }); } // Gérer le résultat du traitement function handleProcessResult(data, token, url) { if (data.success) { // Résultat du traitement const result = data.result; // Construction du message avec détails si disponibles let message = result.message; if (result.details) { message += ` (${result.details})`; } // Ajouter l'entrée au journal avec le nom du token addLogEntry(`[${token.name}] ${message}`, result.status); // Mettre à jour les compteurs if (result.status === 'success') { successCount++; token.errorCount = 0; // Réinitialiser le compteur d'erreurs } else if (result.status === 'error') { errorCount++; // Incrémenter le compteur d'erreurs du token token.errorCount = (token.errorCount || 0) + 1; // Si autoAdjust est activé et qu'il y a beaucoup d'erreurs, augmenter les délais if (migration.options.autoAdjust && token.errorCount >= 2) { if (result.error_type === 'rate_limit') { // Augmenter le délai entre les tokens if (migration.options.delayBetweenTokens < 60) { migration.options.delayBetweenTokens += 5; if (delayBetweenTokens) delayBetweenTokens.value = migration.options.delayBetweenTokens; if (delayValue) delayValue.textContent = migration.options.delayBetweenTokens + 's'; addLogEntry(`Délai entre jetons automatiquement augmenté à ${migration.options.delayBetweenTokens}s en raison de limitations d'API.`, 'warning'); } // Augmenter le délai de cooldown if (migration.options.tokenCooldown < 300) { migration.options.tokenCooldown += 30; if (tokenCooldown) tokenCooldown.value = migration.options.tokenCooldown; if (cooldownValue) cooldownValue.textContent = migration.options.tokenCooldown + 's'; addLogEntry(`Délai de récupération automatiquement augmenté à ${migration.options.tokenCooldown}s.`, 'warning'); } // Mettre à jour le localStorage updateLocalStorage(); } } } else if (result.status === 'info') { skippedCount++; token.errorCount = 0; // Réinitialiser le compteur d'erreurs } else if (result.status === 'warning') { warningCount++; // Si c'est une erreur de rate limit, incrémenter le compteur d'erreurs if (result.error_type === 'rate_limit') { token.errorCount = (token.errorCount || 0) + 1; } } // Mettre à jour les statistiques updateStatistics(); } else { // Erreur lors du traitement addLogEntry(`[${token.name}] Erreur: ${data.message}`, 'error'); errorCount++; updateStatistics(); // Incrémenter le compteur d'erreurs du token token.errorCount = (token.errorCount || 0) + 1; } } // Mettre à jour le statut d'un token function updateTokenStatus(token, status) { token.status = status; if (token.element) { // Mettre à jour la classe CSS token.element.classList.remove('active', 'cooldown', 'error'); // Mettre à jour l'indicateur de statut const statusIndicator = token.element.querySelector('.token-status'); const statusText = token.element.querySelector('.text-muted.small'); if (statusIndicator) { statusIndicator.classList.remove('ready', 'cooldown', 'error', 'idle'); } switch (status) { case 'active': token.element.classList.add('active'); if (statusIndicator) statusIndicator.classList.add('ready'); if (statusText) statusText.textContent = 'En cours d\'utilisation'; break; case 'cooldown': token.element.classList.add('cooldown'); if (statusIndicator) statusIndicator.classList.add('cooldown'); if (statusText && token.cooldownUntil) { const cooldownSeconds = Math.ceil((token.cooldownUntil - Date.now()) / 1000); statusText.textContent = `Récupération: ${cooldownSeconds}s`; // Mettre à jour le compteur de cooldown toutes les secondes const updateInterval = setInterval(() => { const remainingSeconds = Math.ceil((token.cooldownUntil - Date.now()) / 1000); if (remainingSeconds <= 0) { clearInterval(updateInterval); if (statusText) statusText.textContent = 'Prêt'; } else { if (statusText) statusText.textContent = `Récupération: ${remainingSeconds}s`; } }, 1000); } break; case 'error': token.element.classList.add('error'); if (statusIndicator) statusIndicator.classList.add('error'); if (statusText) statusText.textContent = 'Erreur'; break; case 'ready': default: if (statusIndicator) statusIndicator.classList.add('ready'); if (statusText) statusText.textContent = 'Prêt'; break; } } } // Réinitialiser le statut de tous les tokens function resetTokensStatus() { tokens.forEach(token => { updateTokenStatus(token, 'ready'); token.lastUsed = null; token.cooldownUntil = null; token.errorCount = 0; }); } // Nettoyer tous les timers function clearAllTimers() { Object.keys(tokenTimers).forEach(tokenId => { clearTimeout(tokenTimers[tokenId]); delete tokenTimers[tokenId]; }); } // Obtenir une version tronquée d'une URL pour l'affichage function getTruncatedUrl(url) { if (url.length <= 60) return url; const urlObj = new URL(url); const domain = urlObj.hostname; const path = urlObj.pathname; const lastSegment = path.split('/').pop(); return `${domain}/.../${lastSegment}`; } // Générer une chaîne "il y a X minutes/heures" à partir d'une date function getTimeAgoString(date) { const now = new Date(); const diffMs = now - date; const diffSeconds = Math.floor(diffMs / 1000); if (diffSeconds < 60) { return `il y a ${diffSeconds} seconde${diffSeconds > 1 ? 's' : ''}`; } const diffMinutes = Math.floor(diffSeconds / 60); if (diffMinutes < 60) { return `il y a ${diffMinutes} minute${diffMinutes > 1 ? 's' : ''}`; } const diffHours = Math.floor(diffMinutes / 60); if (diffHours < 24) { return `il y a ${diffHours} heure${diffHours > 1 ? 's' : ''}`; } const diffDays = Math.floor(diffHours / 24); return `il y a ${diffDays} jour${diffDays > 1 ? 's' : ''}`; } /** * Réinitialise les données de migration */ function resetMigration() { currentIndex = 0; successCount = 0; errorCount = 0; skippedCount = 0; warningCount = 0; migration = { status: 'not_started', startTime: null, lastUpdateTime: null, progress: { current: 0, total: totalItems, percentage: 0 }, stats: { success: 0, error: 0, skipped: 0, warning: 0 }, options: { delayBetweenTokens: parseInt(delayBetweenTokens ? delayBetweenTokens.value : 15), tokenCooldown: parseInt(tokenCooldown ? tokenCooldown.value : 150), autoAdjust: autoAdjust ? autoAdjust.checked : true } }; // Mettre à jour le localStorage updateLocalStorage(); // Mettre à jour l'interface updateProgress(0); updateStatistics(); } /** * Met à jour les données de migration dans localStorage */ function updateLocalStorage() { migration.lastUpdateTime = Date.now(); migration.progress = { current: currentIndex, total: totalItems, percentage: (currentIndex / totalItems) * 100 }; migration.stats = { success: successCount, error: errorCount, skipped: skippedCount, warning: warningCount }; if (delayBetweenTokens && tokenCooldown && autoAdjust) { migration.options = { delayBetweenTokens: parseInt(delayBetweenTokens.value), tokenCooldown: parseInt(tokenCooldown.value), autoAdjust: autoAdjust.checked }; } // Sauvegarder dans localStorage localStorage.setItem('favmastokey_multitoken_migration', JSON.stringify(migration)); } /** * Terminer la migration avec succès */ function finishMigration() { isProcessing = false; // Nettoyer les timers clearAllTimers(); const summary = `Migration terminée ! ${successCount} publications ajoutées aux favoris, ${errorCount} échecs, ${skippedCount} déjà présentes, ${warningCount} avertissements.`; addLogEntry(summary, 'success'); startMigration.classList.remove('d-none'); startMigration.textContent = 'Terminer'; startMigration.addEventListener('click', function onceFinished() { // Nettoyer localStorage localStorage.removeItem('favmastokey_multitoken_migration'); // Ne pas supprimer les favoris pour permettre une nouvelle migration si nécessaire // localStorage.removeItem('favmastokey_favorites'); // Réinitialiser l'interface resetTokensStatus(); startMigration.textContent = 'Démarrer la migration'; // Retirer cet écouteur d'événement pour éviter les doublons startMigration.removeEventListener('click', onceFinished); // Scroll vers le haut window.scrollTo({ top: 0, behavior: 'smooth' }); }); pauseMigration.classList.add('d-none'); // Mettre à jour la progression à 100% updateProgress(100); // Mettre à jour le statut de la migration migration.status = 'completed'; updateLocalStorage(); } /** * Met à jour la barre de progression */ function updateProgress(forcedValue = null) { const progress = forcedValue !== null ? forcedValue : (currentIndex / totalItems) * 100; if (globalProgress) { globalProgress.style.width = progress + '%'; globalProgress.textContent = Math.round(progress) + '%'; globalProgress.setAttribute('aria-valuenow', progress); } // Mettre à jour le localStorage updateLocalStorage(); } /** * Met à jour les statistiques de progression */ function updateStatistics() { // Calculer les pourcentages pour les barres de progression const total = successCount + errorCount + skippedCount + warningCount; const successPct = total > 0 ? (successCount / total) * 100 : 0; const skipPct = total > 0 ? (skippedCount / total) * 100 : 0; const warnPct = total > 0 ? (warningCount / total) * 100 : 0; const errorPct = total > 0 ? (errorCount / total) * 100 : 0; // Mettre à jour les barres if (successProgress) { successProgress.style.width = successPct + '%'; successProgress.setAttribute('aria-valuenow', successPct); } if (skipProgress) { skipProgress.style.width = skipPct + '%'; skipProgress.setAttribute('aria-valuenow', skipPct); } if (warnProgress) { warnProgress.style.width = warnPct + '%'; warnProgress.setAttribute('aria-valuenow', warnPct); } if (errorProgress) { errorProgress.style.width = errorPct + '%'; errorProgress.setAttribute('aria-valuenow', errorPct); } // Mettre à jour le texte des statistiques if (statsDisplay) { statsDisplay.textContent = `${successCount} succès, ${skippedCount} ignorés, ${warningCount} avertissements, ${errorCount} erreurs`; } // Mettre à jour le localStorage updateLocalStorage(); } /** * Ajoute une entrée dans le journal des opérations */ function addLogEntry(message, status = 'info') { if (!operationLog) return; const entry = document.createElement('div'); entry.className = `log-entry ${status}`; const timestamp = new Date().toLocaleTimeString(); entry.textContent = `[${timestamp}] ${message}`; operationLog.appendChild(entry); // Scroll vers le bas pour voir la dernière entrée const logContainer = document.getElementById('log-container'); if (logContainer) { logContainer.scrollTop = logContainer.scrollHeight; } } // Initialiser la page au chargement initOnLoad(); });