feat: remove state preservation #6

Merged
master merged 1 commits from feature/remove-state-preservation-wuerg into main 2025-09-19 21:18:13 +02:00
6 changed files with 38 additions and 46 deletions

View File

@@ -33,13 +33,13 @@ spore-ui/
├── public/ # Frontend files ├── public/ # Frontend files
│ ├── index.html # Main HTML page │ ├── index.html # Main HTML page
│ ├── styles.css # All CSS styles │ ├── styles.css # All CSS styles
│ ├── framework.js # Enhanced component framework with state preservation │ ├── framework.js # Enhanced component framework
│ ├── components.js # UI components with partial update support │ ├── components.js # UI components with partial update support
│ ├── view-models.js # Data models with UI state management │ ├── view-models.js # Data models with UI state management
│ ├── app.js # Main application logic │ ├── app.js # Main application logic
│ └── test-state-preservation.html # Test interface for state preservation │ └── test-interface.html # Test interface
├── docs/ ├── docs/
│ └── STATE_PRESERVATION.md # Detailed documentation of state preservation system │ └── FRAMEWORK_README.md # Framework documentation
└── README.md # This file └── README.md # This file
``` ```
@@ -48,7 +48,7 @@ spore-ui/
1. **Install dependencies**: `npm install` 1. **Install dependencies**: `npm install`
2. **Start the server**: `npm start` 2. **Start the server**: `npm start`
3. **Open in browser**: `http://localhost:3001` 3. **Open in browser**: `http://localhost:3001`
4. **Test state preservation**: `http://localhost:3001/test-state-preservation.html` 4. **Test interface**: `http://localhost:3001/test-interface.html`
## API Endpoints ## API Endpoints
@@ -62,7 +62,7 @@ spore-ui/
- **Backend**: Express.js, Node.js - **Backend**: Express.js, Node.js
- **Frontend**: Vanilla JavaScript, CSS3, HTML5 - **Frontend**: Vanilla JavaScript, CSS3, HTML5
- **Framework**: Custom component-based architecture with state preservation - **Framework**: Custom component-based architecture
- **API**: SPORE Embedded System API - **API**: SPORE Embedded System API
- **Design**: Glassmorphism, CSS Grid, Flexbox - **Design**: Glassmorphism, CSS Grid, Flexbox

View File

