feat: add hidden files configuration with per-vault settings for includeHidden and hiddenWhitelist, supporting environment variables and UI controls for selective indexing of dot-prefixed files and folders

This commit is contained in:
Bruno Charest 2026-03-25 09:54:34 -04:00
parent 8c30b0d238
commit 9e42fb072b
8 changed files with 790 additions and 13 deletions

154
HIDDEN_FILES_GUIDE.md Normal file
View File

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

View File

@ -1,8 +1,10 @@
import asyncio import asyncio
import logging import logging
import threading
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Set from typing import Dict, List, Optional, Set
import threading
from backend.indexer import _should_include_path
logger = logging.getLogger("obsigate.attachment_indexer") logger = logging.getLogger("obsigate.attachment_indexer")
@ -34,7 +36,7 @@ def clear_resolution_cache(vault_name: Optional[str] = None) -> None:
del _resolution_cache[key] 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. """Synchronously scan a vault directory for image attachments.
Walks the vault tree and builds a filename -> absolute path mapping 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: Args:
vault_name: Display name of the vault. vault_name: Display name of the vault.
vault_path: Absolute filesystem path to the vault root. vault_path: Absolute filesystem path to the vault root.
vault_cfg: Optional vault configuration dict with hidden files settings.
Returns: Returns:
Dict mapping lowercase filenames to lists of absolute paths. 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) vault_root = Path(vault_path)
index: Dict[str, List[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(): if not vault_root.exists():
logger.warning(f"Vault path does not exist for attachment scan: {vault_path}") logger.warning(f"Vault path does not exist for attachment scan: {vault_path}")
return index return index
@ -58,9 +65,9 @@ def _scan_vault_attachments(vault_name: str, vault_path: str) -> Dict[str, List[
try: try:
for fpath in vault_root.rglob("*"): 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 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 continue
# Only process files # Only process files
@ -121,7 +128,7 @@ async def build_attachment_index(vault_config: Dict[str, Dict[str, any]]) -> Non
new_index[name] = {} new_index[name] = {}
continue 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: for name, task in tasks:
new_index[name] = await task 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") 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. """Rescan attachments for a single vault.
Args: Args:
vault_name: Name of the vault to rescan. vault_name: Name of the vault to rescan.
vault_path: Absolute path to the vault root. vault_path: Absolute path to the vault root.
vault_cfg: Optional vault configuration dict with hidden files settings.
Returns: Returns:
Number of attachments indexed. Number of attachments indexed.
""" """
loop = asyncio.get_event_loop() 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: with _attachment_lock:
attachment_index[vault_name] = new_vault_index attachment_index[vault_name] = new_vault_index

View File

