hm_ai_qc_report_tool/static/js/progress.js
nickviljoen f21e41afc3 v1.2.0: Add Docker deployment, simplify auth to local login, production config
- Add Dockerfile, docker-compose.yml, .dockerignore for containerised deployment
- Add deploy/ scripts (deploy.sh, nginx/apache configs, password generator)
- Replace MSAL/Azure AD auth with local username/password authentication
- Add login.html template
- Simplify app.py, middleware, and auth routes for production use
- Update gunicorn_config.py and wsgi.py for Docker/production
- Update templates to work with new auth and URL prefix handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:37:53 +02:00

313 lines
9.2 KiB
JavaScript

/**
* 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 = `
<div class="progress-widget">
<div class="progress mb-3" style="height: 1.5rem;">
<div class="progress-bar progress-bar-animated progress-bar-striped"
role="progressbar"
style="width: 0%"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100">
0%
</div>
</div>
<div class="progress-message text-center">
<i class="bi bi-hourglass-split me-2"></i>
<span>Initializing...</span>
</div>
<div class="progress-details mt-2 text-muted small" style="display: none;"></div>
</div>
`;
// 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 = `
<i class="bi bi-${icon} me-2"></i>
<span>${data.message}</span>
`;
}
// 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 += `<div><strong>${key}:</strong> ${value}</div>`;
}
} else {
detailsHtml = data.details;
}
detailsEl.innerHTML = detailsHtml;
}
}
/**
* Show error in progress widget
* @param {HTMLElement} container - Container element
* @param {Error} error - Error object
*/
function showProgressError(container, error) {
const progressBar = container.querySelector('.progress-bar');
const messageEl = container.querySelector('.progress-message');
if (progressBar) {
progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped');
progressBar.classList.add('bg-danger');
}
if (messageEl) {
messageEl.innerHTML = `
<i class="bi bi-exclamation-triangle me-2"></i>
<span class="text-danger">Error: ${error.message || 'Unknown error occurred'}</span>
`;
}
}
// Make functions available globally
window.ProgressTracker = ProgressTracker;
window.initProgressWidget = initProgressWidget;
window.updateProgressWidget = updateProgressWidget;
window.showProgressError = showProgressError;