From bc8083fd6bd2473a6360326c94fa69e9e1e2d5ff Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Thu, 4 Jun 2026 15:22:02 -0400 Subject: [PATCH] fix: backup storage relative to vault parent + global capture click handler for close button - _backup_file: resolve relative backup path against vault parent (survives CWD changes) - _get_backup_root: same resolution logic for listing - backups.js: global document-level capture-phase click handler for .backup-close-btn (works regardless of innerHTML replacements, text node targets, or event bubbling) --- backend/main.py | 35 ++++++++++++++++++++++++++++++----- frontend/js/backups.js | 42 ++++++++++++++++-------------------------- 2 files changed, 46 insertions(+), 31 deletions(-) diff --git a/backend/main.py b/backend/main.py index 509e273..0f7c551 100644 --- a/backend/main.py +++ b/backend/main.py @@ -715,22 +715,29 @@ def _resolve_safe_path(vault_root: Path, relative_path: str) -> Path: def _backup_file(file_path: Path, vault_name: str, relative_path: str): """Create a timestamped backup of a file before modification. - Backups are stored in .obsigate-backup/{vault}/{relative_path}.{timestamp}.bak + Backups are stored in {backup_root}/{vault}/{relative_path}.{timestamp}.bak Silently skips if the file doesn't exist or can't be read. """ try: if not file_path.exists() or not file_path.is_file(): + logger.debug(f"Backup skipped: file not found {file_path}") return backup_root = Path(os.environ.get("OBSIGATE_BACKUP_DIR", ".obsigate-backup")) + # Resolve relative to vault root for consistency + if not backup_root.is_absolute(): + vault_data = get_vault_data(vault_name) + if vault_data: + vault_root_path = Path(vault_data["path"]) + backup_root = vault_root_path.parent / backup_root backup_dir = backup_root / vault_name / Path(relative_path).parent backup_dir.mkdir(parents=True, exist_ok=True) timestamp = int(time.time()) backup_name = f"{file_path.name}.{timestamp}.bak" backup_path = backup_dir / backup_name shutil.copy2(file_path, backup_path) - logger.debug(f"Backed up {relative_path} to {backup_path}") + logger.info(f"Backup created: {relative_path} -> {backup_path}") except Exception as e: - logger.warning(f"Failed to backup {relative_path}: {e}") + logger.warning(f"Failed to backup {relative_path} (vault={vault_name}): {e}", exc_info=True) def _check_vault_writable(vault_root: Path) -> bool: @@ -1928,8 +1935,26 @@ async def api_file_move( # --------------------------------------------------------------------------- def _get_backup_root() -> Path: - """Return the backup root directory as configured.""" - return Path(os.environ.get("OBSIGATE_BACKUP_DIR", ".obsigate-backup")) + """Return the backup root directory as configured. + + If the configured path is relative, it is resolved relative to the + first vault's parent directory for consistency with _backup_file. + """ + root = Path(os.environ.get("OBSIGATE_BACKUP_DIR", ".obsigate-backup")) + if not root.is_absolute(): + # Resolve relative to the first vault's parent so backups survive CWD changes + try: + from backend.indexer import get_vault_data as _get_vd + vaults = index # use the module-level index + if vaults: + first_name = next(iter(vaults.keys())) + vd = _get_vd(first_name) + if vd: + vault_parent = Path(vd["path"]).parent + root = vault_parent / root + except Exception: + pass # fall back to relative + return root def _get_backup_dir(vault_name: str, relative_path: str) -> Path: diff --git a/frontend/js/backups.js b/frontend/js/backups.js index d579f15..f6e00cf 100644 --- a/frontend/js/backups.js +++ b/frontend/js/backups.js @@ -84,6 +84,16 @@ export async function openBackupViewer(vault, path) { // DOM helpers // --------------------------------------------------------------------------- +// Global click handler: catches close button clicks anywhere in the document +document.addEventListener('click', function(e) { + const btn = e.target.closest ? e.target.closest('.backup-close-btn') : null; + if (btn && state.overlay) { + e.stopPropagation(); + e.preventDefault(); + close(); + } +}, true); // capture phase — fires before other handlers + function createOverlay() { const overlay = document.createElement('div'); overlay.className = 'obsigate-modal-overlay backup-viewer-overlay'; @@ -91,6 +101,11 @@ function createOverlay() { if (e.target === overlay) close(); }); + const panel = document.createElement('div'); + panel.className = 'backup-viewer-panel'; + panel.id = 'backup-viewer-panel'; + overlay.appendChild(panel); + // Keyboard: Escape closes the viewer _escHandler = (e) => { if (e.key === 'Escape' && state.overlay) { @@ -99,37 +114,12 @@ function createOverlay() { }; document.addEventListener('keydown', _escHandler); - 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(); - }); - } + if (panel) panel.innerHTML = html; } let _escHandler = null;