@ -61,12 +61,16 @@ def load_vault_config() -> Dict[str, Dict[str, Any]]:
Also reads optional configuration: Also reads optional configuration:
- VAULT_N_ATTACHMENTS_PATH: relative path to attachments folder - VAULT_N_ATTACHMENTS_PATH: relative path to attachments folder
- VAULT_N_SCAN_ATTACHMENTS: "true"/"false" to enable/disable scanning - 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: Returns:
Dict mapping vault names to configuration dicts with keys: Dict mapping vault names to configuration dicts with keys:
- path: filesystem path (required) - path: filesystem path (required)
- attachmentsPath: relative attachments folder (optional) - attachmentsPath: relative attachments folder (optional)
- scanAttachmentsOnStartup: boolean (default True) - 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" - type: "VAULT" or "DIR"
""" """
vaults: Dict[str, Dict[str, Any]] = {} vaults: Dict[str, Dict[str, Any]] = {}
@ -80,11 +84,16 @@ def load_vault_config() -> Dict[str, Dict[str, Any]]:
# Optional configuration # Optional configuration
attachments_path = os.environ.get(f"VAULT_{n}_ATTACHMENTS_PATH") attachments_path = os.environ.get(f"VAULT_{n}_ATTACHMENTS_PATH")
scan_attachments = os.environ.get(f"VAULT_{n}_SCAN_ATTACHMENTS", "true").lower() == "true" 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] = { vaults[name] = {
"path": path, "path": path,
"attachmentsPath": attachments_path, "attachmentsPath": attachments_path,
"scanAttachmentsOnStartup": scan_attachments, "scanAttachmentsOnStartup": scan_attachments,
"includeHidden": include_hidden,
"hiddenWhitelist": hidden_whitelist,
"type": "VAULT" "type": "VAULT"
} }
n += 1 n += 1
@ -96,10 +105,16 @@ def load_vault_config() -> Dict[str, Dict[str, Any]]:
if not name or not path: if not name or not path:
break 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] = { vaults[name] = {
"path": path, "path": path,
"attachmentsPath": None, "attachmentsPath": None,
"scanAttachmentsOnStartup": False, "scanAttachmentsOnStartup": False,
"includeHidden": include_hidden,
"hiddenWhitelist": hidden_whitelist,
"type": "DIR" "type": "DIR"
} }
n += 1 n += 1
@ -107,15 +122,49 @@ def load_vault_config() -> Dict[str, Dict[str, Any]]:
return vaults 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) # 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) _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 # 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'`[^`]+`') _INLINE_CODE_RE = re.compile(r'`[^`]+`')
def _extract_tags(post: frontmatter.Post) -> List[str]: def _extract_tags(post: frontmatter.Post) -> List[str]:
"""Extract tags from frontmatter metadata. """Extract tags from frontmatter metadata.
Handles tags as comma-separated string, list, or other types. Handles tags as comma-separated string, list, or other types.
Strips leading ``#`` from each tag. Strips leading ``#`` from each tag.
@ -196,7 +245,7 @@ def parse_markdown_file(raw: str) -> frontmatter.Post:
return frontmatter.Post(content, **{}) 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. """Synchronously scan a single vault directory and build file index.
Walks the vault tree, reads supported files, extracts metadata 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: Args:
vault_name: Display name of the vault. vault_name: Display name of the vault.
vault_path: Absolute filesystem path to the vault root. vault_path: Absolute filesystem path to the vault root.
vault_cfg: Optional vault configuration dict with hidden files settings.
Returns: Returns:
Dict with keys ``files`` (list), ``tags`` (counter dict), ``path`` (str), ``paths`` (list). 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]] = [] files: List[Dict[str, Any]] = []
tag_counts: Dict[str, int] = {} tag_counts: Dict[str, int] = {}
paths: List[Dict[str, str]] = [] 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(): if not vault_root.exists():
logger.warning(f"Vault path does not exist: {vault_path}") logger.warning(f"Vault path does not exist: {vault_path}")
return {"files": [], "tags": {}, "path": vault_path, "paths": []} return {"files": [], "tags": {}, "path": vault_path, "paths": []}
for fpath in vault_root.rglob("*"): 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 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 continue
rel_path_str = str(fpath.relative_to(vault_root)).replace("\\", "/") 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]): async def _process_vault(name: str, config: Dict[str, Any]):
vault_path = config["path"] 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 vault_data["config"] = config
# Build lookup entries for the new vault # 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, "path": vault_path,
"attachmentsPath": None, "attachmentsPath": None,
"scanAttachmentsOnStartup": True, "scanAttachmentsOnStartup": True,
"includeHidden": False,
"hiddenWhitelist": [],
} }
loop = asyncio.get_event_loop() 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] vault_data["config"] = vault_config[vault_name]
# Build lookup entries for the new vault # Build lookup entries for the new vault

View File

@ -44,6 +44,12 @@ from backend.indexer import (
from backend.search import search, get_all_tags, advanced_search, suggest_titles, suggest_tags from backend.search import search, get_all_tags, advanced_search, suggest_titles, suggest_tags
from backend.image_processor import preprocess_images from backend.image_processor import preprocess_images
from backend.attachment_indexer import rescan_vault_attachments, get_attachment_stats 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( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
@ -1385,6 +1391,104 @@ async def api_attachment_stats(vault: Optional[str] = Query(None, description="V
return {"vaults": stats} 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 # Configuration API
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

121
backend/vault_settings.py Normal file
View File

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

View File

@ -2968,6 +2968,7 @@
renderConfigFilters(); renderConfigFilters();
loadConfigFields(); loadConfigFields();
loadDiagnostics(); loadDiagnostics();
await loadHiddenFilesSettings();
safeCreateIcons(); safeCreateIcons();
}); });
@ -3009,6 +3010,13 @@
const diagBtn = document.getElementById("cfg-refresh-diag"); const diagBtn = document.getElementById("cfg-refresh-diag");
if (diagBtn) diagBtn.addEventListener("click", loadDiagnostics); 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) => { document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) { if (e.key === "Escape" && modal.classList.contains("active")) {
closeConfigModal(); closeConfigModal();
@ -3221,6 +3229,224 @@
}); });
} }
// --- Hidden Files Configuration ---
async function loadHiddenFilesSettings() {
const container = document.getElementById("hidden-files-vault-list");
if (!container) return;
container.innerHTML = '<div style="padding:12px;color:var(--text-muted)">Chargement...</div>';
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 = '<div style="padding:12px;color:var(--error)">Erreur de chargement</div>';
}
}
function renderHiddenFilesSettings(container, allSettings) {
container.innerHTML = "";
if (!allVaults || allVaults.length === 0) {
container.innerHTML = '<div style="padding:12px;color:var(--text-muted)">Aucun vault configuré</div>';
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() { function renderConfigFilters() {
const config = TagFilterService.getConfig(); const config = TagFilterService.getConfig();
const filters = config.tagFilters || TagFilterService.defaultFilters; const filters = config.tagFilters || TagFilterService.defaultFilters;

View File

@ -548,6 +548,21 @@
</div> </div>
</section> </section>
<!-- Hidden Files/Folders Configuration -->
<section class="config-section">
<h2>📁 Fichiers et dossiers cachés</h2>
<p class="config-description">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.</p>
<div id="hidden-files-vault-list">
<!-- Vault-specific settings will be injected here -->
</div>
<div class="config-actions-row" style="margin-top: 16px;">
<button class="config-btn-save" id="cfg-save-hidden-files">Sauvegarder les paramètres</button>
<button class="config-btn-secondary" id="cfg-reindex-hidden">Réindexer avec nouveaux paramètres</button>
</div>
</section>
<!-- Diagnostics --> <!-- Diagnostics -->
<section class="config-section"> <section class="config-section">
<h2>Diagnostics</h2> <h2>Diagnostics</h2>

View File

@ -3408,6 +3408,99 @@ body.resizing-v {
background: #22c55e; 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 Utility hidden class
--------------------------------------------------------------------------- */ --------------------------------------------------------------------------- */