diff --git a/app.js b/app.js index 397ee79..1d9c058 100644 --- a/app.js +++ b/app.js @@ -1,49 +1,50 @@ import Hyperswarm from 'hyperswarm'; import b4a from 'b4a'; -import { Terminal } from 'xterm'; -import { FitAddon } from 'xterm-addon-fit'; - -const swarm = new Hyperswarm(); -const connections = {}; -let activePeer = null; -let terminalSessions = {}; // Track terminal states per container -let xterm = null; // The current terminal instance -let fitAddon = null; // FitAddon instance +import { startTerminal, appendTerminalOutput } from './libs/terminal.js'; // DOM Elements -const terminalModal = document.getElementById('terminal-modal'); -const terminalTitle = document.getElementById('terminal-title'); -const terminalContainer = document.getElementById('terminal-container'); -const tray = document.getElementById('tray'); 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 = {}; +let activePeer = null; +window.activePeer = null; // Expose to other modules // Initialize the app console.log('[INFO] Client app initialized'); -// Add Kill Terminal button functionality -document.getElementById('kill-terminal-btn').onclick = () => { - const containerId = terminalTitle.dataset.containerId; - if (containerId && terminalSessions[containerId]) { - console.log(`[INFO] Killing terminal session for container: ${containerId}`); - - // Send kill command to server - window.sendCommand('killTerminal', { containerId }); - - // Clean up terminal state - terminalModal.style.display = 'none'; - delete terminalSessions[containerId]; - xterm.dispose(); // Dispose of the current terminal instance - xterm = null; // Reset xterm instance - } else { - console.error('[ERROR] No terminal session found to kill.'); - } -}; +// 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') ? '>' : '<'; +}); // Add a new connection -document.getElementById('add-connection-form').addEventListener('submit', (e) => { +addConnectionForm.addEventListener('submit', (e) => { e.preventDefault(); - const topicHex = document.getElementById('new-connection-topic').value; + const topicHex = newConnectionTopic.value.trim(); + if (topicHex) { + addConnection(topicHex); + newConnectionTopic.value = ''; + } +}); + +// Function to add a new connection +function addConnection(topicHex) { const topic = b4a.from(topicHex, 'hex'); const topicId = topicHex.substring(0, 12); @@ -55,14 +56,27 @@ document.getElementById('add-connection-form').addEventListener('submit', (e) => connectionItem.innerHTML = ` ${topicId} `; - connectionItem.addEventListener('click', () => window.switchConnection(topicId)); - document.getElementById('connection-list').appendChild(connectionItem); + connectionItem.addEventListener('click', () => switchConnection(topicId)); + connectionList.appendChild(connectionItem); - connections[topicId] = { topic, peer: null }; + connections[topicId] = { topic, peer: null, swarm: null }; + + // Create a new swarm for this connection + 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}`); + + // Prevent duplicate connections + if (connections[topicId].peer) { + console.warn(`[WARN] Duplicate connection detected for topic: ${topicId}. Closing.`); + peer.destroy(); + return; + } + connections[topicId].peer = peer; updateConnectionStatus(topicId, true); @@ -72,9 +86,20 @@ document.getElementById('add-connection-form').addEventListener('submit', (e) => console.log(`[DEBUG] Received data from server: ${JSON.stringify(response)}`); if (response.type === 'containers') { - renderContainers(response.data); + if (window.activePeer === peer) { + renderContainers(response.data); + } } else if (response.type === 'terminalOutput') { - appendTerminalOutput(response.data, response.containerId); + appendTerminalOutput(response.data, response.containerId, response.encoding); + } else if (response.type === 'containerConfig') { + if (window.inspectContainerCallback) { + window.inspectContainerCallback(response.data); + window.inspectContainerCallback = null; // Reset the callback + } + } else if (response.type === 'stats') { + updateContainerStats(response.data); + } else if (response.error) { + console.log(`Error: ${response.error}`); } } catch (err) { console.error(`[ERROR] Failed to parse data from server: ${err.message}`); @@ -84,41 +109,50 @@ document.getElementById('add-connection-form').addEventListener('submit', (e) => peer.on('close', () => { console.log(`[INFO] Disconnected from peer for topic: ${topicHex}`); updateConnectionStatus(topicId, false); - }); - }); -}); + connections[topicId].peer = null; // Clear the peer reference -// Collapse/Expand Sidebar -document.getElementById('collapse-sidebar-btn').addEventListener('click', () => { - const sidebar = document.getElementById('sidebar'); - sidebar.classList.toggle('collapsed'); - const btn = document.getElementById('collapse-sidebar-btn'); - btn.textContent = sidebar.classList.contains('collapsed') ? '>' : '<'; -}); + if (window.activePeer === peer) { + window.activePeer = null; + connectionTitle.textContent = 'Disconnected'; + dashboard.classList.add('hidden'); + containerList.innerHTML = ''; + } + }); + + // If this is the first connection, switch to it + if (!window.activePeer) { + switchConnection(topicId); + } + }); +} // Update connection status function updateConnectionStatus(topicId, isConnected) { const connectionItem = document.querySelector(`[data-topic-id="${topicId}"] .connection-status`); - connectionItem.className = `connection-status ${isConnected ? 'status-connected' : 'status-disconnected'}`; + if (connectionItem) { + connectionItem.className = `connection-status ${isConnected ? 'status-connected' : 'status-disconnected'}`; + } } // Switch between connections function switchConnection(topicId) { - activePeer = connections[topicId].peer; - const connectionTitle = document.getElementById('connection-title'); - if (!connectionTitle) { - console.error('[ERROR] Connection title element is missing.'); + const connection = connections[topicId]; + if (!connection) { + console.error(`[ERROR] No connection found for topicId: ${topicId}`); return; } - connectionTitle.textContent = `Connection: ${topicId}`; - document.getElementById('dashboard').classList.remove('hidden'); - if (activePeer) { - console.log('[INFO] Sending "listContainers" command'); - window.sendCommand('listContainers'); - } else { - console.error('[ERROR] No active peer to send command.'); + if (!connection.peer) { + console.error('[ERROR] No active peer for this connection.'); + return; } + + window.activePeer = connection.peer; + connectionTitle.textContent = `Connection: ${topicId}`; + dashboard.classList.remove('hidden'); + + console.log('[INFO] Sending "listContainers" command'); + sendCommand('listContainers'); } // Attach switchConnection to the global window object @@ -126,10 +160,10 @@ window.switchConnection = switchConnection; // Send a command to the active peer function sendCommand(command, args = {}) { - if (activePeer) { + if (window.activePeer) { const message = JSON.stringify({ command, args }); console.log(`[DEBUG] Sending command to server: ${message}`); - activePeer.write(message); + window.activePeer.write(message); } else { console.error('[ERROR] No active peer to send command.'); } @@ -140,110 +174,165 @@ window.sendCommand = sendCommand; // Render the container list function renderContainers(containers) { - console.log(`[INFO] Rendering ${containers.length} containers`); - containerList.innerHTML = ''; + console.log(`[INFO] Rendering ${containers.length} containers`); + containerList.innerHTML = ''; // Clear the current list + + containers.forEach((container) => { + const name = container.Names[0].replace(/^\//, ''); // Remove leading slash from container names + const image = container.Image; + const containerId = container.Id; + const row = document.createElement('tr'); + row.dataset.containerId = containerId; // Store container ID for reference + row.innerHTML = ` +