Files
2025-12-29 07:34:20 +01:00

458 lines
16 KiB
JavaScript

// DB Restore Tool - Frontend JavaScript
let currentPath = '';
let selectedFile = null;
let isConnected = false;
let currentJobId = null;
let pollInterval = null;
// Initialize
document.addEventListener('DOMContentLoaded', function() {
loadAvailableKeys();
setupEventListeners();
});
function setupEventListeners() {
// Auth type toggle
document.getElementById('auth-type').addEventListener('change', function() {
const passwordAuth = document.getElementById('password-auth');
const keyAuth = document.getElementById('key-auth');
if (this.value === 'password') {
passwordAuth.classList.remove('hidden');
keyAuth.classList.add('hidden');
} else {
passwordAuth.classList.add('hidden');
keyAuth.classList.remove('hidden');
}
});
// Connect form
document.getElementById('connect-form').addEventListener('submit', function(e) {
e.preventDefault();
connect();
});
// Disconnect button
document.getElementById('disconnect-btn').addEventListener('click', disconnect);
// Restore button
document.getElementById('restore-btn').addEventListener('click', startRestore);
// Cancel button
document.getElementById('cancel-btn').addEventListener('click', cancelRestore);
}
async function loadAvailableKeys() {
try {
const response = await fetch('/api/keys');
const data = await response.json();
const select = document.getElementById('key-file');
select.innerHTML = '<option value="">Select a key...</option>';
data.keys.forEach(key => {
const option = document.createElement('option');
option.value = key;
option.textContent = key;
select.appendChild(option);
});
} catch (error) {
console.error('Failed to load keys:', error);
}
}
async function connect() {
const authType = document.getElementById('auth-type').value;
const password = document.getElementById('password').value;
const keyFile = document.getElementById('key-file').value;
const keyPassphrase = document.getElementById('key-passphrase').value;
const btn = document.getElementById('connect-btn');
btn.disabled = true;
btn.textContent = 'Connecting...';
try {
const response = await fetch('/api/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
auth_type: authType,
password: password,
key_file: keyFile,
key_passphrase: keyPassphrase
})
});
const data = await response.json();
if (data.success) {
isConnected = true;
currentPath = data.path;
showStatus('Connected to ' + data.host, 'success');
renderFiles(data.files, data.path);
updateBreadcrumb(data.path);
// Toggle buttons
btn.classList.add('hidden');
document.getElementById('disconnect-btn').classList.remove('hidden');
// Clear password field for security
document.getElementById('password').value = '';
} else {
showStatus('Connection failed: ' + data.error, 'error');
}
} catch (error) {
showStatus('Connection error: ' + error.message, 'error');
} finally {
btn.disabled = false;
btn.textContent = 'Connect';
}
}
async function disconnect() {
try {
await fetch('/api/disconnect', { method: 'POST' });
} catch (e) {}
isConnected = false;
selectedFile = null;
currentPath = '';
// Reset UI
document.getElementById('connect-btn').classList.remove('hidden');
document.getElementById('disconnect-btn').classList.add('hidden');
document.getElementById('file-browser').innerHTML = `
<div class="p-8 text-center text-gray-400">
<svg class="w-16 h-16 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 14v3m4-3v3m4-3v3M3 21h18M3 10h18M3 7l9-4 9 4M4 10h16v11H4V10z"/>
</svg>
<p>Connect to browse remote files</p>
</div>
`;
document.getElementById('breadcrumb').innerHTML = '<span class="text-gray-400">Not connected</span>';
document.getElementById('selected-file-info').innerHTML = '<p class="text-gray-500 text-sm">No file selected</p>';
document.getElementById('restore-btn').disabled = true;
hideStatus();
}
async function browse(path) {
if (!isConnected) return;
try {
const response = await fetch('/api/browse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: path })
});
const data = await response.json();
if (data.success) {
currentPath = data.path;
renderFiles(data.files, data.path);
updateBreadcrumb(data.path);
} else {
showStatus('Browse failed: ' + data.error, 'error');
}
} catch (error) {
showStatus('Browse error: ' + error.message, 'error');
}
}
function renderFiles(files, path) {
const container = document.getElementById('file-browser');
if (files.length === 0) {
container.innerHTML = '<div class="p-8 text-center text-gray-400">Empty directory</div>';
return;
}
let html = '<div class="divide-y">';
// Parent directory link
if (path !== '/') {
const parentPath = path.split('/').slice(0, -1).join('/') || '/';
html += `
<div class="file-item p-3 cursor-pointer flex items-center" onclick="browse('${parentPath}')">
<svg class="w-5 h-5 mr-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12"/>
</svg>
<span class="text-gray-600">..</span>
</div>
`;
}
files.forEach(file => {
const isSelected = selectedFile && selectedFile.path === file.path;
const selectedClass = isSelected ? 'selected' : '';
if (file.is_dir) {
html += `
<div class="file-item p-3 cursor-pointer flex items-center ${selectedClass}" onclick="browse('${file.path}')">
<svg class="w-5 h-5 mr-3 text-yellow-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M10 4H4a2 2 0 00-2 2v12a2 2 0 002 2h16a2 2 0 002-2V8a2 2 0 00-2-2h-8l-2-2z"/>
</svg>
<span class="flex-1 font-medium">${file.name}</span>
</div>
`;
} else if (file.is_sql) {
html += `
<div class="file-item p-3 cursor-pointer flex items-center ${selectedClass}" onclick="selectFile(${JSON.stringify(file).replace(/"/g, '&quot;')})">
<svg class="w-5 h-5 mr-3 text-green-500" fill="currentColor" viewBox="0 0 24 24">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z"/>
<path d="M14 2v6h6"/>
</svg>
<span class="flex-1">${file.name}</span>
<span class="text-sm text-gray-500 mr-4">${file.size_human}</span>
<span class="text-sm text-gray-400">${file.mtime_human}</span>
</div>
`;
} else {
html += `
<div class="file-item p-3 flex items-center opacity-50">
<svg class="w-5 h-5 mr-3 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M14 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V8l-6-6z"/>
</svg>
<span class="flex-1 text-gray-500">${file.name}</span>
<span class="text-sm text-gray-400">${file.size_human}</span>
</div>
`;
}
});
html += '</div>';
container.innerHTML = html;
}
function selectFile(file) {
selectedFile = file;
document.getElementById('selected-file-info').innerHTML = `
<p class="font-medium text-gray-800">${file.name}</p>
<p class="text-sm text-gray-500">${file.size_human} - ${file.mtime_human}</p>
`;
document.getElementById('restore-btn').disabled = false;
// Auto-detect target database from filename
const filename = file.name.toLowerCase();
const targetDbSelect = document.getElementById('target-db');
const availableDbs = Array.from(targetDbSelect.options).map(o => o.value);
for (const db of availableDbs) {
if (filename.includes(db.toLowerCase())) {
targetDbSelect.value = db;
break;
}
}
// Re-render to show selection
browse(currentPath);
}
function updateBreadcrumb(path) {
const parts = path.split('/').filter(p => p);
let html = `<span class="cursor-pointer hover:text-blue-600" onclick="browse('/')">/</span>`;
let currentPathBuild = '';
parts.forEach((part, index) => {
currentPathBuild += '/' + part;
const isLast = index === parts.length - 1;
html += `
<span class="mx-1">/</span>
<span class="${isLast ? 'font-medium' : 'cursor-pointer hover:text-blue-600'}"
${isLast ? '' : `onclick="browse('${currentPathBuild}')"`}>${part}</span>
`;
});
document.getElementById('breadcrumb').innerHTML = html;
}
async function startRestore() {
if (!selectedFile) return;
const targetDb = document.getElementById('target-db').value;
if (!confirm(`Are you sure you want to restore ${selectedFile.name} to database "${targetDb}"?\n\nThis will DROP ALL TABLES in the database!`)) {
return;
}
const btn = document.getElementById('restore-btn');
btn.disabled = true;
btn.textContent = 'Starting...';
try {
const response = await fetch('/api/restore', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
file: selectedFile.path,
database: targetDb
})
});
const data = await response.json();
if (data.success) {
currentJobId = data.job_id;
showProgressPanel();
startPolling();
} else {
showStatus('Restore failed: ' + data.error, 'error');
btn.disabled = false;
btn.textContent = 'Start Restore';
}
} catch (error) {
showStatus('Restore error: ' + error.message, 'error');
btn.disabled = false;
btn.textContent = 'Start Restore';
}
}
function showProgressPanel() {
document.getElementById('progress-panel').classList.remove('hidden');
document.getElementById('progress-bar').style.width = '0%';
document.getElementById('progress-bar').classList.remove('bg-green-600', 'bg-red-600', 'bg-yellow-600');
document.getElementById('progress-bar').classList.add('bg-blue-600');
document.getElementById('progress-percent').textContent = '0%';
document.getElementById('progress-status').textContent = 'Starting...';
document.getElementById('progress-message').textContent = 'Initializing...';
document.getElementById('cancel-btn').classList.remove('hidden');
document.getElementById('cancel-btn').disabled = false;
document.getElementById('cancel-btn').textContent = 'Cancel Restore';
}
function startPolling() {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(async () => {
try {
const response = await fetch(`/api/status/${currentJobId}`);
const data = await response.json();
if (data.success) {
updateProgress(data);
if (data.status === 'completed' || data.status === 'error' || data.status === 'cancelled') {
stopPolling();
document.getElementById('restore-btn').disabled = false;
document.getElementById('restore-btn').textContent = 'Start Restore';
if (data.status === 'completed') {
showStatus('Restore completed successfully!', 'success');
} else if (data.status === 'cancelled') {
showStatus('Restore was cancelled', 'error');
} else {
showStatus('Restore failed: ' + data.error, 'error');
}
}
}
} catch (error) {
console.error('Polling error:', error);
}
}, 250);
}
function stopPolling() {
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
}
async function cancelRestore() {
if (!currentJobId) return;
if (!confirm('Are you sure you want to cancel the restore?')) {
return;
}
const btn = document.getElementById('cancel-btn');
btn.disabled = true;
btn.textContent = 'Cancelling...';
try {
const response = await fetch(`/api/cancel/${currentJobId}`, {
method: 'POST'
});
const data = await response.json();
if (data.success) {
showStatus('Cancellation requested...', 'error');
} else {
showStatus('Cancel failed: ' + data.error, 'error');
btn.disabled = false;
btn.textContent = 'Cancel Restore';
}
} catch (error) {
showStatus('Cancel error: ' + error.message, 'error');
btn.disabled = false;
btn.textContent = 'Cancel Restore';
}
}
function updateProgress(data) {
const bar = document.getElementById('progress-bar');
const percent = document.getElementById('progress-percent');
const status = document.getElementById('progress-status');
const message = document.getElementById('progress-message');
const elapsed = document.getElementById('progress-elapsed');
const cancelBtn = document.getElementById('cancel-btn');
bar.style.width = data.progress + '%';
percent.textContent = data.progress + '%';
// Update status label
const statusLabels = {
'starting': 'Starting',
'downloading': 'Downloading',
'restoring': 'Restoring',
'completed': 'Completed',
'error': 'Error',
'cancelled': 'Cancelled'
};
status.textContent = statusLabels[data.status] || data.status;
// Update color based on status
bar.classList.remove('bg-green-600', 'bg-red-600', 'bg-yellow-600');
if (data.status === 'completed') {
bar.classList.remove('bg-blue-600');
bar.classList.add('bg-green-600');
} else if (data.status === 'error') {
bar.classList.remove('bg-blue-600');
bar.classList.add('bg-red-600');
} else if (data.status === 'cancelled') {
bar.classList.remove('bg-blue-600');
bar.classList.add('bg-yellow-600');
}
// Show/hide cancel button based on job status
if (data.status === 'completed' || data.status === 'error' || data.status === 'cancelled') {
cancelBtn.classList.add('hidden');
} else {
cancelBtn.classList.remove('hidden');
cancelBtn.disabled = false;
cancelBtn.textContent = 'Cancel Restore';
}
message.textContent = data.message || '';
if (data.elapsed) {
elapsed.textContent = 'Elapsed: ' + data.elapsed;
}
}
function showStatus(message, type) {
const status = document.getElementById('connection-status');
status.classList.remove('hidden', 'bg-green-100', 'bg-red-100', 'text-green-800', 'text-red-800');
if (type === 'success') {
status.classList.add('bg-green-100', 'text-green-800');
} else if (type === 'error') {
status.classList.add('bg-red-100', 'text-red-800');
}
status.textContent = message;
}
function hideStatus() {
document.getElementById('connection-status').classList.add('hidden');
}