/** * Backup Manager module for ObsiGate. * * Full-page view to browse, preview, restore, and delete backups across vaults. * Accessible from the backup/diff viewer footer. */ import { api } from './auth.js'; import { showToast } from './ui.js'; import { safeCreateIcons } from './utils.js'; var state = { backups: [], filter: '', preview: null }; export function openBackupManager() { var area = document.getElementById('content-area'); if (!area) return; area.innerHTML = ''; var container = document.createElement('div'); container.className = 'backup-manager'; container.id = 'backup-manager'; container.innerHTML = '
' + '

Gestion des backups

' + '
' + '' + '' + '
' + '
' + '
' + '
' + '
' + '' + '
'; area.appendChild(container); safeCreateIcons(); document.getElementById('bm-filter').addEventListener('input', function() { state.filter = this.value.toLowerCase(); render(); }); document.getElementById('bm-refresh').addEventListener('click', load); document.getElementById('bm-preview-close').addEventListener('click', function() { document.getElementById('bm-preview').style.display = 'none'; }); state.preview = null; load(); } async function load() { var list = document.getElementById('bm-list'); list.innerHTML = '
'; try { var data = await api('/api/backups'); state.backups = data.backups || []; updateStats(data); render(); } catch (err) { list.innerHTML = '
Erreur: ' + escapeHtml(err.message || 'Inconnue') + '
'; } } function updateStats(data) { var stats = document.getElementById('bm-stats'); if (!stats) return; var totalSize = formatSize(data.total_size_bytes || 0); stats.innerHTML = '' + data.total + ' backups' + '' + totalSize + ' total'; } function render() { var list = document.getElementById('bm-list'); if (!list) return; var filtered = state.backups; if (state.filter) { filtered = state.backups.filter(function(b) { return b.file.toLowerCase().indexOf(state.filter) !== -1 || b.vault.toLowerCase().indexOf(state.filter) !== -1; }); } if (filtered.length === 0) { list.innerHTML = '
Aucun backup trouve.
'; return; } var grouped = {}; filtered.forEach(function(b) { if (!grouped[b.vault]) grouped[b.vault] = []; grouped[b.vault].push(b); }); var html = ''; var vaultNames = Object.keys(grouped).sort(); vaultNames.forEach(function(vault) { var vaultBackups = grouped[vault]; var vaultSize = vaultBackups.reduce(function(s, b) { return s + b.size; }, 0); html += '
'; html += '
'; html += '

' + escapeHtml(vault) + '

'; html += '' + vaultBackups.length + ' backups · ' + formatSize(vaultSize) + ''; html += ''; html += '
'; var byFile = {}; vaultBackups.forEach(function(b) { if (!byFile[b.file]) byFile[b.file] = []; byFile[b.file].push(b); }); var fileNames = Object.keys(byFile).sort(); fileNames.forEach(function(file) { var backups = byFile[file]; backups.sort(function(a, b) { return b.timestamp - a.timestamp; }); var totalFileSize = backups.reduce(function(s, b) { return s + b.size; }, 0); html += '
'; html += '
'; html += ' ' + escapeHtml(file) + ''; html += '' + backups.length + ' versions · ' + formatSize(totalFileSize) + ''; html += ''; html += '
'; html += '
'; backups.forEach(function(b) { var d = new Date(b.timestamp * 1000); var ds = d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); html += '
'; html += '' + ds + ''; html += '' + formatSize(b.size) + ''; html += '
'; html += ''; html += ''; html += ''; html += '
'; html += '
'; }); html += '
'; html += '
'; }); html += '
'; }); list.innerHTML = html; safeCreateIcons(); // Bind vault purge list.querySelectorAll('.bm-vault-purge').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); if (confirm('Supprimer TOUS les backups du vault "' + btn.dataset.vault + '" ?')) { purgeVault(btn.dataset.vault); } }); }); // Bind file purge list.querySelectorAll('.bm-file-purge').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); if (confirm('Supprimer TOUS les backups de "' + btn.dataset.file + '" ?')) { purgeFile(btn.dataset.vault, btn.dataset.file); } }); }); // Bind backup item clicks (preview) and action buttons list.querySelectorAll('.bm-backup-item').forEach(function(item) { item.addEventListener('click', function(e) { // Don't trigger preview if clicking an action button if (e.target.closest('button')) return; state.preview = item.dataset.path; render(); previewBackup(item.dataset.path); }); }); // Bind action buttons list.querySelectorAll('.bm-backup-preview').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); var item = btn.closest('.bm-backup-item'); state.preview = item.dataset.path; render(); previewBackup(item.dataset.path); }); }); list.querySelectorAll('.bm-backup-restore').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); var item = btn.closest('.bm-backup-item'); restoreBackup(item.dataset.vault, item.dataset.file); }); }); list.querySelectorAll('.bm-backup-delete').forEach(function(btn) { btn.addEventListener('click', function(e) { e.stopPropagation(); if (confirm('Supprimer ce backup ?')) { deleteBackups([btn.dataset.path]); } }); }); } async function previewBackup(fpath) { var panel = document.getElementById('bm-preview'); var content = document.getElementById('bm-preview-content'); var title = document.getElementById('bm-preview-title'); panel.style.display = 'block'; content.textContent = t("common.loading"); try { var data = await api('/api/backups/content?path=' + encodeURIComponent(fpath)); content.textContent = data.content || '(fichier vide)'; title.textContent = 'Apercu - ' + (data.name || ''); } catch (err) { content.textContent = 'Erreur: ' + (err.message || 'Inconnue'); } } async function restoreBackup(vault, file) { // Find the backup timestamp from the currently selected backup var item = document.querySelector('.bm-backup-item.active'); if (!item) { showToast(t('backup.select_to_restore'), 'error'); return; } var backupPath = item.dataset.path; // Extract timestamp from the filename var parts = backupPath.replace(/\\/g, '/').split('/').pop().split('.'); // The timestamp is the second-to-last component before .bak var ts = null; for (var i = parts.length - 1; i >= 0; i--) { if (parts[i] === 'bak' && i > 0 && /^\d+$/.test(parts[i-1])) { ts = parseInt(parts[i-1]); break; } } if (!ts) { showToast(t('backup.timestamp_not_found'), 'error'); return; } if (!confirm('Restaurer "' + file + '" a la version du backup selectionne ?\nLa version actuelle sera sauvegardee.')) return; try { var resp = await api('/api/file/' + encodeURIComponent(vault) + '/restore?path=' + encodeURIComponent(file), { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ version: ts }), }); if (resp && resp.success) { showToast(t('backup.file_restored'), 'success'); load(); } } catch (err) { showToast('Erreur: ' + (err.message || 'Inconnue'), 'error'); } } async function deleteBackups(paths) { try { var resp = await api('/api/backups/delete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ paths: paths }), }); showToast(resp.deleted + ' backup(s) supprime(s)', 'success'); load(); } catch (err) { showToast('Erreur: ' + err.message, 'error'); } } async function purgeFile(vault, file) { try { var resp = await api('/api/backups/purge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vault: vault, file: file }), }); showToast(resp.deleted + ' backup(s) supprime(s)', 'success'); load(); } catch (err) { showToast('Erreur: ' + err.message, 'error'); } } async function purgeVault(vault) { try { var resp = await api('/api/backups/purge', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ vault: vault }), }); showToast(resp.deleted + ' backup(s) supprime(s)', 'success'); load(); } catch (err) { showToast('Erreur: ' + err.message, 'error'); } } function formatSize(bytes) { if (bytes < 1024) return bytes + ' o'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko'; return (bytes / (1024 * 1024)).toFixed(1) + ' Mo'; } function pad(n) { return String(n).padStart(2, '0'); } function escapeHtml(str) { var div = document.createElement('div'); div.textContent = str; return div.innerHTML; }