/** * Playbook Editor Module * * Éditeur de playbooks Ansible avec: * - Intégration ansible-lint * - Score de qualité en temps réel * - Validation YAML basique */ // État global de l'éditeur const PlaybookEditor = { // État state: { filename: '', originalContent: '', isDirty: false, isNew: false, initialized: false, // Lint lintStatus: 'idle', // idle | running | success | warnings | errors lintResults: null, lintDebounceTimer: null, lastLintTime: null, // Qualité qualityScore: null, // Config autoLint: false, lintDelay: 2000, }, /** * Initialise l'éditeur (utilise le textarea existant) */ init(containerId, content, filename, isNew = false) { console.log('[PlaybookEditor] Initializing for:', filename); // Reset state this.state.filename = filename; this.state.originalContent = content; this.state.isNew = isNew; this.state.isDirty = false; this.state.lintStatus = 'idle'; this.state.lintResults = null; this.state.qualityScore = null; this.state.initialized = true; // Le textarea existe déjà dans le HTML avec id="playbook-editor-content" const textarea = document.getElementById('playbook-editor-content'); if (!textarea) { console.error('[PlaybookEditor] Textarea not found: playbook-editor-content'); return; } // Setup des listeners sur le textarea textarea.addEventListener('input', () => this.onContentChange()); // Support Tab = 2 espaces textarea.addEventListener('keydown', (e) => { if (e.key === 'Tab') { e.preventDefault(); const start = textarea.selectionStart; const end = textarea.selectionEnd; textarea.value = textarea.value.substring(0, start) + ' ' + textarea.value.substring(end); textarea.selectionStart = textarea.selectionEnd = start + 2; this.onContentChange(); } }); // Validation YAML initiale this.validateYaml(content); console.log('[PlaybookEditor] Initialized successfully'); }, /** * Récupère le contenu de l'éditeur */ getContent() { const textarea = document.getElementById('playbook-editor-content'); if (textarea) { return textarea.value; } console.warn('[PlaybookEditor] Textarea not found, returning empty'); return ''; }, /** * Met à jour le contenu de l'éditeur */ setContent(content) { const textarea = document.getElementById('playbook-editor-content'); if (textarea) { textarea.value = content; } }, /** * Appelé quand le contenu change */ onContentChange() { const content = this.getContent(); this.state.isDirty = content !== this.state.originalContent; // Mettre à jour le titre avec indicateur de modification this.updateTitle(); // Validation YAML basique this.validateYaml(content); // Debounce lint auto (si activé) if (this.state.autoLint) { clearTimeout(this.state.lintDebounceTimer); this.state.lintDebounceTimer = setTimeout(() => { this.runLint(); }, this.state.lintDelay); } }, /** * Validation YAML basique */ validateYaml(content) { const statusEl = document.getElementById('yaml-validation-status'); if (!statusEl) return; const errors = []; const lines = content.split('\n'); lines.forEach((line, index) => { // Tabs if (line.includes('\t')) { errors.push({ line: index + 1, message: 'Utilisez des espaces au lieu des tabs' }); } // Indentation impaire const leadingSpaces = (line.match(/^(\s*)/) || ['', ''])[1].length; if (leadingSpaces % 2 !== 0 && line.trim()) { errors.push({ line: index + 1, message: 'Indentation impaire détectée' }); } }); if (errors.length > 0) { statusEl.innerHTML = ` Ligne ${errors[0].line}: ${errors[0].message} `; } else { statusEl.innerHTML = ` YAML valide `; } }, /** * Exécute ansible-lint */ async runLint() { const content = this.getContent(); if (!content.trim()) { console.warn('[PlaybookEditor] Cannot lint empty content'); return; } console.log('[PlaybookEditor] Running lint for:', this.state.filename); // Mettre à jour le statut this.setLintStatus('running'); try { // Récupérer le token d'authentification (accessToken est utilisé par le dashboard) const token = localStorage.getItem('accessToken'); const headers = { 'Content-Type': 'application/json', }; if (token) { headers['Authorization'] = `Bearer ${token}`; } const response = await fetch(`/api/playbooks/${encodeURIComponent(this.state.filename)}/lint`, { method: 'POST', headers: headers, body: JSON.stringify({ content }), }); if (!response.ok) { const errorText = await response.text(); console.error('[PlaybookEditor] Lint API error:', response.status, errorText); throw new Error(`HTTP ${response.status}`); } const result = await response.json(); console.log('[PlaybookEditor] Lint result:', result); this.state.lintResults = result; this.state.lastLintTime = new Date(); this.state.qualityScore = result.quality_score; // Déterminer le statut if (result.summary.errors > 0) { this.setLintStatus('errors'); } else if (result.summary.warnings > 0) { this.setLintStatus('warnings'); } else { this.setLintStatus('success'); } // Mettre à jour l'UI this.updateQualityBadge(); this.updateProblemsPanel(); this.updateProblemsCount(); } catch (error) { console.error('[PlaybookEditor] Lint error:', error); this.setLintStatus('idle'); if (error.message.includes('503')) { window.dashboard?.showNotification('ansible-lint non disponible sur le serveur', 'warning'); } else if (!error.message.includes('401') && !error.message.includes('403')) { window.dashboard?.showNotification(`Erreur lint: ${error.message}`, 'error'); } } }, /** * Met à jour le statut du lint */ setLintStatus(status) { this.state.lintStatus = status; const btn = document.getElementById('lint-button'); if (!btn) return; const icons = { idle: '', running: '', success: '', warnings: '', errors: '', }; const labels = { idle: 'Lint', running: 'Analyse...', success: 'OK', warnings: '', errors: '', }; const count = this.state.lintResults?.summary?.total || 0; const label = labels[status] || ''; const countBadge = (status === 'warnings' || status === 'errors') && count > 0 ? `${count}` : ''; btn.innerHTML = `${icons[status]}${label ? ' ' + label : ''}${countBadge}`; btn.disabled = status === 'running'; // Classes de style btn.classList.remove('success', 'warnings', 'errors'); if (status === 'success' || status === 'warnings' || status === 'errors') { btn.classList.add(status); } }, /** * Met à jour le badge de qualité */ updateQualityBadge() { const badge = document.getElementById('quality-badge'); if (!badge || this.state.qualityScore === null) return; const score = this.state.qualityScore; let colorClass = 'bg-green-600'; if (score < 50) colorClass = 'bg-red-600'; else if (score < 70) colorClass = 'bg-yellow-600'; else if (score < 90) colorClass = 'bg-blue-600'; badge.innerHTML = ` Quality: ${score}/100 `; badge.style.display = 'block'; }, /** * Met à jour le compteur de problèmes */ updateProblemsCount() { const countEl = document.getElementById('problems-count'); const tabEl = document.getElementById('problems-tab'); if (countEl) { const count = this.state.lintResults?.summary?.total || 0; countEl.textContent = count; } if (tabEl) { tabEl.classList.remove('errors', 'warnings'); if (this.state.lintResults?.summary?.errors > 0) { tabEl.classList.add('errors'); } else if (this.state.lintResults?.summary?.warnings > 0) { tabEl.classList.add('warnings'); } } }, /** * Met à jour le panneau des problèmes */ updateProblemsPanel() { const panel = document.getElementById('problems-panel'); if (!panel) return; const results = this.state.lintResults; if (!results || results.issues.length === 0) { panel.innerHTML = `
Aucun problème détecté
Temps: ${results?.execution_time_ms || 0}ms