// Homelab Dashboard JavaScript - Intégration API class DashboardManager { constructor() { // Configuration API - JWT token stored in localStorage this.apiBase = window.location.origin; this.accessToken = localStorage.getItem('accessToken') || null; this.currentUser = null; this.setupRequired = false; // Données locales (seront remplies par l'API) this.hosts = []; this.tasks = []; this.logs = []; this.serverLogs = []; this.logsView = 'server'; this.currentLogsSearch = ''; this.ansibleHosts = []; this.ansibleGroups = []; this.playbooks = []; // Alertes (centre de messages) this.alerts = []; this.alertsUnread = 0; // Logs de tâches depuis les fichiers markdown this.taskLogs = []; this.taskLogsStats = { total: 0, completed: 0, failed: 0, running: 0, pending: 0 }; this.taskLogsDates = { years: {} }; // Filtres actifs this.currentStatusFilter = 'all'; this.currentDateFilter = { year: '', month: '', day: '' }; // Sélection de dates via le calendrier (liste de chaînes YYYY-MM-DD) this.selectedTaskDates = []; this.taskCalendarMonth = new Date(); // Filtres d'heure this.currentHourStart = ''; this.currentHourEnd = ''; // Filtre par type de source (scheduled, manual, adhoc) this.currentSourceTypeFilter = 'all'; this.currentGroupFilter = 'all'; this.currentBootstrapFilter = 'all'; this.currentHostsSearch = ''; this.currentCategoryFilter = 'all'; this.currentSubcategoryFilter = 'all'; this.currentTargetFilter = 'all'; this.expandedHostDiskDetails = new Set(); // Pagination côté serveur this.tasksTotalCount = 0; this.tasksHasMore = false; // Groupes pour la gestion des hôtes this.envGroups = []; this.roleGroups = []; // Catégories de playbooks this.playbookCategories = {}; // Filtres playbooks this.currentPlaybookCategoryFilter = 'all'; this.currentPlaybookSearch = ''; // Historique des commandes ad-hoc this.adhocHistory = []; this.adhocCategories = []; // Métriques des hôtes (collectées par les builtin playbooks) this.hostMetrics = {}; // Map host_id -> HostMetricsSummary this.builtinPlaybooks = []; this.metricsLoading = false; // Schedules (Planificateur) this.schedules = []; this.schedulesStats = { total: 0, active: 0, paused: 0, failures_24h: 0 }; this.schedulesUpcoming = []; this.currentScheduleFilter = 'all'; this.scheduleSearchQuery = ''; this.scheduleCalendarMonth = new Date(); this.editingScheduleId = null; this.scheduleModalStep = 1; // WebSocket this.ws = null; // Terminal SSH this.terminalSession = null; this.terminalDrawerOpen = false; this.terminalFeatureAvailable = false; // Configuration: collecte métriques this.metricsCollectionInterval = 'off'; // Polling des tâches en cours this.runningTasksPollingInterval = null; this.pollingIntervalMs = 2000; // Polling toutes les 2 secondes // Pagination des tâches this.tasksDisplayedCount = 20; this.tasksPerPage = 20; this.init(); } async init() { this.setupEventListeners(); this.setupScrollAnimations(); this.startAnimations(); this.loadThemePreference(); // Check authentication status first const authOk = await this.checkAuthStatus(); if (!authOk) { // Show login screen this.showLoginScreen(); return; } // Hide login screen if visible this.hideLoginScreen(); // Charger les données depuis l'API await this.loadAllData(); // Connecter WebSocket pour les mises à jour temps réel this.connectWebSocket(); // Rafraîchir périodiquement les métriques setInterval(() => this.loadMetrics(), 30000); // Démarrer le polling des tâches en cours this.startRunningTasksPolling(); } setActiveNav(pageName) { if (typeof navigateTo === 'function') { navigateTo(pageName); return; } // Fallback minimal si navigateTo n'est pas disponible document.querySelectorAll('.page-section').forEach(page => { page.classList.remove('active'); }); const target = document.getElementById(`page-${pageName}`); if (target) target.classList.add('active'); } // ===== AUTHENTICATION ===== async checkAuthStatus() { try { const response = await fetch(`${this.apiBase}/api/auth/status`, { headers: this.getAuthHeaders() }); if (!response.ok) { return false; } const data = await response.json(); this.setupRequired = data.setup_required; if (data.setup_required) { this.showSetupScreen(); return false; } if (data.authenticated && data.user) { this.currentUser = data.user; this.updateUserDisplay(); return true; } return false; } catch (error) { console.error('Auth status check failed:', error); return false; } } handleTaskLogDeleted(payload) { const logId = payload && payload.id; if (!logId) return; const current = Array.isArray(this.taskLogs) ? this.taskLogs : []; const next = current.filter(l => String(l.id) !== String(logId)); if (next.length === current.length) return; this.taskLogs = next; this.renderTasks(); } handleTaskLogCreated(log) { if (!log || !log.id) return; const current = Array.isArray(this.taskLogs) ? this.taskLogs : []; const exists = current.some(l => String(l.id) === String(log.id)); if (exists) return; this.taskLogs = [log, ...current]; this.renderTasks(); } getAuthHeaders() { const headers = { 'Content-Type': 'application/json' }; if (this.accessToken) { headers['Authorization'] = `Bearer ${this.accessToken}`; } // No fallback - require JWT authentication return headers; } async login(username, password) { try { const response = await fetch(`${this.apiBase}/api/auth/login/json`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Échec de connexion'); } const data = await response.json(); this.accessToken = data.access_token; localStorage.setItem('accessToken', data.access_token); // Get user info await this.checkAuthStatus(); // Re-initialize dashboard this.hideLoginScreen(); await this.loadAllData(); this.connectWebSocket(); this.startRunningTasksPolling(); this.showNotification('Connexion réussie', 'success'); return true; } catch (error) { console.error('Login failed:', error); this.showNotification(error.message, 'error'); return false; } } async setupAdmin(username, password, email = null, displayName = null) { try { const response = await fetch(`${this.apiBase}/api/auth/setup`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password, email: email || null, display_name: displayName || null }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.detail || 'Échec de configuration'); } // Auto-login after setup return await this.login(username, password); } catch (error) { console.error('Setup failed:', error); this.showNotification(error.message, 'error'); return false; } } logout() { this.accessToken = null; this.currentUser = null; localStorage.removeItem('accessToken'); // Stop polling if (this.runningTasksPollingInterval) { clearInterval(this.runningTasksPollingInterval); } // Close WebSocket if (this.ws) { this.ws.close(); } this.showLoginScreen(); this.showNotification('Déconnexion réussie', 'success'); } showLoginScreen() { const loginScreen = document.getElementById('login-screen'); const mainContent = document.getElementById('main-content'); if (loginScreen) { loginScreen.classList.remove('hidden'); if (this.setupRequired) { document.getElementById('login-form-container').classList.add('hidden'); document.getElementById('setup-form-container').classList.remove('hidden'); } else { document.getElementById('login-form-container').classList.remove('hidden'); document.getElementById('setup-form-container').classList.add('hidden'); } } if (mainContent) { mainContent.classList.add('hidden'); } } showSetupScreen() { this.setupRequired = true; this.showLoginScreen(); } hideLoginScreen() { const loginScreen = document.getElementById('login-screen'); const mainContent = document.getElementById('main-content'); if (loginScreen) { loginScreen.classList.add('hidden'); } if (mainContent) { mainContent.classList.remove('hidden'); } } updateUserDisplay() { const userNameEl = document.getElementById('current-user-name'); const userMenuNameEl = document.getElementById('user-menu-name'); const userRoleEl = document.getElementById('current-user-role'); if (this.currentUser) { const displayName = this.currentUser.display_name || this.currentUser.username; if (userNameEl) { userNameEl.textContent = displayName; } if (userMenuNameEl) { userMenuNameEl.textContent = displayName; } if (userRoleEl) { const roleLabels = { 'admin': 'Administrateur', 'operator': 'Opérateur', 'viewer': 'Lecteur' }; userRoleEl.textContent = roleLabels[this.currentUser.role] || this.currentUser.role; } } } // ===== API CALLS ===== async apiCall(endpoint, options = {}) { const url = `${this.apiBase}${endpoint}`; const defaultOptions = { headers: this.getAuthHeaders() }; try { const response = await fetch(url, { ...defaultOptions, ...options }); if (!response.ok) { // Handle 401 Unauthorized - redirect to login if (response.status === 401) { this.showNotification('Session expirée, reconnexion requise', 'error'); this.logout(); const err = new Error('Session expirée'); err.status = 401; throw err; } let errorDetail = null; try { const contentType = response.headers.get('content-type') || ''; if (contentType.includes('application/json')) { errorDetail = await response.json(); } else { const text = await response.text(); errorDetail = text ? { detail: text } : null; } } catch (_) { errorDetail = null; } const serverMessage = (errorDetail && (errorDetail.detail || errorDetail.message || errorDetail.error)) ? (errorDetail.detail || errorDetail.message || errorDetail.error) : response.statusText; const err = new Error(`HTTP ${response.status}: ${serverMessage || 'Erreur inconnue'}`); err.status = response.status; err.detail = errorDetail; throw err; } return await response.json(); } catch (error) { console.error(`API Error (${endpoint}):`, error); throw error; } } async loadAllData() { try { // Charger en parallèle const [hostsData, tasksData, logsData, metricsData, inventoryData, playbooksData, taskLogsData, taskStatsData, taskDatesData, adhocHistoryData, adhocCategoriesData, schedulesData, schedulesStatsData, hostMetricsData, builtinPlaybooksData, serverLogsData, alertsUnreadData] = await Promise.all([ this.apiCall('/api/hosts').catch(() => []), this.apiCall('/api/tasks').catch(() => []), this.apiCall('/api/logs').catch(() => []), this.apiCall('/api/metrics').catch(() => ({})), this.apiCall('/api/ansible/inventory').catch(() => ({ hosts: [], groups: [] })), this.apiCall('/api/ansible/playbooks').catch(() => ({ playbooks: [] })), this.apiCall('/api/tasks/logs').catch(() => ({ logs: [], count: 0 })), this.apiCall('/api/tasks/logs/stats').catch(() => ({ total: 0, completed: 0, failed: 0, running: 0, pending: 0 })), this.apiCall('/api/tasks/logs/dates').catch(() => ({ years: {} })), this.apiCall('/api/adhoc/history').catch(() => ({ commands: [], count: 0 })), this.apiCall('/api/adhoc/categories').catch(() => ({ categories: [] })), this.apiCall('/api/schedules').catch(() => ({ schedules: [], count: 0 })), this.apiCall('/api/schedules/stats').catch(() => ({ stats: {}, upcoming: [] })), this.apiCall('/api/metrics/all-hosts').catch(() => ({})), this.apiCall('/api/builtin-playbooks').catch(() => []), this.apiCall('/api/server/logs?limit=500&offset=0').catch(() => ({ logs: [] })), this.apiCall('/api/alerts/unread-count').catch(() => ({ unread: 0 })) ]); this.hosts = hostsData; this.tasks = tasksData; this.logs = logsData; this.serverLogs = serverLogsData.logs || []; this.ansibleHosts = inventoryData.hosts || []; this.ansibleGroups = inventoryData.groups || []; this.playbooks = playbooksData.playbooks || []; this.playbookCategories = playbooksData.categories || {}; this.alertsUnread = alertsUnreadData.unread || 0; this.updateAlertsBadge(); // Logs de tâches markdown this.taskLogs = taskLogsData.logs || []; this.taskLogsStats = taskStatsData; this.taskLogsDates = taskDatesData; // Historique ad-hoc this.adhocHistory = adhocHistoryData.commands || []; this.adhocCategories = adhocCategoriesData.categories || []; // Schedules (Planificateur) this.schedules = schedulesData.schedules || []; this.schedulesStats = schedulesStatsData.stats || { total: 0, active: 0, paused: 0, failures_24h: 0 }; this.schedulesUpcoming = schedulesStatsData.upcoming || []; // Host metrics (builtin playbooks data) this.hostMetrics = hostMetricsData || {}; this.builtinPlaybooks = builtinPlaybooksData || []; console.log('Data loaded:', { taskLogs: this.taskLogs.length, taskLogsStats: this.taskLogsStats, adhocHistory: this.adhocHistory.length, adhocCategories: this.adhocCategories.length, schedules: this.schedules.length }); // Charger les résultats de lint depuis l'API await this.loadPlaybookLintResults(); // Mettre à jour l'affichage this.renderHosts(); this.renderTasks(); this.renderLogs(); this.renderAlerts(); this.renderPlaybooks(); this.renderSchedules(); this.renderAdhocWidget(); this.updateMetricsDisplay(metricsData); this.updateDateFilters(); this.updateTaskCounts(); } catch (error) { console.error('Erreur chargement données:', error); this.showNotification('Erreur de connexion à l\'API', 'error'); } } async loadMetrics() { try { const metrics = await this.apiCall('/api/metrics'); this.updateMetricsDisplay(metrics); } catch (error) { console.error('Erreur chargement métriques:', error); } } updateMetricsDisplay(metrics) { if (!metrics) return; const elements = { 'online-hosts': metrics.online_hosts, 'total-tasks': metrics.total_tasks, 'success-rate': `${metrics.success_rate}%`, 'uptime': `${metrics.uptime}%` }; Object.entries(elements).forEach(([id, value]) => { const el = document.getElementById(id); if (el && value !== undefined) { el.textContent = value; } }); } // ===== WEBSOCKET ===== connectWebSocket() { const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${wsProtocol}//${window.location.host}/ws`; try { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket connecté'); }; this.ws.onmessage = (event) => { const data = JSON.parse(event.data); this.handleWebSocketMessage(data); }; this.ws.onclose = () => { console.log('WebSocket déconnecté, reconnexion dans 5s...'); setTimeout(() => this.connectWebSocket(), 5000); }; this.ws.onerror = (error) => { console.error('WebSocket erreur:', error); }; } catch (error) { console.error('Erreur WebSocket:', error); } } handleWebSocketMessage(data) { switch (data.type) { case 'task_created': // Nouvelle tâche créée - mettre à jour immédiatement this.handleTaskCreated(data.data); break; case 'task_completed': case 'task_failed': // Tâche terminée - mettre à jour et rafraîchir les logs this.handleTaskCompleted(data.data); break; case 'task_cancelled': // Tâche annulée - mettre à jour l'UI this.handleTaskCancelled(data.data); break; case 'task_progress': // Mise à jour de progression - mettre à jour l'UI dynamiquement this.handleTaskProgress(data.data); break; case 'task_log_deleted': this.handleTaskLogDeleted(data.data); break; case 'task_log_created': this.handleTaskLogCreated(data.data); break; case 'host_created': case 'host_deleted': this.loadAllData(); break; case 'new_log': case 'logs_cleared': this.loadLogs(); break; case 'alert_created': this.handleAlertCreated(data.data); break; case 'alerts_unread_count': if (data.data && typeof data.data.unread === 'number') { this.alertsUnread = data.data.unread; this.updateAlertsBadge(); } break; case 'ansible_execution': this.showNotification( data.data.success ? 'Playbook exécuté avec succès' : 'Échec du playbook', data.data.success ? 'success' : 'error' ); break; case 'bootstrap_success': this.showNotification( `Bootstrap réussi pour ${data.data.host}`, 'success' ); this.loadAllData(); break; case 'bootstrap_status_updated': this.loadAllData(); break; case 'schedule_created': this.handleScheduleCreated(data.data); break; case 'schedule_updated': this.handleScheduleUpdated(data.data); break; case 'schedule_deleted': this.handleScheduleDeleted(data.data); break; case 'schedule_run_started': this.handleScheduleRunStarted(data.data); break; case 'schedule_run_finished': this.handleScheduleRunFinished(data.data); break; case 'metrics_collection_complete': this.showNotification( `Collecte des métriques terminée: ${data.data?.success || 0}/${data.data?.total || 0} réussi(es)`, (data.data && data.data.failed && data.data.failed > 0) ? 'warning' : 'success' ); this.loadHostMetrics().then(() => { this.renderHosts(); }); this.loadLogs().then(() => { this.renderLogs(); }); break; } } // ===== HANDLERS WEBSOCKET SCHEDULES ===== handleScheduleCreated(schedule) { this.schedules.unshift(schedule); this.renderSchedules(); this.showNotification(`Schedule "${schedule.name}" créé`, 'success'); } handleScheduleUpdated(schedule) { const index = this.schedules.findIndex(s => s.id === schedule.id); if (index !== -1) { this.schedules[index] = schedule; } this.renderSchedules(); } handleScheduleDeleted(data) { this.schedules = this.schedules.filter(s => s.id !== data.id); this.renderSchedules(); this.showNotification(`Schedule "${data.name}" supprimé`, 'warning'); } handleScheduleRunStarted(data) { this.showNotification(`Schedule "${data.schedule_name}" démarré`, 'info'); // Mettre à jour le statut du schedule const schedule = this.schedules.find(s => s.id === data.schedule_id); if (schedule) { schedule.last_status = 'running'; this.renderSchedules(); } } handleScheduleRunFinished(data) { const statusMsg = data.success ? 'terminé avec succès' : 'échoué'; const notifType = data.success ? 'success' : 'error'; this.showNotification(`Schedule "${data.schedule_name}" ${statusMsg}`, notifType); // Mettre à jour le schedule const schedule = this.schedules.find(s => s.id === data.schedule_id); if (schedule && data.run) { schedule.last_status = data.run.status; schedule.last_run_at = data.run.finished_at; } this.renderSchedules(); // Rafraîchir les stats this.refreshSchedulesStats(); } // ===== POLLING DES TÂCHES EN COURS ===== startRunningTasksPolling() { // Arrêter le polling existant si présent this.stopRunningTasksPolling(); // Démarrer le polling this.runningTasksPollingInterval = setInterval(() => { this.pollRunningTasks(); }, this.pollingIntervalMs); // Exécuter immédiatement une première fois this.pollRunningTasks(); console.log('Polling des tâches en cours démarré'); } stopRunningTasksPolling() { if (this.runningTasksPollingInterval) { clearInterval(this.runningTasksPollingInterval); this.runningTasksPollingInterval = null; console.log('Polling des tâches en cours arrêté'); } } async pollRunningTasks() { try { const result = await this.apiCall('/api/tasks/running'); const runningTasks = result.tasks || []; // Vérifier si des tâches ont changé de statut const previousRunningIds = this.tasks .filter(t => t.status === 'running' || t.status === 'pending') .map(t => t.id); const currentRunningIds = runningTasks.map(t => t.id); // Détecter les tâches terminées const completedTaskIds = previousRunningIds.filter(id => !currentRunningIds.includes(id)); if (completedTaskIds.length > 0) { // Des tâches ont été terminées - rafraîchir les logs console.log('Tâches terminées détectées:', completedTaskIds); await this.refreshTaskLogs(); } // Mettre à jour les tâches en cours this.updateRunningTasks(runningTasks); } catch (error) { console.error('Erreur polling tâches:', error); } } updateRunningTasks(runningTasks) { // Mettre à jour la liste des tâches en mémoire const nonRunningTasks = this.tasks.filter(t => t.status !== 'running' && t.status !== 'pending'); this.tasks = [...runningTasks, ...nonRunningTasks]; // Mettre à jour l'affichage dynamiquement this.updateRunningTasksUI(runningTasks); this.updateTaskCounts(); } updateRunningTasksUI(runningTasks) { const container = document.getElementById('tasks-list'); if (!container) return; // Trouver ou créer la section des tâches en cours let runningSection = container.querySelector('.running-tasks-section'); if (runningTasks.length === 0) { // Supprimer la section si plus de tâches en cours if (runningSection) { runningSection.remove(); } return; } // Créer la section si elle n'existe pas if (!runningSection) { runningSection = document.createElement('div'); runningSection.className = 'running-tasks-section mb-4'; runningSection.innerHTML = '
Cible: ${this.escapeHtml(task.host)}
Début: ${startTime} • Durée: ${duration}
${progress}% complété
Aucun hôte trouvé ${this.currentGroupFilter !== 'all' ? `dans le groupe "${this.currentGroupFilter}"` : ''} ${this.currentBootstrapFilter && this.currentBootstrapFilter !== 'all' ? `avec le statut "${this.currentBootstrapFilter === 'ready' ? 'Ansible Ready' : 'Non configuré'}"` : ''}
${host.ip} • ${host.os}${envGroup ? ` (${envGroup})` : ''}
Hôte cible
${this.escapeHtml(hostName)}
Vous êtes sur le point de supprimer l'hôte ${hostName} de l'inventaire Ansible.
Cette action supprimera l'hôte de tous les groupes et ne peut pas être annulée.
Aucun groupe d'${typeLabel} trouvé
${g.name}
${g.hosts_count} hôte(s)
${groups.length} groupe(s) d'${typeLabel}
Les ${hostsCount} hôte(s) seront déplacés vers ce groupe.
Vous êtes sur le point de supprimer le groupe d'${typeLabel} ${displayName}.
${hostsCount > 0 ? `Ce groupe contient ${hostsCount} hôte(s).
` : ''}Aucun playbook disponible
'}Aucune tâche trouvée
Utilisez "Actions Rapides" ou la Console pour lancer une commande
${status.text} • Cible: ${this.escapeHtml(result.log.target || parsed.target || 'N/A')}
${this.formatAnsibleOutput(hostOutputs.length > 0 ? hostOutputs[0].output : parsed.output, isSuccess)}
${this.escapeHtml(parsed.error)}
${totalHosts} hôte(s) • Cible: ${this.escapeHtml(metadata.target)} • ${metadata.duration}
${this.escapeHtml(metadata.error)}
${this.formatAnsibleOutput(host.output, !isFailed)}
${parsedOutput.stats.totalHosts} hôte(s) • ${parsedOutput.stats.totalTasks} tâche(s) • ${metadata.duration || 'N/A'}
${this.escapeHtml(result.output)}
` : ''}
Erreur de chargement
Cible: ${task.host}
Début: ${startTime} • Durée: ${duration}
${progressBar} ${task.output ? `${this.escapeHtml(task.output.substring(0, 150))}${task.output.length > 150 ? '...' : ''}
${this.escapeHtml(task.error.substring(0, 150))}${task.error.length > 150 ? '...' : ''}
${this.escapeHtml(task.output)}
${this.escapeHtml(task.error)}
${this.escapeHtml(cmd.command)}
${cmd.target}
Catégories:
Aucune commande dans l\'historique
Exécutez une commande pour la sauvegarder
${this.escapeHtml(cmd.command)}
${cmd.target}
Aucune commande dans l\'historique
Exécutez une commande pour la sauvegarder
${this.formatAnsibleOutput(hostOutputs[0].output, isSuccess)}
`;
}
switchHostTab(index) {
if (!this.currentHostOutputs || !this.currentHostOutputs[index]) return;
const hostOutput = this.currentHostOutputs[index];
const stdoutPre = document.getElementById('adhoc-stdout');
const tabs = document.querySelectorAll('.host-tab');
// Mettre à jour l'onglet actif
tabs.forEach((tab, i) => {
if (i === index) {
const host = this.currentHostOutputs[i];
const statusColor = host.status === 'changed' || host.status === 'success'
? 'bg-green-600'
: host.status === 'failed' || host.status === 'unreachable'
? 'bg-red-600'
: 'bg-gray-600';
tab.className = `host-tab flex items-center gap-2 px-3 py-1.5 rounded-t-lg text-xs font-medium transition-all ${statusColor} text-white`;
} else {
tab.className = 'host-tab flex items-center gap-2 px-3 py-1.5 rounded-t-lg text-xs font-medium transition-all bg-gray-700/50 text-gray-400 hover:text-white';
}
});
// Afficher le contenu
if (stdoutPre) {
stdoutPre.innerHTML = this.formatAnsibleOutput(hostOutput.output, hostOutput.status === 'changed' || hostOutput.status === 'success');
}
}
showAllHostsOutput() {
if (!this.currentHostOutputs) return;
const stdoutPre = document.getElementById('adhoc-stdout');
if (stdoutPre) {
const allOutput = this.currentHostOutputs.map(h => h.output).join('\n\n');
stdoutPre.innerHTML = this.formatAnsibleOutput(allOutput, true);
}
// Désélectionner tous les onglets
document.querySelectorAll('.host-tab').forEach(tab => {
tab.className = 'host-tab flex items-center gap-2 px-3 py-1.5 rounded-t-lg text-xs font-medium transition-all bg-gray-700/50 text-gray-400 hover:text-white';
});
}
renderLogs() {
const container = document.getElementById('logs-container');
if (!container) return;
container.innerHTML = '';
const items = this.logsView === 'db' ? (this.logs || []) : (this.serverLogs || []);
const q = (this.currentLogsSearch || '').trim().toLowerCase();
const filteredItems = q
? items.filter(log => {
const timestamp = (log.timestamp || '').toString();
const level = (log.level || '').toString();
const message = (log.message || '').toString();
const source = (log.source || '').toString();
const host = (log.host || '').toString();
const haystack = `${timestamp} ${level} ${message} ${source} ${host}`.toLowerCase();
return haystack.includes(q);
})
: items;
if (items.length === 0) {
container.innerHTML = `
Aucun log disponible
Aucun résultat
Configuration terminée!
L'hôte ${host} est prêt pour Ansible
${result.stdout || 'Pas de sortie'}
Bootstrap échoué
${errorDetail}
${stderr}
${stdout}
Nom:
${host.name}
IP:
${host.ip}
OS:
${host.os}
Statut:
${host.status}
Dernière connexion:
${lastSeen}
Hôte: ${task.host}
Statut: ${this.getStatusBadge(task.status)}
Progression: ${task.progress}%
Durée: ${task.duration}
• Démarrage de la tâche...
• Connexion SSH établie
• Exécution des commandes...
• Tâche terminée avec succès
Aucun playbook trouvé
${this.currentPlaybookSearch || this.currentPlaybookCategoryFilter !== 'all' ? 'Essayez de modifier vos filtres
' : ''}${this.escapeHtml(playbook.description)}
` : ''}Lettres, chiffres, tirets et underscores uniquement
Cliquez sur "Lint" pour analyser le playbook
Aucun problème détecté
Temps d'exécution: ${execution_time_ms}ms
Vous êtes sur le point de supprimer le playbook ${this.escapeHtml(filename)}.
Cette action est irréversible.
Aucune exécution
Ouvrez la console pour exécuter des commandes
${this.escapeHtml(cmd.command || 'N/A')}
${this.escapeHtml(cmd.stdout)}
${this.escapeHtml(cmd.stderr)}
Aucun schedule trouvé pour ces critères
Aucun schedule actif
'; return; } container.innerHTML = upcoming.map(s => { const nextRun = new Date(s.next_run_at); return `Aucune exécution planifiée
'; return; } container.innerHTML = activeSchedules.map(s => { const nextRun = new Date(s.next_run_at); return `Les notifications ntfy sont actuellement désactivées dans la configuration du serveur (NTFY_ENABLED=false). Les paramètres ci-dessous seront ignorés.
Aucune exécution enregistrée
Le schedule n'a pas encore été exécuté.
Aucune alerte pour le moment