# 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: auteur: Bruno Charest creation_date: modification_date: 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
  • ``` #### 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