/** * Docker Section Management * Handles Docker hosts, containers, images, volumes, and alerts */ const dockerSection = { hosts: [], currentHostId: null, _initialized: false, stats: { total_hosts: 0, enabled_hosts: 0, total_containers: 0, running_containers: 0, total_images: 0, open_alerts: 0 }, async init() { await this.loadDockerHosts(); await this.loadStats(); this.setupEventListeners(); this.setupWebSocket(); }, async ensureInit() { if (this._initialized) return; this._initialized = true; await this.init(); }, setupEventListeners() { // Search filter const searchInput = document.getElementById('docker-search'); if (searchInput) { searchInput.addEventListener('input', (e) => { this.filterHosts(e.target.value); }); } // Tab switching document.querySelectorAll('.docker-tab').forEach(tab => { tab.addEventListener('click', (e) => { const tabName = e.currentTarget.dataset.tab; this.switchTab(tabName); }); }); }, setupWebSocket() { if (window.dashboard && window.dashboard.ws) { const originalOnMessage = window.dashboard.ws.onmessage; window.dashboard.ws.onmessage = (event) => { if (originalOnMessage) originalOnMessage(event); try { const data = JSON.parse(event.data); this.handleWebSocketMessage(data); } catch (e) { console.error('Docker WS parse error:', e); } }; } }, handleWebSocketMessage(data) { switch (data.type) { case 'docker_host_updated': this.updateHostCard(data.host || data.data); break; case 'docker_alert_opened': this.showAlertNotification(data.data); this.updateAlertsBadge(); break; case 'docker_alert_closed': case 'docker_alert_acknowledged': this.updateAlertsBadge(); break; } }, async fetchAPI(endpoint, options = {}) { const token = localStorage.getItem('accessToken'); const apiKey = localStorage.getItem('apiKey'); const headers = { 'Content-Type': 'application/json', ...options.headers }; if (token) { headers['Authorization'] = `Bearer ${token}`; } else if (apiKey) { headers['X-API-Key'] = apiKey; } const response = await fetch(`${window.location.origin}${endpoint}`, { ...options, headers }); if (!response.ok) { // Handle 401 Unauthorized - redirect to login if (response.status === 401) { this.showToast('Session expirée, reconnexion requise', 'error'); if (window.dashboard && typeof window.dashboard.logout === 'function') { window.dashboard.logout(); } return null; } throw new Error(`API Error: ${response.status}`); } return response.json(); }, async loadDockerHosts() { try { const response = await this.fetchAPI('/api/docker/hosts'); this.hosts = response.hosts || []; this.renderHostsGrid(); } catch (error) { console.error('Error loading Docker hosts:', error); this.renderError('Erreur lors du chargement des hosts Docker'); } }, async loadStats() { try { const stats = await this.fetchAPI('/api/docker/stats'); this.stats = stats; this.updateStatsDisplay(); } catch (error) { console.error('Error loading Docker stats:', error); } }, updateStatsDisplay() { const el = (id, value) => { const elem = document.getElementById(id); if (elem) elem.textContent = value; }; el('docker-stat-hosts', this.stats.enabled_hosts || 0); el('docker-stat-containers', this.stats.running_containers || 0); el('docker-stat-images', this.stats.total_images || 0); el('docker-stat-alerts', this.stats.open_alerts || 0); // Update alerts badge const badge = document.getElementById('docker-alerts-badge'); if (badge) { if (this.stats.open_alerts > 0) { badge.textContent = this.stats.open_alerts; badge.classList.remove('hidden'); } else { badge.classList.add('hidden'); } } }, renderHostsGrid() { const grid = document.getElementById('docker-hosts-grid'); if (!grid) return; if (this.hosts.length === 0) { grid.innerHTML = `

Aucun host Docker configuré

Activez Docker sur un host dans la section Hosts

`; return; } grid.innerHTML = this.hosts.map(host => this.renderHostCard(host)).join(''); }, renderHostCard(host) { const statusColor = host.docker_status === 'online' ? 'green' : host.docker_status === 'offline' ? 'red' : 'yellow'; const statusText = host.docker_status || 'unknown'; const lastCollect = host.docker_last_collect_at ? this.formatRelativeTime(host.docker_last_collect_at) : 'Jamais'; return `

${host.host_name}

${statusText}
${host.host_ip}
${host.containers_running}/${host.containers_total} containers
${host.images_total} images
${host.open_alerts > 0 ? `
${host.open_alerts} alerte(s)
` : ''}
${lastCollect} ${host.docker_version ? `v${host.docker_version}` : ''}
`; }, filterHosts(query) { const cards = document.querySelectorAll('#docker-hosts-grid > div[data-host-id]'); const lowerQuery = query.toLowerCase(); cards.forEach(card => { const hostId = card.dataset.hostId; const host = this.hosts.find(h => h.host_id === hostId); if (host) { const matches = host.host_name.toLowerCase().includes(lowerQuery) || host.host_ip.toLowerCase().includes(lowerQuery); card.style.display = matches ? '' : 'none'; } }); }, async viewDetails(hostId) { this.currentHostId = hostId; const host = this.hosts.find(h => h.host_id === hostId); if (!host) return; document.getElementById('docker-host-name').innerHTML = `${host.host_name}`; // Show modal document.getElementById('docker-detail-modal').classList.remove('hidden'); // Load containers by default this.switchTab('containers'); await this.loadContainers(hostId); }, closeModal() { document.getElementById('docker-detail-modal').classList.add('hidden'); this.currentHostId = null; }, switchTab(tabName) { // Update tab buttons document.querySelectorAll('.docker-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.tab === tabName); tab.classList.toggle('text-gray-400', tab.dataset.tab !== tabName); }); // Update tab content document.querySelectorAll('.docker-tab-content').forEach(content => { content.classList.toggle('hidden', !content.id.endsWith(tabName)); }); // Load content for the tab if (this.currentHostId) { switch (tabName) { case 'containers': this.loadContainers(this.currentHostId); break; case 'images': this.loadImages(this.currentHostId); break; case 'volumes': this.loadVolumes(this.currentHostId); break; case 'host-alerts': this.loadHostAlerts(this.currentHostId); break; } } }, async loadContainers(hostId) { const container = document.getElementById('docker-containers-list'); container.innerHTML = '
'; try { const containers = await this.fetchContainers(hostId); if (containers.length === 0) { container.innerHTML = '

Aucun container

'; return; } container.innerHTML = containers.map(c => this.renderContainerRow(c, hostId)).join(''); } catch (error) { container.innerHTML = `

Erreur: ${error.message}

`; } }, async fetchContainers(hostId) { const response = await this.fetchAPI(`/api/docker/hosts/${hostId}/containers`); if (!response) return []; return response.containers || []; }, async collectHostDockerInfo(hostId) { try { const response = await this.fetchAPI(`/api/docker/hosts/${hostId}/collect`, { method: 'POST' }); if (!response) return false; // 401 handled return !!response.success; } catch (error) { return false; } }, renderContainerRow(c, hostId) { const stateColors = { running: 'green', exited: 'red', paused: 'yellow', created: 'blue', dead: 'red' }; const stateColor = stateColors[c.state] || 'gray'; const healthBadge = c.health ? ` ${c.health} ` : ''; // Parse ports and create clickable links const portLinks = this.parsePortLinks(c.ports, hostId); return `
${c.name} ${c.compose_project ? `${c.compose_project}` : ''} ${healthBadge} ${portLinks}
${c.image}
${c.status || c.state}
${c.state !== 'running' ? ` ` : ` `}
`; }, parsePortLinks(ports, hostId) { if (!ports) return ''; // Get host IP for building URLs const host = this.hosts.find(h => h.host_id === hostId); const hostIp = host ? host.host_ip : 'localhost'; // Parse ports from the raw string or object const portStr = ports.raw || (typeof ports === 'string' ? ports : ''); if (!portStr) return ''; // Extract port mappings like "0.0.0.0:8080->80/tcp" or "8080/tcp" const portRegex = /(?:([\d.]+):)?(\d+)->\d+\/tcp/g; const links = []; const seenHostPorts = new Set(); let match; while ((match = portRegex.exec(portStr)) !== null) { const bindIp = match[1] || '0.0.0.0'; const hostPort = match[2]; // Skip if bound to 127.0.0.1 only (not accessible externally) if (bindIp === '127.0.0.1' || bindIp === '::1') continue; if (seenHostPorts.has(hostPort)) continue; seenHostPorts.add(hostPort); // Determine protocol (https for common secure ports) const protocol = ['443', '8443', '9443'].includes(hostPort) ? 'https' : 'http'; const url = `${protocol}://${hostIp}:${hostPort}`; links.push(` ${hostPort} `); } return links.join(''); }, async loadImages(hostId) { const container = document.getElementById('docker-images-list'); container.innerHTML = '
'; try { const response = await this.fetchAPI(`/api/docker/hosts/${hostId}/images`); if (!response) return; // 401 handled const images = response.images || []; if (images.length === 0) { container.innerHTML = '

Aucune image

'; return; } // Show unused count if any const unusedInfo = response.unused_count > 0 ? `
${response.unused_count} image(s) non utilisée(s)
` : ''; container.innerHTML = unusedInfo + images.map(img => { const unusedBadge = !img.in_use ? `Unused` : ''; return `
${img.repo_tags?.[0] || img.image_id.substring(0, 12)} ${unusedBadge}
${this.formatBytes(img.size)} ${this.formatRelativeTime(img.created)}
`; }).join(''); } catch (error) { container.innerHTML = `

Erreur: ${error.message}

`; } }, async loadVolumes(hostId) { const container = document.getElementById('docker-volumes-list'); container.innerHTML = '
'; try { const response = await this.fetchAPI(`/api/docker/hosts/${hostId}/volumes`); const volumes = response.volumes || []; if (volumes.length === 0) { container.innerHTML = '

Aucun volume

'; return; } container.innerHTML = volumes.map(vol => `
${vol.name}
${vol.driver} ${vol.mountpoint}
`).join(''); } catch (error) { container.innerHTML = `

Erreur: ${error.message}

`; } }, async loadHostAlerts(hostId) { const container = document.getElementById('docker-host-alerts-list'); container.innerHTML = '
'; try { const response = await this.fetchAPI(`/api/docker/alerts?host_id=${hostId}`); const alerts = response.alerts || []; if (alerts.length === 0) { container.innerHTML = '

Aucune alerte

'; return; } container.innerHTML = alerts.map(alert => this.renderAlertRow(alert)).join(''); } catch (error) { container.innerHTML = `

Erreur: ${error.message}

`; } }, renderAlertRow(alert) { const severityColors = { warning: 'yellow', error: 'red', critical: 'red' }; const color = severityColors[alert.severity] || 'gray'; return `
${alert.container_name} ${alert.severity}
${alert.state === 'open' ? ` ` : ` ${alert.state} `}
${alert.message}
${this.formatRelativeTime(alert.opened_at)}
`; }, // Container Actions async startContainer(hostId, containerId) { await this.executeAction(hostId, containerId, 'start'); }, async stopContainer(hostId, containerId) { await this.executeAction(hostId, containerId, 'stop'); }, async restartContainer(hostId, containerId) { await this.executeAction(hostId, containerId, 'restart'); }, async executeAction(hostId, containerId, action) { try { const response = await this.fetchAPI(`/api/docker/containers/${hostId}/${containerId}/${action}`, { method: 'POST' }); if (!response) return; // 401 handled if (response.success) { this.showToast(`Container ${action} successful`, 'success'); await this.collectHostDockerInfo(hostId); await this.refreshContainersAfterAction(hostId, containerId, action); await this.loadDockerHosts(); await this.loadStats(); } else { this.showToast(`Failed: ${response.error || response.message}`, 'error'); } } catch (error) { this.showToast(`Error: ${error.message}`, 'error'); } }, async refreshContainersAfterAction(hostId, containerId, action) { const expectedRunning = action === 'start' || action === 'restart'; const expectedStopped = action === 'stop'; const delays = [0, 500, 1000, 1500, 2000]; for (const delayMs of delays) { if (delayMs) { await new Promise(resolve => setTimeout(resolve, delayMs)); } const containers = await this.fetchContainers(hostId); const found = containers.find(c => String(c.container_id) === String(containerId)); if (!found) { // If the container is not returned, still re-render latest list. await this.loadContainers(hostId); return; } const isRunning = found.state === 'running' || found.state === 'restarting'; if ((expectedRunning && isRunning) || (expectedStopped && !isRunning)) { await this.loadContainers(hostId); return; } } // Fallback: render whatever we have await this.loadContainers(hostId); }, async showLogs(hostId, containerId, containerName) { document.getElementById('docker-logs-title').innerHTML = `${containerName} - Logs`; document.getElementById('docker-logs-content').textContent = 'Chargement des logs...'; document.getElementById('docker-logs-modal').classList.remove('hidden'); try { const response = await this.fetchAPI(`/api/docker/containers/${hostId}/${containerId}/logs?tail=500`); document.getElementById('docker-logs-content').textContent = response.logs || 'Aucun log disponible'; } catch (error) { document.getElementById('docker-logs-content').textContent = `Erreur: ${error.message}`; } }, closeLogsModal() { document.getElementById('docker-logs-modal').classList.add('hidden'); }, async showInspect(hostId, containerId, containerName) { document.getElementById('docker-logs-title').innerHTML = `${containerName} - Inspect`; document.getElementById('docker-logs-content').textContent = 'Chargement des informations...'; document.getElementById('docker-logs-modal').classList.remove('hidden'); try { const response = await this.fetchAPI(`/api/docker/containers/${hostId}/${containerId}/inspect`); if (!response) return; // 401 handled // Format JSON nicely const inspectData = response.inspect_data || {}; document.getElementById('docker-logs-content').textContent = JSON.stringify(inspectData, null, 2); } catch (error) { document.getElementById('docker-logs-content').textContent = `Erreur: ${error.message}`; } }, async removeImage(hostId, imageId, canDeleteDirectly = false) { const force = !canDeleteDirectly; const confirmMsg = force ? 'Cette image est utilisée par un ou plusieurs containers. Voulez-vous forcer la suppression ?' : 'Voulez-vous supprimer cette image ?'; if (!confirm(confirmMsg)) return; try { const response = await this.fetchAPI(`/api/docker/images/${hostId}/${imageId}?force=${force}`, { method: 'DELETE' }); if (!response) return; // 401 handled if (response.success) { this.showToast('Image supprimée avec succès', 'success'); // Refresh images list await this.loadImages(hostId); } else { this.showToast(`Erreur: ${response.error || response.message}`, 'error'); } } catch (error) { this.showToast(`Erreur: ${error.message}`, 'error'); } }, async collectNow(hostId) { try { this.showToast('Collection en cours...', 'info'); const response = await this.fetchAPI(`/api/docker/hosts/${hostId}/collect`, { method: 'POST' }); if (response.success) { this.showToast(`Collection terminée: ${response.containers_count} containers`, 'success'); await this.loadDockerHosts(); await this.loadStats(); } else { this.showToast(`Erreur: ${response.error}`, 'error'); } } catch (error) { this.showToast(`Erreur: ${error.message}`, 'error'); } }, async collectAll() { try { this.showToast('Collection de tous les hosts...', 'info'); const response = await this.fetchAPI('/api/docker/collect-all', { method: 'POST' }); this.showToast(`Collecte terminée: ${response.successful}/${response.total_hosts} hosts`, response.failed > 0 ? 'warning' : 'success'); await this.loadDockerHosts(); await this.loadStats(); } catch (error) { this.showToast(`Erreur: ${error.message}`, 'error'); } }, async toggleDockerEnabled(hostId, enabled) { try { await this.fetchAPI(`/api/docker/hosts/${hostId}/enable`, { method: 'POST', body: JSON.stringify({ enabled }) }); this.showToast(`Docker monitoring ${enabled ? 'activé' : 'désactivé'}`, 'success'); await this.loadDockerHosts(); await this.loadStats(); } catch (error) { this.showToast(`Erreur: ${error.message}`, 'error'); } }, async acknowledgeAlert(alertId) { try { await this.fetchAPI(`/api/docker/alerts/${alertId}/acknowledge`, { method: 'POST' }); this.showToast('Alerte acquittée', 'success'); if (this.currentHostId) { await this.loadHostAlerts(this.currentHostId); } await this.loadStats(); } catch (error) { this.showToast(`Erreur: ${error.message}`, 'error'); } }, showAlerts() { // Navigate to alerts tab in modal or show alerts modal if (this.currentHostId) { this.switchTab('host-alerts'); } }, updateHostCard(hostData) { const card = document.querySelector(`[data-host-id="${hostData.host_id}"]`); if (card) { card.outerHTML = this.renderHostCard(hostData); } }, async updateAlertsBadge() { await this.loadStats(); }, showAlertNotification(alert) { this.showToast(`Alerte Docker: ${alert.container_name} - ${alert.message}`, 'warning'); }, renderError(message) { const grid = document.getElementById('docker-hosts-grid'); if (grid) { grid.innerHTML = `

${message}

`; } }, showToast(message, type = 'info') { // Use existing toast system or create simple one if (window.dashboard && window.dashboard.showToast) { window.dashboard.showToast(message, type); } else { console.log(`[${type.toUpperCase()}] ${message}`); } }, formatRelativeTime(dateStr) { if (!dateStr) return 'N/A'; const date = new Date(dateStr); const now = new Date(); const diffMs = now - date; const diffMins = Math.floor(diffMs / 60000); const diffHours = Math.floor(diffMins / 60); const diffDays = Math.floor(diffHours / 24); if (diffMins < 1) return 'À l\'instant'; if (diffMins < 60) return `Il y a ${diffMins}min`; if (diffHours < 24) return `Il y a ${diffHours}h`; return `Il y a ${diffDays}j`; }, formatBytes(bytes) { if (!bytes) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } }; function setupDockerAutoInit() { // Initialize when Docker page is shown const dockerPage = document.getElementById('page-docker'); if (!dockerPage) return; const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { if ( mutation.target.id === 'page-docker' && mutation.target.classList.contains('active') ) { dockerSection.ensureInit(); } }); }); observer.observe(dockerPage, { attributes: true, attributeFilter: ['class'] }); // Also initialize if already on Docker page if (dockerPage.classList.contains('active')) { dockerSection.ensureInit(); } } // If this script is loaded after DOMContentLoaded (common when injected late), // a DOMContentLoaded listener would never fire. Handle both cases. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', setupDockerAutoInit); } else { setupDockerAutoInit(); }