import Hyperswarm from 'hyperswarm'; import b4a from 'b4a'; import { startTerminal, appendTerminalOutput } from './libs/terminal.js'; import { startDockerTerminal, cleanUpDockerTerminal } from './libs/dockerTerminal.js'; import { fetchTemplates, displayTemplateList, openDeployModal } from './libs/templateDeploy.js'; // DOM Elements const containerList = document.getElementById('container-list'); const connectionList = document.getElementById('connection-list'); const addConnectionForm = document.getElementById('add-connection-form'); const newConnectionTopic = document.getElementById('new-connection-topic'); const connectionTitle = document.getElementById('connection-title'); const dashboard = document.getElementById('dashboard'); // Modal Elements const duplicateModalElement = document.getElementById('duplicateModal'); const duplicateModal = new bootstrap.Modal(duplicateModalElement); const duplicateContainerForm = document.getElementById('duplicate-container-form'); // Global variables const connections = {}; window.openTerminals = {}; let activePeer = null; window.activePeer = null; // Expose to other modules hideStatusIndicator(); let statsInterval = null; let lastStatsUpdate = Date.now(); function stopStatsInterval() { if (statsInterval) { clearInterval(statsInterval); statsInterval = null; console.log('[INFO] Stats interval stopped.'); } } function closeAllModals() { // Find and hide all open modals const modals = document.querySelectorAll('.modal.show'); // Adjust selector if necessary modals.forEach(modal => { const modalInstance = bootstrap.Modal.getInstance(modal); // Get Bootstrap modal instance modalInstance.hide(); // Close the modal }); } document.addEventListener('DOMContentLoaded', () => { const dockerTerminalModal = document.getElementById('dockerTerminalModal'); if (dockerTerminalModal) { dockerTerminalModal.addEventListener('hidden.bs.modal', () => { console.log('[INFO] Modal fully closed. Performing additional cleanup.'); cleanUpDockerTerminal(); }); } }); function startStatsInterval() { if (statsInterval) { clearInterval(statsInterval); } statsInterval = setInterval(() => { if (window.activePeer) { const now = Date.now(); if (now - lastStatsUpdate >= 500) { // Ensure at least 500ms between updates // sendCommand('allStats', {}); // Adjust command if necessary lastStatsUpdate = now; } } else { console.warn('[WARN] No active peer; skipping stats request.'); } }, 100); // Poll every 100ms for better reactivity } const smoothedStats = {}; // Container-specific smoothing storage function smoothStats(containerId, newStats, smoothingFactor = 0.2) { if (!smoothedStats[containerId]) { smoothedStats[containerId] = { cpu: 0, memory: 0, ip: newStats.ip || 'No IP Assigned' }; } smoothedStats[containerId].cpu = smoothedStats[containerId].cpu * (1 - smoothingFactor) + newStats.cpu * smoothingFactor; smoothedStats[containerId].memory = smoothedStats[containerId].memory * (1 - smoothingFactor) + newStats.memory * smoothingFactor; // Preserve the latest IP address smoothedStats[containerId].ip = newStats.ip || smoothedStats[containerId].ip; return smoothedStats[containerId]; } function refreshContainerStats() { console.log('[INFO] Refreshing container stats...'); sendCommand('listContainers'); // Request an updated container list startStatsInterval(); // Restart stats interval } function waitForPeerResponse(expectedMessageFragment, timeout = 900000) { console.log(`[DEBUG] Waiting for peer response with fragment: "${expectedMessageFragment}"`); return new Promise((resolve, reject) => { const startTime = Date.now(); window.handlePeerResponse = (response) => { console.log(`[DEBUG] Received response: ${JSON.stringify(response)}`); console.log(response.message) if (response && response.success && response.message.includes(expectedMessageFragment)) { console.log(`[DEBUG] Expected response received: ${response.message}`); resolve(response); } else if (Date.now() - startTime > timeout) { console.warn('[WARN] Timeout while waiting for peer response'); reject(new Error('Timeout waiting for peer response')); } }; // Timeout fallback setTimeout(() => { console.warn('[WARN] Timed out waiting for response'); reject(new Error('Timed out waiting for peer response')); }, timeout); }); } // Utility functions for managing cookies function setCookie(name, value, days = 365) { const date = new Date(); date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); const expires = `expires=${date.toUTCString()}`; document.cookie = `${name}=${encodeURIComponent(value)};${expires};path=/`; } function getCookie(name) { const cookies = document.cookie.split('; '); for (let i = 0; i < cookies.length; i++) { const [key, value] = cookies[i].split('='); if (key === name) return decodeURIComponent(value); } return null; } function deleteCookie(name) { document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`; } // Load connections from cookies function loadConnections() { const savedConnections = getCookie('connections'); const connections = savedConnections ? JSON.parse(savedConnections) : {}; // Recreate the topic Buffer from the hex string for (const topicId in connections) { const { topicHex } = connections[topicId]; connections[topicId] = { topic: b4a.from(topicHex, 'hex'), topicHex, peer: null, // Initialize additional properties swarm: null, }; } return connections; } // Save connections to cookies function saveConnections() { const serializableConnections = {}; for (const topicId in connections) { const { topic, topicHex } = connections[topicId]; // Only serialize simple properties serializableConnections[topicId] = { topicHex, topic: b4a.toString(topic, 'hex'), // Convert Buffer to hex string }; } setCookie('connections', JSON.stringify(serializableConnections)); } // Add Reset Connections Button // Toggle Reset Connections Button Visibility function toggleResetButtonVisibility() { const resetConnectionsBtn = document.querySelector('#sidebar .btn-danger'); if (!resetConnectionsBtn) return; // Show or hide the button based on active connections resetConnectionsBtn.style.display = Object.keys(connections).length > 0 ? 'block' : 'none'; } // Add Reset Connections Button const resetConnectionsBtn = document.createElement('button'); resetConnectionsBtn.textContent = 'Reset Connections'; resetConnectionsBtn.className = 'btn btn-danger w-100 mt-2'; resetConnectionsBtn.addEventListener('click', () => { console.log('[INFO] Resetting connections and clearing cookies.'); Object.keys(connections).forEach((topicId) => { disconnectConnection(topicId); }); deleteCookie('connections'); resetConnectionsView(); showWelcomePage(); toggleResetButtonVisibility(); // Ensure button visibility is updated }); document.getElementById('sidebar').appendChild(resetConnectionsBtn); // Initialize the app console.log('[INFO] Client app initialized'); // Load connections from cookies and restore them document.addEventListener('DOMContentLoaded', () => { const savedConnections = loadConnections(); console.log('[INFO] Restoring saved connections:', savedConnections); // Restore saved connections Object.keys(savedConnections).forEach((topicId) => { let topicHex = savedConnections[topicId].topic; // Ensure topicHex is a string if (typeof topicHex !== 'string') { topicHex = b4a.toString(topicHex, 'hex'); } addConnection(topicHex); }); if (Object.keys(connections).length > 0) { hideWelcomePage(); } else { showWelcomePage(); } assertVisibility(); // Ensure visibility reflects the restored connections }); // Show Status Indicator // Modify showStatusIndicator to recreate it dynamically function showStatusIndicator(message = 'Processing...') { const statusIndicator = document.createElement('div'); statusIndicator.id = 'status-indicator'; statusIndicator.className = 'position-fixed top-0 start-0 w-100 h-100 d-flex justify-content-center align-items-center bg-dark bg-opacity-75'; statusIndicator.innerHTML = `
Loading...

