const API_BASE = '/api'; const state = { tab: 'dashboard', containers: [], images: [], info: null, connected: false, logsContainerId: null, }; function init() { setupEventListeners(); fetchInitialData(); setInterval(fetchInitialData, 30000); } function setupEventListeners() { document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); state.tab = e.target.dataset.tab; renderTabs(); }); }); document.querySelector('.modal-close').addEventListener('click', closeLogsModal); document.getElementById('logs-modal').addEventListener('click', (e) => { if (e.target.id === 'logs-modal') closeLogsModal(); }); document.getElementById('logs-refresh').addEventListener('click', refreshLogs); document.getElementById('logs-tail').addEventListener('change', refreshLogs); document.getElementById('confirm-cancel').addEventListener('click', closeConfirmModal); document.getElementById('confirm-ok').addEventListener('click', confirmAction); document.getElementById('confirm-modal').addEventListener('click', (e) => { if (e.target.id === 'confirm-modal') closeConfirmModal(); }); document.getElementById('pull-btn').addEventListener('click', pullImage); document.getElementById('pull-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') pullImage(); }); } async function fetchInitialData() { try { await Promise.all([ fetchInfo(), fetchContainers(), fetchImages(), ]); state.connected = true; updateStatusIndicator(); } catch (err) { state.connected = false; updateStatusIndicator(); } renderTabs(); } async function fetchInfo() { const res = await fetch(`${API_BASE}/info`); if (!res.ok) throw new Error('Failed to fetch info'); state.info = await res.json(); } async function fetchContainers() { const res = await fetch(`${API_BASE}/containers`); if (!res.ok) throw new Error('Failed to fetch containers'); state.containers = (await res.json()) || []; } async function fetchImages() { const res = await fetch(`${API_BASE}/images`); if (!res.ok) throw new Error('Failed to fetch images'); state.images = (await res.json()) || []; } function updateStatusIndicator() { const indicator = document.querySelector('.status-indicator'); const statusText = document.querySelector('.status-text'); if (state.connected) { indicator.classList.add('connected'); statusText.textContent = 'Connected'; statusText.classList.add('connected'); statusText.classList.remove('disconnected'); } else { indicator.classList.remove('connected'); statusText.textContent = 'Disconnected'; statusText.classList.add('disconnected'); statusText.classList.remove('connected'); } } function renderTabs() { document.querySelectorAll('.tab-content').forEach(tab => { tab.classList.remove('active'); }); document.getElementById(state.tab).classList.add('active'); if (state.tab === 'dashboard') { renderDashboard(); } else if (state.tab === 'containers') { renderContainers(); } else if (state.tab === 'images') { renderImages(); } } function renderDashboard() { const running = state.containers.filter(c => c.State === 'running').length; const stopped = state.containers.filter(c => c.State === 'exited').length; const total = state.containers.length; document.getElementById('containers-total-count').textContent = total; document.getElementById('containers-running-count').textContent = running; document.getElementById('containers-stopped-count').textContent = stopped; document.getElementById('images-count').textContent = state.images.length; document.getElementById('docker-version').textContent = state.info?.ServerVersion || '-'; document.getElementById('docker-os').textContent = `${state.info?.Os || '-'} / ${state.info?.Architecture || '-'}`; document.getElementById('docker-cpus').textContent = state.info?.NCPU ? `${state.info.NCPU}` : '-'; document.getElementById('docker-memory').textContent = state.info?.MemTotal ? formatBytes(state.info.MemTotal) : '-'; document.getElementById('docker-kernel').textContent = state.info?.KernelVersion || '-'; document.getElementById('docker-server').textContent = state.info?.ServerVersion || '-'; } function renderContainers() { const container = document.getElementById('containers-list'); if (!state.containers.length) { container.innerHTML = '
📦

No containers found

'; return; } const html = ` ${state.containers.map(c => { const name = (c.Names || []).map(n => n.replace(/^\//,'')).join(', ') || c.Id.slice(0, 12); const image = c.Image || '-'; const status = getStatusBadge(c.State); const ports = getPortsHtml(c.Ports); const created = formatRelativeTime(c.Created); const actions = getContainerActions(c); return ` `; }).join('')}
Name Image Status Ports Created Actions
${escapeHtml(name)} ${escapeHtml(image)} ${status} ${ports} ${created} ${actions}
`; container.innerHTML = html; attachContainerEventListeners(); } function getPortsHtml(ports) { if (!ports || !ports.length) return 'No ports'; const unique = new Map(); ports.forEach(p => { const key = `${p.PrivatePort}/${p.Type}`; if (!unique.has(key)) { unique.set(key, p.PublicPort || '-'); } }); return Array.from(unique.entries()) .map(([priv, pub]) => `${pub}→${priv}`) .join(''); } function getContainerActions(container) { const isRunning = container.State === 'running'; return `
${isRunning ? ` ` : ` `}
`; } function attachContainerEventListeners() { document.querySelectorAll('[data-action]').forEach(btn => { btn.addEventListener('click', (e) => { const action = e.target.closest('[data-action]').dataset.action; const id = e.target.closest('[data-action]').dataset.id; handleContainerAction(action, id); }); }); } function getStatusBadge(state) { const badges = { 'running': { class: 'badge-running', label: 'running' }, 'paused': { class: 'badge-paused', label: 'paused' }, 'exited': { class: 'badge-exited', label: 'exited' }, 'created': { class: 'badge-created', label: 'created' }, }; const badge = badges[state] || { class: 'badge-exited', label: state }; return `${badge.label}`; } async function handleContainerAction(action, id) { try { if (action === 'logs') { state.logsContainerId = id; await fetchAndShowLogs(); return; } if (action === 'remove') { showConfirmModal( 'Remove Container', 'Are you sure you want to remove this container?', async () => { await executeContainerAction(action, id); } ); return; } await executeContainerAction(action, id); } catch (err) { showToast(`Error: ${err.message}`, 'error'); } } async function executeContainerAction(action, id) { let url = ''; let method = 'POST'; switch (action) { case 'start': url = `${API_BASE}/containers/${id}/start`; break; case 'stop': url = `${API_BASE}/containers/${id}/stop`; break; case 'restart': url = `${API_BASE}/containers/${id}/restart`; break; case 'remove': url = `${API_BASE}/containers/${id}`; method = 'DELETE'; break; default: return; } const res = await fetch(url, { method }); if (!res.ok) throw new Error(`Failed to ${action} container`); showToast(`Container ${action}ed successfully`, 'success'); await Promise.all([fetchContainers(), fetchInitialData()]); renderTabs(); } async function fetchAndShowLogs() { try { const tail = document.getElementById('logs-tail').value; const res = await fetch(`${API_BASE}/containers/${state.logsContainerId}/logs?tail=${tail}`); if (!res.ok) throw new Error('Failed to fetch logs'); const data = await res.json(); let logs = data.logs || 'No logs available'; logs = stripAnsi(logs); document.getElementById('logs-content').textContent = logs; document.getElementById('logs-modal').style.display = 'flex'; } catch (err) { showToast(`Error: ${err.message}`, 'error'); } } function stripAnsi(text) { return text.replace(/\x1b\[[0-9;]*m/g, ''); } function refreshLogs() { if (state.logsContainerId) { fetchAndShowLogs(); } } function closeLogsModal() { document.getElementById('logs-modal').style.display = 'none'; } function renderImages() { const container = document.getElementById('images-list'); if (!state.images.length) { container.innerHTML = '
🖼

No images found

'; return; } const rows = []; state.images.forEach((img) => { const tags = img.RepoTags || ['untagged']; const size = formatBytes(img.Size || 0); const created = formatRelativeTime(img.Created); tags.forEach((tag, tagIdx) => { const isFirstTag = tagIdx === 0; const actions = isFirstTag ? `` : ''; rows.push(` ${escapeHtml(tag.split(':')[0] || '-')} ${escapeHtml(tag.split(':')[1] || '-')} ${escapeHtml(img.Id.slice(0, 12))} ${isFirstTag ? size : ''} ${isFirstTag ? created : ''} ${actions} `); }); }); const html = ` ${rows.join('')}
Repository Tag ID Size Created Actions
`; container.innerHTML = html; attachImageEventListeners(); } function attachImageEventListeners() { document.querySelectorAll('[data-action="remove-image"]').forEach(btn => { btn.addEventListener('click', (e) => { const id = e.target.dataset.id; showConfirmModal( 'Remove Image', 'Are you sure you want to remove this image?', async () => { try { const res = await fetch(`${API_BASE}/images/${id}`, { method: 'DELETE' }); if (!res.ok) throw new Error('Failed to remove image'); showToast('Image removed successfully', 'success'); await fetchImages(); renderTabs(); } catch (err) { showToast(`Error: ${err.message}`, 'error'); } } ); }); }); } async function pullImage() { const input = document.getElementById('pull-input'); const name = input.value.trim(); if (!name) { showToast('Please enter an image name', 'error'); return; } const progressDiv = document.getElementById('pull-progress'); progressDiv.style.display = 'block'; progressDiv.innerHTML = ''; const layerStates = {}; try { const res = await fetch(`${API_BASE}/images/pull?name=${encodeURIComponent(name)}`); if (!res.ok) throw new Error('Failed to pull image'); const reader = res.body.getReader(); const decoder = new TextDecoder(); while (true) { const { done, value } = await reader.read(); if (done) break; const lines = decoder.decode(value).split('\n').filter(l => l); for (const line of lines) { try { const data = JSON.parse(line); if (data.id && data.status) { const layerId = data.id; if (!layerStates[layerId]) { layerStates[layerId] = document.createElement('div'); layerStates[layerId].className = 'pull-progress-item'; layerStates[layerId].dataset.layerId = layerId; progressDiv.appendChild(layerStates[layerId]); } layerStates[layerId].textContent = `${layerId.slice(0, 12)}: ${data.status}`; progressDiv.scrollTop = progressDiv.scrollHeight; } } catch (e) { // Not JSON, skip } } } showToast('Image pulled successfully', 'success'); await fetchImages(); renderTabs(); input.value = ''; progressDiv.style.display = 'none'; } catch (err) { showToast(`Error: ${err.message}`, 'error'); progressDiv.style.display = 'none'; } } function showConfirmModal(title, message, onConfirm) { window.pendingConfirmAction = onConfirm; document.getElementById('confirm-title').textContent = title; document.getElementById('confirm-message').textContent = message; document.getElementById('confirm-modal').style.display = 'flex'; } function closeConfirmModal() { window.pendingConfirmAction = null; document.getElementById('confirm-modal').style.display = 'none'; } function confirmAction() { if (window.pendingConfirmAction) { window.pendingConfirmAction(); } closeConfirmModal(); } function formatBytes(bytes) { if (!bytes) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; } function formatRelativeTime(timestamp) { if (!timestamp) return '-'; const date = new Date(timestamp); const now = new Date(); const diff = now - date; const seconds = Math.floor(diff / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (seconds < 60) return `${seconds}s ago`; if (minutes < 60) return `${minutes}m ago`; if (hours < 24) return `${hours}h ago`; return `${days}d ago`; } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function showToast(message, type = 'info') { const toast = document.getElementById('toast'); toast.textContent = message; toast.className = `toast show ${type}`; setTimeout(() => { toast.classList.remove('show'); }, 3000); } init();