Initialize i18n before auth so t() works when rendering auth-dependent UI, and build the command palette lazily so translations are loaded first. Bump service worker cache version.
325 lines
12 KiB
JavaScript
325 lines
12 KiB
JavaScript
/**
|
|
* 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 =
|
|
'<div class="backup-manager-header">' +
|
|
'<h2><i data-lucide="archive" style="width:20px;height:20px"></i> Gestion des backups</h2>' +
|
|
'<div class="backup-manager-actions">' +
|
|
'<input type="text" id="bm-filter" class="backup-manager-filter" placeholder="Filtrer par fichier...">' +
|
|
'<button class="backup-manager-btn" id="bm-refresh" title="Rafraichir"><i data-lucide="refresh-cw" style="width:14px;height:14px"></i></button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="backup-manager-stats" id="bm-stats"></div>' +
|
|
'<div class="backup-manager-body">' +
|
|
'<div class="backup-manager-list" id="bm-list"></div>' +
|
|
'<div class="backup-manager-preview" id="bm-preview" style="display:none">' +
|
|
'<div class="bm-preview-header">' +
|
|
'<h3 id="bm-preview-title">Apercu</h3>' +
|
|
'<button class="backup-manager-btn" id="bm-preview-close" title="Fermer"><i data-lucide="x" style="width:14px;height:14px"></i></button>' +
|
|
'</div>' +
|
|
'<pre class="bm-preview-content" id="bm-preview-content"></pre>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
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 = '<div class="tree-loading" style="padding:2rem;text-align:center"><div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div></div>';
|
|
|
|
try {
|
|
var data = await api('/api/backups');
|
|
state.backups = data.backups || [];
|
|
updateStats(data);
|
|
render();
|
|
} catch (err) {
|
|
list.innerHTML = '<div class="backup-manager-error">Erreur: ' + escapeHtml(err.message || 'Inconnue') + '</div>';
|
|
}
|
|
}
|
|
|
|
function updateStats(data) {
|
|
var stats = document.getElementById('bm-stats');
|
|
if (!stats) return;
|
|
var totalSize = formatSize(data.total_size_bytes || 0);
|
|
stats.innerHTML =
|
|
'<span class="bm-stat"><strong>' + data.total + '</strong> backups</span>' +
|
|
'<span class="bm-stat"><strong>' + totalSize + '</strong> total</span>';
|
|
}
|
|
|
|
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 = '<div class="backup-manager-empty">Aucun backup trouve.</div>';
|
|
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 += '<div class="bm-vault-group">';
|
|
html += '<div class="bm-vault-header">';
|
|
html += '<h3 class="bm-vault-name"><i data-lucide="database" style="width:14px;height:14px"></i> ' + escapeHtml(vault) + '</h3>';
|
|
html += '<span class="bm-vault-meta">' + vaultBackups.length + ' backups · ' + formatSize(vaultSize) + '</span>';
|
|
html += '<button class="bm-vault-purge" data-vault="' + escapeHtml(vault) + '" title="Supprimer tous les backups de ce vault"><i data-lucide="trash-2" style="width:14px;height:14px"></i> Vider</button>';
|
|
html += '</div>';
|
|
|
|
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 += '<div class="bm-file-group">';
|
|
html += '<div class="bm-file-header">';
|
|
html += '<span class="bm-file-name"><i data-lucide="file-text" style="width:14px;height:14px"></i> ' + escapeHtml(file) + '</span>';
|
|
html += '<span class="bm-file-meta">' + backups.length + ' versions · ' + formatSize(totalFileSize) + '</span>';
|
|
html += '<button class="bm-file-purge" data-vault="' + escapeHtml(vault) + '" data-file="' + escapeHtml(file) + '" title="Supprimer tous les backups de ce fichier"><i data-lucide="trash-2" style="width:12px;height:12px"></i></button>';
|
|
html += '</div>';
|
|
|
|
html += '<div class="bm-backup-list">';
|
|
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 += '<div class="bm-backup-item' + (state.preview === b.full_path ? ' active' : '') + '" data-path="' + escapeHtml(b.full_path) + '" data-vault="' + escapeHtml(b.vault) + '" data-file="' + escapeHtml(b.file) + '">';
|
|
html += '<span class="bm-backup-date">' + ds + '</span>';
|
|
html += '<span class="bm-backup-size">' + formatSize(b.size) + '</span>';
|
|
html += '<div class="bm-backup-actions">';
|
|
html += '<button class="bm-backup-preview" title="Apercu"><i data-lucide="eye" style="width:12px;height:12px"></i></button>';
|
|
html += '<button class="bm-backup-restore" title="Restaurer cette version"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button>';
|
|
html += '<button class="bm-backup-delete" data-path="' + escapeHtml(b.full_path) + '" title="Supprimer"><i data-lucide="x" style="width:12px;height:12px"></i></button>';
|
|
html += '</div>';
|
|
html += '</div>';
|
|
});
|
|
html += '</div>';
|
|
html += '</div>';
|
|
});
|
|
|
|
html += '</div>';
|
|
});
|
|
|
|
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;
|
|
}
|