369 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			369 lines
		
	
	
		
			9.6 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Front-matter Enrichment & Quick Links
 | |
| 
 | |
| ## Vue d'ensemble
 | |
| 
 | |
| Cette fonctionnalité ajoute deux améliorations majeures à ObsiViewer :
 | |
| 
 | |
| 1. **Enrichissement automatique de la front-matter YAML** : À chaque ouverture d'un fichier Markdown, le système valide et enrichit automatiquement la front-matter avec des propriétés standardisées.
 | |
| 
 | |
| 2. **Quick Links avec filtres** : Ajout de liens rapides (Favorites, Templates, Tasks) dans le sidebar qui permettent de filtrer les notes selon leurs propriétés.
 | |
| 
 | |
| ## Architecture
 | |
| 
 | |
| ### Backend
 | |
| 
 | |
| #### 1. Enrichissement de la front-matter (`server/ensureFrontmatter.mjs`)
 | |
| 
 | |
| **Responsabilités :**
 | |
| - Valider et enrichir la front-matter YAML des fichiers Markdown
 | |
| - Garantir l'idempotence (pas de modification sur les fichiers déjà conformes)
 | |
| - Préserver les propriétés existantes
 | |
| - Maintenir l'ordre strict des clés
 | |
| 
 | |
| **Propriétés garanties (ordre exact) :**
 | |
| ```yaml
 | |
| ---
 | |
| titre: <nom du fichier sans .md>
 | |
| auteur: Bruno Charest
 | |
| creation_date: <ISO 8601 avec timezone America/Toronto>
 | |
| modification_date: <ISO 8601 avec timezone America/Toronto>
 | |
| catégorie: ""
 | |
| tags: []
 | |
| aliases: []
 | |
| status: en-cours
 | |
| publish: false
 | |
| favoris: false
 | |
| template: false
 | |
| task: false
 | |
| archive: false
 | |
| draft: false
 | |
| private: false
 | |
| ---
 | |
| ```
 | |
| 
 | |
| **Caractéristiques techniques :**
 | |
| - Utilisation de la librairie `yaml` pour préserver les types et l'ordre
 | |
| - Mutex en mémoire pour éviter les écritures concurrentes
 | |
| - Écriture atomique (temp file + rename) pour éviter la corruption
 | |
| - Format de date : ISO 8601 avec offset `-04:00` (America/Toronto)
 | |
| 
 | |
| **Exemple d'utilisation :**
 | |
| ```javascript
 | |
| import { enrichFrontmatterOnOpen } from './ensureFrontmatter.mjs';
 | |
| 
 | |
| const result = await enrichFrontmatterOnOpen('/path/to/note.md');
 | |
| if (result.modified) {
 | |
|   console.log('File was enriched');
 | |
|   // Trigger reindex in Meilisearch
 | |
| }
 | |
| ```
 | |
| 
 | |
| #### 2. Intégration dans l'endpoint GET /api/files
 | |
| 
 | |
| Le middleware d'enrichissement est appelé automatiquement lors de la lecture d'un fichier Markdown :
 | |
| 
 | |
| ```javascript
 | |
| app.get('/api/files', async (req, res) => {
 | |
|   // ... validation du path ...
 | |
|   
 | |
|   if (!isExcalidraw && ext === '.md') {
 | |
|     const enrichResult = await enrichFrontmatterOnOpen(abs);
 | |
|     
 | |
|     if (enrichResult.modified) {
 | |
|       // Trigger Meilisearch reindex
 | |
|       upsertFile(abs).catch(err => console.warn(err));
 | |
|     }
 | |
|     
 | |
|     return res.send(enrichResult.content);
 | |
|   }
 | |
|   
 | |
|   // ... reste du code ...
 | |
| });
 | |
| ```
 | |
| 
 | |
| #### 3. Indexation Meilisearch
 | |
| 
 | |
| Les propriétés booléennes `favoris`, `template` et `task` sont extraites et indexées pour permettre un filtrage rapide :
 | |
| 
 | |
