diff --git a/HIDDEN_FILES_GUIDE.md b/HIDDEN_FILES_GUIDE.md new file mode 100644 index 0000000..d0259c1 --- /dev/null +++ b/HIDDEN_FILES_GUIDE.md @@ -0,0 +1,154 @@ +# Guide de configuration des fichiers et dossiers cachés + +## Vue d'ensemble + +ObsiGate prend désormais en charge les fichiers et dossiers cachés (ceux qui commencent par un point, comme `.obsidian`, `.git`, etc.) avec une configuration flexible par vault. + +## Fonctionnalités + +### 1. **Activation globale par vault** +Vous pouvez activer l'indexation de TOUS les fichiers cachés pour un vault spécifique. + +### 2. **Liste blanche flexible** +Ajoutez des dossiers cachés individuels à une liste blanche, même si l'activation globale est désactivée. + +### 3. **Configuration persistante** +Les paramètres sont sauvegardés et persistent entre les redémarrages. + +## Configuration + +### Via variables d'environnement + +Ajoutez ces variables à votre configuration Docker ou `.env` : + +```bash +# Pour activer tous les fichiers cachés dans un vault +VAULT_1_INCLUDE_HIDDEN=true + +# Pour ajouter des dossiers spécifiques à la liste blanche +VAULT_1_HIDDEN_WHITELIST=.obsidian,.github,.vscode + +# Exemple pour un deuxième vault +VAULT_2_INCLUDE_HIDDEN=false +VAULT_2_HIDDEN_WHITELIST=.obsidian +``` + +### Via l'interface web + +1. Ouvrez le menu **Options** (icône d'engrenage) +2. Cliquez sur **Configurations** +3. Faites défiler jusqu'à la section **📁 Fichiers et dossiers cachés** +4. Pour chaque vault, vous pouvez : + - **Activer/désactiver** l'inclusion de tous les fichiers cachés + - **Ajouter des dossiers** à la liste blanche individuellement + - **Supprimer des dossiers** de la liste blanche + +5. Cliquez sur **Sauvegarder les paramètres** +6. Cliquez sur **Réindexer avec nouveaux paramètres** pour appliquer les changements + +## Exemples d'utilisation + +### Cas 1 : Indexer uniquement le dossier .obsidian + +```bash +VAULT_1_INCLUDE_HIDDEN=false +VAULT_1_HIDDEN_WHITELIST=.obsidian +``` + +Ou via l'interface : +- Désactiver "Inclure tous les fichiers cachés" +- Ajouter `.obsidian` à la liste blanche + +### Cas 2 : Indexer tous les fichiers cachés + +```bash +VAULT_1_INCLUDE_HIDDEN=true +``` + +Ou via l'interface : +- Activer "Inclure tous les fichiers cachés" + +### Cas 3 : Indexer plusieurs dossiers cachés spécifiques + +```bash +VAULT_1_INCLUDE_HIDDEN=false +VAULT_1_HIDDEN_WHITELIST=.obsidian,.github,.vscode +``` + +Ou via l'interface : +- Désactiver "Inclure tous les fichiers cachés" +- Ajouter `.obsidian`, `.github`, `.vscode` à la liste blanche + +## API Endpoints + +### Obtenir les paramètres d'un vault + +```http +GET /api/vaults/{vault_name}/settings +``` + +Réponse : +```json +{ + "includeHidden": false, + "hiddenWhitelist": [".obsidian", ".github"] +} +``` + +### Mettre à jour les paramètres d'un vault + +```http +POST /api/vaults/{vault_name}/settings +Content-Type: application/json + +{ + "includeHidden": true, + "hiddenWhitelist": [".obsidian"] +} +``` + +### Obtenir les paramètres de tous les vaults + +```http +GET /api/vaults/settings/all +``` + +## Architecture technique + +### Backend + +- **`backend/indexer.py`** : Fonction `_should_include_path()` qui vérifie si un chemin doit être inclus +- **`backend/vault_settings.py`** : Gestion de la persistance des paramètres +- **`backend/main.py`** : Endpoints API pour gérer les paramètres +- **`backend/attachment_indexer.py`** : Respect des paramètres pour les pièces jointes + +### Frontend + +- **`frontend/index.html`** : Section de configuration dans le modal +- **`frontend/app.js`** : Fonctions `loadHiddenFilesSettings()`, `saveHiddenFilesSettings()`, etc. +- **`frontend/style.css`** : Styles pour l'interface de configuration + +## Notes importantes + +1. **Réindexation requise** : Après modification des paramètres, une réindexation est nécessaire pour appliquer les changements +2. **Persistance** : Les paramètres sont sauvegardés dans `/app/data/vault_settings.json` +3. **Priorité** : Les variables d'environnement sont chargées au démarrage, mais peuvent être écrasées via l'interface web +4. **Performance** : L'activation de tous les fichiers cachés peut augmenter le temps d'indexation selon le nombre de fichiers + +## Dépannage + +### Les fichiers cachés n'apparaissent pas après activation + +1. Vérifiez que les paramètres sont bien sauvegardés +2. Déclenchez une réindexation manuelle +3. Vérifiez les logs du serveur pour d'éventuelles erreurs + +### Les paramètres ne persistent pas + +1. Vérifiez que le dossier `/app/data/` est accessible en écriture +2. Vérifiez les permissions du fichier `vault_settings.json` +3. Consultez les logs pour les erreurs de sauvegarde + +### Conflit entre variables d'environnement et interface web + +Les paramètres de l'interface web écrasent les variables d'environnement. Pour revenir aux variables d'environnement, supprimez le fichier `vault_settings.json` et redémarrez. diff --git a/backend/attachment_indexer.py b/backend/attachment_indexer.py index 86bf411..2d36d20 100644 --- a/backend/attachment_indexer.py +++ b/backend/attachment_indexer.py @@ -1,8 +1,10 @@ import asyncio import logging -import threading from pathlib import Path from typing import Dict, List, Optional, Set +import threading + +from backend.indexer import _should_include_path logger = logging.getLogger("obsigate.attachment_indexer") @@ -34,7 +36,7 @@ def clear_resolution_cache(vault_name: Optional[str] = None) -> None: del _resolution_cache[key] -def _scan_vault_attachments(vault_name: str, vault_path: str) -> Dict[str, List[Path]]: +def _scan_vault_attachments(vault_name: str, vault_path: str, vault_cfg: dict = None) -> Dict[str, List[Path]]: """Synchronously scan a vault directory for image attachments. Walks the vault tree and builds a filename -> absolute path mapping @@ -43,6 +45,7 @@ def _scan_vault_attachments(vault_name: str, vault_path: str) -> Dict[str, List[ Args: vault_name: Display name of the vault. vault_path: Absolute filesystem path to the vault root. + vault_cfg: Optional vault configuration dict with hidden files settings. Returns: Dict mapping lowercase filenames to lists of absolute paths. @@ -50,6 +53,10 @@ def _scan_vault_attachments(vault_name: str, vault_path: str) -> Dict[str, List[ vault_root = Path(vault_path) index: Dict[str, List[Path]] = {} + # Default config if not provided + if vault_cfg is None: + vault_cfg = {"includeHidden": False, "hiddenWhitelist": []} + if not vault_root.exists(): logger.warning(f"Vault path does not exist for attachment scan: {vault_path}") return index @@ -58,9 +65,9 @@ def _scan_vault_attachments(vault_name: str, vault_path: str) -> Dict[str, List[ try: for fpath in vault_root.rglob("*"): - # Skip hidden files and directories + # Check if path should be included based on hidden files configuration rel_parts = fpath.relative_to(vault_root).parts - if any(part.startswith(".") for part in rel_parts): + if not _should_include_path(rel_parts, vault_cfg): continue # Only process files @@ -121,7 +128,7 @@ async def build_attachment_index(vault_config: Dict[str, Dict[str, any]]) -> Non new_index[name] = {} continue - tasks.append((name, loop.run_in_executor(None, _scan_vault_attachments, name, vault_path))) + tasks.append((name, loop.run_in_executor(None, _scan_vault_attachments, name, vault_path, config))) for name, task in tasks: new_index[name] = await task @@ -136,18 +143,19 @@ async def build_attachment_index(vault_config: Dict[str, Dict[str, any]]) -> Non logger.info(f"Attachment index built: {len(attachment_index)} vaults, {total_attachments} total attachments") -async def rescan_vault_attachments(vault_name: str, vault_path: str) -> int: +async def rescan_vault_attachments(vault_name: str, vault_path: str, vault_cfg: dict = None) -> int: """Rescan attachments for a single vault. Args: vault_name: Name of the vault to rescan. vault_path: Absolute path to the vault root. + vault_cfg: Optional vault configuration dict with hidden files settings. Returns: Number of attachments indexed. """ loop = asyncio.get_event_loop() - new_vault_index = await loop.run_in_executor(None, _scan_vault_attachments, vault_name, vault_path) + new_vault_index = await loop.run_in_executor(None, _scan_vault_attachments, vault_name, vault_path, vault_cfg) with _attachment_lock: attachment_index[vault_name] = new_vault_index diff --git a/backend/indexer.py b/backend/indexer.py index 9a72d5c..4c7ff11 100644 --- a/backend/indexer.py +++ b/backend/indexer.py @@ -61,12 +61,16 @@ def load_vault_config() -> Dict[str, Dict[str, Any]]: Also reads optional configuration: - VAULT_N_ATTACHMENTS_PATH: relative path to attachments folder - VAULT_N_SCAN_ATTACHMENTS: "true"/"false" to enable/disable scanning + - VAULT_N_INCLUDE_HIDDEN: "true"/"false" to include all hidden files/folders + - VAULT_N_HIDDEN_WHITELIST: comma-separated list of hidden paths to include (e.g., ".obsidian,.github") Returns: Dict mapping vault names to configuration dicts with keys: - path: filesystem path (required) - attachmentsPath: relative attachments folder (optional) - scanAttachmentsOnStartup: boolean (default True) + - includeHidden: boolean (default False) - include all hidden files/folders + - hiddenWhitelist: list of hidden paths to include even if includeHidden is False - type: "VAULT" or "DIR" """ vaults: Dict[str, Dict[str, Any]] = {} @@ -80,11 +84,16 @@ def load_vault_config() -> Dict[str, Dict[str, Any]]: # Optional configuration attachments_path = os.environ.get(f"VAULT_{n}_ATTACHMENTS_PATH") scan_attachments = os.environ.get(f"VAULT_{n}_SCAN_ATTACHMENTS", "true").lower() == "true" + include_hidden = os.environ.get(f"VAULT_{n}_INCLUDE_HIDDEN", "false").lower() == "true" + hidden_whitelist_str = os.environ.get(f"VAULT_{n}_HIDDEN_WHITELIST", "") + hidden_whitelist = [item.strip() for item in hidden_whitelist_str.split(",") if item.strip()] vaults[name] = { "path": path, "attachmentsPath": attachments_path, "scanAttachmentsOnStartup": scan_attachments, + "includeHidden": include_hidden, + "hiddenWhitelist": hidden_whitelist, "type": "VAULT" } n += 1 @@ -96,10 +105,16 @@ def load_vault_config() -> Dict[str, Dict[str, Any]]: if not name or not path: break + include_hidden = os.environ.get(f"DIR_{n}_INCLUDE_HIDDEN", "false").lower() == "true" + hidden_whitelist_str = os.environ.get(f"DIR_{n}_HIDDEN_WHITELIST", "") + hidden_whitelist = [item.strip() for item in hidden_whitelist_str.split(",") if item.strip()] + vaults[name] = { "path": path, "attachmentsPath": None, "scanAttachmentsOnStartup": False, + "includeHidden": include_hidden, + "hiddenWhitelist": hidden_whitelist, "type": "DIR" } n += 1 @@ -107,15 +122,49 @@ def load_vault_config() -> Dict[str, Dict[str, Any]]: return vaults +def _should_include_path(rel_parts: tuple, vault_config: Dict[str, Any]) -> bool: + """Check if a path should be included based on hidden files configuration. + + Args: + rel_parts: Tuple of path parts relative to vault root + vault_config: Vault configuration dict with includeHidden and hiddenWhitelist + + Returns: + True if the path should be included, False otherwise + """ + include_hidden = vault_config.get("includeHidden", False) + hidden_whitelist = vault_config.get("hiddenWhitelist", []) + + # Check if any part of the path starts with a dot (hidden) + hidden_parts = [part for part in rel_parts if part.startswith(".")] + + if not hidden_parts: + # No hidden parts, always include + return True + + if include_hidden: + # Include all hidden files/folders + return True + + # Check if any hidden part is in the whitelist + for hidden_part in hidden_parts: + if hidden_part in hidden_whitelist: + return True + + # Not in whitelist and includeHidden is False + return False + + # Regex for extracting inline #tags from markdown body (excludes code blocks) _INLINE_TAG_RE = re.compile(r'(?:^|\s)#([a-zA-Z][a-zA-Z0-9_/-]{1,50})', re.MULTILINE) # Regex patterns for stripping code blocks before inline tag extraction -_CODE_BLOCK_RE = re.compile(r'```.*?```', re.DOTALL) +_CODE_BLOCK_RE = re.compile(r'```[\s\S]*?```', re.MULTILINE) _INLINE_CODE_RE = re.compile(r'`[^`]+`') def _extract_tags(post: frontmatter.Post) -> List[str]: """Extract tags from frontmatter metadata. + Handles tags as comma-separated string, list, or other types. Strips leading ``#`` from each tag. @@ -196,7 +245,7 @@ def parse_markdown_file(raw: str) -> frontmatter.Post: return frontmatter.Post(content, **{}) -def _scan_vault(vault_name: str, vault_path: str) -> Dict[str, Any]: +def _scan_vault(vault_name: str, vault_path: str, vault_cfg: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """Synchronously scan a single vault directory and build file index. Walks the vault tree, reads supported files, extracts metadata @@ -206,6 +255,7 @@ def _scan_vault(vault_name: str, vault_path: str) -> Dict[str, Any]: Args: vault_name: Display name of the vault. vault_path: Absolute filesystem path to the vault root. + vault_cfg: Optional vault configuration dict with hidden files settings. Returns: Dict with keys ``files`` (list), ``tags`` (counter dict), ``path`` (str), ``paths`` (list). @@ -214,15 +264,19 @@ def _scan_vault(vault_name: str, vault_path: str) -> Dict[str, Any]: files: List[Dict[str, Any]] = [] tag_counts: Dict[str, int] = {} paths: List[Dict[str, str]] = [] + + # Default config if not provided + if vault_cfg is None: + vault_cfg = {"includeHidden": False, "hiddenWhitelist": []} if not vault_root.exists(): logger.warning(f"Vault path does not exist: {vault_path}") return {"files": [], "tags": {}, "path": vault_path, "paths": []} for fpath in vault_root.rglob("*"): - # Skip hidden files and directories + # Check if path should be included based on hidden files configuration rel_parts = fpath.relative_to(vault_root).parts - if any(part.startswith(".") for part in rel_parts): + if not _should_include_path(rel_parts, vault_cfg): continue rel_path_str = str(fpath.relative_to(vault_root)).replace("\\", "/") @@ -327,7 +381,7 @@ async def build_index(progress_callback=None) -> None: async def _process_vault(name: str, config: Dict[str, Any]): vault_path = config["path"] - vault_data = await loop.run_in_executor(None, _scan_vault, name, vault_path) + vault_data = await loop.run_in_executor(None, _scan_vault, name, vault_path, config) vault_data["config"] = config # Build lookup entries for the new vault @@ -698,10 +752,12 @@ async def add_vault_to_index(vault_name: str, vault_path: str) -> Dict[str, Any] "path": vault_path, "attachmentsPath": None, "scanAttachmentsOnStartup": True, + "includeHidden": False, + "hiddenWhitelist": [], } loop = asyncio.get_event_loop() - vault_data = await loop.run_in_executor(None, _scan_vault, vault_name, vault_path) + vault_data = await loop.run_in_executor(None, _scan_vault, vault_name, vault_path, vault_config[vault_name]) vault_data["config"] = vault_config[vault_name] # Build lookup entries for the new vault diff --git a/backend/main.py b/backend/main.py index d15a95c..c68982c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -44,6 +44,12 @@ from backend.indexer import ( from backend.search import search, get_all_tags, advanced_search, suggest_titles, suggest_tags from backend.image_processor import preprocess_images from backend.attachment_indexer import rescan_vault_attachments, get_attachment_stats +from backend.vault_settings import ( + get_vault_setting, + update_vault_setting, + get_all_vault_settings, + delete_vault_setting, +) logging.basicConfig( level=logging.INFO, @@ -1385,6 +1391,104 @@ async def api_attachment_stats(vault: Optional[str] = Query(None, description="V return {"vaults": stats} +# --------------------------------------------------------------------------- +# Vault Settings API — Hidden files configuration +# --------------------------------------------------------------------------- + +@app.get("/api/vaults/{vault_name}/settings") +async def api_get_vault_settings(vault_name: str, current_user=Depends(require_auth)): + """Get settings for a specific vault including hidden files configuration. + + Args: + vault_name: Name of the vault. + + Returns: + Dict with vault settings including includeHidden and hiddenWhitelist. + """ + if vault_name not in index: + raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found") + + # Get current vault config from environment + vault_cfg = vault_config.get(vault_name, {}) + + # Get persisted settings + persisted = get_vault_setting(vault_name) or {} + + # Merge with defaults + settings = { + "includeHidden": vault_cfg.get("includeHidden", False), + "hiddenWhitelist": vault_cfg.get("hiddenWhitelist", []), + } + settings.update(persisted) + + return settings + + +@app.post("/api/vaults/{vault_name}/settings") +async def api_update_vault_settings(vault_name: str, body: dict = Body(...), current_user=Depends(require_admin)): + """Update settings for a specific vault. + + Args: + vault_name: Name of the vault. + body: Dict with settings to update (includeHidden, hiddenWhitelist). + + Returns: + Updated settings dict. + """ + if vault_name not in index: + raise HTTPException(status_code=404, detail=f"Vault '{vault_name}' not found") + + # Validate settings + settings_to_update = {} + + if "includeHidden" in body: + if not isinstance(body["includeHidden"], bool): + raise HTTPException(status_code=400, detail="includeHidden must be a boolean") + settings_to_update["includeHidden"] = body["includeHidden"] + + if "hiddenWhitelist" in body: + if not isinstance(body["hiddenWhitelist"], list): + raise HTTPException(status_code=400, detail="hiddenWhitelist must be a list") + # Validate each item is a string + if not all(isinstance(item, str) for item in body["hiddenWhitelist"]): + raise HTTPException(status_code=400, detail="All hiddenWhitelist items must be strings") + settings_to_update["hiddenWhitelist"] = body["hiddenWhitelist"] + + # Update persisted settings + updated = update_vault_setting(vault_name, settings_to_update) + + # Update in-memory vault config + if vault_name in vault_config: + vault_config[vault_name].update(settings_to_update) + + logger.info(f"Updated settings for vault '{vault_name}': {settings_to_update}") + + return updated + + +@app.get("/api/vaults/settings/all") +async def api_get_all_vault_settings(current_user=Depends(require_auth)): + """Get settings for all vaults. + + Returns: + Dict mapping vault names to their settings. + """ + all_settings = {} + + for vault_name in index.keys(): + vault_cfg = vault_config.get(vault_name, {}) + persisted = get_vault_setting(vault_name) or {} + + settings = { + "includeHidden": vault_cfg.get("includeHidden", False), + "hiddenWhitelist": vault_cfg.get("hiddenWhitelist", []), + } + settings.update(persisted) + all_settings[vault_name] = settings + + return all_settings + + # --------------------------------------------------------------------------- # Configuration API # --------------------------------------------------------------------------- diff --git a/backend/vault_settings.py b/backend/vault_settings.py new file mode 100644 index 0000000..42e9633 --- /dev/null +++ b/backend/vault_settings.py @@ -0,0 +1,121 @@ +"""Vault-specific settings management. + +Provides persistent storage for per-vault configuration like hidden files settings. +Settings are stored in /app/data/vault_settings.json +""" + +import json +import logging +from pathlib import Path +from typing import Dict, Any, Optional +import threading + +logger = logging.getLogger("obsigate.vault_settings") + +_SETTINGS_PATH = Path("/app/data/vault_settings.json") +_settings_lock = threading.Lock() + +# In-memory cache of vault settings +_vault_settings: Dict[str, Dict[str, Any]] = {} + + +def load_vault_settings() -> Dict[str, Dict[str, Any]]: + """Load vault settings from disk. + + Returns: + Dict mapping vault names to their settings. + """ + global _vault_settings + + with _settings_lock: + if _SETTINGS_PATH.exists(): + try: + content = _SETTINGS_PATH.read_text(encoding="utf-8") + _vault_settings = json.loads(content) + logger.info(f"Loaded settings for {len(_vault_settings)} vaults") + except Exception as e: + logger.warning(f"Failed to load vault settings: {e}") + _vault_settings = {} + else: + _vault_settings = {} + + return dict(_vault_settings) + + +def save_vault_settings() -> None: + """Persist vault settings to disk.""" + with _settings_lock: + try: + _SETTINGS_PATH.parent.mkdir(parents=True, exist_ok=True) + _SETTINGS_PATH.write_text( + json.dumps(_vault_settings, indent=2, ensure_ascii=False), + encoding="utf-8" + ) + logger.info(f"Saved settings for {len(_vault_settings)} vaults") + except Exception as e: + logger.error(f"Failed to save vault settings: {e}") + raise + + +def get_vault_setting(vault_name: str) -> Optional[Dict[str, Any]]: + """Get settings for a specific vault. + + Args: + vault_name: Name of the vault. + + Returns: + Dict with vault settings or None if not found. + """ + with _settings_lock: + return _vault_settings.get(vault_name) + + +def update_vault_setting(vault_name: str, settings: Dict[str, Any]) -> Dict[str, Any]: + """Update settings for a specific vault. + + Args: + vault_name: Name of the vault. + settings: Dict with settings to update (partial update supported). + + Returns: + Updated settings dict for the vault. + """ + with _settings_lock: + if vault_name not in _vault_settings: + _vault_settings[vault_name] = {} + + _vault_settings[vault_name].update(settings) + save_vault_settings() + + return dict(_vault_settings[vault_name]) + + +def delete_vault_setting(vault_name: str) -> bool: + """Delete settings for a specific vault. + + Args: + vault_name: Name of the vault. + + Returns: + True if settings were deleted, False if vault not found. + """ + with _settings_lock: + if vault_name in _vault_settings: + del _vault_settings[vault_name] + save_vault_settings() + return True + return False + + +def get_all_vault_settings() -> Dict[str, Dict[str, Any]]: + """Get all vault settings. + + Returns: + Dict mapping vault names to their settings. + """ + with _settings_lock: + return dict(_vault_settings) + + +# Initialize settings on module load +load_vault_settings() diff --git a/frontend/app.js b/frontend/app.js index 25c6d88..f2da100 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -2968,6 +2968,7 @@ renderConfigFilters(); loadConfigFields(); loadDiagnostics(); + await loadHiddenFilesSettings(); safeCreateIcons(); }); @@ -3009,6 +3010,13 @@ const diagBtn = document.getElementById("cfg-refresh-diag"); if (diagBtn) diagBtn.addEventListener("click", loadDiagnostics); + // Hidden files configuration + const saveHiddenBtn = document.getElementById("cfg-save-hidden-files"); + if (saveHiddenBtn) saveHiddenBtn.addEventListener("click", saveHiddenFilesSettings); + + const reindexHiddenBtn = document.getElementById("cfg-reindex-hidden"); + if (reindexHiddenBtn) reindexHiddenBtn.addEventListener("click", reindexWithHiddenFiles); + document.addEventListener("keydown", (e) => { if (e.key === "Escape" && modal.classList.contains("active")) { closeConfigModal(); @@ -3221,6 +3229,224 @@ }); } + // --- Hidden Files Configuration --- + + async function loadHiddenFilesSettings() { + const container = document.getElementById("hidden-files-vault-list"); + if (!container) return; + + container.innerHTML = '
Chargement...
'; + + try { + const settings = await api("/api/vaults/settings/all"); + renderHiddenFilesSettings(container, settings); + } catch (err) { + console.error("Failed to load hidden files settings:", err); + container.innerHTML = '
Erreur de chargement
'; + } + } + + function renderHiddenFilesSettings(container, allSettings) { + container.innerHTML = ""; + + if (!allVaults || allVaults.length === 0) { + container.innerHTML = '
Aucun vault configuré
'; + return; + } + + allVaults.forEach(vault => { + const settings = allSettings[vault.name] || { includeHidden: false, hiddenWhitelist: [] }; + + const vaultCard = el("div", { class: "hidden-files-vault-card", "data-vault": vault.name }); + + // Vault header + const header = el("div", { class: "hidden-files-vault-header" }, [ + el("h3", {}, [document.createTextNode(vault.name)]), + el("span", { class: "hidden-files-vault-type" }, [document.createTextNode(vault.type || "VAULT")]) + ]); + + // Include all hidden toggle + const toggleRow = el("div", { class: "config-row" }, [ + el("label", { class: "config-label", for: `hidden-include-${vault.name}` }, [ + document.createTextNode("Inclure tous les fichiers cachés") + ]), + el("label", { class: "config-toggle" }, [ + el("input", { + type: "checkbox", + id: `hidden-include-${vault.name}`, + "data-vault": vault.name, + checked: settings.includeHidden || false + }), + el("span", { class: "config-toggle-slider" }) + ]), + el("span", { class: "config-hint" }, [ + document.createTextNode("Activer pour indexer tous les fichiers et dossiers cachés (commençant par un point)") + ]) + ]); + + // Whitelist section + const whitelistSection = el("div", { class: "hidden-files-whitelist" }); + const whitelistLabel = el("label", { class: "config-label" }, [ + document.createTextNode("Liste blanche (dossiers cachés spécifiques)") + ]); + const whitelistHint = el("span", { class: "config-hint", style: "display:block;margin-bottom:8px" }, [ + document.createTextNode("Ajoutez des dossiers cachés individuels à indexer (ex: .obsidian, .github)") + ]); + + const whitelistItems = el("div", { class: "hidden-files-whitelist-items", id: `whitelist-items-${vault.name}` }); + + // Render existing whitelist items + (settings.hiddenWhitelist || []).forEach(item => { + const itemEl = createWhitelistItem(vault.name, item); + whitelistItems.appendChild(itemEl); + }); + + // Add new item input + const addRow = el("div", { class: "hidden-files-add-row" }, [ + el("input", { + type: "text", + class: "config-input", + id: `whitelist-input-${vault.name}`, + placeholder: "Ex: .obsidian" + }), + el("button", { + class: "config-btn-add", + type: "button", + "data-vault": vault.name + }, [document.createTextNode("Ajouter")]) + ]); + + whitelistSection.appendChild(whitelistLabel); + whitelistSection.appendChild(whitelistHint); + whitelistSection.appendChild(whitelistItems); + whitelistSection.appendChild(addRow); + + vaultCard.appendChild(header); + vaultCard.appendChild(toggleRow); + vaultCard.appendChild(whitelistSection); + + container.appendChild(vaultCard); + + // Event listeners + const addBtn = addRow.querySelector("button"); + const input = addRow.querySelector("input"); + + addBtn.addEventListener("click", () => addWhitelistItem(vault.name)); + input.addEventListener("keypress", (e) => { + if (e.key === "Enter") addWhitelistItem(vault.name); + }); + }); + } + + function createWhitelistItem(vaultName, itemValue) { + const item = el("div", { class: "hidden-files-whitelist-item" }, [ + el("span", { class: "whitelist-item-text" }, [document.createTextNode(itemValue)]), + el("button", { + class: "whitelist-item-remove", + type: "button", + title: "Supprimer", + "data-vault": vaultName, + "data-item": itemValue + }, [document.createTextNode("×")]) + ]); + + const removeBtn = item.querySelector("button"); + removeBtn.addEventListener("click", () => { + item.remove(); + }); + + return item; + } + + function addWhitelistItem(vaultName) { + const input = document.getElementById(`whitelist-input-${vaultName}`); + const container = document.getElementById(`whitelist-items-${vaultName}`); + + if (!input || !container) return; + + const value = input.value.trim(); + if (!value) return; + + // Validate format (should start with a dot) + if (!value.startsWith(".")) { + showToast("Le nom doit commencer par un point (ex: .obsidian)", "error"); + return; + } + + // Check for duplicates + const existing = Array.from(container.querySelectorAll(".whitelist-item-text")) + .map(el => el.textContent); + if (existing.includes(value)) { + showToast("Cet élément existe déjà", "error"); + return; + } + + const item = createWhitelistItem(vaultName, value); + container.appendChild(item); + input.value = ""; + } + + async function saveHiddenFilesSettings() { + const btn = document.getElementById("cfg-save-hidden-files"); + if (btn) { btn.disabled = true; btn.textContent = "Sauvegarde..."; } + + try { + const vaultCards = document.querySelectorAll(".hidden-files-vault-card"); + const promises = []; + + vaultCards.forEach(card => { + const vaultName = card.dataset.vault; + const includeHidden = document.getElementById(`hidden-include-${vaultName}`)?.checked || false; + + const whitelistItems = Array.from( + document.querySelectorAll(`#whitelist-items-${vaultName} .whitelist-item-text`) + ).map(el => el.textContent); + + const settings = { + includeHidden, + hiddenWhitelist: whitelistItems + }; + + promises.push( + fetch(`/api/vaults/${encodeURIComponent(vaultName)}/settings`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(settings) + }) + ); + }); + + await Promise.all(promises); + showToast("Paramètres des fichiers cachés sauvegardés", "success"); + } catch (err) { + console.error("Failed to save hidden files settings:", err); + showToast("Erreur de sauvegarde", "error"); + } finally { + if (btn) { btn.disabled = false; btn.textContent = "Sauvegarder les paramètres"; } + } + } + + async function reindexWithHiddenFiles() { + const btn = document.getElementById("cfg-reindex-hidden"); + if (btn) { btn.disabled = true; btn.textContent = "Réindexation..."; } + + try { + // First save the settings + await saveHiddenFilesSettings(); + + // Then trigger reindex + await api("/api/index/reload"); + showToast("Réindexation avec nouveaux paramètres terminée", "success"); + loadDiagnostics(); + await Promise.all([loadVaults(), loadTags()]); + } catch (err) { + console.error("Reindex with hidden files error:", err); + showToast("Erreur de réindexation", "error"); + } finally { + if (btn) { btn.disabled = false; btn.textContent = "Réindexer avec nouveaux paramètres"; } + } + } + function renderConfigFilters() { const config = TagFilterService.getConfig(); const filters = config.tagFilters || TagFilterService.defaultFilters; diff --git a/frontend/index.html b/frontend/index.html index b94bbed..36d75cf 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -548,6 +548,21 @@ + +
+

