/** * Photo Processing Progress Tracking * * Отслеживает прогресс асинхронной обработки фото через Celery. * Показывает прогресс-бар и обновляет UI по мере обработки. */ class PhotoProgressTracker { constructor(options = {}) { this.options = { pollInterval: options.pollInterval || 2000, // 2 секунды maxRetries: options.maxRetries || 5, onSuccess: options.onSuccess || (() => {}), onError: options.onError || (() => {}), onProgress: options.onProgress || (() => {}), }; this.activeTrackers = new Map(); // task_id -> status } /** * Запустить отслеживание одного фото * * @param {string} taskId - ID задачи Celery * @param {HTMLElement} photoElement - DOM элемент фото (опционально) * @param {Function} onComplete - Callback при завершении */ trackPhoto(taskId, photoElement = null, onComplete = null) { console.log(`[PhotoProgress] Tracking photo: ${taskId}`); this.activeTrackers.set(taskId, { status: 'pending', progress: 0, photoElement: photoElement, onComplete: onComplete, retries: 0, }); // Запускаем периодическое опрашивание статуса this._pollStatus(taskId); } /** * Отслеживание нескольких фото одновременно * * @param {string[]} taskIds - Массив task_id * @param {Function} onAllComplete - Callback когда все завершены */ trackMultiple(taskIds, onAllComplete = null) { console.log(`[PhotoProgress] Tracking ${taskIds.length} photos`); taskIds.forEach((taskId, index) => { this.trackPhoto(taskId, null, () => { // Проверяем все ли завершены const allCompleted = taskIds.every(id => { const tracker = this.activeTrackers.get(id); return tracker && tracker.status === 'success'; }); if (allCompleted && onAllComplete) { console.log('[PhotoProgress] All photos processed successfully'); onAllComplete(); } }); }); } /** * Приватный метод для опрашивания статуса */ _pollStatus(taskId) { const statusUrl = `/products/api/photos/status/${taskId}/`; fetch(statusUrl) .then(response => { if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); }) .then(data => { this._handleStatusUpdate(taskId, data); }) .catch(error => { console.error(`[PhotoProgress] Error polling status for ${taskId}:`, error); this._handleError(taskId, error); }); } /** * Обработка обновления статуса */ _handleStatusUpdate(taskId, data) { const tracker = this.activeTrackers.get(taskId); if (!tracker) return; const celeryStatus = data.status; // PENDING, STARTED, SUCCESS, FAILURE, RETRY let progress = data.progress || 0; let status = 'processing'; console.log(`[PhotoProgress] ${taskId}: ${celeryStatus} (${progress}%)`); switch (celeryStatus) { case 'PENDING': progress = 0; status = 'pending'; this._updateUI(tracker, progress, '⏳ В очереди на обработку...'); // Продолжаем опрашивать setTimeout(() => this._pollStatus(taskId), this.options.pollInterval); break; case 'STARTED': progress = 50; status = 'processing'; this._updateUI(tracker, progress, '⚙️ Обрабатывается изображение...'); // Продолжаем опрашивать setTimeout(() => this._pollStatus(taskId), this.options.pollInterval); break; case 'SUCCESS': progress = 100; status = 'success'; this._updateUI(tracker, progress, '✅ Обработано успешно'); tracker.status = 'success'; this.activeTrackers.set(taskId, tracker); // Вызываем callback if (tracker.onComplete) { tracker.onComplete(data.result); } this.options.onSuccess(taskId, data.result); break; case 'FAILURE': progress = 0; status = 'failed'; this._updateUI(tracker, progress, '❌ Ошибка при обработке'); tracker.status = 'failed'; this.activeTrackers.set(taskId, tracker); // Вызываем обработчик ошибки this.options.onError(taskId, data.error || 'Unknown error'); break; case 'RETRY': progress = 25; status = 'retrying'; this._updateUI(tracker, progress, '🔄 Повторная попытка...'); // Продолжаем опрашивать setTimeout(() => this._pollStatus(taskId), this.options.pollInterval); break; default: // Неизвестный статус, продолжаем опрашивать setTimeout(() => this._pollStatus(taskId), this.options.pollInterval); } // Обновляем в кэше tracker.status = status; tracker.progress = progress; this.activeTrackers.set(taskId, tracker); // Вызываем callback прогресса this.options.onProgress(taskId, progress, status); } /** * Обновление UI элемента */ _updateUI(tracker, progress, message) { if (!tracker.photoElement) return; const element = tracker.photoElement; // Обновляем/создаем прогресс-бар let progressBar = element.querySelector('.photo-progress-bar'); if (!progressBar) { progressBar = document.createElement('div'); progressBar.className = 'photo-progress-bar'; progressBar.innerHTML = `