| **Fichier : `server/meilisearch-indexer.mjs`**
 | |
| ```javascript
 | |
| return {
 | |
|   id: safeId,
 | |
|   path: rel,
 | |
|   // ... autres propriétés ...
 | |
|   favoris: fm.favoris === true,
 | |
|   template: fm.template === true,
 | |
|   task: fm.task === true
 | |
| };
 | |
| ```
 | |
| 
 | |
| **Configuration Meilisearch : `server/meilisearch.client.mjs`**
 | |
| ```javascript
 | |
| filterableAttributes: [
 | |
|   'tags',
 | |
|   'file',
 | |
|   'path',
 | |
|   'parentDirs',
 | |
|   'properties.*',
 | |
|   'year',
 | |
|   'month',
 | |
|   'favoris',    // ← Nouveau
 | |
|   'template',   // ← Nouveau
 | |
|   'task'        // ← Nouveau
 | |
| ]
 | |
| ```
 | |
| 
 | |
| #### 4. Endpoint des compteurs
 | |
| 
 | |
| **Route : `GET /api/quick-links/counts`**
 | |
| 
 | |
| Retourne les compteurs pour chaque type de Quick Link :
 | |
| 
 | |
| ```json
 | |
| {
 | |
|   "favorites": 12,
 | |
|   "templates": 5,
 | |
|   "tasks": 8
 | |
| }
 | |
| ```
 | |
| 
 | |
| **Implémentation :**
 | |
| ```javascript
 | |
| app.get('/api/quick-links/counts', async (req, res) => {
 | |
|   const [favoritesResult, templatesResult, tasksResult] = await Promise.all([
 | |
|     index.search('', { filter: 'favoris = true', limit: 0 }),
 | |
|     index.search('', { filter: 'template = true', limit: 0 }),
 | |
|     index.search('', { filter: 'task = true', limit: 0 })
 | |
|   ]);
 | |
|   
 | |
|   res.json({
 | |
|     favorites: favoritesResult.estimatedTotalHits || 0,
 | |
|     templates: templatesResult.estimatedTotalHits || 0,
 | |
|     tasks: tasksResult.estimatedTotalHits || 0
 | |
|   });
 | |
| });
 | |
| ```
 | |
| 
 | |
| ### Frontend
 | |
| 
 | |
| #### 1. Composant Quick Links (`src/app/features/quick-links/quick-links.component.ts`)
 | |
| 
 | |
| **Améliorations :**
 | |
| - Chargement dynamique des compteurs depuis l'API
 | |
| - Affichage des compteurs à côté de chaque lien
 | |
| - Mise à jour automatique au chargement du composant
 | |
| 
 | |
| **Template :**
 | |
| ```html
 | |
| <li>
 | |
|   <button (click)="select('favorites')" class="...">
 | |
|     <span class="flex items-center gap-2">
 | |
|       <span>❤️</span> 
 | |
|       <span>Favorites</span>
 | |
|     </span>
 | |
|     <span *ngIf="counts" class="text-xs text-gray-500 font-medium">
 | |
|       ({{ counts.favorites }})
 | |
|     </span>
 | |
|   </button>
 | |
| </li>
 | |
| ```
 | |
| 
 | |
| #### 2. Gestion des filtres (`src/app/layout/app-shell-nimbus/app-shell-nimbus.component.ts`)
 | |
| 
 | |
| **Nouvelle propriété :**
 | |
| ```typescript
 | |
| quickLinkFilter: 'favoris' | 'template' | 'task' | null = null;
 | |
| ```
 | |
| 
 | |
| **Gestion des clics :**
 | |
| ```typescript
 | |
| onQuickLink(_id: string) {
 | |
|   if (_id === 'favorites') {
 | |
|     this.folderFilter = null;
 | |
|     this.tagFilter = null;
 | |
|     this.quickLinkFilter = 'favoris';
 | |
|     this.listQuery = '';
 | |
|     // Switch to list view on mobile/tablet
 | |
|   }
 | |
|   // ... similaire pour templates et tasks ...
 | |
| }
 | |
| ```
 | |
