/** * 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) { setBody(renderError('Failed to load backups')); 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); return overlay; } function setBody(html) { const panel = document.getElementById('backup-viewer-panel'); if (panel) panel.innerHTML = html; } function close() { if (state.overlay) { state.overlay.classList.remove('active'); setTimeout(() => { if (state.overlay) state.overlay.remove(); state.overlay = null; }, 200); } } // --------------------------------------------------------------------------- // Rendering // --------------------------------------------------------------------------- function renderLoading() { return `

Backups

Chargement des backups...

`; } function renderError(msg) { return `

Backups

${escapeHtml(msg)}

`; } function renderEmpty() { return `

Backups — ${escapeHtml(state.path)}

Aucun backup disponible pour ce fichier.

Les backups sont créés automatiquement avant chaque modification.

`; } function renderMainView() { const b = state.backups; const leftOpts = b.map(bk => { const sel = bk.timestamp === state.leftVersion ? 'selected' : ''; return ``; }).join(''); const rightOpts = [ ``, ...b.map(bk => { const sel = bk.timestamp === state.rightVersion ? 'selected' : ''; return ``; }), ].join(''); const html = `

Backups

${escapeHtml(state.path)}
${b.length} backup${b.length > 1 ? 's' : ''}
vs
`; setBody(html); // Bind events bindEvents(); safeCreateIcons(); } // --------------------------------------------------------------------------- // Event bindings // --------------------------------------------------------------------------- function bindEvents() { const panel = document.getElementById('backup-viewer-panel'); if (!panel) return; // Close button panel.querySelector('.backup-close-btn')?.addEventListener('click', close); // 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 = `
`; 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 = `
Erreur: ${escapeHtml(err?.detail || err?.message || 'Unknown')}
`; } } 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 = ' Restaurer'; safeCreateIcons(); } } } // --------------------------------------------------------------------------- // Diff rendering // --------------------------------------------------------------------------- function renderDiff(diffText) { if (!diffText) { return '
Aucune différence — les versions sont identiques.
'; } let html = '
'; 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 += ``; } html += '
${prefix}
${escapeHtml(line)}
'; 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; }