327 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			327 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Phase 1 Implementation Guide - Windsurf Edition
 | |
| 
 | |
| ## Status: Ready for Implementation
 | |
| 
 | |
| All new files have been created:
 | |
| - ✅ `server/vault-metadata-loader.mjs` - Fast metadata loader
 | |
| - ✅ `server/performance-config.mjs` - Performance configuration
 | |
| - ✅ `src/app/services/vault.service.ts` - New VaultService with metadata-first
 | |
| - ✅ `scripts/test-performance.mjs` - Performance testing script
 | |
| 
 | |
| ## Next Steps: Minimal Modifications to Existing Files
 | |
| 
 | |
| ### Step 1: Add Metadata Endpoint to `server/index.mjs`
 | |
| 
 | |
| **Location**: After line 478 (after `/api/files/list` endpoint)
 | |
| 
 | |
| **Add these imports at the top of the file** (around line 9):
 | |
| ```javascript
 | |
| import { loadVaultMetadataOnly } from './vault-metadata-loader.mjs';
 | |
| import { MetadataCache, PerformanceLogger } from './performance-config.mjs';
 | |
| ```
 | |
| 
 | |
| **Add this cache instance** (around line 34, after `const vaultEventClients = new Set();`):
 | |
| ```javascript
 | |
| const metadataCache = new MetadataCache();
 | |
| ```
 | |
| 
 | |
| **Add this endpoint** (after line 478):
 | |
| ```javascript
 | |
| // Fast metadata endpoint - no content, no enrichment
 | |
| // Used for initial UI load (Phase 1 optimization)
 | |
| app.get('/api/vault/metadata', async (req, res) => {
 | |
|   try {
 | |
|     // Check cache first
 | |
|     const cached = metadataCache.get();
 | |
|     if (cached) {
 | |
|       console.log('[/api/vault/metadata] Returning cached metadata');
 | |
|       return res.json(cached);
 | |
|     }
 | |
| 
 | |
|     // Try Meilisearch first (already indexed)
 | |
|     const client = meiliClient();
 | |
|     const indexUid = vaultIndexName(vaultDir);
 | |
|     const index = await ensureIndexSettings(client, indexUid);
 | |
|     
 | |
|     const result = await index.search('', {
 | |
|       limit: 10000,
 | |
|       attributesToRetrieve: ['id', 'title', 'path', 'createdAt', 'updatedAt']
 | |
|     });
 | |
| 
 | |
|     const items = Array.isArray(result.hits) ? result.hits.map(hit => ({
 | |
|       id: hit.id,
 | |
|       title: hit.title,
 | |
|       filePath: hit.path,
 | |
|       createdAt: typeof hit.createdAt === 'number' ? new Date(hit.createdAt).toISOString() : hit.createdAt,
 | |
|       updatedAt: typeof hit.updatedAt === 'number' ? new Date(hit.updatedAt).toISOString() : hit.updatedAt,
 | |
|     })) : [];
 | |
| 
 | |
|     metadataCache.set(items);
 | |
|     console.log(`[/api/vault/metadata] Returned ${items.length} items from Meilisearch`);
 | |
|     res.json(items);
 | |
|   } catch (error) {
 | |
|     console.error('Failed to load metadata via Meilisearch, falling back to FS:', error);
 | |
|     try {
 | |
|       // Fallback: fast filesystem scan without enrichment
 | |
|       const notes = await loadVaultMetadataOnly(vaultDir);
 | |
|       const metadata = notes.map(n => ({
 | |
|         id: n.id,
 | |
|         title: n.title,
 | |
|         filePath: n.filePath,
 | |
|         createdAt: n.createdAt,
 | |
|         updatedAt: n.updatedAt
 | |
|       }));
 | |
|       
 | |
|       metadataCache.set(metadata);
 | |
|       console.log(`[/api/vault/metadata] Returned ${metadata.length} items from filesystem`);
 | |
|       res.json(metadata);
 | |
|     } catch (err2) {
 | |
|       console.error('FS fallback failed:', err2);
 | |
|       res.status(500).json({ error: 'Unable to load vault metadata.' });
 | |
|     }
 | |
|   }
 | |
| });
 | |
| ```
 | |
| 
 | |
| ### Step 2: Disable Enrichment During Startup in `server/index.mjs`
 | |
| 
 | |
| **Location**: Lines 138-141 in `loadVaultNotes()` function
 | |
| 
 | |
| **BEFORE:**
 | |
| ```javascript
 | |
| try {
 | |
|   // Enrichir automatiquement le frontmatter lors du chargement
 | |
|   const absPath = entryPath;
 | |
|   const enrichResult = await enrichFrontmatterOnOpen(absPath);
 | |
|   const content = enrichResult.content;
 | |
|   
 | |
|   const stats = fs.statSync(entryPath);
 | |
| ```
 | |
| 
 | |
| **AFTER:**
 | |
| ```javascript
 | |
| try {
 | |
|   // Skip enrichment during initial load for performance (Phase 1)
 | |
|   // Enrichment will happen on-demand when file is opened via /api/files
 | |
|   const content = fs.readFileSync(entryPath, 'utf-8');
 | |
|   
 | |
|   const stats = fs.statSync(entryPath);
 | |
| ```
 | |
