feat(import): add real-time progress tracking for CSV imports
This commit is contained in:
@@ -432,6 +432,8 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
setTimeout(() => {
|
||||
document.getElementById('uploadMode').value = 'batch';
|
||||
switchUploadMode();
|
||||
// Resume progress monitoring if a batch is already running for this user
|
||||
resumeBatchProgressIfRunning();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
@@ -857,20 +859,109 @@ function displayImportResults(result) {
|
||||
panel.classList.remove('hidden');
|
||||
}
|
||||
|
||||
function showProgress(show, message = '') {
|
||||
function showProgress(show, message = '', percent = null) {
|
||||
const panel = document.getElementById('progressPanel');
|
||||
const status = document.getElementById('progressStatus');
|
||||
const bar = document.getElementById('progressBar');
|
||||
|
||||
if (show) {
|
||||
status.textContent = message;
|
||||
bar.style.width = '100%';
|
||||
if (percent === null || isNaN(percent)) {
|
||||
bar.style.width = '100%';
|
||||
} else {
|
||||
const clamped = Math.max(0, Math.min(100, Number(percent)));
|
||||
bar.style.width = clamped + '%';
|
||||
}
|
||||
panel.classList.remove('hidden');
|
||||
} else {
|
||||
panel.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------
|
||||
// Batch progress monitoring
|
||||
// -----------------------------
|
||||
const TERMINAL_BATCH_STATUSES = new Set(['success', 'completed_with_errors', 'failed']);
|
||||
let batchProgress = { timer: null, auditId: null };
|
||||
|
||||
async function fetchCurrentBatch() {
|
||||
try {
|
||||
const resp = await window.http.wrappedFetch('/api/import/current-batch');
|
||||
if (!resp.ok) return null;
|
||||
const json = await resp.json();
|
||||
return json && json.running ? json : null;
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
|
||||
function stopBatchProgressPolling() {
|
||||
if (batchProgress.timer) {
|
||||
clearInterval(batchProgress.timer);
|
||||
batchProgress.timer = null;
|
||||
}
|
||||
batchProgress.auditId = null;
|
||||
}
|
||||
|
||||
async function pollBatchProgressOnce(auditId) {
|
||||
try {
|
||||
const resp = await window.http.wrappedFetch(`/api/import/batch-progress/${encodeURIComponent(auditId)}`);
|
||||
if (!resp.ok) return;
|
||||
const p = await resp.json();
|
||||
const percent = Number(p.percent || 0);
|
||||
const total = Number(p.total_files || 0);
|
||||
const processed = Number(p.processed_files || 0);
|
||||
const status = String(p.status || 'running');
|
||||
const statusNice = status.replaceAll('_', ' ');
|
||||
const msg = total > 0
|
||||
? `Processing ${processed}/${total} (${percent.toFixed(1)}%) · ${statusNice}`
|
||||
: `Processing… ${statusNice}`;
|
||||
showProgress(true, msg, percent);
|
||||
if (TERMINAL_BATCH_STATUSES.has(status)) {
|
||||
stopBatchProgressPolling();
|
||||
}
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
function startBatchProgressPolling(auditId) {
|
||||
stopBatchProgressPolling();
|
||||
batchProgress.auditId = auditId;
|
||||
// immediate + interval polling
|
||||
pollBatchProgressOnce(auditId);
|
||||
batchProgress.timer = setInterval(() => pollBatchProgressOnce(auditId), 1500);
|
||||
}
|
||||
|
||||
async function ensureAuditIdWithRetry(maxAttempts = 10, delayMs = 500) {
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const cur = await fetchCurrentBatch();
|
||||
if (cur && cur.audit_id) return cur.audit_id;
|
||||
await new Promise(r => setTimeout(r, delayMs));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function monitorBatchProgressDuring(promise) {
|
||||
try {
|
||||
const id = await ensureAuditIdWithRetry();
|
||||
if (id) {
|
||||
startBatchProgressPolling(id);
|
||||
}
|
||||
} catch (_) {}
|
||||
try {
|
||||
await promise; // wait for upload completion
|
||||
} finally {
|
||||
stopBatchProgressPolling();
|
||||
}
|
||||
}
|
||||
|
||||
async function resumeBatchProgressIfRunning() {
|
||||
try {
|
||||
const cur = await fetchCurrentBatch();
|
||||
if (cur && cur.audit_id) {
|
||||
showProgress(true, 'Resuming import progress…', 0);
|
||||
startBatchProgressPolling(cur.audit_id);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function clearTable() {
|
||||
const fileType = document.getElementById('clearTableType').value;
|
||||
|
||||
@@ -1389,12 +1480,14 @@ async function handleBatchImport(event) {
|
||||
formData.append('replace_existing', replaceExisting);
|
||||
|
||||
try {
|
||||
showProgress(true, 'Processing batch import...');
|
||||
|
||||
const response = await window.http.wrappedFetch('/api/import/batch-upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
showProgress(true, 'Processing batch import...', 0);
|
||||
// Kick off upload and monitor progress concurrently
|
||||
const uploadPromise = window.http.wrappedFetch('/api/import/batch-upload', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
monitorBatchProgressDuring(uploadPromise);
|
||||
const response = await uploadPromise;
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
|
||||
Reference in New Issue
Block a user