peardock/app.js
2024-11-30 06:53:07 -05:00

1066 lines
35 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Hyperswarm from 'hyperswarm';
import b4a from 'b4a';
import { startTerminal, appendTerminalOutput } from './libs/terminal.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 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('stats', {}); // 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)}`);
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);
});
}
function saveConnections() {
console.log('[DEBUG] Saving connections:', connections);
const serializableConnections = {};
for (const topicId in connections) {
const { topic, topicHex, connectionName } = connections[topicId];
if (!serializableConnections[topicId] && topicHex) {
serializableConnections[topicId] = {
topicHex,
connectionName: connectionName || 'Unnamed Connection',
topic: b4a.toString(topic, 'hex'),
};
} else {
console.warn(`[WARN] Skipping duplicate or invalid connection: ${topicId}`);
}
}
localStorage.setItem('connections', JSON.stringify(serializableConnections));
}
function loadConnections() {
// Clear any previously loaded connections in memory
Object.keys(connections).forEach((topicId) => {
delete connections[topicId];
});
const savedConnections = localStorage.getItem('connections');
const connectionsData = savedConnections ? JSON.parse(savedConnections) : {};
for (const topicId in connectionsData) {
const { topicHex, connectionName } = connectionsData[topicId];
if (!topicHex) {
console.warn(`[WARN] Skipping connection with missing topicHex: ${topicId}`);
continue;
}
console.log(`[DEBUG] Loading connection: ${topicHex}, Name: ${connectionName}`);
connections[topicId] = {
topic: b4a.from(topicHex, 'hex'),
topicHex,
connectionName: connectionName || 'Unnamed Connection',
peer: null,
swarm: null,
};
}
return connections;
}
function renderConnections() {
console.log('[DEBUG] Rendering connections in the UI...');
connectionList.innerHTML = ''; // Clear the current list
Object.keys(connections).forEach((topicId) => {
const { topicHex, connectionName } = connections[topicId];
// Render the connection
const connectionItem = document.createElement('li');
connectionItem.className = 'list-group-item d-flex align-items-center justify-content-between';
connectionItem.dataset.topicId = topicId;
connectionItem.innerHTML = `
<span>
<span class="connection-status status-disconnected"></span>${connectionName || 'Unnamed Connection'} (${topicId})
</span>
<button class="btn btn-sm btn-danger disconnect-btn">
<i class="fas fa-plug"></i>
</button>
`;
connectionItem.querySelector('span').addEventListener('click', () => switchConnection(topicId));
connectionItem.querySelector('.disconnect-btn').addEventListener('click', (e) => {
e.stopPropagation();
disconnectConnection(topicId, connectionItem);
});
connectionList.appendChild(connectionItem);
});
console.log('[DEBUG] Connections rendered successfully.');
}
function deleteConnections() {
localStorage.removeItem('connections');
}
// 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 local storage.');
Object.keys(connections).forEach((topicId) => {
disconnectConnection(topicId);
});
deleteConnections();
resetConnectionsView();
showWelcomePage();
toggleResetButtonVisibility();
});
document.getElementById('sidebar').appendChild(resetConnectionsBtn);
// 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 = `
<div class="text-center">
<div class="spinner-border text-light" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-3 text-light">${message}</p>
</div>
`;
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
function showAlert(type, message) {
const alertContainer = document.getElementById('alert-container');
// Create alert element
const alert = document.createElement('div');
alert.className = `alert ${type}`;
alert.innerHTML = `
<span>${message}</span>
<button class="close-btn" aria-label="Close">&times;</button>
`;
// Add close button functionality
const closeButton = alert.querySelector('.close-btn');
closeButton.addEventListener('click', () => {
alert.remove(); // Remove alert on close
});
// Append alert to container
alertContainer.appendChild(alert);
// Automatically remove alert after 5 seconds
setTimeout(() => {
alert.remove();
}, 5000);
}
// 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') ? '&gt;' : '&lt;';
// 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)}`);
// 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 'stats':
console.log('[INFO] Updating container stats...');
// Ensure IP is included and passed to updateContainerStats
const stats = response.data;
stats.ip = stats.ip || 'No IP Assigned'; // Add a fallback for missing IPs
console.log(`[DEBUG] Passing stats to updateContainerStats: ${JSON.stringify(stats, null, 2)}`);
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;
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) {
openConnectionNameModal(topicHex); // Open the modal to ask for the connection name
newConnectionTopic.value = '';
}
});
function openConnectionNameModal(topicHex) {
const connectionNameModalElement = document.getElementById('connectionNameModal');
const connectionNameModal = new bootstrap.Modal(connectionNameModalElement);
const connectionNameInput = document.getElementById('connection-name-input');
const saveConnectionNameBtn = document.getElementById('save-connection-name-btn');
// Clear the input and show the modal
connectionNameInput.value = '';
connectionNameModal.show();
// Add event listener for the save button
saveConnectionNameBtn.onclick = () => {
const connectionName = connectionNameInput.value.trim();
if (!connectionName) {
showAlert('danger', 'Please enter a connection name.');
return;
}
// Hide the modal and add the connection
connectionNameModal.hide();
addConnection(topicHex, connectionName);
};
}
function addConnection(topicHex, connectionName) {
const topicId = topicHex.substring(0, 12);
// Check if the connection exists
if (connections[topicId]) {
console.warn(`[WARN] Connection with topic ${topicHex} already exists.`);
if (!connections[topicId].swarm || !connections[topicId].peer) {
console.log(`[INFO] Reinitializing connection: ${topicHex}`);
connections[topicId].swarm = new Hyperswarm();
const topic = b4a.from(topicHex, 'hex');
connections[topicId].topic = topic;
const swarm = connections[topicId].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', () => {
console.log(`[INFO] Peer disconnected for topic: ${topicId}`);
updateConnectionStatus(topicId, false);
if (window.activePeer === peer) {
window.activePeer = null;
dashboard.classList.add('hidden');
containerList.innerHTML = '';
stopStatsInterval();
}
});
if (!window.activePeer) {
window.activePeer = connections[topicId].peer;
} else {
console.warn(`[WARN] Switching active peer. Current: ${window.activePeer}, New: ${connections[topicId].peer}`);
}
});
}
renderConnections(); // Ensure the sidebar list is updated
return;
}
console.log(`[DEBUG] Adding connection with topic: ${topicHex} and name: ${connectionName}`);
const topic = b4a.from(topicHex, 'hex');
const swarm = new Hyperswarm();
connections[topicId] = { topic, topicHex, connectionName, peer: null, 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();
}
});
if (!window.activePeer) {
switchConnection(topicId);
}
});
saveConnections();
renderConnections(); // Ensure the sidebar list is updated
}
// Initialize the app
console.log('[INFO] Client app initialized');
// Load connections from cookies and restore them
// Initialize the app
console.log('[INFO] Client app initialized');
// Load connections from cookies and restore them
document.addEventListener('DOMContentLoaded', () => {
console.log('[INFO] Initializing the app...');
const savedConnections = loadConnections();
console.log('[INFO] Restoring saved connections:', savedConnections);
Object.keys(savedConnections).forEach((topicId) => {
const { topicHex, connectionName } = savedConnections[topicId];
addConnection(topicHex, connectionName); // Initialize each connection
});
if (Object.keys(connections).length > 0) {
hideWelcomePage();
const firstConnection = Object.keys(connections)[0];
switchConnection(firstConnection); // Auto-switch to the first connection
} else {
showWelcomePage();
}
console.log('[INFO] App initialized successfully.');
});
function disconnectConnection(topicId, connectionItem) {
const connection = connections[topicId];
if (!connection) {
console.error(`[ERROR] No connection found for topicId: ${topicId}`);
return;
}
if (connection.peer) {
connection.peer.destroy();
}
if (connection.swarm) {
connection.swarm.destroy();
}
delete connections[topicId];
saveConnections();
if (connectionItem) {
connectionList.removeChild(connectionItem);
}
if (window.activePeer === connection.peer) {
window.activePeer = null;
const connectionTitle = document.getElementById('connection-title');
if (connectionTitle) {
connectionTitle.textContent = 'Choose a Connection';
}
const dashboard = document.getElementById('dashboard');
if (dashboard) {
dashboard.classList.add('hidden');
}
resetContainerList();
}
renderConnections(); // Ensure the sidebar list is updated
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 = `
<span>
<span class="connection-status ${connections[topicId].peer ? 'status-connected' : 'status-disconnected'}"></span>${topicId}
</span>
<button class="btn btn-sm btn-danger disconnect-btn">Disconnect</button>
`;
// 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) {
if (!connections[topicId]) {
console.error(`[ERROR] No connection found for topic: ${topicId}`);
return;
}
const connectionItem = document.querySelector(`[data-topic-id="${topicId}"] .connection-status`);
if (connectionItem) {
connectionItem.className = `connection-status ${isConnected ? 'status-connected' : 'status-disconnected'}`;
}
console.log(`[DEBUG] Connection ${topicId} status updated to: ${isConnected ? 'connected' : 'disconnected'}`);
}
setInterval(() => {
Object.keys(connections).forEach((topicId) => {
const connection = connections[topicId];
if (connection.peer && !connection.peer.destroyed) {
updateConnectionStatus(topicId, true);
} else {
updateConnectionStatus(topicId, false);
}
});
}, 1000); // Adjust interval as needed
// Switch between connections
function switchConnection(topicId) {
const connection = connections[topicId];
if (!connection || !connection.peer) {
console.warn(`[WARN] No active peer for connection: ${topicId}`);
return; // Skip switching if no active peer is found
}
console.log(`[INFO] Switching to connection: ${topicId}`);
window.activePeer = connection.peer;
resetContainerList();
const connectionTitle = document.getElementById('connection-title');
if (connectionTitle) {
connectionTitle.textContent = connection.connectionName || 'Unnamed Connection';
}
hideWelcomePage();
startStatsInterval();
sendCommand('listContainers');
}
// 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 = `
<td>${name}</td>
<td>${image}</td>
<td>${container.State || 'Unknown'}</td>
<td class="cpu">0</td>
<td class="memory">0</td>
<td class="ip-address">${ipAddress}</td>
<td>
<button class="btn btn-success btn-sm action-start" ${container.State === 'running' ? 'disabled' : ''}>
<i class="fas fa-play"></i>
</button>
<button class="btn btn-info btn-sm action-restart" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-redo"></i>
</button>
<button class="btn btn-warning btn-sm action-stop" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-stop"></i>
</button>
<button class="btn btn-danger btn-sm action-remove">
<i class="fas fa-trash"></i>
</button>
<button class="btn btn-primary btn-sm action-terminal" ${container.State !== 'running' ? 'disabled' : ''}>
<i class="fas fa-terminal"></i>
</button>
<button class="btn btn-secondary btn-sm action-duplicate">
<i class="fas fa-clone"></i>
</button>
</td>
`;
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();
}
});
// 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();
}
}
});