| 
 | |
| ### Step 3: Invalidate Cache on File Changes in `server/index.mjs`
 | |
| 
 | |
| **Location**: Around line 260-275 (in the watcher 'add' event)
 | |
| 
 | |
| **MODIFY the watcher.on('add') handler:**
 | |
| ```javascript
 | |
| vaultWatcher.on('add', async (filePath) => {
 | |
|   if (filePath.toLowerCase().endsWith('.md')) {
 | |
|     // Invalidate metadata cache
 | |
|     metadataCache.invalidate();
 | |
|     
 | |
|     // Enrichir le frontmatter pour les nouveaux fichiers
 | |
|     try {
 | |
|       const enrichResult = await enrichFrontmatterOnOpen(filePath);
 | |
|       if (enrichResult.modified) {
 | |
|         console.log('[Watcher] Enriched frontmatter for new file:', path.basename(filePath));
 | |
|       }
 | |
|     } catch (enrichError) {
 | |
|       console.warn('[Watcher] Failed to enrich frontmatter for new file:', enrichError);
 | |
|     }
 | |
|     
 | |
|     // Puis indexer dans Meilisearch
 | |
|     upsertFile(filePath).catch(err => console.error('[Meili] Upsert on add failed:', err));
 | |
|   }
 | |
| });
 | |
| ```
 | |
| 
 | |
| **MODIFY the watcher.on('change') handler:**
 | |
| ```javascript
 | |
| vaultWatcher.on('change', (filePath) => {
 | |
|   if (filePath.toLowerCase().endsWith('.md')) {
 | |
|     // Invalidate metadata cache
 | |
|     metadataCache.invalidate();
 | |
|     
 | |
|     upsertFile(filePath).catch(err => console.error('[Meili] Upsert on change failed:', err));
 | |
|   }
 | |
| });
 | |
| ```
 | |
| 
 | |
| **MODIFY the watcher.on('unlink') handler:**
 | |
| ```javascript
 | |
| vaultWatcher.on('unlink', (filePath) => {
 | |
|   if (filePath.toLowerCase().endsWith('.md')) {
 | |
|     // Invalidate metadata cache
 | |
|     metadataCache.invalidate();
 | |
|     
 | |
|     const relativePath = path.relative(vaultDir, filePath).replace(/\\/g, '/');
 | |
|     deleteFile(relativePath).catch(err => console.error('[Meili] Delete failed:', err));
 | |
|   }
 | |
| });
 | |
| ```
 | |
| 
 | |
| ### Step 4: Update `src/app.component.ts`
 | |
| 
 | |
| **Location**: In the `ngOnInit()` method (around line 330-360)
 | |
| 
 | |
| **Find the ngOnInit() method and add vault initialization:**
 | |
| 
 | |
| ```typescript
 | |
| ngOnInit(): void {
 | |
|   // Initialize theme from storage
 | |
|   this.themeService.initFromStorage();
 | |
|   
 | |
|   // Initialize vault with metadata-first approach (Phase 1)
 | |
|   this.vaultService.initializeVault().then(() => {
 | |
|     console.log(`[AppComponent] Vault initialized with ${this.vaultService.getNotesCount()} notes`);
 | |
|     this.logService.log('VAULT_INITIALIZED', {
 | |
|       notesCount: this.vaultService.getNotesCount()
 | |
|     });
 | |
|   }).catch(error => {
 | |
|     console.error('[AppComponent] Failed to initialize vault:', error);
 | |
|     this.logService.log('VAULT_INIT_FAILED', { 
 | |
|       error: error instanceof Error ? error.message : String(error) 
 | |
|     }, 'error');
 | |
|   });
 | |
|   
 | |
|   // Log app start
 | |
|   this.logService.log('APP_START', {
 | |
|     viewport: {
 | |
|       width: typeof window !== 'undefined' ? window.innerWidth : 0,
 | |
|       height: typeof window !== 'undefined' ? window.innerHeight : 0,
 | |
|     },
 | |
|   });
 | |
| }
 | |
| ```
 | |
| 
 | |
| **Location**: In the `selectNote()` method (around line 380-400)
 | |
| 
 | |
| **Find the selectNote() method and ensure content is loaded:**
 | |
| 
 | |
| ```typescript
 | |
| async selectNote(noteId: string): Promise<void> {
 | |
|   let note = this.vaultService.getNoteById(noteId);
 | |
|   if (!note) {
 | |
|     // Try to lazy-load using Meilisearch/slug mapping
 | |
|     console.error(`Note not found: ${noteId}`);
 | |
|     return;
 | |
|   }
 | |
|   
 | |
|   // Ensure content is loaded before rendering (Phase 1)
 | |
|   if (!note.content) {
 | |
|     console.log(`[AppComponent] Loading content for note: ${noteId}`);
 | |
|     note = await this.vaultService.ensureNoteContent(noteId);
 | |
|     if (!note) {
 | |
|       console.error(`[AppComponent] Failed to load note: ${noteId}`);
 | |
|       return;
 | |
|     }
 | |
|   }
 | |
|   
 | |
|   this.selectedNoteId.set(noteId);
 | |
|   this.logService.log('NOTE_SELECTED', { noteId });
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## Testing
 | |
| 
 | |
| ### Test 1: Verify Metadata Endpoint
 | |
| 
 | |
| ```bash
 | |
