document.addEventListener('DOMContentLoaded', () => { const dropZone = document.getElementById('drop-zone'); const fileInput = document.getElementById('file-input'); const fileDetails = document.getElementById('file-details'); const fileNameEl = document.getElementById('file-name'); const fileSizeEl = document.getElementById('file-size'); const btnCancelFile = document.getElementById('btn-cancel-file'); const btnConvert = document.getElementById('btn-convert'); const progressContainer = document.getElementById('progress-container'); const progressStatus = document.getElementById('progress-status'); const progressPercentage = document.getElementById('progress-percentage'); const progressBarFill = document.getElementById('progress-bar-fill'); const terminalContainer = document.getElementById('terminal-container'); const terminalLogs = document.getElementById('terminal-logs'); const successContainer = document.getElementById('success-container'); const outFileNameEl = document.getElementById('out-file-name'); const btnDownload = document.getElementById('btn-download'); const btnReset = document.getElementById('btn-reset'); let selectedFile = null; let eventSource = null; // Helper: format bytes to human readable format function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } // Handle Drag & Drop events ['dragenter', 'dragover'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); dropZone.classList.add('dragover'); }, false); }); ['dragleave', 'drop'].forEach(eventName => { dropZone.addEventListener(eventName, (e) => { e.preventDefault(); dropZone.classList.remove('dragover'); }, false); }); dropZone.addEventListener('drop', (e) => { const dt = e.dataTransfer; const files = dt.files; if (files.length > 0) { handleFileSelection(files[0]); } }); fileInput.addEventListener('change', (e) => { if (fileInput.files.length > 0) { handleFileSelection(fileInput.files[0]); } }); function handleFileSelection(file) { if (!file.name.toLowerCase().endsWith('.ost')) { alert('Please select an Outlook OST file (.ost).'); return; } selectedFile = file; fileNameEl.textContent = file.name; fileSizeEl.textContent = formatBytes(file.size); dropZone.classList.add('hidden'); fileDetails.classList.remove('hidden'); } btnCancelFile.addEventListener('click', () => { resetUI(); }); // Reset UI to initial state function resetUI() { selectedFile = null; fileInput.value = ''; if (eventSource) { eventSource.close(); eventSource = null; } dropZone.classList.remove('hidden'); fileDetails.classList.add('hidden'); progressContainer.classList.add('hidden'); successContainer.classList.add('hidden'); document.getElementById('summary-card').classList.add('hidden'); progressBarFill.style.width = '0%'; progressPercentage.textContent = '0%'; terminalLogs.innerHTML = '
Waiting for process start...
'; } // Show/hide the combine-VCF sub-option based on selected format const vcfCombineWrap = document.getElementById('vcf-combine-wrap'); document.querySelectorAll('input[name="output-format"]').forEach(radio => { radio.addEventListener('change', () => { if (radio.value === 'mbox' && radio.checked) { vcfCombineWrap.classList.remove('hidden'); } else { vcfCombineWrap.classList.add('hidden'); document.getElementById('combine-vcf').checked = false; } }); }); // Convert button click handler btnConvert.addEventListener('click', () => { if (!selectedFile) return; // Update UI states fileDetails.classList.add('hidden'); progressContainer.classList.remove('hidden'); progressStatus.textContent = 'Uploading OST file to local server...'; // Start file upload uploadFile(selectedFile); }); function uploadFile(file) { const xhr = new XMLHttpRequest(); const formData = new FormData(); formData.append('ostFile', file); const selectedFormat = document.querySelector('input[name="output-format"]:checked').value; formData.append('format', selectedFormat); const combineVcf = document.getElementById('combine-vcf').checked; formData.append('combineVcf', combineVcf ? 'true' : 'false'); // Upload progress listener xhr.upload.addEventListener('progress', (e) => { if (e.lengthComputable) { const percentComplete = Math.round((e.loaded / e.total) * 100); progressBarFill.style.width = percentComplete + '%'; progressPercentage.textContent = percentComplete + '%'; } }); xhr.onload = () => { if (xhr.status === 200) { const response = JSON.parse(xhr.responseText); startStream(response.taskId); } else { showError('Upload failed: ' + (xhr.statusText || 'Server error')); } }; xhr.onerror = () => { showError('Network error occurred during upload.'); }; xhr.open('POST', '/api/upload', true); xhr.send(formData); } function startStream(taskId) { progressStatus.textContent = 'Converting OST to PST (processing folders)...'; progressBarFill.style.width = '100%'; progressPercentage.textContent = 'Running'; // Terminal is always visible in the right column — no toggle needed eventSource = new EventSource(`/api/stream/${taskId}`); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); if (data.status === 'starting') { appendLog(data.message, 'system'); } else if (data.status === 'running') { const log = data.log; // Filter out Java compiler noise (deprecation warnings, caret markers, raw code lines) if (log.trim() === '^' || /^\s+\w.*new MboxrdStorageWriter/.test(log) || /\.java:\d+: warning:/.test(log) || /\d+ warning/.test(log)) { return; // Silently drop compiler noise } // Style warnings/errors/successes let type = ''; if (log.toLowerCase().includes('warning') || log.toLowerCase().includes('skipping')) type = 'warning'; else if (log.toLowerCase().includes('error')) type = 'error'; else if (log.toLowerCase().includes('success') || log.toLowerCase().includes('completed')) type = 'system'; appendLog(log, type); } else if (data.status === 'completed') { eventSource.close(); showSuccess(data.fileName, data.downloadUrl, data.summary); } else if (data.status === 'failed') { eventSource.close(); showError(data.error); } }; eventSource.onerror = (err) => { console.error('SSE Error:', err); eventSource.close(); showError('Lost connection to conversion engine.'); }; } function appendLog(message, type = '') { const logLine = document.createElement('div'); logLine.className = 'log-line'; if (type) logLine.classList.add(type); logLine.textContent = message; terminalLogs.appendChild(logLine); // Auto-scroll to bottom of logs terminalLogs.scrollTop = terminalLogs.scrollHeight; } function showSuccess(fileName, downloadUrl, summary) { progressContainer.classList.add('hidden'); // Keep terminal logs visible so the user can inspect completion states/warnings successContainer.classList.remove('hidden'); outFileNameEl.textContent = fileName; btnDownload.href = downloadUrl; btnDownload.setAttribute('download', fileName); const isMbox = fileName.endsWith('.zip'); btnDownload.textContent = isMbox ? 'Download MBOX ZIP Archive' : 'Download PST File'; const summaryCard = document.getElementById('summary-card'); const summaryFoldersCount = document.getElementById('summary-folders-count'); const summaryMessagesCount = document.getElementById('summary-messages-count'); const summaryDuration = document.getElementById('summary-duration'); const summaryTableBody = document.getElementById('summary-table-body'); if (summary) { summaryCard.classList.remove('hidden'); summaryFoldersCount.textContent = summary.folders ? summary.folders.length : 0; summaryMessagesCount.textContent = summary.totalMessages || 0; summaryDuration.textContent = (summary.duration || 0).toFixed(1) + 's'; summaryTableBody.innerHTML = ''; if (summary.folders && summary.folders.length > 0) { summary.folders.forEach(f => { const row = document.createElement('tr'); row.innerHTML = ` ${escapeHtml(f.folder)} ${f.messages} `; summaryTableBody.appendChild(row); }); } else { summaryTableBody.innerHTML = `No folders containing emails were converted.`; } } else { summaryCard.classList.add('hidden'); } } function escapeHtml(unsafe) { return unsafe .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """) .replace(/'/g, "'"); } function showError(errorMessage) { progressStatus.textContent = 'Error'; progressPercentage.textContent = 'Failed'; progressBarFill.style.backgroundColor = 'var(--error-color)'; appendLog(`[ERROR] ${errorMessage}`, 'error'); } btnReset.addEventListener('click', () => { resetUI(); }); });