# đŸ—ïž Architecture Technique - Conseil de Classe ## Vue d'ensemble Le module **Conseil de Classe** implĂ©mente une architecture en couches avec sĂ©paration des responsabilitĂ©s, suivant les patterns **Repository**, **Service Layer** et **Factory**. ``` ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ 🎹 Frontend │ │ 📡 Backend │ │ đŸ—„ïž Database │ │ │ │ │ │ │ │ ‱ Mode Focus │◄──â–ș│ ‱ Services │◄──â–ș│ ‱ Models │ │ ‱ Auto-save │ │ ‱ Repositories │ │ ‱ Relationships │ │ ‱ Sync bidirec. │ │ ‱ API Routes │ │ ‱ Constraints │ └─────────────────┘ └─────────────────┘ └─────────────────┘ ``` ## 🔧 Backend Architecture ### 1. Services Layer #### CouncilPreparationService **ResponsabilitĂ©** : Orchestration principale et agrĂ©gation des donnĂ©es ```python class CouncilPreparationService: def __init__(self, student_evaluation_service: StudentEvaluationService, appreciation_service: AppreciationService, assessment_repo: AssessmentRepository): self.student_evaluation = student_evaluation_service self.appreciation = appreciation_service self.assessment_repo = assessment_repo def prepare_council_data(self, class_group_id: int, trimester: int) -> CouncilPreparationData: """ Point d'entrĂ©e principal - agrĂšge toutes les donnĂ©es nĂ©cessaires Flow: 1. RĂ©cupĂšre rĂ©sumĂ©s Ă©lĂšves via StudentEvaluationService 2. Calcule statistiques classe 3. RĂ©cupĂšre statistiques apprĂ©ciations via AppreciationService 4. Retourne CouncilPreparationData consolidĂ© """ student_summaries = self.student_evaluation.get_students_summaries(class_group_id, trimester) class_statistics = self._calculate_class_statistics(student_summaries) appreciation_stats = self.appreciation.get_completion_stats(class_group_id, trimester) return CouncilPreparationData(...) ``` #### StudentEvaluationService **ResponsabilitĂ©** : Calculs de performances et moyennes Ă©lĂšves ```python class StudentEvaluationService: def calculate_student_trimester_average(self, student_id: int, trimester: int) -> Optional[float]: """ Algorithme de calcul de moyenne pondĂ©rĂ©e: weighted_sum = ÎŁ(score_evaluation × coefficient_evaluation) total_coefficient = ÎŁ(coefficient_evaluation) moyenne = weighted_sum / total_coefficient Gestion des cas spĂ©ciaux: - Notes manquantes : Exclus du calcul - Valeurs '.' : Comptent comme 0 mais incluent le coefficient - Valeurs 'd' : DispensĂ©, exclu complĂštement """ assessments = self.assessment_repo.find_completed_by_class_trimester(class_id, trimester) weighted_sum = total_coefficient = 0.0 for assessment in assessments: score = self._calculate_assessment_score_for_student(assessment, student_id) if score is not None: weighted_sum += score * assessment.coefficient total_coefficient += assessment.coefficient return round(weighted_sum / total_coefficient, 2) if total_coefficient > 0 else None def _determine_performance_status(self, average: Optional[float]) -> str: """ Classification automatique des performances: - excellent: ≄ 16/20 - good: 14-15.99/20 - average: 10-13.99/20 - struggling: < 10/20 - no_data: Pas de notes disponibles """ if not average: return 'no_data' if average >= 16: return 'excellent' elif average >= 14: return 'good' elif average >= 10: return 'average' else: return 'struggling' ``` #### AppreciationService **ResponsabilitĂ©** : CRUD et workflow des apprĂ©ciations ```python class AppreciationService: def save_appreciation(self, data: Dict) -> CouncilAppreciation: """ Sauvegarde avec logique de crĂ©ation/mise Ă  jour automatique Business Rules: - CrĂ©ation si pas d'apprĂ©ciation existante - Mise Ă  jour si existe dĂ©jĂ  - Horodatage automatique (last_modified) - Validation des champs requis - Gestion du statut (draft/finalized) """ return self.appreciation_repo.create_or_update( student_id=data['student_id'], class_group_id=data['class_group_id'], trimester=data['trimester'], data={ 'general_appreciation': data.get('general_appreciation'), 'strengths': data.get('strengths'), 'areas_for_improvement': data.get('areas_for_improvement'), 'status': data.get('status', 'draft') } ) def get_completion_stats(self, class_group_id: int, trimester: int) -> Dict: """ Calcul des statistiques de complĂ©tion: - completed_appreciations: Nombre avec contenu - total_students: Nombre total d'Ă©lĂšves - completion_rate: Pourcentage de complĂ©tion - average_length: Longueur moyenne des apprĂ©ciations """ return self.appreciation_repo.get_completion_stats(class_group_id, trimester) ``` ### 2. Repository Layer #### Architecture gĂ©nĂ©rale ```python class BaseRepository: """Repository gĂ©nĂ©rique avec opĂ©rations CRUD communes""" def get_or_404(self, id: int) -> Model: """RĂ©cupĂ©ration avec gestion 404 automatique""" def find_by_filters(self, **filters) -> List[Model]: """RequĂȘte avec filtres dynamiques""" def create_or_update(self, **data) -> Model: """Upsert pattern avec gestion des conflits""" class AppreciationRepository(BaseRepository): def find_by_student_trimester(self, student_id: int, class_group_id: int, trimester: int) -> Optional[CouncilAppreciation]: """ RequĂȘte optimisĂ©e avec index composite: INDEX idx_appreciation_lookup ON council_appreciations (student_id, class_group_id, trimester) """ return CouncilAppreciation.query.filter_by( student_id=student_id, class_group_id=class_group_id, trimester=trimester ).first() def get_completion_stats(self, class_group_id: int, trimester: int) -> Dict: """ RequĂȘte d'agrĂ©gation optimisĂ©e avec sous-requĂȘtes: SELECT COUNT(CASE WHEN general_appreciation IS NOT NULL AND general_appreciation != '' THEN 1 END) as completed, COUNT(DISTINCT s.id) as total_students, AVG(LENGTH(general_appreciation)) as avg_length FROM students s LEFT JOIN council_appreciations ca ON ... WHERE s.class_group_id = ? """ ``` ### 3. API Routes Layer #### Structure des endpoints ```python # /routes/classes.py @bp.route('//council') def council_preparation(id): """ Page principale - Rendu HTML complet Validations: - Trimestre obligatoire et valide (1,2,3) - Classe existe et accessible - DonnĂ©es prĂ©parĂ©es via CouncilServiceFactory Template: class_council_preparation.html Context: class_group, trimester, council_data, student_summaries, statistics """ trimester = request.args.get('trimestre', type=int) if not trimester or trimester not in [1, 2, 3]: flash('Veuillez sĂ©lectionner un trimestre pour prĂ©parer le conseil de classe.', 'error') return redirect(url_for('classes.dashboard', id=id)) council_service = CouncilServiceFactory.create_council_preparation_service() council_data = council_service.prepare_council_data(id, trimester) return render_template('class_council_preparation.html', ...) @bp.route('//council/appreciation/', methods=['POST']) def save_appreciation_api(class_id, student_id): """ API AJAX - Sauvegarde d'apprĂ©ciation Input validation: - JSON content-type requis - student_id appartient Ă  class_id (security) - trimester valide (1,2,3) - Longueur apprĂ©ciation < 2000 chars Response format: { "success": true, "appreciation_id": 123, "last_modified": "2025-08-10T14:30:00.000Z", "status": "draft", "has_content": true } Error handling: - 400: DonnĂ©es invalides - 403: ÉlĂšve pas dans cette classe - 500: Erreur serveur """ try: data = request.get_json() if not data: return jsonify({'success': False, 'error': 'DonnĂ©es manquantes'}), 400 # Security: VĂ©rifier appartenance Ă©lĂšve Ă  classe student = Student.query.get_or_404(student_id) if student.class_group_id != class_id: return jsonify({'success': False, 'error': 'ÉlĂšve non trouvĂ© dans cette classe'}), 403 # Business logic via service appreciation_service = CouncilServiceFactory.create_appreciation_service() result = appreciation_service.save_appreciation({ 'student_id': student_id, 'class_group_id': class_id, 'trimester': data.get('trimester'), 'general_appreciation': data.get('appreciation', '').strip() or None, 'status': 'draft' }) return jsonify({ 'success': True, 'appreciation_id': result.id, 'last_modified': result.last_modified.isoformat(), 'status': result.status, 'has_content': result.has_content }) except Exception as e: current_app.logger.error(f'Erreur sauvegarde apprĂ©ciation Ă©lĂšve {student_id}: {e}') return jsonify({'success': False, 'error': 'Erreur lors de la sauvegarde'}), 500 ``` ### 4. Data Models #### CouncilAppreciation ```python class CouncilAppreciation(db.Model): __tablename__ = 'council_appreciations' # Primary Key id = db.Column(db.Integer, primary_key=True) # Foreign Keys avec contraintes student_id = db.Column(db.Integer, db.ForeignKey('students.id'), nullable=False) class_group_id = db.Column(db.Integer, db.ForeignKey('class_groups.id'), nullable=False) # Business Data trimester = db.Column(db.Integer, nullable=False) # 1, 2, ou 3 general_appreciation = db.Column(db.Text) strengths = db.Column(db.Text) areas_for_improvement = db.Column(db.Text) status = db.Column(db.String(20), default='draft') # draft, finalized # Metadata created_at = db.Column(db.DateTime, default=datetime.utcnow) last_modified = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) # Constraints __table_args__ = ( # Un seul apprĂ©ciation par Ă©lĂšve/classe/trimestre db.UniqueConstraint('student_id', 'class_group_id', 'trimester'), # Index pour les requĂȘtes frĂ©quentes db.Index('idx_appreciation_lookup', 'student_id', 'class_group_id', 'trimester'), db.Index('idx_class_trimester', 'class_group_id', 'trimester'), # Validation trimestre db.CheckConstraint('trimester IN (1, 2, 3)'), # Validation statut db.CheckConstraint("status IN ('draft', 'finalized')") ) # Relationships student = db.relationship('Student', backref='council_appreciations') class_group = db.relationship('ClassGroup', backref='council_appreciations') @property def has_content(self) -> bool: """VĂ©rifie si l'apprĂ©ciation a du contenu significatif""" return bool( (self.general_appreciation and self.general_appreciation.strip()) or (self.strengths and self.strengths.strip()) or (self.areas_for_improvement and self.areas_for_improvement.strip()) ) def to_dict(self) -> Dict: """SĂ©rialisation pour API JSON""" return { 'id': self.id, 'student_id': self.student_id, 'class_group_id': self.class_group_id, 'trimester': self.trimester, 'general_appreciation': self.general_appreciation, 'strengths': self.strengths, 'areas_for_improvement': self.areas_for_improvement, 'status': self.status, 'has_content': self.has_content, 'created_at': self.created_at.isoformat() if self.created_at else None, 'last_modified': self.last_modified.isoformat() if self.last_modified else None } ``` ## 🎹 Frontend Architecture ### 1. Modular JavaScript Architecture #### Structure gĂ©nĂ©rale ```javascript // Pattern: Composition over Inheritance class CouncilPreparation { constructor(classId, options = {}) { // État centralisĂ© this.state = { currentTrimester: 2, expandedStudents: new Set(), savingStates: new Map(), modifiedAppreciations: new Set(), // Focus mode state isFocusMode: false, focusCurrentIndex: 0, filteredStudents: [] }; // Composition des gestionnaires spĂ©cialisĂ©s this.stateManager = new StateManager(this); // URL state & persistence this.filterManager = new FilterManager(this); // Search, sort, filters this.autoSaveManager = new AutoSaveManager(this); // Auto-save logic this.uiManager = new UIManager(this); // Card animations this.focusManager = new FocusManager(this); // Focus mode } } ``` #### StateManager - Persistance Ă©tat ```javascript class StateManager { restoreState() { """ Restauration depuis URL et localStorage: URL params: ?trimestre=2&sort=average&filter=struggling localStorage: expanded_students, focus_mode_preference Flow: 1. Parse URL parameters 2. Restore localStorage preferences 3. Apply initial state to DOM elements 4. Trigger initial filters/sorts """ const params = new URLSearchParams(location.search); this.parent.state.sortBy = params.get('sort') || 'alphabetical'; this.parent.state.filterStatus = params.get('filter') || 'all'; // Apply to DOM this.applyInitialState(); } saveState() { """ Persistance dans URL pour bookmarking/refresh: Format: /classes/5/council?trimestre=2&sort=average&filter=struggling Benefits: - État persistant sur F5 - URLs partageables - Navigation browser (back/forward) """ const params = new URLSearchParams(location.search); params.set('sort', this.parent.state.sortBy); params.set('filter', this.parent.state.filterStatus); history.replaceState(null, '', `${location.pathname}?${params.toString()}`); } } ``` #### FilterManager - Filtrage intelligent ```javascript class FilterManager { applyFilters() { """ Algorithme de filtrage multi-critĂšres avec performances optimisĂ©es: 1. Single DOM query pour tous les Ă©lĂ©ments 2. Filtrage en mĂ©moire (shouldShowStudent) 3. Application CSS display/order en batch 4. Animations staggered pour UX fluide Performance: O(n) oĂč n = nombre d'Ă©lĂšves """ const students = Array.from(document.querySelectorAll('[data-student-card]')); let visibleCount = 0; students.forEach((studentCard, index) => { const isVisible = this.shouldShowStudent(studentCard); if (isVisible) { studentCard.style.display = ''; visibleCount++; // Staggered animation pour UX fluide setTimeout(() => { studentCard.style.opacity = '1'; studentCard.style.transform = 'translateY(0)'; }, index * 50); } else { studentCard.style.display = 'none'; } }); this.applySorting(); this.updateResultsCounter(visibleCount, students.length); // Notification au FocusManager pour mise Ă  jour this.parent.focusManager?.onFiltersChanged(); } shouldShowStudent(studentCard) { """ CritĂšres de filtrage combinĂ©s: 1. Recherche textuelle (nom/prĂ©nom) 2. Statut de performance (excellent/good/average/struggling) 3. État apprĂ©ciation (completed/pending) Logic: AND entre tous les critĂšres actifs """ const studentName = studentCard.dataset.studentName?.toLowerCase() || ''; const performanceStatus = studentCard.dataset.performanceStatus; const hasAppreciation = studentCard.dataset.hasAppreciation === 'true'; // Text search filter if (this.parent.state.searchTerm && !studentName.includes(this.parent.state.searchTerm)) { return false; } // Performance status filter if (this.parent.state.filterStatus !== 'all') { switch (this.parent.state.filterStatus) { case 'completed': return hasAppreciation; case 'pending': return !hasAppreciation; case 'struggling': return performanceStatus === 'struggling'; } } return true; } } ``` #### AutoSaveManager - Sauvegarde intelligente ```javascript class AutoSaveManager { constructor(councilPrep) { this.parent = councilPrep; this.pendingSaves = new Map(); // Par Ă©lĂšve this.saveQueue = []; // File FIFO this.isSaving = false; // Mutex } queueSave(studentId, appreciation, immediate = false) { """ File de sauvegarde avec deduplication automatique: Algorithm: 1. Remove previous queued save for same student (deduplication) 2. Add new save task to queue 3. Process queue if not already processing 4. Immediate saves bypass queue for user-triggered actions Benefits: - Évite les requĂȘtes multiples pour mĂȘme Ă©lĂšve - Throttling automatique (100ms entre saves) - PrioritĂ© aux sauvegardes utilisateur (immediate=true) """ const saveTask = { studentId, appreciation, timestamp: Date.now(), immediate }; if (immediate) { this.executeSave(saveTask); } else { // Deduplication: Remove previous save for this student this.saveQueue = this.saveQueue.filter(task => task.studentId !== studentId); this.saveQueue.push(saveTask); this.processSaveQueue(); } } async executeSave(saveTask) { """ ExĂ©cution HTTP avec gestion d'erreurs robuste: Flow: 1. Show saving indicator 2. HTTP POST avec retry logic 3. Parse response et validation 4. Update UI states (success/error) 5. Sync avec Ă©lĂ©ment original si mode focus Error handling: - Network errors: Retry + user notification - Validation errors: Show specific message - Server errors: Log + generic message """ const { studentId, appreciation } = saveTask; try { this.showSavingState(studentId, true); const response = await fetch(`/classes/${this.parent.classId}/council/appreciation/${studentId}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ appreciation: appreciation, trimester: this.parent.state.currentTrimester }) }); const result = await response.json(); if (response.ok && result.success) { this.showSavedState(studentId); this.parent.state.modifiedAppreciations.delete(studentId); // Update UI metadata this.updateLastModified(studentId, result.last_modified); this.updateAppreciationStatus(studentId, result.has_content); } else { throw new Error(result.error || 'Erreur de sauvegarde'); } } catch (error) { console.error('Erreur sauvegarde apprĂ©ciation:', error); this.showErrorState(studentId, error.message); this.parent.showToast('Erreur sauvegarde', 'error'); } finally { this.showSavingState(studentId, false); } } } ``` ### 2. Focus Mode - Architecture avancĂ©e #### Concept clĂ© : Clonage d'Ă©lĂ©ments avec Ă©vĂ©nements ```javascript class FocusManager { showCurrentStudent() { """ Clonage intelligent avec attachement d'Ă©vĂ©nements: Problem: DOM cloneNode() ne clone pas les event listeners Solution: Re-attach events avec bindFocusStudentEvents() Flow: 1. Clone l'Ă©lĂ©ment DOM Ă©lĂšve courant 2. Marquer avec data-focus-clone-of pour sync 3. Force expand appreciation section 4. Re-attach tous les event listeners 5. Auto-focus sur textarea 6. Optimize layout (no-scroll) """ const currentStudent = this.parent.state.filteredStudents[this.parent.state.focusCurrentIndex]; const clonedStudent = currentStudent.cloneNode(true); // TraçabilitĂ© pour synchronisation const studentId = clonedStudent.dataset.studentCard; clonedStudent.setAttribute('data-focus-clone-of', studentId); // Force expand + styling const detailsSection = clonedStudent.querySelector('[data-student-details]'); detailsSection.classList.remove('hidden'); detailsSection.style.height = 'auto'; clonedStudent.classList.add('focus-mode-student'); // Replace content + re-attach events focusContainer.innerHTML = ''; focusContainer.appendChild(clonedStudent); this.bindFocusStudentEvents(clonedStudent, studentId); // UX enhancements this.focusAppreciationTextarea(clonedStudent); this.optimizeHeight(); } bindFocusStudentEvents(clonedStudent, studentId) { """ Re-attachement complet des Ă©vĂ©nements pour Ă©lĂ©ment clonĂ©: Events Ă  re-crĂ©er: 1. textarea input/blur → auto-save avec sync 2. save button click → manual save 3. finalize button → confirmation workflow 4. character counter → real-time update Sync bidirectionnelle: - Focus → Original: syncAppreciationToOriginal() - Focus → Original: syncAppreciationStatusToOriginal() """ const textarea = clonedStudent.querySelector(`[data-appreciation-textarea][data-student-id="${studentId}"]`); if (textarea) { // Auto-save avec debounce const saveHandler = this.parent.autoSaveManager.debounce(() => { this.saveFocusAppreciation(studentId, textarea.value); }, this.parent.options.debounceTime); // Input avec sync temps rĂ©el textarea.addEventListener('input', (e) => { this.parent.state.modifiedAppreciations.add(studentId); this.syncAppreciationToOriginal(studentId, e.target.value); // Sync bidirectionnelle saveHandler(); }); // Blur avec save immĂ©diat textarea.addEventListener('blur', () => { if (this.parent.state.modifiedAppreciations.has(studentId)) { this.saveFocusAppreciation(studentId, textarea.value, true); } }); } } syncAppreciationToOriginal(studentId, value) { """ Synchronisation Focus → Liste en temps rĂ©el: Challenge: Maintenir cohĂ©rence entre Ă©lĂ©ment clonĂ© et original Solution: Sync immĂ©diate sur chaque modification Benefits: - Pas de perte de donnĂ©es si switch de mode - État cohĂ©rent entre vues - UX fluide """ const originalTextarea = document.querySelector(`[data-student-card="${studentId}"] [data-appreciation-textarea]`); if (originalTextarea && originalTextarea.value !== value) { originalTextarea.value = value; } } } ``` #### Navigation et UX ```javascript // Keyboard shortcuts avec gestion d'Ă©tat bindKeyboardShortcuts() { document.addEventListener('keydown', (e) => { if (!this.parent.state.isFocusMode) return; switch (e.key) { case 'Escape': // Quick exit avec confirmation si modifications if (this.parent.state.modifiedAppreciations.size > 0) { if (confirm('Des modifications non sauvegardĂ©es seront perdues. Continuer ?')) { this.toggleFocusMode(false); } } else { this.toggleFocusMode(false); } break; case 'ArrowLeft': this.navigatePrevious(); break; case 'ArrowRight': this.navigateNext(); break; } }); } focusAppreciationTextarea(clonedStudent) { """ Auto-focus intelligent avec gestion de contexte: Features: 1. Focus sur textarea avec dĂ©lai pour animation 2. Curseur positionnĂ© en fin de texte existant 3. Scroll smooth vers Ă©lĂ©ment si nĂ©cessaire 4. Compatible mobile (pas de keyboard pop automatique) """ setTimeout(() => { const textarea = clonedStudent.querySelector('[data-appreciation-textarea]'); if (textarea) { textarea.focus(); // Cursor Ă  la fin pour continuer Ă©criture const textLength = textarea.value.length; textarea.setSelectionRange(textLength, textLength); // Scroll smooth si nĂ©cessaire textarea.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' }); } }, 100); } ``` ## 🔄 Patterns et Optimisations ### 1. Repository Pattern - **Avantages** : DĂ©couplage, testabilitĂ©, rĂ©utilisabilitĂ© - **Implementation** : BaseRepository avec mĂ©thodes communes - **Optimisations** : RequĂȘtes avec jointures, index optimaux ### 2. Service Layer Pattern - **Avantages** : Logique mĂ©tier centralisĂ©e, transactions - **Implementation** : Services spĂ©cialisĂ©s avec injection de dĂ©pendances - **Factory** : CouncilServiceFactory pour crĂ©ation avec dĂ©pendances ### 3. Frontend State Management - **Centralized State** : Un seul object state par instance - **Immutable Updates** : Pas de mutation directe de state - **Event-driven** : Communication entre modules via Ă©vĂ©nements ### 4. Performance Optimizations #### Backend - **Database** : Index composites sur foreign keys + trimester - **Queries** : Eager loading avec joinedload() pour Ă©viter N+1 - **Caching** : Pas de cache DB (donnĂ©es temps rĂ©el requises) #### Frontend - **DOM Queries** : Cache des sĂ©lecteurs dans this.elements - **Debouncing** : Auto-save et recherche avec dĂ©lais optimaux - **Animation** : CSS transitions > JavaScript animations - **Memory** : Cleanup des event listeners sur mode changes ## đŸ§Ș Testing Strategy ### Backend Tests ```python # tests/test_council_services.py class TestStudentEvaluationService: def test_calculate_trimester_average_with_coefficients(self): """Test calcul moyenne pondĂ©rĂ©e avec diffĂ©rents coefficients""" # Given: Évaluations avec coefficients diffĂ©rents # When: Calcul moyenne Ă©lĂšve # Then: RĂ©sultat pondĂ©rĂ© correct def test_performance_status_classification(self): """Test classification automatique des performances""" # Test cases: 18.5→excellent, 14.2→good, 11.8→average, 8.5→struggling, None→no_data class TestAppreciationService: def test_create_or_update_logic(self): """Test logique crĂ©ation/mise Ă  jour d'apprĂ©ciation""" def test_completion_stats_calculation(self): """Test calcul statistiques de completion""" class TestCouncilPreparationService: def test_prepare_council_data_integration(self): """Test d'intĂ©gration complet du workflow""" ``` ### Frontend Tests ```javascript // tests/council-preparation.test.js describe('FocusManager', () => { test('should sync appreciation between focus and list mode', () => { // Given: Text entered in focus mode // When: Switch to list mode // Then: Same text appears in list mode }); test('should auto-focus textarea on student navigation', () => { // Given: Focus mode active // When: Navigate to next student // Then: Textarea has focus and cursor at end }); }); describe('AutoSaveManager', () => { test('should debounce multiple saves for same student', () => { // Given: Multiple rapid text changes // When: Changes stop // Then: Only one HTTP request sent after debounce delay }); }); ``` ## 🚀 Deployment & Monitoring ### Performance Metrics - **Page Load Time** : < 2s pour classe de 35 Ă©lĂšves - **Auto-save Latency** : < 500ms pour sauvegarde simple - **Memory Usage** : < 50MB JavaScript heap pour session complĂšte - **Database** : < 100ms pour requĂȘtes agrĂ©gĂ©es ### Error Tracking - **JavaScript Errors** : Console logging + remote tracking - **API Failures** : HTTP status codes + error messages - **User Experience** : Toast notifications + retry mechanisms --- Cette architecture garantit **performance**, **maintenabilitĂ©** et **Ă©volutivitĂ©** pour le module Conseil de Classe de Notytex.