further updates

This commit is contained in:
Raven Scott
2025-11-24 16:28:21 -05:00
parent 6469f620e6
commit 4bf3381270
6 changed files with 428 additions and 321 deletions
+291 -180
View File
@@ -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);
}