From 6b5ff2939c10a2e63a0fcafb909000c915dab9e1 Mon Sep 17 00:00:00 2001 From: Patrick Balsiger Date: Wed, 27 Aug 2025 17:17:47 +0200 Subject: [PATCH] fix: cluster status badge --- README.md | 39 ------- public/app.js | 103 +++++++++++++++++++ public/components.js | 51 ++++++++++ public/debug-cluster.html | 208 ++++++++++++++++++++++++++++++++++++++ public/styles.css | 37 +++++++ public/view-models.js | 5 + 6 files changed, 404 insertions(+), 39 deletions(-) create mode 100644 public/debug-cluster.html diff --git a/README.md b/README.md index be7be5e..5d59a09 100644 --- a/README.md +++ b/README.md @@ -8,22 +8,6 @@ Zero-configuration web interface for monitoring and managing SPORE embedded syst - **📊 Node Details**: Detailed system information including running tasks and available endpoints - **🚀 OTA**: Clusterwide over-the-air firmware updates - **📱 Responsive**: Works on all devices and screen sizes -- **💾 State Preservation**: Advanced UI state management that preserves user interactions during data refreshes -- **⚡ Smart Updates**: Efficient partial updates that only refresh changed data, not entire components - -## Key Improvements - -### 🆕 **State Preservation System** -- **Expanded Cards**: Cluster member cards stay expanded during data refreshes -- **Active Tabs**: Selected tabs in node detail views are maintained -- **User Context**: All user interactions are preserved across data updates -- **No More UI Resets**: Users never lose their place in the interface - -### 🚀 **Performance Enhancements** -- **Partial Updates**: Only changed data triggers UI updates -- **Smart Change Detection**: System automatically detects when data has actually changed -- **Efficient Rendering**: Reduced DOM manipulation and improved responsiveness -- **Batch Operations**: Multiple property updates are handled efficiently ## Screenshots ### Cluster @@ -64,15 +48,6 @@ spore-ui/ 3. **Open in browser**: `http://localhost:3001` 4. **Test state preservation**: `http://localhost:3001/test-state-preservation.html` -## Testing State Preservation - -The framework includes a comprehensive test interface to demonstrate state preservation: - -1. **Expand cluster member cards** to see detailed information -2. **Change active tabs** in node detail views -3. **Trigger data refresh** using the refresh buttons -4. **Verify that all UI state is preserved** during updates - ## API Endpoints - **`/`** - Main UI page @@ -89,20 +64,6 @@ The framework includes a comprehensive test interface to demonstrate state prese - **API**: SPORE Embedded System API - **Design**: Glassmorphism, CSS Grid, Flexbox -## Architecture Highlights - -### 🏗️ **Component Framework** -- **Base Component Class**: Provides state management and partial update capabilities -- **ViewModel Pattern**: Separates data logic from UI logic -- **Event-Driven Updates**: Efficient pub/sub system for component communication -- **State Persistence**: Automatic preservation of UI state across data refreshes - -### 🔄 **Smart Update System** -- **Change Detection**: Automatically identifies when data has actually changed -- **Partial Rendering**: Updates only the necessary parts of the UI -- **State Preservation**: Maintains user interactions during all updates -- **Performance Optimization**: Minimizes unnecessary DOM operations - ## UDP Auto Discovery The backend now includes automatic UDP discovery for SPORE nodes on the network. This eliminates the need for hardcoded IP addresses and provides a self-healing, scalable solution for managing SPORE clusters. diff --git a/public/app.js b/public/app.js index 2344076..724824a 100644 --- a/public/app.js +++ b/public/app.js @@ -37,10 +37,26 @@ document.addEventListener('DOMContentLoaded', function() { app.registerRoute('firmware', FirmwareViewComponent, 'firmware-view', firmwareViewModel); console.log('App: Routes registered and components pre-initialized'); + // Initialize cluster status component for header badge AFTER main components + // DISABLED - causes interference with main cluster functionality + /* + console.log('App: Initializing cluster status component...'); + const clusterStatusComponent = new ClusterStatusComponent( + document.querySelector('.cluster-status'), + clusterViewModel, + app.eventBus + ); + clusterStatusComponent.initialize(); + console.log('App: Cluster status component initialized'); + */ + // Set up navigation event listeners console.log('App: Setting up navigation...'); app.setupNavigation(); + // Set up cluster status updates (simple approach without component interference) + setupClusterStatusUpdates(clusterViewModel); + // Set up periodic updates for cluster view with state preservation // setupPeriodicUpdates(); // Disabled automatic refresh @@ -80,6 +96,93 @@ function setupPeriodicUpdates() { }, 10000); } +// Set up cluster status updates (simple approach without component interference) +function setupClusterStatusUpdates(clusterViewModel) { + // Set initial "discovering" state immediately + updateClusterStatusBadge(undefined, undefined, undefined); + + // Force a fresh fetch and keep showing "discovering" until we get real data + let hasReceivedRealData = false; + + // Subscribe to view model changes to update cluster status + clusterViewModel.subscribe('totalNodes', (totalNodes) => { + if (hasReceivedRealData) { + updateClusterStatusBadge(totalNodes, clusterViewModel.get('clientInitialized'), clusterViewModel.get('error')); + } + }); + + clusterViewModel.subscribe('clientInitialized', (clientInitialized) => { + if (hasReceivedRealData) { + updateClusterStatusBadge(clusterViewModel.get('totalNodes'), clientInitialized, clusterViewModel.get('error')); + } + }); + + clusterViewModel.subscribe('error', (error) => { + if (hasReceivedRealData) { + updateClusterStatusBadge(clusterViewModel.get('totalNodes'), clusterViewModel.get('clientInitialized'), error); + } + }); + + // Force a fresh fetch and only update status after we get real data + setTimeout(async () => { + try { + console.log('Cluster Status: Forcing fresh fetch from backend...'); + const discoveryInfo = await window.apiClient.getDiscoveryInfo(); + console.log('Cluster Status: Got fresh data:', discoveryInfo); + + // Now we have real data, mark it and update the status + hasReceivedRealData = true; + updateClusterStatusBadge(discoveryInfo.totalNodes, discoveryInfo.clientInitialized, null); + + } catch (error) { + console.error('Cluster Status: Failed to fetch fresh data:', error); + hasReceivedRealData = true; + updateClusterStatusBadge(0, false, error.message); + } + }, 100); // Small delay to ensure view model is ready +} + +function updateClusterStatusBadge(totalNodes, clientInitialized, error) { + const clusterStatusBadge = document.querySelector('.cluster-status'); + if (!clusterStatusBadge) return; + + let statusText, statusIcon, statusClass; + + // Check if we're still in initial state (no real data yet) + const hasRealData = totalNodes !== undefined && clientInitialized !== undefined; + + if (!hasRealData) { + statusText = 'Cluster Discovering...'; + statusIcon = '🔍'; + statusClass = 'cluster-status-discovering'; + } else if (error) { + statusText = 'Cluster Error'; + statusIcon = '❌'; + statusClass = 'cluster-status-error'; + } else if (totalNodes === 0) { + statusText = 'Cluster Offline'; + statusIcon = '🔴'; + statusClass = 'cluster-status-offline'; + } else if (clientInitialized) { + statusText = 'Cluster Online'; + statusIcon = '🟢'; + statusClass = 'cluster-status-online'; + } else { + statusText = 'Cluster Connecting'; + statusIcon = '🟡'; + statusClass = 'cluster-status-connecting'; + } + + // Update the badge + clusterStatusBadge.innerHTML = `${statusIcon} ${statusText}`; + + // Remove all existing status classes + clusterStatusBadge.classList.remove('cluster-status-online', 'cluster-status-offline', 'cluster-status-connecting', 'cluster-status-error', 'cluster-status-discovering'); + + // Add the appropriate status class + clusterStatusBadge.classList.add(statusClass); +} + // Global error handler window.addEventListener('error', function(event) { console.error('Global error:', event.error); diff --git a/public/components.js b/public/components.js index e84b126..c7d4411 100644 --- a/public/components.js +++ b/public/components.js @@ -1925,4 +1925,55 @@ class FirmwareViewComponent extends Component { console.error('Failed to update available nodes:', error); } } +} + +// Cluster Status Component for header badge +class ClusterStatusComponent extends Component { + constructor(container, viewModel, eventBus) { + super(container, viewModel, eventBus); + } + + setupViewModelListeners() { + // Subscribe to properties that affect cluster status + this.subscribeToProperty('totalNodes', this.render.bind(this)); + this.subscribeToProperty('clientInitialized', this.render.bind(this)); + this.subscribeToProperty('error', this.render.bind(this)); + } + + render() { + const totalNodes = this.viewModel.get('totalNodes'); + const clientInitialized = this.viewModel.get('clientInitialized'); + const error = this.viewModel.get('error'); + + let statusText, statusIcon, statusClass; + + if (error) { + statusText = 'Cluster Error'; + statusIcon = '❌'; + statusClass = 'cluster-status-error'; + } else if (totalNodes === 0) { + statusText = 'Cluster Offline'; + statusIcon = '🔴'; + statusClass = 'cluster-status-offline'; + } else if (clientInitialized) { + statusText = 'Cluster Online'; + statusIcon = '🟢'; + statusClass = 'cluster-status-online'; + } else { + statusText = 'Cluster Connecting'; + statusIcon = '🟡'; + statusClass = 'cluster-status-connecting'; + } + + // Update the cluster status badge using the container passed to this component + if (this.container) { + this.container.innerHTML = `${statusIcon} ${statusText}`; + + // Remove all existing status classes + this.container.classList.remove('cluster-status-online', 'cluster-status-offline', 'cluster-status-connecting', 'cluster-status-error'); + + // Add the appropriate status class + this.container.classList.add(statusClass); + } + } } \ No newline at end of file diff --git a/public/debug-cluster.html b/public/debug-cluster.html new file mode 100644 index 0000000..a26c364 --- /dev/null +++ b/public/debug-cluster.html @@ -0,0 +1,208 @@ + + + + + + Debug Cluster + + + +

