feat: backup management page + max backups config + auto-cleanup
- Config: max_backups_per_file (default 10) in config.json + UI field - Auto-cleanup: _backup_file deletes oldest backups when limit exceeded - New endpoints: GET /api/backups (list all), POST /api/backups/delete, POST /api/backups/purge (by file or vault) - New module: backup-manager.js - full-page view with filter, grouped by vault/file, individual delete, per-file purge, stats - Link from backup viewer footer: 'Gerer tous les backups'
This commit is contained in:
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Backup Manager module for ObsiGate.
|
||||
*
|
||||
* Provides a full-page view to browse, inspect, and delete all backups
|
||||
* across all vaults. Accessible from the backup/diff viewer.
|
||||
*/
|
||||
import { api } from './auth.js';
|
||||
import { showToast } from './ui.js';
|
||||
import { safeCreateIcons } from './utils.js';
|
||||
|
||||
var state = { backups: [], filter: '' };
|
||||
|
||||
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-list" id="bm-list">' +
|
||||
'<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>' +
|
||||
'</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);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Group by vault
|
||||
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) {
|
||||
html += '<div class="bm-vault-group">';
|
||||
html += '<h3 class="bm-vault-name"><i data-lucide="database" style="width:14px;height:14px"></i> ' + escapeHtml(vault) + ' (' + grouped[vault].length + ')</h3>';
|
||||
|
||||
// Group by file
|
||||
var byFile = {};
|
||||
grouped[vault].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:14px;height:14px"></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">';
|
||||
html += '<span class="bm-backup-date">' + ds + '</span>';
|
||||
html += '<span class="bm-backup-size">' + formatSize(b.size) + '</span>';
|
||||
html += '<button class="bm-backup-delete" data-path="' + escapeHtml(b.full_path) + '" title="Supprimer ce backup"><i data-lucide="x" style="width:12px;height:12px"></i></button>';
|
||||
html += '</div>';
|
||||
});
|
||||
html += '</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
list.innerHTML = html;
|
||||
safeCreateIcons();
|
||||
|
||||
// Bind delete buttons
|
||||
list.querySelectorAll('.bm-backup-delete').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
if (confirm('Supprimer ce backup ?')) {
|
||||
deleteBackups([btn.dataset.path]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Bind purge buttons
|
||||
list.querySelectorAll('.bm-file-purge').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
if (confirm('Supprimer TOUS les backups de "' + btn.dataset.file + '" ?')) {
|
||||
purgeFile(btn.dataset.vault, btn.dataset.file);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -213,6 +213,7 @@ function renderMainView() {
|
||||
|
||||
'<div class="backup-footer">' +
|
||||
'<span class="backup-footer-hint">Backups crees lors de la sauvegarde manuelle (Ctrl+S)</span>' +
|
||||
'<button class="backup-footer-manage-btn" id="backup-manage-btn">Gerer tous les backups</button>' +
|
||||
'</div>';
|
||||
|
||||
setBody(html);
|
||||
@@ -249,6 +250,14 @@ function bindEvents() {
|
||||
}
|
||||
|
||||
var modeBtn = document.getElementById('backup-diff-mode-btn');
|
||||
var manageBtn = document.getElementById('backup-manage-btn');
|
||||
if (manageBtn) {
|
||||
manageBtn.addEventListener('click', function() {
|
||||
close();
|
||||
import('./backup-manager.js').then(function(m) { m.openBackupManager(); });
|
||||
});
|
||||
}
|
||||
|
||||
if (modeBtn) {
|
||||
modeBtn.addEventListener('click', function() {
|
||||
state.diffMode = state.diffMode === 'sidebyside' ? 'unified' : 'sidebyside';
|
||||
|
||||
@@ -665,6 +665,7 @@ async function loadConfigFields() {
|
||||
_setField("cfg-tag-boost", data.tag_boost);
|
||||
_setField("cfg-prefix-exp", data.prefix_max_expansions);
|
||||
_setField("cfg-recent-limit", data.recent_files_limit || 20);
|
||||
_setField("cfg-max-backups", data.max_backups_per_file || 10);
|
||||
// Watcher config
|
||||
_setCheckbox("cfg-watcher-enabled", data.watcher_enabled !== false);
|
||||
_setCheckbox("cfg-watcher-polling", data.watcher_use_polling === true);
|
||||
@@ -716,6 +717,7 @@ async function saveBackendConfig() {
|
||||
tag_boost: _getFieldNum("cfg-tag-boost", 2.0),
|
||||
prefix_max_expansions: _getFieldNum("cfg-prefix-exp", 50),
|
||||
recent_files_limit: _getFieldNum("cfg-recent-limit", 20),
|
||||
max_backups_per_file: _getFieldNum("cfg-max-backups", 10),
|
||||
watcher_enabled: _getCheckbox("cfg-watcher-enabled"),
|
||||
watcher_use_polling: _getCheckbox("cfg-watcher-polling"),
|
||||
watcher_polling_interval: _getFieldNum("cfg-watcher-interval", 5.0),
|
||||
|
||||
Reference in New Issue
Block a user