diff --git a/internal/web/static/css/style.css b/internal/web/static/css/style.css index bb8021c..7ce361e 100644 --- a/internal/web/static/css/style.css +++ b/internal/web/static/css/style.css @@ -578,6 +578,50 @@ main.container { to { transform: rotate(360deg); } } +.job-progress { + position: fixed; + top: 70px; + left: 50%; + transform: translateX(-50%); + background: var(--color-bg-card); + border: 1px solid var(--color-border); + border-radius: 12px; + padding: 0.75rem 1rem; + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.3); + z-index: 1900; + min-width: 320px; + max-width: 520px; +} + +.job-progress-header { + display: flex; + justify-content: space-between; + align-items: center; + font-weight: 600; + color: var(--color-text-primary); +} + +.job-progress-bar { + margin-top: 0.6rem; + height: 10px; + background: var(--color-bg-elevated); + border-radius: 999px; + overflow: hidden; +} + +.job-progress-fill { + height: 100%; + background: linear-gradient(135deg, var(--color-brand) 0%, var(--color-keypoint) 100%); + width: 0%; + transition: width 0.2s ease; +} + +.job-progress-message { + margin-top: 0.4rem; + font-size: 0.9rem; + color: var(--color-text-secondary); +} + /* Detail views */ .breadcrumb { margin-bottom: 1.5rem; diff --git a/internal/web/static/js/app.js b/internal/web/static/js/app.js index 6870429..63094d0 100644 --- a/internal/web/static/js/app.js +++ b/internal/web/static/js/app.js @@ -97,164 +97,62 @@ function displayGlobalSearchResults(data) { // Bulk Import Functions async function bulkImportAll() { - showLoader('Importing all data from TPDB...'); - if (!confirm('This will import ALL data from TPDB. This may take several hours. Continue?')) { - hideLoader(); - return; - } - - setImportStatus('import-all', 'Importing all data from TPDB... This may take a while.', false); - + showLoader('Importing everything from TPDB...'); + startJobProgress('TPDB bulk import'); try { - const response = await fetch('/api/import/all', { - method: 'POST' - }); - - const result = await response.json(); - - if (result.success) { - let message = result.message + '\n\n'; - if (result.data) { - result.data.forEach(r => { - message += `${r.EntityType}: ${r.Imported}/${r.Total} imported, ${r.Failed} failed\n`; - }); - } - setImportStatus('import-all', message, true); - setTimeout(() => { - closeModal('import-all-modal'); - location.reload(); - }, 3000); - } else { - setImportStatus('import-all', result.message, false); - } - } catch (error) { - setImportStatus('import-all', 'Error: ' + error.message, false); + await importWithProgress('/api/import/all-performers/progress', 'Performers'); + await importWithProgress('/api/import/all-studios/progress', 'Studios'); + await importWithProgress('/api/import/all-scenes/progress', 'Scenes'); + setImportStatus('import-all', 'Bulk import complete', true); + setTimeout(() => location.reload(), 1500); + } catch (err) { + setImportStatus('import-all', `Bulk import error: ${err.message}`, false); } finally { + stopJobProgress(); hideLoader(); } } async function bulkImportPerformers() { showLoader('Importing performers from TPDB...'); - if (!confirm('This will import ALL performers from TPDB. Continue?')) { + startJobProgress('Importing performers from TPDB'); + try { + await importWithProgress('/api/import/all-performers/progress', 'Performers'); + setTimeout(() => location.reload(), 1000); + } catch (err) { + setImportStatus('performer', `Error: ${err.message}`, false); + } finally { + stopJobProgress(); hideLoader(); - return; } - - // Show progress modal - showProgressModal('performers'); - - // Connect to SSE endpoint - const eventSource = new EventSource('/api/import/all-performers/progress'); - - eventSource.onmessage = function(event) { - const data = JSON.parse(event.data); - - if (data.error) { - updateProgress('performers', 0, 0, data.error, true); - eventSource.close(); - return; - } - - if (data.complete) { - updateProgress('performers', 100, 100, `Complete! Imported ${data.result.Imported}/${data.result.Total} performers`, false); - eventSource.close(); - setTimeout(() => { - closeProgressModal(); - location.reload(); - }, 2000); - } else { - updateProgress('performers', data.current, data.total, data.message, false); - } - }; - - eventSource.onerror = function() { - updateProgress('performers', 0, 0, 'Connection error', true); - eventSource.close(); - hideLoader(); - }; } async function bulkImportStudios() { showLoader('Importing studios from TPDB...'); - if (!confirm('This will import ALL studios from TPDB. Continue?')) { + startJobProgress('Importing studios from TPDB'); + try { + await importWithProgress('/api/import/all-studios/progress', 'Studios'); + setTimeout(() => location.reload(), 1000); + } catch (err) { + setImportStatus('studio', `Error: ${err.message}`, false); + } finally { + stopJobProgress(); hideLoader(); - return; } - - // Show progress modal - showProgressModal('studios'); - - // Connect to SSE endpoint - const eventSource = new EventSource('/api/import/all-studios/progress'); - - eventSource.onmessage = function(event) { - const data = JSON.parse(event.data); - - if (data.error) { - updateProgress('studios', 0, 0, data.error, true); - eventSource.close(); - return; - } - - if (data.complete) { - updateProgress('studios', 100, 100, `Complete! Imported ${data.result.Imported}/${data.result.Total} studios`, false); - eventSource.close(); - setTimeout(() => { - closeProgressModal(); - location.reload(); - }, 2000); - } else { - updateProgress('studios', data.current, data.total, data.message, false); - } - }; - - eventSource.onerror = function() { - updateProgress('studios', 0, 0, 'Connection error', true); - eventSource.close(); - hideLoader(); - }; } async function bulkImportScenes() { showLoader('Importing scenes from TPDB...'); - if (!confirm('This will import ALL scenes from TPDB. Continue?')) { + startJobProgress('Importing scenes from TPDB'); + try { + await importWithProgress('/api/import/all-scenes/progress', 'Scenes'); + setTimeout(() => location.reload(), 1000); + } catch (err) { + setImportStatus('scene', `Error: ${err.message}`, false); + } finally { + stopJobProgress(); hideLoader(); - return; } - - // Show progress modal - showProgressModal('scenes'); - - // Connect to SSE endpoint - const eventSource = new EventSource('/api/import/all-scenes/progress'); - - eventSource.onmessage = function(event) { - const data = JSON.parse(event.data); - - if (data.error) { - updateProgress('scenes', 0, 0, data.error, true); - eventSource.close(); - return; - } - - if (data.complete) { - updateProgress('scenes', 100, 100, `Complete! Imported ${data.result.Imported}/${data.result.Total} scenes`, false); - eventSource.close(); - setTimeout(() => { - closeProgressModal(); - location.reload(); - }, 2000); - } else { - updateProgress('scenes', data.current, data.total, data.message, false); - } - }; - - eventSource.onerror = function() { - updateProgress('scenes', 0, 0, 'Connection error', true); - eventSource.close(); - hideLoader(); - }; } function bulkImportMovies() { @@ -581,6 +479,75 @@ function hideLoader() { const overlay = document.getElementById('global-loader'); if (overlay) overlay.style.display = 'none'; } + +// Unified SSE import helper with progress bar +function importWithProgress(url, label) { + return new Promise((resolve, reject) => { + const eventSource = new EventSource(url); + startJobProgress(label); + eventSource.onmessage = function(event) { + const data = JSON.parse(event.data); + if (data.error) { + updateJobProgress(0, 0, data.error, true); + eventSource.close(); + reject(new Error(data.error)); + return; + } + if (data.complete) { + updateJobProgress(data.result.Imported, data.result.Total, `${label} complete (${data.result.Imported}/${data.result.Total})`, false); + eventSource.close(); + resolve(data.result); + return; + } + updateJobProgress(data.current, data.total, data.message, false); + }; + eventSource.onerror = function() { + updateJobProgress(0, 0, `${label} connection error`, true); + eventSource.close(); + reject(new Error('Connection error')); + }; + }); +} + +function startJobProgress(label) { + const container = document.getElementById('job-progress'); + const lbl = document.getElementById('job-progress-label'); + const msg = document.getElementById('job-progress-message'); + const fill = document.getElementById('job-progress-fill'); + const count = document.getElementById('job-progress-count'); + if (container && lbl && msg && fill && count) { + container.style.display = 'block'; + lbl.textContent = label || 'Working...'; + msg.textContent = ''; + count.textContent = ''; + fill.style.width = '0%'; + } +} + +function updateJobProgress(current, total, message, isError) { + const container = document.getElementById('job-progress'); + const msg = document.getElementById('job-progress-message'); + const fill = document.getElementById('job-progress-fill'); + const count = document.getElementById('job-progress-count'); + if (container && fill && msg && count) { + const percent = total > 0 ? Math.min(100, (current / total) * 100) : 0; + fill.style.width = `${percent}%`; + count.textContent = total > 0 ? `${current}/${total}` : ''; + msg.textContent = message || ''; + if (isError) { + fill.style.background = '#ff8a8a'; + msg.style.color = '#ff8a8a'; + } else { + fill.style.background = 'linear-gradient(135deg, var(--color-brand) 0%, var(--color-keypoint) 100%)'; + msg.style.color = 'var(--color-text-secondary)'; + } + } +} + +function stopJobProgress() { + const container = document.getElementById('job-progress'); + if (container) container.style.display = 'none'; +} window.onclick = function(event) { if (event.target.classList.contains('modal')) { event.target.classList.remove('active'); diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index 1007011..f47a95a 100644 --- a/internal/web/templates/layout.html +++ b/internal/web/templates/layout.html @@ -63,4 +63,14 @@
Working...
+ {{end}}