Some checks failed
Build & Push Multi-Arch Image / build-and-push (push) Has been cancelled
281 lines
11 KiB
JavaScript
281 lines
11 KiB
JavaScript
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 = '<div class="log-line system">Waiting for process start...</div>';
|
|
}
|
|
|
|
// 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 = `
|
|
<td>${escapeHtml(f.folder)}</td>
|
|
<td style="text-align: right; font-weight: 600;">${f.messages}</td>
|
|
`;
|
|
summaryTableBody.appendChild(row);
|
|
});
|
|
} else {
|
|
summaryTableBody.innerHTML = `<tr><td colspan="2" style="text-align: center; color: var(--text-secondary);">No folders containing emails were converted.</td></tr>`;
|
|
}
|
|
} else {
|
|
summaryCard.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
function escapeHtml(unsafe) {
|
|
return unsafe
|
|
.replace(/&/g, "&")
|
|
.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();
|
|
});
|
|
});
|