Add backup recovery center
This commit is contained in:
@@ -15,6 +15,9 @@ const repairMessage = document.querySelector('#repair-message');
|
||||
const previewRepairButton = document.querySelector('#preview-repair');
|
||||
const applyRepairButton = document.querySelector('#apply-repair');
|
||||
const restoreRepairButton = document.querySelector('#restore-repair');
|
||||
const loadBackupsButton = document.querySelector('#load-backups');
|
||||
const backupSummary = document.querySelector('#backup-summary');
|
||||
const backupList = document.querySelector('#backup-list');
|
||||
let latestAnalysis = null;
|
||||
let selectedDuplicateGroup = null;
|
||||
let previewedRepair = null;
|
||||
@@ -34,6 +37,52 @@ function escapeHtml(value) {
|
||||
}[character]));
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes) return '0 B';
|
||||
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const unit = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
||||
return `${(bytes / (1024 ** unit)).toFixed(unit ? 1 : 0)} ${units[unit]}`;
|
||||
}
|
||||
|
||||
function formatDate(value) {
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.valueOf()) ? value : date.toLocaleString([], {dateStyle: 'medium', timeStyle: 'short'});
|
||||
}
|
||||
|
||||
async function loadBackups() {
|
||||
loadBackupsButton.disabled = true;
|
||||
backupSummary.textContent = 'Looking for recovery snapshots…';
|
||||
try {
|
||||
const response = await fetch('/api/backups', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({serato: expandHome(document.querySelector('#serato-path').value)})});
|
||||
const data = await response.json();
|
||||
if (!response.ok) throw new Error(data.error || 'Could not load backups');
|
||||
backupSummary.textContent = `${data.total} backup${data.total === 1 ? '' : 's'} · ${formatBytes(data.size_bytes)} on disk`;
|
||||
backupList.innerHTML = data.items.length ? data.items.map((backup) => `
|
||||
<article class="backup-item">
|
||||
<div><span class="backup-status ${backup.status}">${backup.status === 'restored' ? 'Restored' : 'Ready to restore'}</span><strong>${formatDate(backup.created_at)}</strong><small>Kept: ${escapeHtml(backup.keeper)}</small><small>${backup.replaced.length} original file${backup.replaced.length === 1 ? '' : 's'} · ${formatBytes(backup.size_bytes)}</small></div>
|
||||
<button type="button" data-restore-backup="${escapeHtml(backup.path)}" ${backup.status === 'restored' ? 'disabled' : ''}>${backup.status === 'restored' ? 'Already restored' : 'Restore'}</button>
|
||||
</article>
|
||||
`).join('') : '<div class="empty-detail">No repair backups found for this library yet.</div>';
|
||||
} catch (error) {
|
||||
backupSummary.textContent = error.message;
|
||||
backupList.innerHTML = '';
|
||||
} finally { loadBackupsButton.disabled = false; }
|
||||
}
|
||||
|
||||
loadBackupsButton.addEventListener('click', loadBackups);
|
||||
backupList.addEventListener('click', async (event) => {
|
||||
const button = event.target.closest('[data-restore-backup]');
|
||||
if (!button || button.disabled) return;
|
||||
if (!window.confirm('Restore the original duplicate files from this backup? Existing real files will never be overwritten.')) return;
|
||||
button.disabled = true; button.textContent = 'Restoring…';
|
||||
try {
|
||||
const response = await fetch('/api/backups/restore', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({serato: expandHome(document.querySelector('#serato-path').value), backup: button.dataset.restoreBackup})});
|
||||
const result = await response.json();
|
||||
if (!response.ok) throw new Error(result.error || 'Restore failed');
|
||||
await loadBackups();
|
||||
} catch (error) { backupSummary.textContent = error.message; button.disabled = false; button.textContent = 'Restore'; }
|
||||
});
|
||||
|
||||
function detailLines(item) {
|
||||
if (item.files) {
|
||||
return item.files.map((file) => `<li>${escapeHtml(file)}</li>`).join('');
|
||||
@@ -127,6 +176,7 @@ applyRepairButton.addEventListener('click', async () => {
|
||||
repairMessage.textContent = `Cleanup complete. Restore backup: ${result.backup}`;
|
||||
previewRepairButton.disabled = true;
|
||||
restoreRepairButton.hidden = false;
|
||||
await loadBackups();
|
||||
} catch (error) { repairMessage.textContent = error.message; applyRepairButton.disabled = false; }
|
||||
});
|
||||
|
||||
@@ -139,6 +189,7 @@ restoreRepairButton.addEventListener('click', async () => {
|
||||
if (!response.ok) throw new Error(result.error || 'Restore failed');
|
||||
repairMessage.textContent = `Restore complete. ${result.restored.length} original file(s) returned.`;
|
||||
restoreRepairButton.hidden = true;
|
||||
await loadBackups();
|
||||
} catch (error) { repairMessage.textContent = error.message; restoreRepairButton.disabled = false; }
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user