/**
* 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 response = await this.fetchAPI(`/api/docker/hosts/${hostId}/containers`);
const containers = response.containers || [];
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}
`;
}
},
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 = [];
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') continue;
// 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.success) {
this.showToast(`Container ${action} successful`, 'success');
// Refresh containers list
setTimeout(() => this.loadContainers(hostId), 1000);
} else {
this.showToast(`Failed: ${response.error || response.message}`, 'error');
}
} catch (error) {
this.showToast(`Error: ${error.message}`, 'error');
}
},
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 = `
`;
}
},
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();
}