further updates
This commit is contained in:
@@ -3,34 +3,53 @@ import b4a from 'b4a';
|
||||
import { startTerminal, appendTerminalOutput } from './libs/terminal.js';
|
||||
import { startDockerTerminal, cleanUpDockerTerminal } from './libs/dockerTerminal.js';
|
||||
import { fetchTemplates, displayTemplateList, openDeployModal, collectDuplicateFormData, populateDuplicateForm } from './libs/templateDeploy.js';
|
||||
import { showContainerSkeleton, createProgressBar, updateProgressBar, removeProgressBar, showOperationStatus } from './libs/loadingStates.js';
|
||||
import { showContainerSkeleton, createProgressBar, updateProgressBar, removeProgressBar } from './libs/loadingStates.js';
|
||||
import { closeAllModals, showStatusIndicator, hideStatusIndicator, showAlert } from './libs/uiUtils.js';
|
||||
import notificationManager from './libs/notifications.js';
|
||||
|
||||
// DOM Elements - Cache frequently accessed 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');
|
||||
const welcomePage = document.getElementById('welcome-page');
|
||||
const sidebar = document.getElementById('sidebar');
|
||||
const collapseSidebarBtn = document.getElementById('collapse-sidebar-btn');
|
||||
const alertContainer = document.getElementById('alert-container');
|
||||
// DOM Elements - Lazy-loaded to avoid blocking initialization
|
||||
let containerList = null;
|
||||
let connectionList = null;
|
||||
let addConnectionForm = null;
|
||||
let newConnectionTopic = null;
|
||||
let connectionTitle = null;
|
||||
let dashboard = null;
|
||||
let welcomePage = null;
|
||||
let sidebar = null;
|
||||
let collapseSidebarBtn = null;
|
||||
let alertContainer = null;
|
||||
|
||||
// Modal Elements
|
||||
const duplicateModalElement = document.getElementById('duplicateModal');
|
||||
const duplicateModal = new bootstrap.Modal(duplicateModalElement);
|
||||
const duplicateContainerForm = document.getElementById('duplicate-container-form');
|
||||
// Modal Elements - Lazy-loaded
|
||||
let duplicateModalElement = null;
|
||||
let duplicateModal = null;
|
||||
let duplicateContainerForm = null;
|
||||
|
||||
// Initialize DOM elements (called on DOMContentLoaded)
|
||||
function initDOMElements() {
|
||||
containerList = document.getElementById('container-list');
|
||||
connectionList = document.getElementById('connection-list');
|
||||
addConnectionForm = document.getElementById('add-connection-form');
|
||||
newConnectionTopic = document.getElementById('new-connection-topic');
|
||||
connectionTitle = document.getElementById('connection-title');
|
||||
dashboard = document.getElementById('dashboard');
|
||||
welcomePage = document.getElementById('welcome-page');
|
||||
sidebar = document.getElementById('sidebar');
|
||||
collapseSidebarBtn = document.getElementById('collapse-sidebar-btn');
|
||||
alertContainer = document.getElementById('alert-container');
|
||||
|
||||
// Modal elements
|
||||
duplicateModalElement = document.getElementById('duplicateModal');
|
||||
if (duplicateModalElement) {
|
||||
duplicateModal = new bootstrap.Modal(duplicateModalElement);
|
||||
}
|
||||
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() {
|
||||
@@ -101,6 +120,10 @@ function smoothStats(containerId, newStats, smoothingFactor = 0.2) {
|
||||
|
||||
|
||||
function refreshContainerStats() {
|
||||
if (!window.activePeer) {
|
||||
// Don't try to refresh if there's no active peer
|
||||
return;
|
||||
}
|
||||
console.log('[INFO] Refreshing container stats...');
|
||||
sendCommand('listContainers'); // Request an updated container list
|
||||
startStatsInterval(); // Restart stats interval
|
||||
@@ -345,22 +368,7 @@ console.log('[INFO] Client app initialized');
|
||||
|
||||
|
||||
|
||||
// Collapse Sidebar Functionality
|
||||
if (collapseSidebarBtn) {
|
||||
collapseSidebarBtn.addEventListener('click', () => {
|
||||
// Use cached DOM elements
|
||||
if (sidebar) {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
collapseSidebarBtn.innerHTML = sidebar.classList.contains('collapsed') ? '>' : '<';
|
||||
|
||||
// Toggle Reset Connections Button Visibility
|
||||
const resetConnectionsBtn = sidebar.querySelector('.btn-danger');
|
||||
if (resetConnectionsBtn) {
|
||||
resetConnectionsBtn.style.display = sidebar.classList.contains('collapsed') ? 'none' : 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
// Collapse Sidebar Functionality - set up in DOMContentLoaded
|
||||
|
||||
function handlePeerData(data, topicId, peer) {
|
||||
try {
|
||||
@@ -444,16 +452,7 @@ function handlePeerData(data, topicId, peer) {
|
||||
|
||||
|
||||
|
||||
// Add a new connection
|
||||
addConnectionForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const topicHex = newConnectionTopic.value.trim();
|
||||
if (topicHex) {
|
||||
addConnection(topicHex);
|
||||
newConnectionTopic.value = '';
|
||||
}
|
||||
});
|
||||
// Add a new connection - event listener set up in DOMContentLoaded
|
||||
|
||||
function addConnection(topicHex) {
|
||||
console.log(`[DEBUG] Adding connection with topic: ${topicHex}`);
|
||||
@@ -645,43 +644,238 @@ function openTemplateDeployModal(topicId) {
|
||||
|
||||
// Initialize connections from cookies on page load
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const savedConnections = loadConnections();
|
||||
console.log('[INFO] Loading saved connections:', savedConnections);
|
||||
|
||||
// Restore saved connections with error handling
|
||||
Object.keys(savedConnections).forEach((topicId) => {
|
||||
try {
|
||||
let topicHex = savedConnections[topicId].topic;
|
||||
|
||||
// Ensure topicHex is a string
|
||||
if (typeof topicHex !== 'string') {
|
||||
topicHex = b4a.toString(topicHex, 'hex');
|
||||
}
|
||||
// Initialize DOM elements first
|
||||
initDOMElements();
|
||||
hideStatusIndicator();
|
||||
|
||||
// Set up event listeners that depend on DOM elements
|
||||
if (addConnectionForm) {
|
||||
addConnectionForm.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const topicHex = newConnectionTopic ? newConnectionTopic.value.trim() : '';
|
||||
if (topicHex) {
|
||||
addConnection(topicHex);
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Failed to restore connection ${topicId}: ${err.message}`);
|
||||
if (newConnectionTopic) {
|
||||
newConnectionTopic.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set up sidebar collapse functionality
|
||||
if (collapseSidebarBtn) {
|
||||
collapseSidebarBtn.addEventListener('click', () => {
|
||||
if (sidebar) {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
collapseSidebarBtn.innerHTML = sidebar.classList.contains('collapsed') ? '>' : '<';
|
||||
|
||||
if (Object.keys(connections).length > 0) {
|
||||
hideWelcomePage();
|
||||
startStatsInterval(); // Start stats polling for active peers
|
||||
} else {
|
||||
showWelcomePage();
|
||||
// Toggle Reset Connections Button Visibility
|
||||
const resetConnectionsBtn = sidebar.querySelector('.btn-danger');
|
||||
if (resetConnectionsBtn) {
|
||||
resetConnectionsBtn.style.display = sidebar.classList.contains('collapsed') ? 'none' : 'block';
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show UI immediately - default to welcome page
|
||||
if (welcomePage) {
|
||||
welcomePage.classList.remove('hidden');
|
||||
}
|
||||
if (dashboard) {
|
||||
dashboard.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Initialize container filtering (lightweight, doesn't block)
|
||||
initContainerFiltering();
|
||||
|
||||
// Notification tray will be initialized after connections are restored
|
||||
if (duplicateContainerForm) {
|
||||
duplicateContainerForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let formData;
|
||||
try {
|
||||
formData = collectDuplicateFormData();
|
||||
} catch (collectError) {
|
||||
console.error('[ERROR] Failed to collect duplicate form data:', collectError);
|
||||
showAlert('danger', 'Failed to collect form data. Check console for details.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!formData.containerName || !formData.image) {
|
||||
showAlert('danger', 'Container name and image are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get container name for notifications
|
||||
const containerName = formData.containerName || 'container';
|
||||
|
||||
// Close modal immediately before async operation
|
||||
if (duplicateModal) {
|
||||
duplicateModal.hide();
|
||||
}
|
||||
closeAllModals();
|
||||
|
||||
// Add notification for container creation
|
||||
notificationManager.add('info', `Creating container "${containerName}"...`, { autoDismiss: false });
|
||||
showStatusIndicator('Duplicating container...');
|
||||
|
||||
try {
|
||||
// Use deployContainer command with the collected form data
|
||||
// This reuses the same deployment logic
|
||||
const originalHandler = window.handlePeerResponse;
|
||||
let timeoutId = null;
|
||||
let isResolved = false;
|
||||
|
||||
const duplicateHandler = (response) => {
|
||||
if (isResolved) {
|
||||
if (typeof originalHandler === 'function') {
|
||||
originalHandler(response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isDuplicateResponse =
|
||||
(response.success && response.message && typeof response.message === 'string' && response.message.includes('deployed successfully')) ||
|
||||
(response.error && (
|
||||
(response.message && typeof response.message === 'string' && response.message.includes('deploy')) ||
|
||||
(typeof response.error === 'string' && (response.error.includes('deploy') || response.error.includes('Container')))
|
||||
));
|
||||
|
||||
if (isDuplicateResponse) {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
window.handlePeerResponse = originalHandler;
|
||||
isResolved = true;
|
||||
|
||||
if (response.success && response.message && response.message.includes('deployed successfully')) {
|
||||
hideStatusIndicator();
|
||||
// Update notification to success
|
||||
notificationManager.add('success', `Container "${formData.containerName}" created successfully!`);
|
||||
showAlert('success', `Container "${formData.containerName}" duplicated successfully!`);
|
||||
sendCommand('listContainers');
|
||||
} else if (response.error) {
|
||||
hideStatusIndicator();
|
||||
const errorMessage = typeof response.error === 'string'
|
||||
? response.error
|
||||
: (response.error?.message || response.error?.toString() || 'Unknown error');
|
||||
// Update notification to error
|
||||
notificationManager.add('danger', `Failed to create container "${formData.containerName}"`);
|
||||
showAlert('danger', errorMessage);
|
||||
}
|
||||
} else {
|
||||
if (typeof originalHandler === 'function') {
|
||||
originalHandler(response);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.handlePeerResponse = duplicateHandler;
|
||||
|
||||
if (typeof window.sendCommand === 'function') {
|
||||
window.sendCommand('deployContainer', formData);
|
||||
} else {
|
||||
window.handlePeerResponse = originalHandler;
|
||||
hideStatusIndicator();
|
||||
showAlert('danger', 'sendCommand is not available. Please ensure app.js is loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!isResolved) {
|
||||
window.handlePeerResponse = originalHandler;
|
||||
isResolved = true;
|
||||
hideStatusIndicator();
|
||||
// Update notification to timeout error
|
||||
notificationManager.add('danger', `Failed to create container "${containerName}" (timeout)`);
|
||||
showAlert('danger', 'Duplication timed out. No response from server.');
|
||||
}
|
||||
}, 60000);
|
||||
} catch (error) {
|
||||
hideStatusIndicator();
|
||||
console.error('[ERROR] Failed to duplicate container:', error);
|
||||
// Update notification to error
|
||||
notificationManager.add('danger', `Failed to create container "${containerName}"`);
|
||||
showAlert('danger', error.message || 'Failed to duplicate container. Check console for details.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Restore connections asynchronously after UI is visible
|
||||
const restoreConnections = () => {
|
||||
try {
|
||||
const savedConnections = loadConnections();
|
||||
console.log('[INFO] Loading saved connections:', savedConnections);
|
||||
|
||||
if (Object.keys(savedConnections).length === 0) {
|
||||
// No connections to restore, ensure welcome page is shown
|
||||
if (welcomePage) {
|
||||
welcomePage.classList.remove('hidden');
|
||||
}
|
||||
if (dashboard) {
|
||||
dashboard.classList.add('hidden');
|
||||
}
|
||||
// Initialize notification tray after UI is ready (no connections to restore)
|
||||
setTimeout(() => {
|
||||
initNotificationTray();
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
// Restore connections one by one with small delays to avoid blocking
|
||||
const connectionKeys = Object.keys(savedConnections);
|
||||
connectionKeys.forEach((topicId, index) => {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
let topicHex = savedConnections[topicId].topic;
|
||||
|
||||
// Ensure topicHex is a string
|
||||
if (typeof topicHex !== 'string') {
|
||||
topicHex = b4a.toString(topicHex, 'hex');
|
||||
}
|
||||
|
||||
addConnection(topicHex);
|
||||
|
||||
// After last connection, update UI state
|
||||
if (index === connectionKeys.length - 1) {
|
||||
setTimeout(() => {
|
||||
if (Object.keys(connections).length > 0) {
|
||||
hideWelcomePage();
|
||||
startStatsInterval(); // Start stats polling for active peers
|
||||
} else {
|
||||
showWelcomePage();
|
||||
}
|
||||
assertVisibility();
|
||||
// Initialize notification tray after connections are fully restored
|
||||
initNotificationTray();
|
||||
}, 100);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Failed to restore connection ${topicId}: ${err.message}`);
|
||||
}
|
||||
}, index * 50); // 50ms delay between each connection
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Failed to initialize connections: ${err.message}`);
|
||||
showWelcomePage(); // Show welcome page on error
|
||||
// Initialize notification tray even if there was an error
|
||||
setTimeout(() => {
|
||||
initNotificationTray();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
assertVisibility(); // Ensure visibility reflects the restored connections
|
||||
|
||||
// Initialize container filtering
|
||||
initContainerFiltering();
|
||||
|
||||
// Initialize notification tray
|
||||
initNotificationTray();
|
||||
} catch (err) {
|
||||
console.error(`[ERROR] Failed to initialize connections: ${err.message}`);
|
||||
showWelcomePage(); // Show welcome page on error
|
||||
};
|
||||
|
||||
// Use requestIdleCallback if available, otherwise setTimeout
|
||||
if (typeof requestIdleCallback !== 'undefined') {
|
||||
requestIdleCallback(restoreConnections, { timeout: 500 });
|
||||
} else {
|
||||
setTimeout(restoreConnections, 100);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -909,7 +1103,8 @@ function sendCommand(command, args = {}) {
|
||||
console.log(`[DEBUG] Sending command to server: ${message}`);
|
||||
window.activePeer.write(message);
|
||||
} else {
|
||||
console.error('[ERROR] No active peer to send command.');
|
||||
// Silently return during initialization - this is expected
|
||||
console.debug('[DEBUG] No active peer to send command (this is normal during initialization).');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1249,8 +1444,14 @@ function addActionListeners(row, container) {
|
||||
|
||||
const confirmDeleteBtn = document.getElementById('confirm-delete-btn');
|
||||
confirmDeleteBtn.onclick = async () => {
|
||||
// Close modal immediately before async operation
|
||||
deleteModal.hide();
|
||||
|
||||
closeAllModals();
|
||||
|
||||
const containerName = container.Names[0]?.replace(/^\//, '') || container.Id.substring(0, 12);
|
||||
|
||||
// Add notification for container deletion
|
||||
notificationManager.add('info', `Deleting container "${containerName}"...`, { autoDismiss: false });
|
||||
showStatusIndicator(`Deleting container "${container.Names[0]}"...`);
|
||||
|
||||
// Check if the container has active terminals
|
||||
@@ -1286,12 +1487,16 @@ function addActionListeners(row, container) {
|
||||
const response = await waitForPeerResponse(expectedMessageFragment);
|
||||
console.log('[DEBUG] Remove container response:', response);
|
||||
|
||||
// Update notification to success
|
||||
notificationManager.add('success', `Container "${containerName}" deleted successfully`);
|
||||
showAlert('success', response.message);
|
||||
|
||||
// Refresh the container list to update states
|
||||
sendCommand('listContainers');
|
||||
} catch (error) {
|
||||
console.error('[ERROR] Failed to delete container:', error.message);
|
||||
// Update notification to error
|
||||
notificationManager.add('danger', `Failed to delete container "${containerName}"`);
|
||||
showAlert('danger', error.message || `Failed to delete container "${container.Names[0]}".`);
|
||||
} finally {
|
||||
console.log('[DEBUG] Hiding status indicator in removeBtn finally block');
|
||||
@@ -1451,7 +1656,9 @@ function openDuplicateModal(container) {
|
||||
}
|
||||
|
||||
// Show the duplicate modal
|
||||
duplicateModal.show();
|
||||
if (duplicateModal) {
|
||||
duplicateModal.show();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[ERROR] Failed to populate modal fields: ${error.message}`);
|
||||
showAlert('danger', 'Failed to populate container configuration fields.');
|
||||
@@ -1860,105 +2067,6 @@ function populateLabelsSection(config) {
|
||||
}
|
||||
|
||||
|
||||
// Handle the Duplicate Container Form Submission
|
||||
duplicateContainerForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
let formData;
|
||||
try {
|
||||
formData = collectDuplicateFormData();
|
||||
} catch (collectError) {
|
||||
console.error('[ERROR] Failed to collect duplicate form data:', collectError);
|
||||
showAlert('danger', 'Failed to collect form data. Check console for details.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
if (!formData.containerName || !formData.image) {
|
||||
showAlert('danger', 'Container name and image are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showStatusIndicator('Duplicating container...');
|
||||
|
||||
// Use deployContainer command with the collected form data
|
||||
// This reuses the same deployment logic
|
||||
const originalHandler = window.handlePeerResponse;
|
||||
let timeoutId = null;
|
||||
let isResolved = false;
|
||||
|
||||
const duplicateHandler = (response) => {
|
||||
if (isResolved) {
|
||||
if (typeof originalHandler === 'function') {
|
||||
originalHandler(response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const isDuplicateResponse =
|
||||
(response.success && response.message && typeof response.message === 'string' && response.message.includes('deployed successfully')) ||
|
||||
(response.error && (
|
||||
(response.message && typeof response.message === 'string' && response.message.includes('deploy')) ||
|
||||
(typeof response.error === 'string' && (response.error.includes('deploy') || response.error.includes('Container')))
|
||||
));
|
||||
|
||||
if (isDuplicateResponse) {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
|
||||
window.handlePeerResponse = originalHandler;
|
||||
isResolved = true;
|
||||
duplicateModal.hide();
|
||||
|
||||
if (response.success && response.message && response.message.includes('deployed successfully')) {
|
||||
hideStatusIndicator();
|
||||
showAlert('success', `Container "${formData.containerName}" duplicated successfully!`);
|
||||
sendCommand('listContainers');
|
||||
} else if (response.error) {
|
||||
hideStatusIndicator();
|
||||
const errorMessage = typeof response.error === 'string'
|
||||
? response.error
|
||||
: (response.error?.message || response.error?.toString() || 'Unknown error');
|
||||
showAlert('danger', errorMessage);
|
||||
}
|
||||
} else {
|
||||
if (typeof originalHandler === 'function') {
|
||||
originalHandler(response);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.handlePeerResponse = duplicateHandler;
|
||||
|
||||
if (typeof window.sendCommand === 'function') {
|
||||
window.sendCommand('deployContainer', formData);
|
||||
} else {
|
||||
window.handlePeerResponse = originalHandler;
|
||||
hideStatusIndicator();
|
||||
showAlert('danger', 'sendCommand is not available. Please ensure app.js is loaded.');
|
||||
return;
|
||||
}
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!isResolved) {
|
||||
window.handlePeerResponse = originalHandler;
|
||||
isResolved = true;
|
||||
hideStatusIndicator();
|
||||
duplicateModal.hide();
|
||||
showAlert('danger', 'Duplication timed out. No response from server.');
|
||||
}
|
||||
}, 60000);
|
||||
} catch (error) {
|
||||
hideStatusIndicator();
|
||||
console.error('[ERROR] Failed to duplicate container:', error);
|
||||
showAlert('danger', error.message || 'Failed to duplicate container. Check console for details.');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
function showWelcomePage() {
|
||||
// Use cached DOM elements
|
||||
|
||||
@@ -2266,7 +2374,10 @@ function initNotificationTray() {
|
||||
}
|
||||
});
|
||||
|
||||
// Initial render
|
||||
updateBadge();
|
||||
renderNotifications();
|
||||
// Initial render (will trigger storage load if needed)
|
||||
// Use a small delay to ensure DOM is ready
|
||||
setTimeout(() => {
|
||||
updateBadge();
|
||||
renderNotifications();
|
||||
}, 0);
|
||||
}
|
||||
Reference in New Issue
Block a user