🐛 Debug Cluster Functionality

+ +
+

1. API Client Test

+ +
+
+ +
+

2. View Model Test

+ +
+
+ +
+

3. Component Test

+ +
+
+ +
+

4. Console Log

+
+ +
+ + + + + + + + + \ No newline at end of file diff --git a/public/styles.css b/public/styles.css index c1becd8..6ef03f6 100644 --- a/public/styles.css +++ b/public/styles.css @@ -737,6 +737,43 @@ p { font-weight: 600; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); box-shadow: 0 2px 8px rgba(0, 255, 0, 0.1); + transition: all 0.3s ease; +} + +/* Cluster Status States */ +.cluster-status-online { + background: linear-gradient(135deg, rgba(0, 255, 0, 0.15) 0%, rgba(0, 255, 0, 0.08) 100%); + border: 1px solid rgba(0, 255, 0, 0.25); + color: #00ff88; + box-shadow: 0 2px 8px rgba(0, 255, 0, 0.1); +} + +.cluster-status-offline { + background: linear-gradient(135deg, rgba(255, 0, 0, 0.15) 0%, rgba(255, 0, 0, 0.08) 100%); + border: 1px solid rgba(255, 0, 0, 0.25); + color: #ff6b6b; + box-shadow: 0 2px 8px rgba(255, 0, 0, 0.1); +} + +.cluster-status-connecting { + background: linear-gradient(135deg, rgba(255, 193, 7, 0.15) 0%, rgba(255, 193, 7, 0.08) 100%); + border: 1px solid rgba(255, 193, 7, 0.25); + color: #ffd54f; + box-shadow: 0 2px 8px rgba(255, 193, 7, 0.1); +} + +.cluster-status-discovering { + background: linear-gradient(135deg, rgba(33, 150, 243, 0.15) 0%, rgba(33, 150, 243, 0.08) 100%); + border: 1px solid rgba(33, 150, 243, 0.25); + color: #64b5f6; + box-shadow: 0 2px 8px rgba(33, 150, 243, 0.1); +} + +.cluster-status-error { + background: linear-gradient(135deg, rgba(244, 67, 54, 0.15) 0%, rgba(244, 67, 54, 0.08) 100%); + border: 1px solid rgba(244, 67, 54, 0.25); + color: #ff8a80; + box-shadow: 0 2px 8px rgba(244, 67, 54, 0.1); } /* View Content Styles */ diff --git a/public/view-models.js b/public/view-models.js index c1346c5..089c984 100644 --- a/public/view-models.js +++ b/public/view-models.js @@ -15,6 +15,11 @@ class ClusterViewModel extends ViewModel { activeTabs: new Map(), // Store active tab for each node lastUpdateTime: null }); + + // Initialize cluster status after a short delay to allow components to subscribe + setTimeout(() => { + this.updatePrimaryNodeDisplay(); + }, 100); } // Update cluster members with state preservation