${message}

`; document.body.appendChild(statusIndicator); } function hideStatusIndicator() { const statusIndicator = document.getElementById('status-indicator'); if (statusIndicator) { console.log('[DEBUG] Hiding status indicator'); statusIndicator.remove(); } else { console.error('[ERROR] Status indicator element not found!'); } } // Show Alert // Show alert message function showAlert(type, message) { const alertBox = document.createElement('div'); alertBox.className = `alert alert-${type}`; alertBox.textContent = message; const container = document.querySelector('#alert-container'); if (container) { container.appendChild(alertBox); setTimeout(() => { container.removeChild(alertBox); }, 5000); } else { console.warn('[WARN] Alert container not found.'); } } // Collapse Sidebar Functionality const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn'); collapseSidebarBtn.addEventListener('click', () => { const sidebar = document.getElementById('sidebar'); sidebar.classList.toggle('collapsed'); const btn = collapseSidebarBtn; btn.innerHTML = sidebar.classList.contains('collapsed') ? '>' : '<'; // Toggle Reset Connections Button Visibility const resetConnectionsBtn = document.querySelector('#sidebar .btn-danger'); resetConnectionsBtn.style.display = sidebar.classList.contains('collapsed') ? 'none' : 'block'; }); function handlePeerData(data, topicId, peer) { try { // Parse the incoming data const response = JSON.parse(data.toString()); console.log(`[DEBUG] Received data from peer (topic: ${topicId}): ${JSON.stringify(response)}`); console.log(response.message) if (response.success && response.message.includes && response.message.includes('deployed successfully')) { console.log(`[INFO] Template deployed successfully: ${response.message}`); closeAllModals(); // Close all modals after successful deployment hideStatusIndicator(); startStatsInterval(); // Restart stats polling showAlert('success', response.message); hideStatusIndicator(); } // Ensure the data is for the active connection if (!connections[topicId]) { console.warn(`[WARN] No connection found for topic: ${topicId}. Ignoring data.`); return; } if (peer !== connections[topicId].peer) { console.warn(`[WARN] Ignoring data from a non-active peer for topic: ${topicId}`); return; } // Delegate handling based on the response type switch (response.type) { case 'allStats': console.log('[INFO] Received aggregated stats for all containers.'); response.data.forEach((stats) => updateContainerStats(stats)); break; case 'containers': console.log('[INFO] Processing container list...'); renderContainers(response.data, topicId); // Render containers specific to this topic break; case 'terminalOutput': console.log('[INFO] Appending terminal output...'); appendTerminalOutput(response.data, response.containerId, response.encoding); break; case 'containerConfig': console.log('[INFO] Handling container configuration...'); if (window.inspectContainerCallback) { window.inspectContainerCallback(response.data); window.inspectContainerCallback = null; // Reset the callback } break; case 'logs': console.log('[INFO] Handling logs output...'); if (window.handleLogOutput) { window.handleLogOutput(response); } break; default: console.warn(`[WARN] Unhandled response type: ${response.type}`); break; } // Handle peer response callback if defined if (typeof window.handlePeerResponse === 'function') { window.handlePeerResponse(response); } } catch (err) { // Catch and log any parsing or processing errors console.error(`[ERROR] Failed to process peer data: ${err.message}`); console.error(`[DEBUG] Raw data received: ${data.toString()}`); showAlert('danger', 'Failed to process peer data. Check the console for details.'); } } // Add a new connection addConnectionForm.addEventListener('submit', (e) => { e.preventDefault(); const topicHex = newConnectionTopic.value.trim(); if (topicHex) { addConnection(topicHex); newConnectionTopic.value = ''; } }); function addConnection(topicHex) { console.log(`[DEBUG] Adding connection with topic: ${topicHex}`); if (Object.keys(connections).length === 0) { hideWelcomePage(); } const topic = b4a.from(topicHex, 'hex'); const topicId = topicHex.substring(0, 12); connections[topicId] = { topic, peer: null, swarm: null, topicHex }; saveConnections(); // Save updated connections to cookies const connectionItem = document.createElement('li'); connectionItem.className = 'list-group-item d-flex align-items-center justify-content-between'; connectionItem.dataset.topicId = topicId; connectionItem.innerHTML = `
${topicId}
`; // Add event listener for "Deploy Template" button connectionItem.querySelector('.deploy-template-btn').addEventListener('click', () => { console.log(`[INFO] Opening template deploy modal for connection: ${topicId}`); openTemplateDeployModal(topicId); }); // Add Docker Terminal button event listener connectionItem.querySelector('.docker-terminal-btn')?.addEventListener('click', (event) => { event.stopPropagation(); console.log('[DEBUG] Docker terminal button clicked.'); if (!topicId) { console.error('[ERROR] Missing topicId. Cannot proceed.'); return; } const connection = connections[topicId]; console.log(`[DEBUG] Retrieved connection for topicId: ${topicId}`, connection); if (connection && connection.peer) { try { console.log(`[DEBUG] Starting Docker terminal for topicId: ${topicId}`); startDockerTerminal(topicId, connection.peer); const dockerTerminalModal = document.getElementById('dockerTerminalModal'); if (dockerTerminalModal) { const modalInstance = new bootstrap.Modal(dockerTerminalModal); modalInstance.show(); console.log('[DEBUG] Docker Terminal modal displayed.'); } else { console.error('[ERROR] Docker Terminal modal not found in the DOM.'); } } catch (error) { console.error(`[ERROR] Failed to start Docker CLI terminal for topicId: ${topicId}`, error); } } else { console.warn(`[WARNING] No active peer found for topicId: ${topicId}. Unable to start Docker CLI terminal.`); } }); connectionItem.querySelector('span').addEventListener('click', () => switchConnection(topicId)); connectionItem.querySelector('.disconnect-btn').addEventListener('click', (e) => { e.stopPropagation(); disconnectConnection(topicId, connectionItem); }); refreshContainerStats(); connectionList.appendChild(connectionItem); const swarm = new Hyperswarm(); connections[topicId].swarm = swarm; swarm.join(topic, { client: true, server: false }); swarm.on('connection', (peer) => { console.log(`[INFO] Connected to peer for topic: ${topicHex}`); if (connections[topicId].peer) { peer.destroy(); return; } connections[topicId].peer = peer; updateConnectionStatus(topicId, true); peer.on('data', (data) => handlePeerData(data, topicId, peer)); peer.on('close', () => { updateConnectionStatus(topicId, false); if (window.activePeer === peer) { window.activePeer = null; dashboard.classList.add('hidden'); containerList.innerHTML = ''; stopStatsInterval(); // Stop stats polling } }); if (!window.activePeer) { switchConnection(topicId); } startStatsInterval(); }); // Collapse the sidebar after adding a connection const sidebar = document.getElementById('sidebar'); const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn'); if (!sidebar.classList.contains('collapsed')) { sidebar.classList.add('collapsed'); collapseSidebarBtn.innerHTML = '>'; console.log('[DEBUG] Sidebar collapsed after adding connection'); } } // Function to open the template deploy modal function openTemplateDeployModal(topicId) { // Pass the topic ID or other connection-specific info if needed console.log(`[INFO] Preparing template deploy modal for topic: ${topicId}`); // Ensure the modal fetches templates fetchTemplates(); // Refresh template list // Show the modal const templateDeployModal = new bootstrap.Modal(document.getElementById('templateDeployModal')); templateDeployModal.show(); } // Initialize connections from cookies on page load document.addEventListener('DOMContentLoaded', () => { const savedConnections = loadConnections(); console.log('[INFO] Loading saved connections:', savedConnections); Object.keys(savedConnections).forEach((topicId) => { const topicHex = savedConnections[topicId].topic; addConnection(topicHex); }); if (Object.keys(connections).length > 0) { hideWelcomePage(); startStatsInterval(); // Start stats polling for active peers } else { showWelcomePage(); } assertVisibility(); }); function disconnectConnection(topicId, connectionItem) { const connection = connections[topicId]; if (!connection) { console.error(`[ERROR] No connection found for topicId: ${topicId}`); return; } // Clean up terminals if (window.openTerminals[topicId]) { console.log(`[INFO] Closing terminals for topic: ${topicId}`); window.openTerminals[topicId].forEach((terminalId) => { try { cleanUpTerminal(terminalId); } catch (err) { console.error(`[ERROR] Failed to clean up terminal ${terminalId}: ${err.message}`); } }); delete window.openTerminals[topicId]; } // Destroy the peer and swarm if (connection.peer) { connection.peer.destroy(); } if (connection.swarm) { connection.swarm.destroy(); } // Remove from global connections delete connections[topicId]; // Save the updated connections to cookies saveConnections(); // Remove the connection item from the UI if (connectionItem) { connectionList.removeChild(connectionItem); } // Reset the connection title if this was the active peer if (window.activePeer === connection.peer) { window.activePeer = null; const connectionTitle = document.getElementById('connection-title'); if (connectionTitle) { connectionTitle.textContent = 'Choose a Connection'; // Reset the title } const dashboard = document.getElementById('dashboard'); if (dashboard) { dashboard.classList.add('hidden'); } resetContainerList(); // Clear containers } // Show welcome page if no connections remain if (Object.keys(connections).length === 0) { showWelcomePage(); } console.log(`[INFO] Disconnected and removed connection: ${topicId}`); } // Function to reset the container list function resetContainerList() { containerList.innerHTML = ''; // Clear the existing list console.log('[INFO] Container list cleared.'); } // Function to reset the connections view function resetConnectionsView() { // Clear the connection list connectionList.innerHTML = ''; // Re-populate the connection list from the `connections` object Object.keys(connections).forEach((topicId) => { const connectionItem = document.createElement('li'); connectionItem.className = 'list-group-item d-flex align-items-center justify-content-between'; connectionItem.dataset.topicId = topicId; connectionItem.innerHTML = ` `; // Add click event to switch connection connectionItem.querySelector('span').addEventListener('click', () => switchConnection(topicId)); // Add click event to the disconnect button const disconnectBtn = connectionItem.querySelector('.disconnect-btn'); disconnectBtn.addEventListener('click', (e) => { e.stopPropagation(); // Prevent triggering the switch connection event disconnectConnection(topicId, connectionItem); }); connectionList.appendChild(connectionItem); }); console.log('[INFO] Connections view reset.'); } // Update connection status function updateConnectionStatus(topicId, isConnected) { const connectionItem = document.querySelector(`[data-topic-id="${topicId}"] .connection-status`); if (connectionItem) { connectionItem.className = `connection-status ${isConnected ? 'status-connected' : 'status-disconnected'}`; } } // Switch between connections function switchConnection(topicId) { const connection = connections[topicId]; if (!connection || !connection.peer) { console.error('[ERROR] No connection found or no active peer.'); showWelcomePage(); stopStatsInterval(); // Stop stats interval if no active peer return; } // Update the active peer window.activePeer = connection.peer; // Clear container list before loading new data resetContainerList(); console.log(`[INFO] Switched to connection: ${topicId}`); // Start the stats interval startStatsInterval(); sendCommand('listContainers'); // Request containers for the new connection } // Attach switchConnection to the global window object window.switchConnection = switchConnection; // Send a command to the active peer function sendCommand(command, args = {}) { if (window.activePeer) { const message = JSON.stringify({ command, args }); console.log(`[DEBUG] Sending command to server: ${message}`); window.activePeer.write(message); } else { console.error('[ERROR] No active peer to send command.'); } } // Attach sendCommand to the global window object window.sendCommand = sendCommand; // Render the container list function renderContainers(containers, topicId) { if (!window.activePeer || !connections[topicId] || window.activePeer !== connections[topicId].peer) { console.warn('[WARN] Active peer mismatch or invalid connection. Skipping container rendering.'); return; } console.log(`[INFO] Rendering ${containers.length} containers for topic: ${topicId}`); containerList.innerHTML = ''; // Clear the current list containers.forEach((container) => { const name = container.Names[0]?.replace(/^\//, '') || 'Unknown'; // Avoid undefined Names const image = container.Image || '-'; const containerId = container.Id; const ipAddress = container.ipAddress || 'No IP Assigned'; if (ipAddress === 'No IP Assigned') { console.warn(`[WARN] IP address missing for container ${container.Id}. Retrying...`); sendCommand('inspectContainer', { id: container.Id }); } const row = document.createElement('tr'); row.dataset.containerId = containerId; // Store container ID for reference row.innerHTML = ` ${name} ${image} ${container.State || 'Unknown'} 0 0 ${ipAddress}
`; containerList.appendChild(row); // Add event listener for duplicate button const duplicateBtn = row.querySelector('.action-duplicate'); duplicateBtn.addEventListener('click', () => openDuplicateModal(container)); // Add event listeners for action buttons addActionListeners(row, container); }); } function addActionListeners(row, container) { const startBtn = row.querySelector('.action-start'); const stopBtn = row.querySelector('.action-stop'); const removeBtn = row.querySelector('.action-remove'); const terminalBtn = row.querySelector('.action-terminal'); const restartBtn = row.querySelector('.action-restart'); // Start Button startBtn.addEventListener('click', async () => { showStatusIndicator(`Starting container "${container.Names[0]}"...`); sendCommand('startContainer', { id: container.Id }); const expectedMessageFragment = `Container ${container.Id} started`; try { const response = await waitForPeerResponse(expectedMessageFragment); console.log('[DEBUG] Start container response:', response); showAlert('success', response.message); // Refresh the container list to update states sendCommand('listContainers'); // Restart stats interval startStatsInterval(); } catch (error) { console.error('[ERROR] Failed to start container:', error.message); showAlert('danger', error.message || 'Failed to start container.'); } finally { console.log('[DEBUG] Hiding status indicator in startBtn finally block'); hideStatusIndicator(); } }); stopBtn.addEventListener('click', async () => { showStatusIndicator(`Stopping container "${container.Names[0]}"...`); sendCommand('stopContainer', { id: container.Id }); const expectedMessageFragment = `Container ${container.Id} stopped`; try { const response = await waitForPeerResponse(expectedMessageFragment); console.log('[DEBUG] Stop container response:', response); showAlert('success', response.message); // Refresh the container list to update states sendCommand('listContainers'); // Restart stats interval startStatsInterval(); } catch (error) { console.error('[ERROR] Failed to stop container:', error.message); showAlert('danger', error.message || 'Failed to stop container.'); } finally { console.log('[DEBUG] Hiding status indicator in stopBtn finally block'); hideStatusIndicator(); } }); // Restart Button restartBtn.addEventListener('click', async () => { showStatusIndicator(`Restarting container "${container.Names[0]}"...`); sendCommand('restartContainer', { id: container.Id }); const expectedMessageFragment = `Container ${container.Id} restarted`; try { const response = await waitForPeerResponse(expectedMessageFragment); console.log('[DEBUG] Restart container response:', response); showAlert('success', response.message); // Refresh the container list to update states sendCommand('listContainers'); } catch (error) { console.error('[ERROR] Failed to restart container:', error.message); showAlert('danger', error.message || 'Failed to restart container.'); } finally { console.log('[DEBUG] Hiding status indicator in restartBtn finally block'); hideStatusIndicator(); } }); const logsBtn = row.querySelector('.action-logs'); logsBtn.addEventListener('click', () => openLogModal(container.Id)); function openLogModal(containerId) { console.log(`[INFO] Opening logs modal for container: ${containerId}`); const modal = new bootstrap.Modal(document.getElementById('logsModal')); const logContainer = document.getElementById('logs-container'); // Clear any existing logs logContainer.innerHTML = ''; // Request previous logs sendCommand('logs', { id: containerId }); // Listen for logs window.handleLogOutput = (logData) => { const logLine = atob(logData.data); // Decode base64 logs const logElement = document.createElement('pre'); logElement.textContent = logLine; logContainer.appendChild(logElement); // Scroll to the bottom logContainer.scrollTop = logContainer.scrollHeight; }; // Show the modal modal.show(); } // Remove Button removeBtn.addEventListener('click', async () => { const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal')); deleteModal.show(); const confirmDeleteBtn = document.getElementById('confirm-delete-btn'); confirmDeleteBtn.onclick = async () => { deleteModal.hide(); showStatusIndicator(`Deleting container "${container.Names[0]}"...`); // Check if the container has active terminals if (window.openTerminals[container.Id]) { console.log(`[INFO] Closing active terminals for container: ${container.Id}`); window.openTerminals[container.Id].forEach((terminalId) => { try { cleanUpTerminal(terminalId); } catch (err) { console.error(`[ERROR] Failed to clean up terminal ${terminalId}: ${err.message}`); } }); delete window.openTerminals[container.Id]; } // Hide the terminal modal if it is active const terminalModal = document.getElementById('terminal-modal'); if (terminalModal.style.display === 'flex') { console.log(`[INFO] Hiding terminal modal for container: ${container.Id}`); terminalModal.style.display = 'none'; } terminalModal.addEventListener('shown.bs.modal', () => { terminal.focus(); }); sendCommand('removeContainer', { id: container.Id }); const expectedMessageFragment = `Container ${container.Id} removed`; try { const response = await waitForPeerResponse(expectedMessageFragment); console.log('[DEBUG] Remove container response:', response); showAlert('success', response.message); // Refresh the container list to update states sendCommand('listContainers'); } catch (error) { console.error('[ERROR] Failed to delete container:', error.message); showAlert('danger', error.message || `Failed to delete container "${container.Names[0]}".`); } finally { console.log('[DEBUG] Hiding status indicator in removeBtn finally block'); hideStatusIndicator(); } }; }); terminalBtn.addEventListener('click', () => { console.log(`[DEBUG] Opening terminal for container ID: ${container.Id}`); try { startTerminal(container.Id, container.Names[0] || container.Id); } catch (error) { console.error(`[ERROR] Failed to start terminal for container ${container.Id}: ${error.message}`); showAlert('danger', `Failed to start terminal: ${error.message}`); } }); } function updateContainerStats(stats) { if (!stats || !stats.id || typeof stats.cpu === 'undefined' || typeof stats.memory === 'undefined') { console.error('[ERROR] Invalid stats object:', stats); return; } console.log(`[DEBUG] Updating stats for container ID: ${stats.id}`); const row = containerList.querySelector(`tr[data-container-id="${stats.id}"]`); if (!row) { console.warn(`[WARN] No matching row for container ID: ${stats.id}`); return; } // Ensure the IP address is added or retained from existing row const existingIpAddress = row.querySelector('.ip-address')?.textContent || 'No IP Assigned'; stats.ip = stats.ip || existingIpAddress; const smoothed = smoothStats(stats.id, stats); updateStatsUI(row, smoothed); } function updateStatsUI(row, stats) { requestIdleCallback(() => { row.querySelector('.cpu').textContent = stats.cpu.toFixed(2) || '0.00'; row.querySelector('.memory').textContent = (stats.memory / (1024 * 1024)).toFixed(2) || '0.00'; row.querySelector('.ip-address').textContent = stats.ip; }); } // Function to open the Duplicate Modal with container configurations function openDuplicateModal(container) { console.log(`[INFO] Opening Duplicate Modal for container: ${container.Id}`); showStatusIndicator('Fetching container configuration...'); // Send a command to inspect the container sendCommand('inspectContainer', { id: container.Id }); // Listen for the inspectContainer response window.inspectContainerCallback = (config) => { hideStatusIndicator(); if (!config) { console.error('[ERROR] Failed to retrieve container configuration.'); showAlert('danger', 'Failed to retrieve container configuration.'); return; } console.log(`[DEBUG] Retrieved container configuration: ${JSON.stringify(config)}`); // Parse configuration and populate the modal fields try { const CPUs = config.HostConfig?.CpusetCpus?.split(',') || []; document.getElementById('container-name').value = config.Name.replace(/^\//, ''); document.getElementById('container-hostname').value = config.Config.Hostname || ''; document.getElementById('container-image').value = config.Config.Image || ''; document.getElementById('container-netmode').value = config.HostConfig?.NetworkMode || ''; document.getElementById('container-cpu').value = CPUs.length || 0; document.getElementById('container-memory').value = Math.round(config.HostConfig?.Memory / (1024 * 1024)) || 0; document.getElementById('container-config').value = JSON.stringify(config, null, 2); // Show the duplicate modal duplicateModal.show(); } catch (error) { console.error(`[ERROR] Failed to populate modal fields: ${error.message}`); showAlert('danger', 'Failed to populate container configuration fields.'); } }; } // Handle the Duplicate Container Form Submission duplicateContainerForm.addEventListener('submit', (e) => { e.preventDefault(); duplicateModal.hide(); showStatusIndicator('Duplicating container...'); const name = document.getElementById('container-name').value.trim(); const hostname = document.getElementById('container-hostname').value.trim(); const image = document.getElementById('container-image').value.trim(); const netmode = document.getElementById('container-netmode').value.trim(); const cpu = document.getElementById('container-cpu').value.trim(); const memory = document.getElementById('container-memory').value.trim(); const configJSON = document.getElementById('container-config').value.trim(); let config; try { config = JSON.parse(configJSON); } catch (err) { hideStatusIndicator(); showAlert('danger', 'Invalid JSON in configuration.'); return; } sendCommand('duplicateContainer', { name, image, hostname, netmode, cpu, memory, config }); // Simulate delay for the demo setTimeout(() => { hideStatusIndicator(); showAlert('success', 'Container duplicated successfully!'); // Refresh container list sendCommand('listContainers'); }, 2000); // Simulated processing time }); function showWelcomePage() { const welcomePage = document.getElementById('welcome-page'); const dashboard = document.getElementById('dashboard'); const connectionTitle = document.getElementById('connection-title'); if (welcomePage) { welcomePage.classList.remove('hidden'); } if (dashboard) { dashboard.classList.add('hidden'); } if (connectionTitle) { connectionTitle.textContent = ''; } else { console.warn('[WARN] Connection title element not found!'); } } function hideWelcomePage() { const welcomePage = document.getElementById('welcome-page'); const dashboard = document.getElementById('dashboard'); if (welcomePage) { console.log('[DEBUG] Hiding welcome page'); welcomePage.classList.add('hidden'); // Hide the welcome page } else { console.error('[ERROR] Welcome page element not found!'); } if (dashboard) { console.log('[DEBUG] Showing dashboard'); dashboard.classList.remove('hidden'); // Show the dashboard } else { console.error('[ERROR] Dashboard element not found!'); } } function assertVisibility() { const welcomePage = document.getElementById('welcome-page'); const dashboard = document.getElementById('dashboard'); if (Object.keys(connections).length === 0) { console.assert(!welcomePage.classList.contains('hidden'), '[ASSERTION FAILED] Welcome page should be visible.'); console.assert(dashboard.classList.contains('hidden'), '[ASSERTION FAILED] Dashboard should be hidden.'); } else { console.assert(welcomePage.classList.contains('hidden'), '[ASSERTION FAILED] Welcome page should be hidden.'); console.assert(!dashboard.classList.contains('hidden'), '[ASSERTION FAILED] Dashboard should be visible.'); } } // Attach startTerminal to the global window object window.startTerminal = startTerminal; // Handle window unload to clean up swarms and peers window.addEventListener('beforeunload', () => { for (const topicId in connections) { const connection = connections[topicId]; if (connection.peer) { connection.peer.destroy(); } if (connection.swarm) { connection.swarm.destroy(); } } });