@@ -90,7 +90,7 @@ document.addEventListener('DOMContentLoaded', async function() {
}); });
})(); })();
// Set up periodic updates with state preservation // Set up periodic updates
function setupPeriodicUpdates() { function setupPeriodicUpdates() {
// Auto-refresh cluster members every 30 seconds using smart update // Auto-refresh cluster members every 30 seconds using smart update
setInterval(() => { setInterval(() => {
@@ -99,7 +99,7 @@ function setupPeriodicUpdates() {
// Use smart update if available, otherwise fall back to regular update // Use smart update if available, otherwise fall back to regular update
if (viewModel.smartUpdate && typeof viewModel.smartUpdate === 'function') { if (viewModel.smartUpdate && typeof viewModel.smartUpdate === 'function') {
logger.debug('App: Performing smart update to preserve UI state...'); logger.debug('App: Performing smart update...');
viewModel.smartUpdate(); viewModel.smartUpdate();
} else if (viewModel.updateClusterMembers && typeof viewModel.updateClusterMembers === 'function') { } else if (viewModel.updateClusterMembers && typeof viewModel.updateClusterMembers === 'function') {
logger.debug('App: Performing regular update...'); logger.debug('App: Performing regular update...');

View File

@@ -1,4 +1,4 @@
// Cluster Members Component with enhanced state preservation // Cluster Members Component
class ClusterMembersComponent extends Component { class ClusterMembersComponent extends Component {
constructor(container, viewModel, eventBus) { constructor(container, viewModel, eventBus) {
super(container, viewModel, eventBus); super(container, viewModel, eventBus);
@@ -142,7 +142,7 @@ class ClusterMembersComponent extends Component {
logger.debug('ClusterMembersComponent: View model listeners set up'); logger.debug('ClusterMembersComponent: View model listeners set up');
} }
// Handle members update with state preservation // Handle members update
handleMembersUpdate(newMembers, previousMembers) { handleMembersUpdate(newMembers, previousMembers) {
logger.debug('ClusterMembersComponent: Members updated:', { newMembers, previousMembers }); logger.debug('ClusterMembersComponent: Members updated:', { newMembers, previousMembers });
@@ -166,9 +166,9 @@ class ClusterMembersComponent extends Component {
return; return;
} }
if (this.shouldPreserveState(newMembers, previousMembers)) { if (this.shouldSkipFullRender(newMembers, previousMembers)) {
// Perform partial update to preserve UI state // Perform partial update
logger.debug('ClusterMembersComponent: Preserving state, performing partial update'); logger.debug('ClusterMembersComponent: Skipping full render, performing partial update');
this.updateMembersPartially(newMembers, previousMembers); this.updateMembersPartially(newMembers, previousMembers);
} else { } else {
// Full re-render if structure changed significantly // Full re-render if structure changed significantly
@@ -242,8 +242,8 @@ class ClusterMembersComponent extends Component {
} }
} }
// Check if we should preserve UI state during update // Check if we should skip full re-render during update
shouldPreserveState(newMembers, previousMembers) { shouldSkipFullRender(newMembers, previousMembers) {
if (!previousMembers || !Array.isArray(previousMembers)) return false; if (!previousMembers || !Array.isArray(previousMembers)) return false;
if (!Array.isArray(newMembers)) return false; if (!Array.isArray(newMembers)) return false;
@@ -254,7 +254,7 @@ class ClusterMembersComponent extends Component {
const newIps = new Set(newMembers.map(m => m.ip)); const newIps = new Set(newMembers.map(m => m.ip));
const prevIps = new Set(previousMembers.map(m => m.ip)); const prevIps = new Set(previousMembers.map(m => m.ip));
// If IPs are the same, we can preserve state // If IPs are the same, we can skip full re-render
return newIps.size === prevIps.size && return newIps.size === prevIps.size &&
[...newIps].every(ip => prevIps.has(ip)); [...newIps].every(ip => prevIps.has(ip));
} }
@@ -269,9 +269,9 @@ class ClusterMembersComponent extends Component {
return false; return false;
} }
// Update members partially to preserve UI state // Update members partially
updateMembersPartially(newMembers, previousMembers) { updateMembersPartially(newMembers, previousMembers) {
logger.debug('ClusterMembersComponent: Performing partial update to preserve UI state'); logger.debug('ClusterMembersComponent: Performing partial update');
// Build previous map by IP for stable diffs // Build previous map by IP for stable diffs
const prevByIp = new Map((previousMembers || []).map(m => [m.ip, m])); const prevByIp = new Map((previousMembers || []).map(m => [m.ip, m]));

View File

@@ -1,4 +1,4 @@
// Node Details Component with enhanced state preservation // Node Details Component
class NodeDetailsComponent extends Component { class NodeDetailsComponent extends Component {
constructor(container, viewModel, eventBus) { constructor(container, viewModel, eventBus) {
super(container, viewModel, eventBus); super(container, viewModel, eventBus);
@@ -15,14 +15,14 @@ class NodeDetailsComponent extends Component {
this.subscribeToProperty('monitoringResources', this.handleMonitoringResourcesUpdate.bind(this)); this.subscribeToProperty('monitoringResources', this.handleMonitoringResourcesUpdate.bind(this));
} }
// Handle node status update with state preservation // Handle node status update
handleNodeStatusUpdate(newStatus, previousStatus) { handleNodeStatusUpdate(newStatus, previousStatus) {
if (newStatus && !this.viewModel.get('isLoading')) { if (newStatus && !this.viewModel.get('isLoading')) {
this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('endpoints'), this.viewModel.get('monitoringResources')); this.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('endpoints'), this.viewModel.get('monitoringResources'));
} }
} }
// Handle tasks update with state preservation // Handle tasks update
handleTasksUpdate(newTasks, previousTasks) { handleTasksUpdate(newTasks, previousTasks) {
const nodeStatus = this.viewModel.get('nodeStatus'); const nodeStatus = this.viewModel.get('nodeStatus');
if (nodeStatus && !this.viewModel.get('isLoading')) { if (nodeStatus && !this.viewModel.get('isLoading')) {
@@ -51,7 +51,7 @@ class NodeDetailsComponent extends Component {
this.updateActiveTab(newTab, previousTab); this.updateActiveTab(newTab, previousTab);
} }
// Handle endpoints update with state preservation // Handle endpoints update
handleEndpointsUpdate(newEndpoints, previousEndpoints) { handleEndpointsUpdate(newEndpoints, previousEndpoints) {
const nodeStatus = this.viewModel.get('nodeStatus'); const nodeStatus = this.viewModel.get('nodeStatus');
const tasks = this.viewModel.get('tasks'); const tasks = this.viewModel.get('tasks');
@@ -60,7 +60,7 @@ class NodeDetailsComponent extends Component {
} }
} }
// Handle monitoring resources update with state preservation // Handle monitoring resources update
handleMonitoringResourcesUpdate(newResources, previousResources) { handleMonitoringResourcesUpdate(newResources, previousResources) {
const nodeStatus = this.viewModel.get('nodeStatus'); const nodeStatus = this.viewModel.get('nodeStatus');
const tasks = this.viewModel.get('tasks'); const tasks = this.viewModel.get('tasks');

View File

@@ -225,10 +225,7 @@ class ViewModel {
// Batch update with change detection // Batch update with change detection
batchUpdate(updates, options = {}) { batchUpdate(updates, options = {}) {
const { preserveUIState = true, notifyChanges = true } = options; const { notifyChanges = true } = options;
// Optionally preserve UI state snapshot
const currentUIState = preserveUIState ? new Map(this._uiState) : null;
// Track which keys actually change and what the previous values were // Track which keys actually change and what the previous values were
const changedKeys = []; const changedKeys = [];
@@ -245,11 +242,6 @@ class ViewModel {
} }
}); });
// Restore UI state if requested
if (preserveUIState && currentUIState) {
this._uiState = currentUIState;
}
// Notify listeners for changed keys // Notify listeners for changed keys
if (notifyChanges) { if (notifyChanges) {
changedKeys.forEach(key => { changedKeys.forEach(key => {
@@ -259,7 +251,7 @@ class ViewModel {
} }
} }
// Base Component class with enhanced state preservation // Base Component class
class Component { class Component {
constructor(container, viewModel, eventBus) { constructor(container, viewModel, eventBus) {
this.container = container; this.container = container;

View File

@@ -1,6 +1,6 @@
// View Models for SPORE UI Components // View Models for SPORE UI Components
// Cluster View Model with enhanced state preservation // Cluster View Model
class ClusterViewModel extends ViewModel { class ClusterViewModel extends ViewModel {
constructor() { constructor() {
super(); super();
@@ -23,7 +23,7 @@ class ClusterViewModel extends ViewModel {
}, 100); }, 100);
} }
// Update cluster members with state preservation // Update cluster members
async updateClusterMembers() { async updateClusterMembers() {
try { try {
logger.debug('ClusterViewModel: updateClusterMembers called'); logger.debug('ClusterViewModel: updateClusterMembers called');
@@ -45,12 +45,12 @@ class ClusterViewModel extends ViewModel {
? members.filter(m => m && m.status && m.status.toUpperCase() === 'ACTIVE').length ? members.filter(m => m && m.status && m.status.toUpperCase() === 'ACTIVE').length
: 0; : 0;
// Use batch update to preserve UI state // Use batch update
this.batchUpdate({ this.batchUpdate({
members: members, members: members,
lastUpdateTime: new Date().toISOString(), lastUpdateTime: new Date().toISOString(),
onlineNodes: onlineNodes onlineNodes: onlineNodes
}, { preserveUIState: true }); });
// Restore expanded cards and active tabs // Restore expanded cards and active tabs
this.set('expandedCards', currentExpandedCards); this.set('expandedCards', currentExpandedCards);
@@ -69,12 +69,12 @@ class ClusterViewModel extends ViewModel {
} }
} }
// Update primary node display with state preservation // Update primary node display
async updatePrimaryNodeDisplay() { async updatePrimaryNodeDisplay() {
try { try {
const discoveryInfo = await window.apiClient.getDiscoveryInfo(); const discoveryInfo = await window.apiClient.getDiscoveryInfo();
// Use batch update to preserve UI state // Use batch update
const updates = {}; const updates = {};
if (discoveryInfo.primaryNode) { if (discoveryInfo.primaryNode) {
@@ -91,7 +91,7 @@ class ClusterViewModel extends ViewModel {
updates.totalNodes = 0; updates.totalNodes = 0;
} }
this.batchUpdate(updates, { preserveUIState: true }); this.batchUpdate(updates);
} catch (error) { } catch (error) {
console.error('Failed to fetch discovery info:', error); console.error('Failed to fetch discovery info:', error);
@@ -208,7 +208,7 @@ class ClusterViewModel extends ViewModel {
} }
} }
// Node Details View Model with enhanced state preservation // Node Details View Model
class NodeDetailsViewModel extends ViewModel { class NodeDetailsViewModel extends ViewModel {
constructor() { constructor() {
super(); super();
@@ -225,7 +225,7 @@ class NodeDetailsViewModel extends ViewModel {
}); });
} }
// Load node details with state preservation // Load node details
async loadNodeDetails(ip) { async loadNodeDetails(ip) {
try { try {
// Store current UI state // Store current UI state
@@ -237,10 +237,10 @@ class NodeDetailsViewModel extends ViewModel {
const nodeStatus = await window.apiClient.getNodeStatus(ip); const nodeStatus = await window.apiClient.getNodeStatus(ip);
// Use batch update to preserve UI state // Use batch update
this.batchUpdate({ this.batchUpdate({
nodeStatus: nodeStatus nodeStatus: nodeStatus
}, { preserveUIState: true }); });
// Restore active tab // Restore active tab
this.set('activeTab', currentActiveTab); this.set('activeTab', currentActiveTab);
@@ -262,7 +262,7 @@ class NodeDetailsViewModel extends ViewModel {
} }
} }
// Load tasks data with state preservation // Load tasks data
async loadTasksData() { async loadTasksData() {
try { try {
const ip = this.get('nodeIp'); const ip = this.get('nodeIp');
@@ -276,7 +276,7 @@ class NodeDetailsViewModel extends ViewModel {
} }
} }
// Load endpoints data with state preservation // Load endpoints data
async loadEndpointsData() { async loadEndpointsData() {
try { try {
const ip = this.get('nodeIp'); const ip = this.get('nodeIp');
@@ -290,7 +290,7 @@ class NodeDetailsViewModel extends ViewModel {
} }
} }
// Load monitoring resources data with state preservation // Load monitoring resources data
async loadMonitoringResources() { async loadMonitoringResources() {
try { try {
const ip = this.get('nodeIp'); const ip = this.get('nodeIp');