2024-11-26 03:12:55 -05:00
|
|
|
|
import Hyperswarm from 'hyperswarm';
|
|
|
|
|
import b4a from 'b4a';
|
2024-11-26 08:14:43 -05:00
|
|
|
|
import { startTerminal, appendTerminalOutput } from './libs/terminal.js';
|
2024-11-26 03:12:55 -05:00
|
|
|
|
|
|
|
|
|
// DOM Elements
|
|
|
|
|
const containerList = document.getElementById('container-list');
|
2024-11-26 08:14:43 -05:00
|
|
|
|
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 = {};
|
2024-11-28 03:07:24 -05:00
|
|
|
|
window.openTerminals = {};
|
2024-11-26 08:14:43 -05:00
|
|
|
|
let activePeer = null;
|
|
|
|
|
window.activePeer = null; // Expose to other modules
|
2024-11-29 21:57:53 -05:00
|
|
|
|
hideStatusIndicator();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2024-11-26 03:12:55 -05:00
|
|
|
|
|
|
|
|
|
// Initialize the app
|
|
|
|
|
console.log('[INFO] Client app initialized');
|
2024-11-29 21:57:53 -05:00
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
2024-11-29 22:40:26 -05:00
|
|
|
|
console.log('[INFO] Initializing Client App');
|
|
|
|
|
|
|
|
|
|
const connectionTitle = document.getElementById('connection-title');
|
|
|
|
|
if (!connectionTitle) {
|
|
|
|
|
console.error('[ERROR] Connection title element is missing! Creating dynamically...');
|
|
|
|
|
const content = document.getElementById('content');
|
|
|
|
|
const titleElement = document.createElement('h1');
|
|
|
|
|
titleElement.id = 'connection-title';
|
|
|
|
|
titleElement.textContent = '';
|
|
|
|
|
titleElement.classList.add('hidden'); // Initially hidden
|
|
|
|
|
content.insertBefore(titleElement, content.firstChild);
|
2024-11-29 21:57:53 -05:00
|
|
|
|
}
|
2024-11-29 22:40:26 -05:00
|
|
|
|
|
|
|
|
|
if (Object.keys(connections).length === 0) {
|
|
|
|
|
showWelcomePage();
|
|
|
|
|
} else {
|
|
|
|
|
hideWelcomePage();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
assertVisibility(); // Ensure visibility is correct after initialization
|
2024-11-29 21:57:53 -05:00
|
|
|
|
});
|
2024-11-29 22:40:26 -05:00
|
|
|
|
|
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
// 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');
|
|
|
|
|
const alert = document.createElement('div');
|
|
|
|
|
alert.className = `alert alert-${type} alert-dismissible fade show`;
|
|
|
|
|
alert.role = 'alert';
|
|
|
|
|
alert.innerHTML = `
|
|
|
|
|
${message}
|
|
|
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
|
|
|
|
`;
|
|
|
|
|
alertContainer.appendChild(alert);
|
|
|
|
|
|
|
|
|
|
// Automatically hide the status indicator for errors or success
|
|
|
|
|
if (type === 'danger' || type === 'success') {
|
|
|
|
|
hideStatusIndicator();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Automatically remove the alert after 5 seconds
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
alert.classList.remove('show');
|
|
|
|
|
setTimeout(() => alert.remove(), 300); // Bootstrap's fade-out transition duration
|
|
|
|
|
}, 5000);
|
|
|
|
|
}
|
2024-11-26 03:12:55 -05:00
|
|
|
|
|
2024-11-26 08:14:43 -05:00
|
|
|
|
// 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') ? '>' : '<';
|
|
|
|
|
});
|
2024-11-26 03:31:29 -05:00
|
|
|
|
|
2024-11-29 20:27:44 -05:00
|
|
|
|
function handlePeerData(data, topicId, peer) {
|
|
|
|
|
try {
|
|
|
|
|
const response = JSON.parse(data.toString());
|
|
|
|
|
console.log(`[DEBUG] Received data from peer (topic: ${topicId}): ${JSON.stringify(response)}`);
|
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
if (response.error) {
|
|
|
|
|
console.error(`[ERROR] Server error: ${response.error}`);
|
|
|
|
|
showAlert('danger', response.error);
|
|
|
|
|
hideStatusIndicator();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 20:27:44 -05:00
|
|
|
|
if (response.type === 'containers') {
|
|
|
|
|
if (window.activePeer === peer) {
|
|
|
|
|
renderContainers(response.data);
|
|
|
|
|
}
|
2024-11-29 21:57:53 -05:00
|
|
|
|
} else if (response.type === 'stats') {
|
|
|
|
|
console.log(`[DEBUG] Updating stats for container: ${response.data.id}`);
|
|
|
|
|
updateContainerStats(response.data); // Call the stats update function
|
2024-11-29 20:27:44 -05:00
|
|
|
|
} else if (response.type === 'terminalOutput') {
|
|
|
|
|
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
|
|
|
|
|
}
|
2024-11-29 21:57:53 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof window.handlePeerResponse === 'function') {
|
|
|
|
|
window.handlePeerResponse(response);
|
2024-11-29 20:27:44 -05:00
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
2024-11-29 21:57:53 -05:00
|
|
|
|
console.error(`[ERROR] Failed to process peer data: ${err.message}`);
|
|
|
|
|
showAlert('danger', 'Failed to process peer data.');
|
2024-11-29 20:27:44 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2024-11-26 03:12:55 -05:00
|
|
|
|
// Add a new connection
|
2024-11-26 08:14:43 -05:00
|
|
|
|
addConnectionForm.addEventListener('submit', (e) => {
|
2024-11-26 03:12:55 -05:00
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
2024-11-26 08:14:43 -05:00
|
|
|
|
const topicHex = newConnectionTopic.value.trim();
|
|
|
|
|
if (topicHex) {
|
|
|
|
|
addConnection(topicHex);
|
|
|
|
|
newConnectionTopic.value = '';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Function to add a new connection
|
|
|
|
|
function addConnection(topicHex) {
|
2024-11-29 22:40:26 -05:00
|
|
|
|
console.log(`[DEBUG] Adding connection with topic: ${topicHex}`);
|
|
|
|
|
|
|
|
|
|
if (Object.keys(connections).length === 0) {
|
|
|
|
|
console.log('[DEBUG] Hiding welcome page after first connection');
|
|
|
|
|
hideWelcomePage(); // Hide the welcome page when the first connection is added
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 16:40:37 -05:00
|
|
|
|
const topic = b4a.from(topicHex, 'hex');
|
|
|
|
|
const topicId = topicHex.substring(0, 12);
|
|
|
|
|
|
|
|
|
|
console.log(`[INFO] Adding connection with topic: ${topicHex}`);
|
2024-11-29 22:40:26 -05:00
|
|
|
|
assertVisibility(); // Ensure visibility reflects the added connection
|
2024-11-29 16:40:37 -05:00
|
|
|
|
|
|
|
|
|
const connectionItem = document.createElement('li');
|
|
|
|
|
connectionItem.className = 'list-group-item d-flex align-items-center justify-content-between';
|
|
|
|
|
connectionItem.dataset.topicId = topicId;
|
|
|
|
|
connectionItem.innerHTML = `
|
2024-11-28 02:51:47 -05:00
|
|
|
|
<span>
|
|
|
|
|
<span class="connection-status status-disconnected"></span>${topicId}
|
|
|
|
|
</span>
|
2024-11-29 20:27:44 -05:00
|
|
|
|
<button class="btn btn-sm btn-danger disconnect-btn">
|
|
|
|
|
<i class="fas fa-plug"></i>
|
|
|
|
|
</button>
|
|
|
|
|
`;
|
2024-11-29 16:40:37 -05:00
|
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
|
|
|
|
connections[topicId] = { topic, peer: null, swarm: null };
|
|
|
|
|
|
2024-11-29 22:40:26 -05:00
|
|
|
|
console.log('[DEBUG] Updated connections object:', connections);
|
|
|
|
|
|
2024-11-29 16:40:37 -05:00
|
|
|
|
// 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}`);
|
|
|
|
|
|
|
|
|
|
if (connections[topicId].peer) {
|
|
|
|
|
console.warn(`[WARN] Duplicate connection detected for topic: ${topicId}. Closing.`);
|
|
|
|
|
peer.destroy();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connections[topicId].peer = peer;
|
|
|
|
|
updateConnectionStatus(topicId, true);
|
|
|
|
|
|
|
|
|
|
peer.on('data', (data) => {
|
|
|
|
|
handlePeerData(data, topicId, peer);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
peer.on('close', () => {
|
|
|
|
|
console.log(`[INFO] Disconnected from peer for topic: ${topicHex}`);
|
|
|
|
|
updateConnectionStatus(topicId, false);
|
|
|
|
|
|
|
|
|
|
if (window.activePeer === peer) {
|
|
|
|
|
window.activePeer = null;
|
2024-11-29 22:46:32 -05:00
|
|
|
|
// connectionTitle.textContent = 'Disconnected';
|
2024-11-29 16:40:37 -05:00
|
|
|
|
dashboard.classList.add('hidden');
|
|
|
|
|
containerList.innerHTML = '';
|
2024-11-26 08:14:43 -05:00
|
|
|
|
}
|
2024-11-26 03:12:55 -05:00
|
|
|
|
});
|
|
|
|
|
|
2024-11-29 16:40:37 -05:00
|
|
|
|
if (!window.activePeer) {
|
|
|
|
|
switchConnection(topicId);
|
2024-11-28 02:51:47 -05:00
|
|
|
|
}
|
2024-11-29 16:40:37 -05:00
|
|
|
|
});
|
2024-11-29 22:46:32 -05:00
|
|
|
|
|
|
|
|
|
// Automatically collapse the sidebar when a new connection is added
|
|
|
|
|
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 auto-collapsed after adding a new connection');
|
|
|
|
|
}
|
2024-11-29 16:40:37 -05:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 20:27:44 -05:00
|
|
|
|
|
2024-11-29 22:40:26 -05:00
|
|
|
|
|
2024-11-29 16:40:37 -05:00
|
|
|
|
function disconnectConnection(topicId, connectionItem) {
|
|
|
|
|
const connection = connections[topicId];
|
|
|
|
|
if (!connection) {
|
|
|
|
|
console.error(`[ERROR] No connection found for topicId: ${topicId}`);
|
|
|
|
|
return;
|
2024-11-28 02:51:47 -05:00
|
|
|
|
}
|
2024-11-29 16:40:37 -05:00
|
|
|
|
|
2024-11-29 22:40:26 -05:00
|
|
|
|
// Close and clean up open terminals associated with this connection
|
2024-11-29 16:40:37 -05:00
|
|
|
|
if (window.openTerminals[topicId]) {
|
|
|
|
|
console.log(`[INFO] Closing terminals for topic: ${topicId}`);
|
|
|
|
|
window.openTerminals[topicId].forEach((containerId) => {
|
|
|
|
|
try {
|
|
|
|
|
cleanUpTerminal(containerId); // Use the terminal.js cleanup logic
|
|
|
|
|
} catch (err) {
|
2024-11-29 22:40:26 -05:00
|
|
|
|
console.error(`[ERROR] Failed to clean up terminal for container ${containerId}: ${err.message}`);
|
2024-11-29 16:40:37 -05:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
delete window.openTerminals[topicId];
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 22:40:26 -05:00
|
|
|
|
// Hide the terminal modal if it's active
|
2024-11-29 16:40:37 -05:00
|
|
|
|
const terminalModal = document.getElementById('terminal-modal');
|
2024-11-29 22:40:26 -05:00
|
|
|
|
if (terminalModal && terminalModal.style.display === 'flex') {
|
2024-11-29 16:40:37 -05:00
|
|
|
|
console.log(`[INFO] Hiding terminal modal for disconnected topic: ${topicId}`);
|
|
|
|
|
terminalModal.style.display = 'none';
|
2024-11-28 02:51:47 -05:00
|
|
|
|
}
|
2024-11-29 16:40:37 -05:00
|
|
|
|
|
|
|
|
|
// Disconnect the peer and destroy the swarm
|
|
|
|
|
if (connection.peer) {
|
|
|
|
|
connection.peer.destroy();
|
|
|
|
|
connection.peer = null;
|
|
|
|
|
}
|
|
|
|
|
if (connection.swarm) {
|
|
|
|
|
connection.swarm.destroy();
|
|
|
|
|
connection.swarm = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Remove the connection from the global connections object
|
|
|
|
|
delete connections[topicId];
|
|
|
|
|
|
|
|
|
|
// Remove the connection item from the list
|
2024-11-29 22:40:26 -05:00
|
|
|
|
if (connectionItem) {
|
|
|
|
|
connectionList.removeChild(connectionItem);
|
|
|
|
|
}
|
2024-11-29 16:40:37 -05:00
|
|
|
|
|
|
|
|
|
console.log(`[INFO] Disconnected and removed connection: ${topicId}`);
|
|
|
|
|
|
|
|
|
|
// Reset active peer if it was the disconnected connection
|
|
|
|
|
if (window.activePeer === connection.peer) {
|
|
|
|
|
window.activePeer = null;
|
|
|
|
|
connectionTitle.textContent = 'Choose a Connection'; // Reset title
|
|
|
|
|
dashboard.classList.add('hidden');
|
|
|
|
|
containerList.innerHTML = ''; // Clear the container list
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 22:40:26 -05:00
|
|
|
|
// Check if no connections remain, and show the welcome page
|
|
|
|
|
if (Object.keys(connections).length === 0) {
|
|
|
|
|
console.log('[DEBUG] All connections removed. Showing welcome page.');
|
|
|
|
|
showWelcomePage();
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 16:40:37 -05:00
|
|
|
|
// Ensure the container list is cleared regardless of the active connection
|
|
|
|
|
resetContainerList();
|
|
|
|
|
|
|
|
|
|
// Refresh the connections view
|
|
|
|
|
resetConnectionsView();
|
|
|
|
|
}
|
2024-11-29 22:40:26 -05:00
|
|
|
|
|
2024-11-29 16:40:37 -05:00
|
|
|
|
// 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 = `
|
2024-11-28 02:51:47 -05:00
|
|
|
|
<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>
|
|
|
|
|
`;
|
2024-11-29 16:40:37 -05:00
|
|
|
|
|
|
|
|
|
// 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);
|
2024-11-28 02:51:47 -05:00
|
|
|
|
});
|
2024-11-29 16:40:37 -05:00
|
|
|
|
|
|
|
|
|
connectionList.appendChild(connectionItem);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('[INFO] Connections view reset.');
|
|
|
|
|
}
|
2024-11-26 03:12:55 -05:00
|
|
|
|
|
|
|
|
|
// Update connection status
|
|
|
|
|
function updateConnectionStatus(topicId, isConnected) {
|
|
|
|
|
const connectionItem = document.querySelector(`[data-topic-id="${topicId}"] .connection-status`);
|
2024-11-26 08:14:43 -05:00
|
|
|
|
if (connectionItem) {
|
|
|
|
|
connectionItem.className = `connection-status ${isConnected ? 'status-connected' : 'status-disconnected'}`;
|
|
|
|
|
}
|
2024-11-26 03:12:55 -05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Switch between connections
|
|
|
|
|
function switchConnection(topicId) {
|
2024-11-29 16:40:37 -05:00
|
|
|
|
const connection = connections[topicId];
|
2024-11-29 22:40:26 -05:00
|
|
|
|
const connectionTitle = document.getElementById('connection-title');
|
2024-11-26 08:14:43 -05:00
|
|
|
|
|
2024-11-29 22:40:26 -05:00
|
|
|
|
if (!connection || !connection.peer) {
|
|
|
|
|
console.error('[ERROR] No connection found or no active peer.');
|
|
|
|
|
|
|
|
|
|
// Update title if element exists
|
|
|
|
|
if (connectionTitle) {
|
|
|
|
|
connectionTitle.textContent = '';
|
|
|
|
|
connectionTitle.classList.add('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showWelcomePage(); // Show welcome page
|
2024-11-29 16:40:37 -05:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 22:40:26 -05:00
|
|
|
|
// Set active peer and update UI
|
2024-11-29 16:40:37 -05:00
|
|
|
|
window.activePeer = connection.peer;
|
|
|
|
|
|
2024-11-29 22:40:26 -05:00
|
|
|
|
if (connectionTitle) {
|
|
|
|
|
connectionTitle.textContent = `Connection: ${topicId}`;
|
|
|
|
|
connectionTitle.classList.remove('hidden');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
hideWelcomePage(); // Hide the welcome page
|
|
|
|
|
sendCommand('listContainers'); // Request container list
|
2024-11-29 16:40:37 -05:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-26 03:12:55 -05:00
|
|
|
|
// Attach switchConnection to the global window object
|
|
|
|
|
window.switchConnection = switchConnection;
|
|
|
|
|
|
|
|
|
|
// Send a command to the active peer
|
|
|
|
|
function sendCommand(command, args = {}) {
|
2024-11-26 08:14:43 -05:00
|
|
|
|
if (window.activePeer) {
|
2024-11-26 03:12:55 -05:00
|
|
|
|
const message = JSON.stringify({ command, args });
|
|
|
|
|
console.log(`[DEBUG] Sending command to server: ${message}`);
|
2024-11-26 08:14:43 -05:00
|
|
|
|
window.activePeer.write(message);
|
2024-11-26 03:12:55 -05:00
|
|
|
|
} 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) {
|
2024-11-29 16:40:37 -05:00
|
|
|
|
console.log(`[INFO] Rendering ${containers.length} containers`);
|
|
|
|
|
containerList.innerHTML = ''; // Clear the current list
|
|
|
|
|
|
|
|
|
|
containers.forEach((container) => {
|
2024-11-29 18:23:27 -05:00
|
|
|
|
const name = container.Names[0].replace(/^\//, ''); // Remove leading slash
|
2024-11-29 16:40:37 -05:00
|
|
|
|
const image = container.Image;
|
|
|
|
|
const containerId = container.Id;
|
2024-11-29 18:23:27 -05:00
|
|
|
|
const ipAddress = container.ipAddress || '-'; // Use the IP address field
|
|
|
|
|
|
2024-11-29 16:40:37 -05:00
|
|
|
|
const row = document.createElement('tr');
|
|
|
|
|
row.dataset.containerId = containerId; // Store container ID for reference
|
|
|
|
|
row.innerHTML = `
|
2024-11-26 08:14:43 -05:00
|
|
|
|
<td>${name}</td>
|
|
|
|
|
<td>${image}</td>
|
|
|
|
|
<td>${container.State}</td>
|
|
|
|
|
<td class="cpu">0</td>
|
|
|
|
|
<td class="memory">0</td>
|
2024-11-29 18:23:27 -05:00
|
|
|
|
<td class="ip-address">${ipAddress}</td>
|
2024-11-26 08:14:43 -05:00
|
|
|
|
<td>
|
2024-11-29 18:23:27 -05:00
|
|
|
|
<button class="btn btn-success btn-sm action-start" ${container.State === 'running' ? 'disabled' : ''}>
|
|
|
|
|
<i class="fas fa-play"></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>
|
2024-11-26 08:14:43 -05:00
|
|
|
|
</td>
|
2024-11-29 18:23:27 -05:00
|
|
|
|
`;
|
2024-11-29 16:40:37 -05:00
|
|
|
|
containerList.appendChild(row);
|
|
|
|
|
|
|
|
|
|
// Add event listeners for action buttons
|
|
|
|
|
addActionListeners(row, container);
|
2024-11-29 21:57:53 -05:00
|
|
|
|
// Add event listener for duplicate button
|
|
|
|
|
const duplicateBtn = row.querySelector('.action-duplicate');
|
|
|
|
|
duplicateBtn.addEventListener('click', () => openDuplicateModal(container));
|
|
|
|
|
|
2024-11-29 16:40:37 -05:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-26 08:14:43 -05:00
|
|
|
|
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');
|
2024-11-26 03:12:55 -05:00
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
// Start Button
|
|
|
|
|
startBtn.addEventListener('click', async () => {
|
|
|
|
|
showStatusIndicator(`Starting container "${container.Names[0]}"...`);
|
2024-11-26 08:14:43 -05:00
|
|
|
|
sendCommand('startContainer', { id: container.Id });
|
2024-11-29 21:57:53 -05:00
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
} 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();
|
|
|
|
|
}
|
2024-11-26 03:12:55 -05:00
|
|
|
|
});
|
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
stopBtn.addEventListener('click', async () => {
|
|
|
|
|
showStatusIndicator(`Stopping container "${container.Names[0]}"...`);
|
2024-11-26 08:14:43 -05:00
|
|
|
|
sendCommand('stopContainer', { id: container.Id });
|
2024-11-29 21:57:53 -05:00
|
|
|
|
|
|
|
|
|
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');
|
|
|
|
|
} 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();
|
|
|
|
|
}
|
2024-11-26 08:14:43 -05:00
|
|
|
|
});
|
2024-11-29 21:57:53 -05:00
|
|
|
|
|
2024-11-26 03:36:51 -05:00
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
// Remove Button
|
|
|
|
|
removeBtn.addEventListener('click', async () => {
|
2024-11-29 20:17:53 -05:00
|
|
|
|
const deleteModal = new bootstrap.Modal(document.getElementById('deleteModal'));
|
|
|
|
|
deleteModal.show();
|
2024-11-29 20:29:45 -05:00
|
|
|
|
|
2024-11-29 20:17:53 -05:00
|
|
|
|
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
|
2024-11-29 21:57:53 -05:00
|
|
|
|
confirmDeleteBtn.onclick = async () => {
|
|
|
|
|
deleteModal.hide();
|
|
|
|
|
|
|
|
|
|
showStatusIndicator(`Deleting container "${container.Names[0]}"...`);
|
2024-11-29 20:29:45 -05:00
|
|
|
|
|
|
|
|
|
// 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 {
|
2024-11-29 21:57:53 -05:00
|
|
|
|
cleanUpTerminal(terminalId);
|
2024-11-29 20:29:45 -05:00
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error(`[ERROR] Failed to clean up terminal ${terminalId}: ${err.message}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
2024-11-29 21:57:53 -05:00
|
|
|
|
delete window.openTerminals[container.Id];
|
2024-11-29 20:29:45 -05:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
// Hide the terminal modal if it is active
|
2024-11-29 20:29:45 -05:00
|
|
|
|
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';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendCommand('removeContainer', { id: container.Id });
|
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
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();
|
2024-11-29 20:29:45 -05:00
|
|
|
|
}
|
2024-11-29 20:17:53 -05:00
|
|
|
|
};
|
2024-11-26 03:12:55 -05:00
|
|
|
|
});
|
2024-11-29 20:29:45 -05:00
|
|
|
|
|
2024-11-26 03:12:55 -05:00
|
|
|
|
|
2024-11-26 08:14:43 -05:00
|
|
|
|
terminalBtn.addEventListener('click', () => {
|
2024-11-29 21:57:53 -05:00
|
|
|
|
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}`);
|
|
|
|
|
}
|
2024-11-26 08:14:43 -05:00
|
|
|
|
});
|
2024-11-26 03:12:55 -05:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
|
2024-11-26 08:14:43 -05:00
|
|
|
|
// Function to update container statistics
|
|
|
|
|
function updateContainerStats(stats) {
|
2024-11-29 16:40:37 -05:00
|
|
|
|
console.log(`[DEBUG] Updating stats for container ID: ${stats.id}`);
|
|
|
|
|
let row = containerList.querySelector(`tr[data-container-id="${stats.id}"]`);
|
|
|
|
|
|
|
|
|
|
if (!row) {
|
|
|
|
|
console.warn(`[WARN] No row found for container ID: ${stats.id}. Adding a placeholder.`);
|
|
|
|
|
// Create a placeholder row if it doesn't exist
|
|
|
|
|
row = document.createElement('tr');
|
|
|
|
|
row.dataset.containerId = stats.id;
|
|
|
|
|
row.innerHTML = `
|
2024-11-26 08:14:43 -05:00
|
|
|
|
<td>Unknown</td>
|
|
|
|
|
<td>-</td>
|
|
|
|
|
<td>-</td>
|
|
|
|
|
<td class="cpu">0</td>
|
|
|
|
|
<td class="memory">0</td>
|
|
|
|
|
<td class="ip-address">-</td>
|
|
|
|
|
<td>-</td>
|
|
|
|
|
`;
|
2024-11-29 16:40:37 -05:00
|
|
|
|
containerList.appendChild(row);
|
2024-11-26 03:12:55 -05:00
|
|
|
|
}
|
|
|
|
|
|
2024-11-29 16:40:37 -05:00
|
|
|
|
row.querySelector('.cpu').textContent = stats.cpu.toFixed(2);
|
|
|
|
|
row.querySelector('.memory').textContent = (stats.memory / (1024 * 1024)).toFixed(2);
|
|
|
|
|
row.querySelector('.ip-address').textContent = stats.ip || '-';
|
|
|
|
|
}
|
|
|
|
|
|
2024-11-26 08:14:43 -05:00
|
|
|
|
// Function to open the Duplicate Modal with container configurations
|
|
|
|
|
function openDuplicateModal(container) {
|
2024-11-29 16:40:37 -05:00
|
|
|
|
console.log(`[INFO] Opening Duplicate Modal for container: ${container.Id}`);
|
2024-11-26 08:14:43 -05:00
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
showStatusIndicator('Fetching container configuration...');
|
|
|
|
|
|
|
|
|
|
// Send a command to inspect the container
|
2024-11-29 16:40:37 -05:00
|
|
|
|
sendCommand('inspectContainer', { id: container.Id });
|
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
// Listen for the inspectContainer response
|
2024-11-29 16:40:37 -05:00
|
|
|
|
window.inspectContainerCallback = (config) => {
|
2024-11-29 21:57:53 -05:00
|
|
|
|
hideStatusIndicator();
|
|
|
|
|
|
2024-11-29 16:40:37 -05:00
|
|
|
|
if (!config) {
|
2024-11-29 21:57:53 -05:00
|
|
|
|
console.error('[ERROR] Failed to retrieve container configuration.');
|
|
|
|
|
showAlert('danger', 'Failed to retrieve container configuration.');
|
2024-11-26 08:14:43 -05:00
|
|
|
|
return;
|
|
|
|
|
}
|
2024-11-29 16:40:37 -05:00
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
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.');
|
|
|
|
|
}
|
2024-11-29 16:40:37 -05:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Handle the Duplicate Container Form Submission
|
|
|
|
|
duplicateContainerForm.addEventListener('submit', (e) => {
|
|
|
|
|
e.preventDefault();
|
2024-11-29 21:57:53 -05:00
|
|
|
|
duplicateModal.hide();
|
|
|
|
|
|
|
|
|
|
showStatusIndicator('Duplicating container...');
|
2024-11-29 16:40:37 -05:00
|
|
|
|
|
|
|
|
|
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) {
|
2024-11-29 21:57:53 -05:00
|
|
|
|
hideStatusIndicator();
|
|
|
|
|
showAlert('danger', 'Invalid JSON in configuration.');
|
2024-11-29 16:40:37 -05:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
sendCommand('duplicateContainer', { name, image, hostname, netmode, cpu, memory, config });
|
|
|
|
|
|
2024-11-29 21:57:53 -05:00
|
|
|
|
// Simulate delay for the demo
|
2024-11-29 16:40:37 -05:00
|
|
|
|
setTimeout(() => {
|
2024-11-29 21:57:53 -05:00
|
|
|
|
hideStatusIndicator();
|
|
|
|
|
showAlert('success', 'Container duplicated successfully!');
|
|
|
|
|
|
|
|
|
|
// Refresh container list
|
2024-11-29 16:40:37 -05:00
|
|
|
|
sendCommand('listContainers');
|
2024-11-29 21:57:53 -05:00
|
|
|
|
}, 2000); // Simulated processing time
|
2024-11-29 16:40:37 -05:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
2024-11-29 22:40:26 -05:00
|
|
|
|
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.');
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-11-26 08:14:43 -05:00
|
|
|
|
// Attach startTerminal to the global window object
|
|
|
|
|
window.startTerminal = startTerminal;
|
2024-11-26 03:12:55 -05:00
|
|
|
|
|
2024-11-26 08:14:43 -05:00
|
|
|
|
// 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();
|
|
|
|
|
}
|
2024-11-26 03:12:55 -05:00
|
|
|
|
}
|
2024-11-28 07:07:46 -05:00
|
|
|
|
});
|