feature/refactoring #3

Merged
master merged 18 commits from feature/refactoring into main 2025-09-02 13:26:13 +02:00
3 changed files with 51 additions and 87 deletions
Showing only changes of commit 1bdaed9a2c - Show all commits

View File

@@ -51,92 +51,56 @@ class ApiClient {
}
async getClusterMembers() {
try {
return await this.request('/api/cluster/members', { method: 'GET' });
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
return this.request('/api/cluster/members', { method: 'GET' });
}
async getClusterMembersFromNode(ip) {
try {
return await this.request(`/api/cluster/members`, {
method: 'GET',
query: { ip: ip }
});
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
return this.request(`/api/cluster/members`, {
method: 'GET',
query: { ip: ip }
});
}
async getDiscoveryInfo() {
try {
return await this.request('/api/discovery/nodes', { method: 'GET' });
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
return this.request('/api/discovery/nodes', { method: 'GET' });
}
async selectRandomPrimaryNode() {
try {
return await this.request('/api/discovery/random-primary', {
method: 'POST',
body: { timestamp: new Date().toISOString() }
});
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
return this.request('/api/discovery/random-primary', {
method: 'POST',
body: { timestamp: new Date().toISOString() }
});
}
async getNodeStatus(ip) {
try {
return await this.request(`/api/node/status/${encodeURIComponent(ip)}`, { method: 'GET' });
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
return this.request(`/api/node/status/${encodeURIComponent(ip)}`, { method: 'GET' });
}
async getTasksStatus(ip) {
try {
return await this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
return this.request('/api/tasks/status', { method: 'GET', query: ip ? { ip } : undefined });
}
async getCapabilities(ip) {
try {
return await this.request('/api/capabilities', { method: 'GET', query: ip ? { ip } : undefined });
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
return this.request('/api/capabilities', { method: 'GET', query: ip ? { ip } : undefined });
}
async callCapability({ ip, method, uri, params }) {
try {
return await this.request('/api/proxy-call', {
method: 'POST',
body: { ip, method, uri, params }
});
} catch (error) {
throw new Error(`Request failed: ${error.message}`);
}
return this.request('/api/proxy-call', {
method: 'POST',
body: { ip, method, uri, params }
});
}
async uploadFirmware(file, nodeIp) {
try {
const formData = new FormData();
formData.append('file', file);
return await this.request(`/api/node/update`, {
method: 'POST',
query: { ip: nodeIp },
body: formData,
isForm: true,
headers: {},
});
} catch (error) {
throw new Error(`Upload failed: ${error.message}`);
}
const formData = new FormData();
formData.append('file', file);
return this.request(`/api/node/update`, {
method: 'POST',
query: { ip: nodeIp },
body: formData,
isForm: true,
headers: {},
});
}
}

View File

@@ -285,22 +285,11 @@ class ClusterMembersComponent extends Component {
// Check if we should skip rendering during view switches
shouldSkipRender() {
// Skip rendering if we're in the middle of a view switch
const isViewSwitching = document.querySelectorAll('.view-content.active').length === 0;
if (isViewSwitching) {
console.log('ClusterMembersComponent: View switching in progress, skipping render');
// Rely on lifecycle flags controlled by App
if (!this.isMounted || this.isPaused) {
logger.debug('ClusterMembersComponent: Not mounted or paused, skipping render');
return true;
}
// Skip rendering if the component is not visible
const isVisible = this.container.style.display !== 'none' &&
this.container.style.opacity !== '0' &&
this.container.classList.contains('active');
if (!isVisible) {
console.log('ClusterMembersComponent: Component not visible, skipping render');
return true;
}
return false;
}
@@ -308,11 +297,12 @@ class ClusterMembersComponent extends Component {
updateMembersPartially(newMembers, previousMembers) {
console.log('ClusterMembersComponent: Performing partial update to preserve UI state');
// Update only the data that changed, preserving expanded states and active tabs
newMembers.forEach((newMember, index) => {
const prevMember = previousMembers[index];
// Build previous map by IP for stable diffs
const prevByIp = new Map((previousMembers || []).map(m => [m.ip, m]));
newMembers.forEach((newMember) => {
const prevMember = prevByIp.get(newMember.ip);
if (prevMember && this.hasMemberChanged(newMember, prevMember)) {
this.updateMemberCard(newMember, index);
this.updateMemberCard(newMember);
}
});
}
@@ -325,7 +315,7 @@ class ClusterMembersComponent extends Component {
}
// Update a specific member card without re-rendering the entire component
updateMemberCard(member, index) {
updateMemberCard(member) {
const card = this.findElement(`[data-member-ip="${member.ip}"]`);
if (!card) return;
@@ -451,9 +441,9 @@ class ClusterMembersComponent extends Component {
<div class="member-status ${statusClass}">
${statusIcon}
</div>
<div class="member-hostname">${member.hostname || 'Unknown Device'}</div>
<div class="member-hostname">${this.escapeHtml(member.hostname || 'Unknown Device')}</div>
</div>
<div class="member-ip">${member.ip || 'No IP'}</div>
<div class="member-ip">${this.escapeHtml(member.ip || 'No IP')}</div>
<div class="member-latency">
<span class="latency-label">Latency:</span>
<span class="latency-value">${member.latency ? member.latency + 'ms' : 'N/A'}</span>
@@ -462,7 +452,7 @@ class ClusterMembersComponent extends Component {
${member.labels && Object.keys(member.labels).length ? `
<div class="member-row-2">
<div class="member-labels">
${Object.entries(member.labels).map(([key, value]) => `<span class=\"label-chip\">${key}: ${value}</span>`).join('')}
${Object.entries(member.labels).map(([key, value]) => `<span class=\"label-chip\">${this.escapeHtml(key)}: ${this.escapeHtml(value)}</span>`).join('')}
</div>
</div>
` : ''}
@@ -807,8 +797,8 @@ class NodeDetailsComponent extends Component {
}
renderNodeDetails(nodeStatus, tasks, capabilities) {
// Always start with 'status' tab, don't restore previous state
const activeTab = 'status';
// Use persisted active tab from the view model, default to 'status'
const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status';
console.log('NodeDetailsComponent: Rendering with activeTab:', activeTab);
const html = `

View File

@@ -550,7 +550,7 @@ class Component {
}
renderError(message) {
const safe = String(message || 'An error occurred');
const safe = this.escapeHtml(String(message || 'An error occurred'));
const html = `
<div class="error">
<strong>Error:</strong><br>
@@ -569,6 +569,16 @@ class Component {
this.setHTML('', html);
}
// Basic HTML escaping for dynamic values
escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
// Tab helpers
setupTabs(container = this.container, options = {}) {
const { onChange } = options;