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
|