| # Test the new endpoint
 | |
| curl http://localhost:3000/api/vault/metadata | head -50
 | |
| 
 | |
| # Measure response time
 | |
| time curl http://localhost:3000/api/vault/metadata > /dev/null
 | |
| ```
 | |
| 
 | |
| ### Test 2: Run Performance Comparison
 | |
| 
 | |
| ```bash
 | |
| # Run the performance test script
 | |
| node scripts/test-performance.mjs
 | |
| 
 | |
| # With custom base URL
 | |
| BASE_URL=http://localhost:4000 node scripts/test-performance.mjs
 | |
| ```
 | |
| 
 | |
| ### Test 3: Browser Console Measurements
 | |
| 
 | |
| ```javascript
 | |
| // Measure startup time
 | |
| performance.mark('app-start');
 | |
| // ... app loads ...
 | |
| performance.mark('app-ready');
 | |
| performance.measure('startup', 'app-start', 'app-ready');
 | |
| console.log(performance.getEntriesByName('startup')[0].duration);
 | |
| 
 | |
| // Check network requests
 | |
| const requests = performance.getEntriesByType('resource');
 | |
| const metadataReq = requests.find(r => r.name.includes('/api/vault/metadata'));
 | |
| const vaultReq = requests.find(r => r.name.includes('/api/vault'));
 | |
| 
 | |
| if (metadataReq) {
 | |
|   console.log(`Metadata endpoint: ${metadataReq.duration.toFixed(0)}ms, ${(metadataReq.transferSize / 1024).toFixed(0)}KB`);
 | |
| }
 | |
| if (vaultReq) {
 | |
|   console.log(`Full vault endpoint: ${vaultReq.duration.toFixed(0)}ms, ${(vaultReq.transferSize / 1024 / 1024).toFixed(2)}MB`);
 | |
| }
 | |
| ```
 | |
| 
 | |
| ## Expected Results
 | |
| 
 | |
| ### Before Phase 1
 | |
| ```
 | |
| Startup time (1000 files):    15-25 seconds
 | |
| Network payload:              5-10MB
 | |
| Memory usage:                 200-300MB
 | |
| Time to interactive:          20-35 seconds
 | |
| ```
 | |
| 
 | |
| ### After Phase 1
 | |
| ```
 | |
| Startup time (1000 files):    2-4 seconds ✅ (75% faster)
 | |
| Network payload:              0.5-1MB ✅ (90% reduction)
 | |
| Memory usage:                 50-100MB ✅ (75% reduction)
 | |
| Time to interactive:          3-5 seconds ✅ (80% faster)
 | |
| ```
 | |
| 
 | |
| ## Validation Checklist
 | |
| 
 | |
| - [ ] `/api/vault/metadata` endpoint returns data in < 1 second
 | |
| - [ ] Metadata payload is < 1MB for 1000 files
 | |
| - [ ] App UI is interactive within 2-3 seconds
 | |
| - [ ] Clicking on a note loads content smoothly (< 500ms)
 | |
| - [ ] No console errors or warnings
 | |
| - [ ] All existing features still work
 | |
| - [ ] Performance test script shows improvements
 | |
| - [ ] Tests pass: `npm run test`
 | |
| - [ ] E2E tests pass: `npm run test:e2e`
 | |
| 
 | |
| ## Troubleshooting
 | |
| 
 | |
| ### Issue: 404 on /api/vault/metadata
 | |
| - Ensure imports are added at top of `server/index.mjs`
 | |
| - Ensure endpoint is added before catch-all handlers
 | |
| - Check server logs for errors
 | |
| 
 | |
| ### Issue: Notes don't load when clicked
 | |
| - Verify `ensureNoteContent()` is called in `selectNote()`
 | |
| - Check browser console for errors
 | |
| - Verify `/api/files` endpoint is working
 | |
| 
 | |
| ### Issue: Startup time hasn't improved
 | |
| - Verify `/api/vault/metadata` is being called (check Network tab)
 | |
| - Check that enrichment was removed from `loadVaultNotes()`
 | |
| - Verify old `/api/vault` endpoint is not being called
 | |
| 
 | |
| ## Next Steps
 | |
| 
 | |
| 1. ✅ Create new files (DONE)
 | |
| 2. ⏳ Add imports and endpoint to `server/index.mjs`
 | |
| 3. ⏳ Disable enrichment in `loadVaultNotes()`
 | |
| 4. ⏳ Add cache invalidation to watcher
 | |
| 5. ⏳ Update `app.component.ts` initialization
 | |
| 6. ⏳ Test and validate
 | |
| 7. ⏳ Deploy to production
 | |
| 
 |