# Améliorations du Système de Sauvegarde Excalidraw ## Modifications Appliquées ### 1. Hash Stable et Déterministe **Fichier**: `src/app/features/drawings/drawings-editor.component.ts` **Problème résolu**: Hash instable causant des faux positifs/négatifs pour le dirty state. **Solution**: - Tri stable des éléments par `id` pour éviter les changements de hash dus à l'ordre - Tri stable des clés de `files` pour éviter les changements de hash dus à l'ordre des propriétés - Suppression des propriétés volatiles (`version`, `versionNonce`, `updated`) ```typescript private hashScene(scene: ExcalidrawScene | null): string { try { if (!scene) return ''; // Normalize elements: remove volatile properties const normEls = Array.isArray(scene.elements) ? scene.elements.map((el: any) => { const { version, versionNonce, updated, ...rest } = el || {}; return rest; }) : []; // Stable sort of elements by id const sortedEls = normEls.slice().sort((a: any, b: any) => (a?.id || '').localeCompare(b?.id || '') ); // Stable sort of files keys const filesObj = scene.files && typeof scene.files === 'object' ? scene.files : {}; const sortedFilesKeys = Object.keys(filesObj).sort(); const sortedFiles: Record = {}; for (const k of sortedFilesKeys) sortedFiles[k] = filesObj[k]; const stable = { elements: sortedEls, files: sortedFiles }; return btoa(unescape(encodeURIComponent(JSON.stringify(stable)))); } catch (error) { console.error('Error hashing scene:', error); return ''; } } ``` ### 2. Prévention des Sauvegardes Concurrentes **Fichier**: `src/app/features/drawings/drawings-editor.component.ts` **Problème résolu**: Plusieurs PUT peuvent être lancés simultanément, causant des conflits. **Solution**: Ajout d'un filtre pour ignorer les autosaves si une sauvegarde est déjà en cours. ```typescript this.saveSub = sceneChange$.pipe( distinctUntilChanged((prev, curr) => prev.hash === curr.hash), debounceTime(2000), filter(({ hash }) => this.lastSavedHash !== hash), filter(() => !this.isSaving()), // ⬅️ NOUVEAU: Empêche les sauvegardes concurrentes tap(({ hash }) => { console.log('💾 Autosaving... hash:', hash.substring(0, 10)); this.isSaving.set(true); this.error.set(null); }), switchMap(({ scene, hash }) => this.files.put(this.path, scene).pipe(...)) ).subscribe(); ``` ### 3. Suppression du Mode readOnly Pendant la Sauvegarde **Fichier**: `src/app/features/drawings/drawings-editor.component.html` **Problème résolu**: Le passage en `readOnly` pendant la sauvegarde peut perturber les événements `onChange` et bloquer des micro-changements. **Solution**: Suppression de `[readOnly]="isSaving()"`, conservation uniquement de l'indicateur visuel d'opacité. ```html ``` ### 4. Logs de Diagnostic Améliorés **Fichiers**: - `web-components/excalidraw/ExcalidrawElement.tsx` - `web-components/excalidraw/define.ts` - `src/app/features/drawings/drawings-editor.component.html` **Ajouts**: - Log visible `console.log` au lieu de `console.debug` pour `scene-change` - Log du `ready` event dans le web component - Log du `ready` event dans le template Angular - Tous les événements avec `bubbles: true, composed: true` ```typescript // ExcalidrawElement.tsx const onChange = (elements: any[], appState: Partial, files: any) => { if (!host) return; const detail: SceneChangeDetail = { elements, appState, files, source: 'user' }; console.log('[excalidraw-editor] 📝 SCENE-CHANGE dispatched', { elCount: Array.isArray(elements) ? elements.length : 'n/a' }); host.dispatchEvent(new CustomEvent('scene-change', { detail, bubbles: true, composed: true })); }; ``` ```typescript // define.ts const onReady = () => { console.log('[excalidraw-editor] 🎨 READY event dispatched', { apiAvailable: !!(this as any).__excalidrawAPI }); this.dispatchEvent(new CustomEvent('ready', { detail: { apiAvailable: !!(this as any).__excalidrawAPI }, bubbles: true, composed: true })); }; ``` ## Check-list de Test ### 1. Vérifier les Événements de Base Ouvrir la console DevTools et observer: ```javascript // Test manuel dans la console document.querySelector('excalidraw-editor') ?.addEventListener('scene-change', e => console.log('✅ SCENE-CHANGE reçu', e?.detail)); ``` **Attendu**: - ✅ `[excalidraw-editor] 🎨 READY event dispatched` au chargement - ✅ `READY { detail: { apiAvailable: true } }` dans le template Angular - ✅ `🎨 Excalidraw Ready - Binding listeners` dans le composant - ✅ `🔗 Binding Excalidraw host listeners` dans le composant - ✅ `[excalidraw-editor] 📝 SCENE-CHANGE dispatched` à chaque modification - ✅ `✏️ Dirty flagged (event)` dans Angular après chaque modification ### 2. Tester le Dirty State 1. Ouvrir un fichier `.excalidraw.md` 2. Vérifier: indicateur ⚫ Gris "Sauvegardé" 3. Ajouter un rectangle 4. Vérifier: indicateur 🔴 Rouge "Non sauvegardé" immédiatement 5. Attendre 2 secondes 6. Vérifier console: `💾 Autosaving...` 7. Vérifier console: `✅ Autosave successful` 8. Vérifier: indicateur ⚫ Gris "Sauvegardé" ### 3. Tester la Sauvegarde Manuelle 1. Modifier le dessin 2. Appuyer `Ctrl+S` (ou cliquer sur l'indicateur) 3. Vérifier console: `💾 Manual save triggered` 4. Vérifier console: `📤 Sending save request...` 5. Vérifier console: `✅ Manual save successful` 6. Vérifier toast: "Sauvegarde réussie" 7. Vérifier Network: `PUT /api/files?path=...` 8. Vérifier: indicateur ⚫ Gris "Sauvegardé" ### 4. Tester la Stabilité du Hash 1. Ouvrir un fichier 2. Ajouter un élément → attendre autosave 3. Déplacer légèrement l'élément → attendre autosave 4. Vérifier console: les hash doivent être différents 5. Ne rien toucher pendant 5 secondes 6. Vérifier: pas de nouveaux autosaves (hash stable) ### 5. Tester les Sauvegardes Concurrentes 1. Modifier rapidement plusieurs éléments 2. Immédiatement appuyer `Ctrl+S` plusieurs fois 3. Vérifier console: un seul `💾 Autosaving...` à la fois 4. Vérifier Network: pas de requêtes PUT simultanées ### 6. Tester les Conflits (ETag) 1. Ouvrir un fichier 2. Modifier externellement le fichier (autre éditeur) 3. Modifier dans Excalidraw 4. Attendre l'autosave 5. Vérifier: bannière de conflit apparaît 6. Tester "Recharger depuis le disque" ou "Écraser" ## Comportement Attendu ### Séquence de Chargement ``` 1. GET /api/files?path=test.excalidraw.md 2. [excalidraw-editor] 🎨 READY event dispatched 3. READY { detail: { apiAvailable: true } } 4. 🎨 Excalidraw Ready - Binding listeners 5. 🔗 Binding Excalidraw host listeners 6. ✓ Dirty check and Autosave subscriptions active 7. Indicateur: ⚫ Gris "Sauvegardé" ``` ### Séquence de Modification ``` 1. Utilisateur dessine un rectangle 2. [excalidraw-editor] 📝 SCENE-CHANGE dispatched { elCount: 1 } 3. ✏️ Dirty flagged (event) { lastSaved: 'abc123...', current: 'def456...' } 4. Indicateur: 🔴 Rouge "Non sauvegardé" 5. (attente 2s) 6. 💾 Autosaving... hash: def456... 7. PUT /api/files?path=... (avec If-Match: "...") 8. ✅ Autosave successful { newHash: 'def456...' } 9. Indicateur: ⚫ Gris "Sauvegardé" ``` ### Séquence de Sauvegarde Manuelle ``` 1. Utilisateur appuie Ctrl+S 2. 💾 Manual save triggered 3. 📤 Sending save request... 4. 🧩 Snapshot { elements: 3, hasFiles: true } 5. PUT /api/files?path=... 6. 📥 Save response { rev: '...' } 7. 🔁 Verify after save { ok: true, ... } 8. ✅ Manual save successful 9. Toast: "Sauvegarde réussie" 10. Indicateur: ⚫ Gris "Sauvegardé" ``` ## Troubleshooting ### Pas de logs `[excalidraw-editor]` → Le web component ne se charge pas. Vérifier `ngOnInit()` et l'import du custom element. ### `READY` n'apparaît jamais → L'API Excalidraw ne s'initialise pas. Vérifier la console pour des erreurs React. ### `SCENE-CHANGE` n'apparaît jamais → Le `onChange` n'est pas appelé. Vérifier que le composant React reçoit bien `__host`. ### `✏️ Dirty flagged` n'apparaît jamais → Le binding n'a pas eu lieu. Vérifier que `onExcalidrawReady()` est appelé. ### Hash change en permanence (autosave en boucle) → Le hash n'est pas stable. Vérifier que la nouvelle version de `hashScene()` est bien appliquée. ### Indicateur reste rouge après autosave → Vérifier que `this.dirty.set(false)` est bien appelé dans le `tap()` après succès. ### Conflits 409 en permanence → Vérifier que le serveur renvoie bien un `ETag` dans les réponses GET et PUT. ## Fichiers Modifiés 1. ✅ `src/app/features/drawings/drawings-editor.component.ts` - `hashScene()`: tri stable des éléments et files - `bindEditorHostListeners()`: ajout du filtre `!isSaving()` 2. ✅ `src/app/features/drawings/drawings-editor.component.html` - Suppression de `[readOnly]="isSaving()"` - Ajout de log du `ready` event 3. ✅ `web-components/excalidraw/ExcalidrawElement.tsx` - Log visible `console.log` pour `scene-change` 4. ✅ `web-components/excalidraw/define.ts` - Log visible `console.log` pour `ready` - Ajout de `bubbles: true, composed: true` au `ready` event ## Prochaines Étapes Une fois les tests validés: 1. **Retirer les logs de debug** (optionnel, utiles pour le monitoring) 2. **Tester avec des fichiers volumineux** (100+ éléments) 3. **Tester sur mobile** (touch events) 4. **Ajouter des tests E2E** pour la sauvegarde automatique 5. **Documenter le comportement ETag** côté backend ## Résumé ✅ Hash stable et déterministe (tri des éléments et files) ✅ Prévention des sauvegardes concurrentes ✅ Suppression du readOnly pendant la sauvegarde ✅ Logs de diagnostic améliorés ✅ Événements avec bubbles et composed ✅ Check-list de test complète