Files
ObsiGate/frontend/js/backups.js
T
bruno 247d78374c
CI / lint (push) Failing after 8s
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / security (push) Successful in 9s
fix: import getFileIcon from utils.js + close button re-bind after setBody
- vaulthome.js: getFileIcon was imported from viewer.js but lives in utils.js
- backups.js: setBody() now re-binds close button handler after innerHTML replacement
2026-06-04 14:58:07 -04:00

402 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Backup & Diff viewer module for ObsiGate.
*
* Provides a modal UI to browse file backups, compare versions with
* a unified diff view, and restore previous versions.
*
* Usage:
* import { openBackupViewer } from './backups.js';
* openBackupViewer(vaultName, filePath);
*/
import { api } from './auth.js';
import { showToast } from './ui.js';
import { refreshSidebarTreePreservingState } from './sidebar.js';
import { safeCreateIcons } from './utils.js';
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const state = {
vault: null,
path: null,
backups: [],
overlay: null,
leftVersion: null, // timestamp of backup shown on left
rightVersion: null, // timestamp of backup shown on right, or null for current
diffText: '',
};
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Open the backup/diff viewer for a file.
* @param {string} vault - Vault name
* @param {string} path - Relative file path
*/
export async function openBackupViewer(vault, path) {
state.vault = vault;
state.path = path;
state.backups = [];
state.leftVersion = null;
state.rightVersion = null;
state.diffText = '';
// Create and show overlay
const overlay = createOverlay();
document.body.appendChild(overlay);
state.overlay = overlay;
requestAnimationFrame(() => overlay.classList.add('active'));
// Show loading state
setBody(renderLoading());
// Fetch backups
try {
const data = await api(`/api/file/${encodeURIComponent(vault)}/backups?path=${encodeURIComponent(path)}`);
state.backups = data.backups || [];
} catch (err) {
const msg = err?.detail || err?.message || 'Erreur inconnue';
setBody(renderError(`Erreur API: ${msg}`));
return;
}
if (state.backups.length === 0) {
setBody(renderEmpty());
return;
}
// Auto-select: left = most recent, right = current
state.leftVersion = state.backups[0].timestamp;
state.rightVersion = null;
// Render version selectors
renderMainView();
// Load initial diff
await loadDiff();
}
// ---------------------------------------------------------------------------
// DOM helpers
// ---------------------------------------------------------------------------
function createOverlay() {
const overlay = document.createElement('div');
overlay.className = 'obsigate-modal-overlay backup-viewer-overlay';
overlay.addEventListener('click', (e) => {
if (e.target === overlay) close();
});
const panel = document.createElement('div');
panel.className = 'backup-viewer-panel';
panel.id = 'backup-viewer-panel';
overlay.appendChild(panel);
// Event delegation fallback for close button (in case inline onclick fails)
panel.addEventListener('click', (e) => {
// e.target might be a text node (e.g. "×") — use parentElement as fallback
const target = e.target.nodeType === Node.TEXT_NODE ? e.target.parentElement : e.target;
const closeBtn = target && target.closest ? target.closest('.backup-close-btn') : null;
if (closeBtn) {
e.stopPropagation();
close();
}
});
return overlay;
}
function setBody(html) {
const panel = document.getElementById('backup-viewer-panel');
if (!panel) return;
panel.innerHTML = html;
// Re-bind close button after innerHTML replacement
const closeBtn = panel.querySelector('.backup-close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.stopPropagation();
close();
});
}
}
function close() {
if (state.overlay) {
state.overlay.classList.remove('active');
setTimeout(() => {
if (state.overlay) state.overlay.remove();
state.overlay = null;
}, 200);
}
}
// Expose globally for inline onclick handlers (bypasses text-node target issues)
window._backupViewerClose = close;
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
function renderLoading() {
return `
<div class="backup-header">
<h3>Backups</h3>
<button class="backup-close-btn" title="Fermer" onclick="window._backupViewerClose()">&times;</button>
</div>
<div class="backup-body">
<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>
<p style="margin-top:0.75rem;color:var(--text-muted);font-size:0.85rem">Chargement des backups...</p>
</div>
</div>`;
}
function renderError(msg) {
return `
<div class="backup-header">
<h3>Backups</h3>
<button class="backup-close-btn" title="Fermer">&times;</button>
</div>
<div class="backup-body" style="text-align:center;padding:2rem;color:var(--text-muted)">
<p>${escapeHtml(msg)}</p>
</div>`;
}
function renderEmpty() {
return \`
<div class="backup-header">
<h3>Backups — \${escapeHtml(state.path)}</h3>
<button class="backup-close-btn" title="Fermer" onclick="window._backupViewerClose()">&times;</button>
</div>
<div class="backup-body" style="text-align:center;padding:2rem;color:var(--text-muted)">
<p>Aucun backup disponible pour ce fichier.</p>
<p style="font-size:0.8rem;margin-top:0.5rem">Les backups sont créés automatiquement avant chaque modification.</p>
</div>`;
}
function renderMainView() {
const b = state.backups;
const leftOpts = b.map(bk => {
const sel = bk.timestamp === state.leftVersion ? 'selected' : '';
return `<option value="${bk.timestamp}" ${sel}>${formatTimestamp(bk.timestamp)} (${formatSize(bk.size)})</option>`;
}).join('');
const rightOpts = [
`<option value="current" ${state.rightVersion === null ? 'selected' : ''}>Version actuelle</option>`,
...b.map(bk => {
const sel = bk.timestamp === state.rightVersion ? 'selected' : '';
return `<option value="${bk.timestamp}" ${sel}>${formatTimestamp(bk.timestamp)} (${formatSize(bk.size)})</option>`;
}),
].join('');
const html = `
<div class="backup-header">
<div class="backup-header-left">
<h3>Backups</h3>
<span class="backup-file-path">${escapeHtml(state.path)}</span>
</div>
<div class="backup-header-right">
<span class="backup-count">${b.length} backup${b.length > 1 ? 's' : ''}</span>
<button class="backup-close-btn" title="Fermer" onclick="window._backupViewerClose()">&times;</button>
</div>
</div>
<div class="backup-toolbar">
<div class="backup-selector-group">
<label>Comparer :</label>
<select class="backup-select" id="backup-left-select">${leftOpts}</select>
</div>
<span class="backup-vs-label">vs</span>
<div class="backup-selector-group">
<select class="backup-select" id="backup-right-select">${rightOpts}</select>
</div>
<button class="backup-restore-btn" id="backup-restore-btn" title="Restaurer la version de gauche">
<i data-lucide="rotate-ccw" class="icon-sm"></i> Restaurer
</button>
</div>
<div class="backup-diff-container" id="backup-diff-container">
<div class="backup-diff-loading">
<div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div>
</div>
</div>
<div class="backup-footer">
<span class="backup-footer-hint">Backups automatiques avant chaque modification</span>
</div>`;
setBody(html);
// Bind events
bindEvents();
safeCreateIcons();
}
// ---------------------------------------------------------------------------
// Event bindings
// ---------------------------------------------------------------------------
function bindEvents() {
const panel = document.getElementById('backup-viewer-panel');
if (!panel) return;
// Left select
const leftSelect = document.getElementById('backup-left-select');
leftSelect?.addEventListener('change', async () => {
state.leftVersion = parseInt(leftSelect.value);
await loadDiff();
});
// Right select
const rightSelect = document.getElementById('backup-right-select');
rightSelect?.addEventListener('change', async () => {
const val = rightSelect.value;
state.rightVersion = val === 'current' ? null : parseInt(val);
await loadDiff();
});
// Restore button
const restoreBtn = document.getElementById('backup-restore-btn');
restoreBtn?.addEventListener('click', async () => {
await restoreBackup();
});
}
// ---------------------------------------------------------------------------
// Data loading
// ---------------------------------------------------------------------------
async function loadDiff() {
const container = document.getElementById('backup-diff-container');
if (!container) return;
container.innerHTML = `
<div class="backup-diff-loading">
<div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div>
</div>`;
let url = `/api/file/${encodeURIComponent(state.vault)}/diff?path=${encodeURIComponent(state.path)}&version=${state.leftVersion}`;
if (state.rightVersion !== null) {
url += `&compare_with=${state.rightVersion}`;
}
try {
const data = await api(url);
state.diffText = data.diff || '';
container.innerHTML = renderDiff(state.diffText);
} catch (err) {
container.innerHTML = `<div class="backup-diff-error">Erreur: ${escapeHtml(err?.detail || err?.message || 'Unknown')}</div>`;
}
}
async function restoreBackup() {
if (!state.leftVersion) return;
const confirmed = confirm(
`Restaurer "${state.path}" à la version du ${formatTimestamp(state.leftVersion)} ?\n\n` +
`La version actuelle sera sauvegardée avant la restauration.`
);
if (!confirmed) return;
const restoreBtn = document.getElementById('backup-restore-btn');
if (restoreBtn) {
restoreBtn.disabled = true;
restoreBtn.textContent = 'Restauration...';
}
try {
const resp = await api(
`/api/file/${encodeURIComponent(state.vault)}/restore?path=${encodeURIComponent(state.path)}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version: state.leftVersion }),
}
);
if (resp && resp.success) {
showToast('Fichier restauré avec succès', 'success');
// Refresh sidebar and close
await refreshSidebarTreePreservingState();
close();
// Reload current file if open
if (typeof TabManager !== 'undefined' && TabManager.reloadCurrent) {
TabManager.reloadCurrent();
}
} else {
showToast('Échec de la restauration', 'error');
}
} catch (err) {
showToast(`Erreur: ${err?.detail || err?.message || 'Unknown'}`, 'error');
} finally {
if (restoreBtn) {
restoreBtn.disabled = false;
restoreBtn.innerHTML = '<i data-lucide="rotate-ccw" class="icon-sm"></i> Restaurer';
safeCreateIcons();
}
}
}
// ---------------------------------------------------------------------------
// Diff rendering
// ---------------------------------------------------------------------------
function renderDiff(diffText) {
if (!diffText) {
return '<div class="backup-diff-empty">Aucune différence — les versions sont identiques.</div>';
}
let html = '<div class="backup-diff-output"><table class="backup-diff-table">';
const lines = diffText.split('\n');
for (const line of lines) {
let cssClass = 'diff-context';
let prefix = ' ';
if (line.startsWith('@@')) {
cssClass = 'diff-hunk';
prefix = '';
} else if (line.startsWith('+')) {
cssClass = 'diff-add';
prefix = '+';
} else if (line.startsWith('-')) {
cssClass = 'diff-del';
prefix = '-';
} else if (line.startsWith('---') || line.startsWith('+++')) {
cssClass = 'diff-header-line';
prefix = '';
}
html += `<tr class="${cssClass}"><td class="diff-prefix">${prefix}</td><td class="diff-content"><pre>${escapeHtml(line)}</pre></td></tr>`;
}
html += '</table></div>';
return html;
}
// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------
function formatTimestamp(ts) {
const d = new Date(ts * 1000);
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
}
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 escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}