/** * Unified Progress Tracking for HM QC Platform * * Supports both Server-Sent Events (SSE) and polling fallback */ class ProgressTracker { /** * Create a progress tracker * @param {string} sessionId - Unique session identifier * @param {object} options - Configuration options */ constructor(sessionId, options = {}) { this.sessionId = sessionId; this.options = { useSSE: true, // Try SSE first pollInterval: 1000, // Poll every 1 second sseUrl: null, // SSE endpoint URL (auto-detected if null) pollUrl: null, // Polling endpoint URL (auto-detected if null) onUpdate: null, // Callback for progress updates onComplete: null, // Callback for completion onError: null, // Callback for errors ...options }; this.eventSource = null; this.pollTimer = null; this.isRunning = false; } /** * Start tracking progress */ start() { if (this.isRunning) { console.warn('Progress tracker already running'); return; } this.isRunning = true; // Try SSE first if (this.options.useSSE && typeof EventSource !== 'undefined') { this.startSSE(); } else { // Fall back to polling this.startPolling(); } } /** * Start Server-Sent Events tracking */ startSSE() { const url = this.options.sseUrl || `${window.BASE_URL || ''}/hm-qc/progress/${this.sessionId}`; this.eventSource = new EventSource(url); this.eventSource.onmessage = (event) => { try { const data = JSON.parse(event.data); this.handleUpdate(data); } catch (error) { console.error('Error parsing SSE data:', error); } }; this.eventSource.onerror = (error) => { console.error('SSE connection error:', error); this.eventSource.close(); // Fall back to polling console.log('Falling back to polling...'); this.startPolling(); }; } /** * Start polling-based tracking */ startPolling() { const poll = async () => { if (!this.isRunning) { return; } try { const response = await fetch(this.options.pollUrl || `${window.BASE_URL || ''}/hm-qc/api/progress/${this.sessionId}`); const data = await response.json(); this.handleUpdate(data); // Continue polling if not finished if (data.status === 'running' && this.isRunning) { this.pollTimer = setTimeout(poll, this.options.pollInterval); } } catch (error) { console.error('Polling error:', error); if (this.options.onError) { this.options.onError(error); } // Retry polling if (this.isRunning) { this.pollTimer = setTimeout(poll, this.options.pollInterval * 2); } } }; poll(); } /** * Handle progress update * @param {object} data - Progress data */ handleUpdate(data) { // Call update callback if (this.options.onUpdate) { this.options.onUpdate(data); } // Check if complete if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') { this.stop(); if (this.options.onComplete) { this.options.onComplete(data); } } } /** * Stop tracking */ stop() { this.isRunning = false; // Close SSE connection if (this.eventSource) { this.eventSource.close(); this.eventSource = null; } // Clear polling timer if (this.pollTimer) { clearTimeout(this.pollTimer); this.pollTimer = null; } } } /** * Initialize a progress widget * @param {string} sessionId - Session identifier * @param {string} containerId - Container element ID * @param {object} options - Configuration options * @returns {ProgressTracker} Progress tracker instance */ function initProgressWidget(sessionId, containerId, options = {}) { const container = document.getElementById(containerId); if (!container) { console.error(`Container element #${containerId} not found`); return null; } // Create progress UI container.innerHTML = `
`; // Create progress tracker const tracker = new ProgressTracker(sessionId, { ...options, onUpdate: (data) => { updateProgressWidget(container, data); // Call user-provided callback if (options.onUpdate) { options.onUpdate(data); } }, onComplete: (data) => { updateProgressWidget(container, data); // Call user-provided callback if (options.onComplete) { options.onComplete(data); } }, onError: (error) => { showProgressError(container, error); // Call user-provided callback if (options.onError) { options.onError(error); } } }); tracker.start(); return tracker; } /** * Update progress widget UI * @param {HTMLElement} container - Container element * @param {object} data - Progress data */ function updateProgressWidget(container, data) { const progressBar = container.querySelector('.progress-bar'); const messageEl = container.querySelector('.progress-message span'); const detailsEl = container.querySelector('.progress-details'); // Update progress bar if (progressBar) { const percent = Math.round(data.percent || 0); progressBar.style.width = `${percent}%`; progressBar.setAttribute('aria-valuenow', percent); progressBar.textContent = `${percent}%`; // Change color based on status progressBar.classList.remove('bg-success', 'bg-danger', 'bg-warning'); if (data.status === 'completed') { progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped'); progressBar.classList.add('bg-success'); } else if (data.status === 'failed') { progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped'); progressBar.classList.add('bg-danger'); } } // Update message if (messageEl && data.message) { let icon = 'hourglass-split'; if (data.status === 'completed') { icon = 'check-circle'; } else if (data.status === 'failed') { icon = 'x-circle'; } else if (data.status === 'cancelled') { icon = 'exclamation-circle'; } messageEl.parentElement.innerHTML = ` ${data.message} `; } // Update details if (detailsEl && data.details) { detailsEl.style.display = 'block'; let detailsHtml = ''; if (typeof data.details === 'object') { for (const [key, value] of Object.entries(data.details)) { detailsHtml += `