# 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 { 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