// Main SPORE UI Application // Initialize the application when DOM is loaded document.addEventListener('DOMContentLoaded', async function() { logger.debug('=== SPORE UI Application Initialization ==='); // Initialize the framework (but don't navigate yet) logger.debug('App: Creating framework instance...'); const app = window.app; // Components are loaded via script tags in order; no blocking wait required // Create view models logger.debug('App: Creating view models...'); const clusterViewModel = new ClusterViewModel(); const firmwareViewModel = new FirmwareViewModel(); const clusterFirmwareViewModel = new ClusterFirmwareViewModel(); const topologyViewModel = new TopologyViewModel(); const monitoringViewModel = new MonitoringViewModel(); logger.debug('App: View models created:', { clusterViewModel, firmwareViewModel, clusterFirmwareViewModel, topologyViewModel, monitoringViewModel }); // Connect firmware view model to cluster data clusterViewModel.subscribe('members', (members) => { logger.debug('App: Members subscription triggered:', members); if (members && members.length > 0) { // Extract node information for firmware view const nodes = members.map(member => ({ ip: member.ip, hostname: member.hostname || member.ip, labels: member.labels || {} })); firmwareViewModel.updateAvailableNodes(nodes); logger.debug('App: Updated firmware view model with nodes:', nodes); } else { firmwareViewModel.updateAvailableNodes([]); logger.debug('App: Cleared firmware view model nodes'); } }); // Connect cluster firmware view model to cluster data // Note: This subscription is disabled because target nodes should be set explicitly // when opening the firmware deploy drawer, not automatically updated /* clusterViewModel.subscribe('members', (members) => { logger.debug('App: Members subscription triggered for cluster firmware:', members); if (members && members.length > 0) { // Extract node information for cluster firmware view const nodes = members.map(member => ({ ip: member.ip, hostname: member.hostname || member.ip, labels: member.labels || {} })); clusterFirmwareViewModel.setTargetNodes(nodes); logger.debug('App: Updated cluster firmware view model with nodes:', nodes); } else { clusterFirmwareViewModel.setTargetNodes([]); logger.debug('App: Cleared cluster firmware view model nodes'); } }); */ // Register routes with their view models logger.debug('App: Registering routes...'); app.registerRoute('cluster', ClusterViewComponent, 'cluster-view', clusterViewModel); app.registerRoute('topology', TopologyGraphComponent, 'topology-view', topologyViewModel); app.registerRoute('firmware', FirmwareViewComponent, 'firmware-view', firmwareViewModel); app.registerRoute('monitoring', MonitoringViewComponent, 'monitoring-view', monitoringViewModel); logger.debug('App: Routes registered and components pre-initialized'); // Initialize cluster status component for header badge logger.debug('App: Initializing cluster status component...'); const clusterStatusComponent = new ClusterStatusComponent( document.querySelector('.cluster-status'), clusterViewModel, app.eventBus ); clusterStatusComponent.mount(); logger.debug('App: Cluster status component initialized'); // Set up random primary node button logger.debug('App: Setting up random primary node button...'); const randomPrimaryBtn = document.getElementById('random-primary-toggle'); if (randomPrimaryBtn) { randomPrimaryBtn.addEventListener('click', async function() { try { // Add spinning animation randomPrimaryBtn.classList.add('spinning'); randomPrimaryBtn.disabled = true; logger.debug('App: Selecting random primary node...'); await clusterViewModel.selectRandomPrimaryNode(); // Show success state briefly logger.info('App: Random primary node selected successfully'); // Refresh topology to show new primary node connections // Wait a bit for the backend to update, then refresh topology setTimeout(async () => { logger.debug('App: Refreshing topology after primary node change...'); try { await topologyViewModel.updateNetworkTopology(); logger.debug('App: Topology refreshed successfully'); } catch (error) { logger.error('App: Failed to refresh topology:', error); } }, 1000); // Also refresh cluster view to update member list with new primary setTimeout(async () => { logger.debug('App: Refreshing cluster view after primary node change...'); try { if (clusterViewModel.updateClusterMembers) { await clusterViewModel.updateClusterMembers(); } logger.debug('App: Cluster view refreshed successfully'); } catch (error) { logger.error('App: Failed to refresh cluster view:', error); } }, 1000); // Remove spinning animation after delay setTimeout(() => { randomPrimaryBtn.classList.remove('spinning'); randomPrimaryBtn.disabled = false; }, 1500); } catch (error) { logger.error('App: Failed to select random primary node:', error); randomPrimaryBtn.classList.remove('spinning'); randomPrimaryBtn.disabled = false; // Show error notification (could be enhanced with a toast notification) alert('Failed to select random primary node: ' + error.message); } }); logger.debug('App: Random primary node button configured'); } // Set up navigation event listeners logger.debug('App: Setting up navigation...'); app.setupNavigation(); // Now navigate to the default route logger.debug('App: Navigating to default route...'); app.navigateTo('cluster'); logger.debug('=== SPORE UI Application initialization completed ==='); }); // Burger menu toggle for mobile (function setupBurgerMenu(){ document.addEventListener('DOMContentLoaded', function(){ const nav = document.querySelector('.main-navigation'); const burger = document.getElementById('burger-btn'); const navLeft = nav ? nav.querySelector('.nav-left') : null; if (!nav || !burger || !navLeft) return; burger.addEventListener('click', function(e){ e.preventDefault(); nav.classList.toggle('mobile-open'); }); // Close menu when a nav tab is clicked navLeft.addEventListener('click', function(e){ const btn = e.target.closest('.nav-tab'); if (btn && nav.classList.contains('mobile-open')) { nav.classList.remove('mobile-open'); } }); // Close menu on outside click document.addEventListener('click', function(e){ if (!nav.contains(e.target) && nav.classList.contains('mobile-open')) { nav.classList.remove('mobile-open'); } }); }); })(); // Set up periodic updates function setupPeriodicUpdates() { // Auto-refresh cluster members every 30 seconds using smart update setInterval(() => { if (window.app.currentView && window.app.currentView.viewModel) { const viewModel = window.app.currentView.viewModel; // Use smart update if available, otherwise fall back to regular update if (viewModel.smartUpdate && typeof viewModel.smartUpdate === 'function') { logger.debug('App: Performing smart update...'); viewModel.smartUpdate(); } else if (viewModel.updateClusterMembers && typeof viewModel.updateClusterMembers === 'function') { logger.debug('App: Performing regular update...'); viewModel.updateClusterMembers(); } } }, 30000); // Update primary node display every 10 seconds (this is lightweight and doesn't affect UI state) setInterval(() => { if (window.app.currentView && window.app.currentView.viewModel) { const viewModel = window.app.currentView.viewModel; if (viewModel.updatePrimaryNodeDisplay && typeof viewModel.updatePrimaryNodeDisplay === 'function') { viewModel.updatePrimaryNodeDisplay(); } } }, 10000); } // Global error handler window.addEventListener('error', function(event) { logger.error('Global error:', event.error); }); // Global unhandled promise rejection handler window.addEventListener('unhandledrejection', function(event) { logger.error('Unhandled promise rejection:', event.reason); }); // Clean up on page unload window.addEventListener('beforeunload', function() { if (window.app) { logger.debug('App: Cleaning up cached components...'); window.app.cleanup(); } });