feat: add per-vault reindexing with selective reload based on modified vaults, simplify hidden files whitelist logic to include all sub-hidden files when parent is whitelisted, and auto-uncheck includeHidden when adding whitelist items

This commit is contained in:
Bruno Charest 2026-03-26 15:08:01 -04:00
parent 80e2a7fc53
commit ac962bd416
4 changed files with 146 additions and 14 deletions

View File

@ -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())

View File

@ -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
# ---------------------------------------------------------------------------

View File

@ -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

View File

@ -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) {