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:
parent
8c30b0d238
commit
9e42fb072b
154
HIDDEN_FILES_GUIDE.md
Normal file
154
HIDDEN_FILES_GUIDE.md
Normal 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.
|
||||||
@ -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
|
||||||
|
|||||||
@ -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,16 +122,50 @@ 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).
|
||||||
@ -215,14 +265,18 @@ def _scan_vault(vault_name: str, vault_path: str) -> 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
|
||||||
|
|||||||
104
backend/main.py
104
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.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
121
backend/vault_settings.py
Normal 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()
|
||||||
226
frontend/app.js
226
frontend/app.js
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
--------------------------------------------------------------------------- */
|
--------------------------------------------------------------------------- */
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user