diff --git a/backend/indexer.py b/backend/indexer.py index 28c2030..e600b21 100644 --- a/backend/indexer.py +++ b/backend/indexer.py @@ -426,6 +426,80 @@ async def reload_index() -> Dict[str, Any]: return stats +async def reload_single_vault(vault_name: str) -> Dict[str, Any]: + """Force a re-index of a single vault and return its statistics. + + Args: + vault_name: Name of the vault to reindex. + + Returns: + Dict with vault statistics (file_count, tag_count). + + Raises: + ValueError: If vault_name is not found in configuration. + """ + global vault_config + + # Reload vault config from env vars + vault_config.update(load_vault_config()) + + # Merge with saved settings + from backend.vault_settings import get_vault_setting + saved_settings = get_vault_setting(vault_name) + + if vault_name not in vault_config: + raise ValueError(f"Vault '{vault_name}' not found in configuration") + + config = vault_config[vault_name] + + # Override with saved settings if present + if saved_settings: + if "includeHidden" in saved_settings: + config["includeHidden"] = saved_settings["includeHidden"] + if "hiddenWhitelist" in saved_settings: + config["hiddenWhitelist"] = saved_settings["hiddenWhitelist"] + + # Remove old vault data from index structures + await remove_vault_from_index(vault_name) + + # Re-add the vault with updated configuration + vault_path = config["path"] + loop = asyncio.get_event_loop() + vault_data = await loop.run_in_executor(None, _scan_vault, vault_name, vault_path, config) + vault_data["config"] = config + + # Build lookup entries for the vault + new_lookup_entries: Dict[str, List[Dict[str, str]]] = {} + for f in vault_data["files"]: + entry = {"vault": vault_name, "path": f["path"]} + fname = f["path"].rsplit("/", 1)[-1].lower() + fpath_lower = f["path"].lower() + for key in (fname, fpath_lower): + if key not in new_lookup_entries: + new_lookup_entries[key] = [] + new_lookup_entries[key].append(entry) + + async_lock = _get_async_lock() + async with async_lock: + with _index_lock: + index[vault_name] = vault_data + for key, entries in new_lookup_entries.items(): + if key not in _file_lookup: + _file_lookup[key] = [] + _file_lookup[key].extend(entries) + path_index[vault_name] = vault_data.get("paths", []) + global _index_generation + _index_generation += 1 + + # Rebuild attachment index for this vault only + from backend.attachment_indexer import build_attachment_index + await build_attachment_index({vault_name: config}) + + stats = {"file_count": len(vault_data["files"]), "tag_count": len(vault_data["tags"])} + logger.info(f"Vault '{vault_name}' reindexed: {stats['file_count']} files, {stats['tag_count']} tags") + return stats + + def get_vault_names() -> List[str]: """Return the list of all indexed vault names.""" return list(index.keys()) diff --git a/backend/main.py b/backend/main.py index c68982c..904f1e5 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1198,6 +1198,28 @@ async def api_reload(current_user=Depends(require_admin)): return {"status": "ok", "vaults": stats} +@app.get("/api/index/reload/{vault_name}") +async def api_reload_vault(vault_name: str, current_user=Depends(require_admin)): + """Force a re-index of a single vault. + + Args: + vault_name: Name of the vault to reindex. + + Returns: + Dict with vault statistics. + """ + try: + from backend.indexer import reload_single_vault + stats = await reload_single_vault(vault_name) + await sse_manager.broadcast("vault_reloaded", { + "vault": vault_name, + "stats": stats, + }) + return {"status": "ok", "vault": vault_name, "stats": stats} + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + # --------------------------------------------------------------------------- # SSE endpoint — Server-Sent Events stream # --------------------------------------------------------------------------- diff --git a/backend/utils.py b/backend/utils.py index 18268bc..ab20833 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -5,6 +5,12 @@ from typing import Dict, Any, Tuple def should_include_path(rel_parts: Tuple[str, ...], vault_config: Dict[str, Any]) -> bool: """Check if a path should be included based on hidden files configuration. + Simplified logic: + - If includeHidden=True: include ALL hidden files/folders recursively + - If includeHidden=False: check whitelist + - If a whitelisted folder is in the path, include ALL sub-hidden files/folders + - Otherwise, exclude the path + Args: rel_parts: Tuple of path parts relative to vault root vault_config: Vault configuration dict with includeHidden and hiddenWhitelist @@ -23,14 +29,14 @@ def should_include_path(rel_parts: Tuple[str, ...], vault_config: Dict[str, Any] return True if include_hidden: - # Include all hidden files/folders + # Include all hidden files/folders recursively return True - # Check if ALL hidden parts are in the whitelist - # If any hidden part is NOT in the whitelist, exclude the path - for hidden_part in hidden_parts: - if hidden_part not in hidden_whitelist: - return False + # Check if the FIRST hidden part is in the whitelist + # If yes, include all sub-hidden files/folders automatically + first_hidden = hidden_parts[0] + if first_hidden in hidden_whitelist: + return True - # All hidden parts are in the whitelist - return True + # First hidden part not in whitelist, exclude + return False diff --git a/frontend/app.js b/frontend/app.js index d8bd85d..b17992a 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -3231,6 +3231,9 @@ // --- Hidden Files Configuration --- + // Track which vaults have been modified + let modifiedVaults = new Set(); + async function loadHiddenFilesSettings() { const container = document.getElementById("hidden-files-vault-list"); if (!container) return; @@ -3290,7 +3293,7 @@ document.createTextNode("Liste blanche") ]); const whitelistHint = el("span", { class: "config-hint", style: "display:block;margin-bottom:8px" }, [ - document.createTextNode("Dossiers cachés spécifiques à afficher (ex: .obsidian, .github)") + document.createTextNode("Dossiers cachés spécifiques à afficher (tous les sous-dossiers cachés seront inclus)") ]); const whitelistItems = el("div", { class: "hidden-files-whitelist-items", id: `whitelist-items-${vault.name}` }); @@ -3330,10 +3333,20 @@ // Event listeners const addBtn = addRow.querySelector("button"); const input = addRow.querySelector("input"); + const checkbox = toggleRow.querySelector("input[type='checkbox']"); - addBtn.addEventListener("click", () => addWhitelistItem(vault.name)); + addBtn.addEventListener("click", () => { + addWhitelistItem(vault.name); + modifiedVaults.add(vault.name); + }); input.addEventListener("keypress", (e) => { - if (e.key === "Enter") addWhitelistItem(vault.name); + if (e.key === "Enter") { + addWhitelistItem(vault.name); + modifiedVaults.add(vault.name); + } + }); + checkbox.addEventListener("change", () => { + modifiedVaults.add(vault.name); }); }); } @@ -3353,6 +3366,7 @@ const removeBtn = item.querySelector("button"); removeBtn.addEventListener("click", () => { item.remove(); + modifiedVaults.add(vaultName); }); return item; @@ -3361,6 +3375,7 @@ function addWhitelistItem(vaultName) { const input = document.getElementById(`whitelist-input-${vaultName}`); const container = document.getElementById(`whitelist-items-${vaultName}`); + const includeHiddenCheckbox = document.getElementById(`hidden-include-${vaultName}`); if (!input || !container) return; @@ -3378,6 +3393,11 @@ return; } + // Auto-uncheck "Afficher tous les fichiers cachés" when adding to whitelist + if (includeHiddenCheckbox && includeHiddenCheckbox.checked) { + includeHiddenCheckbox.checked = false; + } + const item = createWhitelistItem(vaultName, normalizedValue); container.appendChild(item); input.value = ""; @@ -3414,6 +3434,7 @@ }); await Promise.all(promises); + modifiedVaults.clear(); showToast("✓ Paramètres sauvegardés", "success"); } catch (err) { console.error("Failed to save hidden files settings:", err); @@ -3434,9 +3455,18 @@ // Update button text to show reindexing phase if (btn) { btn.textContent = "⏳ Réindexation..."; } - // Then trigger reindex with saved settings - await api("/api/index/reload"); - showToast("✓ Réindexation terminée", "success"); + // Reindex only modified vaults if any, otherwise reindex all + if (modifiedVaults.size > 0) { + const vaultsToReindex = Array.from(modifiedVaults); + for (const vaultName of vaultsToReindex) { + await api(`/api/index/reload/${encodeURIComponent(vaultName)}`); + } + showToast(`✓ Réindexation terminée (${vaultsToReindex.length} vault${vaultsToReindex.length > 1 ? 's' : ''})`, "success"); + } else { + await api("/api/index/reload"); + showToast("✓ Réindexation terminée", "success"); + } + loadDiagnostics(); await Promise.all([loadVaults(), loadTags()]); } catch (err) {