| 
 | |
| #### 3. Filtrage dans la liste (`src/app/features/list/notes-list.component.ts`)
 | |
| 
 | |
| **Ajout du filtre :**
 | |
| ```typescript
 | |
| quickLinkFilter = input<'favoris' | 'template' | 'task' | null>(null);
 | |
| 
 | |
| filtered = computed(() => {
 | |
|   // ... autres filtres ...
 | |
|   
 | |
|   if (quickLink) {
 | |
|     list = list.filter(n => {
 | |
|       const props = (n as any).properties || {};
 | |
|       return props[quickLink] === true;
 | |
|     });
 | |
|   }
 | |
|   
 | |
|   return [...list].sort((a, b) => (score(b) - score(a)));
 | |
| });
 | |
| ```
 | |
| 
 | |
| ## Tests
 | |
| 
 | |
| ### Tests unitaires (`server/ensureFrontmatter.test.mjs`)
 | |
| 
 | |
| **Exécution :**
 | |
| ```bash
 | |
| node server/ensureFrontmatter.test.mjs
 | |
| ```
 | |
| 
 | |
| **Couverture :**
 | |
| - ✓ Ajout de front-matter sur fichier vierge
 | |
| - ✓ Idempotence (pas de modification sur 2e passage)
 | |
| - ✓ Préservation des propriétés existantes
 | |
| - ✓ Ordre correct des clés
 | |
| - ✓ Absence de lignes vides dans la front-matter
 | |
| - ✓ Types booléens corrects
 | |
| - ✓ Types tableau corrects pour tags/aliases
 | |
| - ✓ Format de date ISO 8601 avec timezone
 | |
| 
 | |
| ### Tests e2e (`e2e/frontmatter-quicklinks.spec.ts`)
 | |
| 
 | |
| **Exécution :**
 | |
| ```bash
 | |
| npm run test:e2e
 | |
| ```
 | |
| 
 | |
| **Couverture :**
 | |
| - ✓ Enrichissement automatique à l'ouverture
 | |
| - ✓ Affichage des compteurs dans Quick Links
 | |
| - ✓ Filtrage par Favorites
 | |
| - ✓ Filtrage par Templates
 | |
| - ✓ Filtrage par Tasks
 | |
| - ✓ Réinitialisation des filtres avec "All pages"
 | |
| - ✓ Idempotence sur ouvertures multiples
 | |
| 
 | |
| ## Migration
 | |
| 
 | |
| ### Réindexation Meilisearch
 | |
| 
 | |
| Après déploiement, il est nécessaire de réindexer pour ajouter les nouveaux champs filtrables :
 | |
| 
 | |
| ```bash
 | |
| npm run meili:reindex
 | |
| ```
 | |
| 
 | |
| Cette commande :
 | |
| 1. Reconfigure l'index avec les nouveaux `filterableAttributes`
 | |
| 2. Réindexe tous les fichiers Markdown
 | |
| 3. Extrait les propriétés `favoris`, `template`, `task` de chaque fichier
 | |
| 
 | |
| ### Enrichissement des fichiers existants
 | |
| 
 | |
| Les fichiers existants seront enrichis automatiquement lors de leur première ouverture. Pour enrichir tous les fichiers d'un coup, créer un script :
 | |
| 
 | |