📁 Fichiers et dossiers cachés

+

Configurez la prise en charge des fichiers et dossiers cachés (commençant par un point) pour chaque vault. Vous pouvez activer tous les fichiers cachés ou ajouter des dossiers spécifiques à une liste blanche.

+ +
+ +
+ +
+ + +
+
+

Diagnostics

diff --git a/frontend/style.css b/frontend/style.css index fc74ab2..85fdbfa 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -3408,6 +3408,99 @@ body.resizing-v { background: #22c55e; } +/* --------------------------------------------------------------------------- + Hidden Files Configuration + --------------------------------------------------------------------------- */ +.hidden-files-vault-card { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 8px; + padding: 16px; + margin-bottom: 16px; +} + +.hidden-files-vault-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--border); +} + +.hidden-files-vault-header h3 { + font-size: 1rem; + font-weight: 600; + color: var(--text-primary); + margin: 0; +} + +.hidden-files-vault-type { + font-size: 0.75rem; + padding: 2px 8px; + background: var(--tag-bg); + color: var(--tag-text); + border-radius: 4px; + font-family: var(--mono); +} + +.hidden-files-whitelist { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid var(--border); +} + +.hidden-files-whitelist-items { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; + min-height: 32px; +} + +.hidden-files-whitelist-item { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 8px 4px 12px; + background: var(--tag-bg); + color: var(--tag-text); + border-radius: 6px; + font-size: 0.875rem; + font-family: var(--mono); + border: 1px solid color-mix(in srgb, var(--tag-text) 30%, transparent); +} + +.whitelist-item-text { + user-select: none; +} + +.whitelist-item-remove { + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + font-size: 1.2rem; + line-height: 1; + padding: 0 4px; + transition: color 150ms ease; +} + +.whitelist-item-remove:hover { + color: var(--danger); +} + +.hidden-files-add-row { + display: flex; + gap: 8px; + align-items: center; +} + +.hidden-files-add-row .config-input { + flex: 1; + max-width: 300px; +} + /* --------------------------------------------------------------------------- Utility — hidden class --------------------------------------------------------------------------- */