| ```javascript
 | |
| // scripts/enrich-all-notes.mjs
 | |
| import { enrichFrontmatterOnOpen } from '../server/ensureFrontmatter.mjs';
 | |
| import fg from 'fast-glob';
 | |
| 
 | |
| const files = await fg(['**/*.md'], { cwd: './vault', absolute: true });
 | |
| 
 | |
| for (const file of files) {
 | |
|   const result = await enrichFrontmatterOnOpen(file);
 | |
|   if (result.modified) {
 | |
|     console.log(`Enriched: ${file}`);
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## Cas limites gérés
 | |
| 
 | |
| 1. **Fichier sans front-matter** → Création du bloc complet
 | |
| 2. **Fichier avec front-matter partielle** → Complétion sans écraser l'existant
 | |
| 3. **Propriétés custom** → Préservées et placées après les propriétés requises
 | |
| 4. **Tags/aliases en chaîne** → Normalisés en tableau
 | |
| 5. **Disque sans birthtime** → Fallback sur ctime puis mtime
 | |
| 6. **Concurrence** → Mutex en mémoire + écriture atomique
 | |
| 7. **Fichiers volumineux** → Pas de réécriture si déjà conforme
 | |
| 
 | |
| ## Performance
 | |
| 
 | |
| - **Enrichissement** : ~5-10ms par fichier (lecture + parsing + écriture)
 | |
| - **Compteurs Quick Links** : ~50-100ms (3 requêtes Meilisearch en parallèle)
 | |
| - **Filtrage** : Instantané (index Meilisearch)
 | |
| - **Idempotence** : Aucun coût sur fichiers déjà conformes (détection rapide)
 | |
| 
 | |
| ## Dépendances ajoutées
 | |
| 
 | |
| ```json
 | |
| {
 | |
|   "dependencies": {
 | |
|     "yaml": "^2.x.x"
 | |
|   }
 | |
| }
 | |
| ```
 | |
| 
 | |
| La librairie `yaml` est préférée à `js-yaml` car elle :
 | |
| - Préserve l'ordre des clés
 | |
| - Maintient les types natifs (booléens, tableaux)
 | |
| - Supporte les commentaires YAML
 | |
| - Offre un contrôle fin sur la sérialisation
 | |
| 
 | |
| ## Maintenance
 | |
| 
 | |
| ### Ajouter une nouvelle propriété requise
 | |
| 
 | |
| 1. Modifier `server/ensureFrontmatter.mjs` :
 | |
| ```javascript
 | |
| const requiredProps = [
 | |
|   // ... propriétés existantes ...
 | |
|   ['nouvelle_prop', 'valeur_par_defaut'],
 | |
| ];
 | |
| ```
 | |
| 
 | |
| 2. Mettre à jour les tests unitaires
 | |
| 3. Réindexer Meilisearch si la propriété doit être filtrable
 | |
| 
 | |
| ### Modifier l'ordre des propriétés
 | |
| 
 | |
| Modifier simplement l'ordre dans le tableau `requiredProps`. L'enrichissement respectera automatiquement le nouvel ordre.
 | |
| 
 | |
| ## Troubleshooting
 | |
| 
 | |
| ### Les compteurs Quick Links sont à zéro
 | |
| 
 | |
| **Cause :** Meilisearch n'a pas été réindexé avec les nouveaux champs.
 | |
| 
 | |
| **Solution :**
 | |
| ```bash
 | |
| npm run meili:reindex
 | |
| ```
 | |
| 
 | |
| ### Un fichier n'est pas enrichi
 | |
| 
 | |
| **Cause possible :** Le fichier n'a pas été ouvert depuis le déploiement.
 | |
| 
 | |
| **Solution :** Ouvrir le fichier dans l'interface, ou utiliser le script d'enrichissement global.
 | |
| 
 | |
| ### Les dates ne sont pas au bon fuseau horaire
 | |
| 
 | |
| **Cause :** Le serveur utilise un fuseau différent.
 | |
| 
 | |
| **Solution :** Modifier la constante `TZ_OFFSET` dans `ensureFrontmatter.mjs`.
 | |
| 
 | |
| ## Roadmap
 | |
| 
 | |
| - [ ] Support de fuseaux horaires configurables
 | |
| - [ ] Interface d'édition de front-matter dans l'UI
 | |
| - [ ] Validation de schéma YAML personnalisable
 | |
| - [ ] Export/import de configurations de front-matter
 | |
| - [ ] Statistiques d'utilisation des Quick Links
 | |
| 
 | |
| ## Auteur
 | |
| 
 | |
| Bruno Charest - Implémentation complète (backend + frontend + tests)
 | |
| 
 | |
| Date : Octobre 2025
 |