feat: add node resource infos

This commit is contained in:
2025-09-16 20:46:17 +02:00
parent 13f771837b
commit fd1c8e5a8c
4 changed files with 375 additions and 28 deletions

View File

@@ -108,6 +108,18 @@ class ApiClient {
} }
return data; return data;
} }
async getMonitoringResources(ip) {
return this.request('/api/proxy-call', {
method: 'POST',
body: {
ip: ip,
method: 'GET',
uri: '/api/monitoring/resources',
params: []
}
});
}
} }
// Global API client instance // Global API client instance

View File

@@ -12,12 +12,13 @@ class NodeDetailsComponent extends Component {
this.subscribeToProperty('error', this.handleErrorUpdate.bind(this)); this.subscribeToProperty('error', this.handleErrorUpdate.bind(this));
this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this)); this.subscribeToProperty('activeTab', this.handleActiveTabUpdate.bind(this));
this.subscribeToProperty('endpoints', this.handleEndpointsUpdate.bind(this)); this.subscribeToProperty('endpoints', this.handleEndpointsUpdate.bind(this));
this.subscribeToProperty('monitoringResources', this.handleMonitoringResourcesUpdate.bind(this));
} }
// Handle node status update with state preservation // Handle node status update with state preservation
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.renderNodeDetails(newStatus, this.viewModel.get('tasks'), this.viewModel.get('endpoints'), this.viewModel.get('monitoringResources'));
} }
} }
@@ -25,7 +26,7 @@ class NodeDetailsComponent extends Component {
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')) {
this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('endpoints')); this.renderNodeDetails(nodeStatus, newTasks, this.viewModel.get('endpoints'), this.viewModel.get('monitoringResources'));
} }
} }
@@ -55,7 +56,17 @@ class NodeDetailsComponent extends Component {
const nodeStatus = this.viewModel.get('nodeStatus'); const nodeStatus = this.viewModel.get('nodeStatus');
const tasks = this.viewModel.get('tasks'); const tasks = this.viewModel.get('tasks');
if (nodeStatus && !this.viewModel.get('isLoading')) { if (nodeStatus && !this.viewModel.get('isLoading')) {
this.renderNodeDetails(nodeStatus, tasks, newEndpoints); this.renderNodeDetails(nodeStatus, tasks, newEndpoints, this.viewModel.get('monitoringResources'));
}
}
// Handle monitoring resources update with state preservation
handleMonitoringResourcesUpdate(newResources, previousResources) {
const nodeStatus = this.viewModel.get('nodeStatus');
const tasks = this.viewModel.get('tasks');
const endpoints = this.viewModel.get('endpoints');
if (nodeStatus && !this.viewModel.get('isLoading')) {
this.renderNodeDetails(nodeStatus, tasks, endpoints, newResources);
} }
} }
@@ -65,6 +76,7 @@ class NodeDetailsComponent extends Component {
const isLoading = this.viewModel.get('isLoading'); const isLoading = this.viewModel.get('isLoading');
const error = this.viewModel.get('error'); const error = this.viewModel.get('error');
const endpoints = this.viewModel.get('endpoints'); const endpoints = this.viewModel.get('endpoints');
const monitoringResources = this.viewModel.get('monitoringResources');
if (isLoading) { if (isLoading) {
this.renderLoading('<div class="loading-details">Loading detailed information...</div>'); this.renderLoading('<div class="loading-details">Loading detailed information...</div>');
@@ -81,10 +93,10 @@ class NodeDetailsComponent extends Component {
return; return;
} }
this.renderNodeDetails(nodeStatus, tasks, endpoints); this.renderNodeDetails(nodeStatus, tasks, endpoints, monitoringResources);
} }
renderNodeDetails(nodeStatus, tasks, endpoints) { renderNodeDetails(nodeStatus, tasks, endpoints, monitoringResources) {
// Use persisted active tab from the view model, default to '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'; const activeTab = (this.viewModel && typeof this.viewModel.get === 'function' && this.viewModel.get('activeTab')) || 'status';
logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab); logger.debug('NodeDetailsComponent: Rendering with activeTab:', activeTab);
@@ -115,26 +127,7 @@ class NodeDetailsComponent extends Component {
</div> </div>
<div class="tab-content ${activeTab === 'status' ? 'active' : ''}" id="status-tab"> <div class="tab-content ${activeTab === 'status' ? 'active' : ''}" id="status-tab">
<div class="detail-row"> ${this.renderStatusTab(nodeStatus, monitoringResources)}
<span class="detail-label">Free Heap:</span>
<span class="detail-value">${Math.round(nodeStatus.freeHeap / 1024)}KB</span>
</div>
<div class="detail-row">
<span class="detail-label">Chip ID:</span>
<span class="detail-value">${nodeStatus.chipId}</span>
</div>
<div class="detail-row">
<span class="detail-label">SDK Version:</span>
<span class="detail-value">${nodeStatus.sdkVersion}</span>
</div>
<div class="detail-row">
<span class="detail-label">CPU Frequency:</span>
<span class="detail-value">${nodeStatus.cpuFreqMHz}MHz</span>
</div>
<div class="detail-row">
<span class="detail-label">Flash Size:</span>
<span class="detail-value">${Math.round(nodeStatus.flashChipSize / 1024)}KB</span>
</div>
</div> </div>
<div class="tab-content ${activeTab === 'endpoints' ? 'active' : ''}" id="endpoints-tab"> <div class="tab-content ${activeTab === 'endpoints' ? 'active' : ''}" id="endpoints-tab">
@@ -187,8 +180,11 @@ class NodeDetailsComponent extends Component {
await this.viewModel.loadEndpointsData(); await this.viewModel.loadEndpointsData();
} else if (activeTab === 'tasks' && typeof this.viewModel.loadTasksData === 'function') { } else if (activeTab === 'tasks' && typeof this.viewModel.loadTasksData === 'function') {
await this.viewModel.loadTasksData(); await this.viewModel.loadTasksData();
} else if (activeTab === 'status' && typeof this.viewModel.loadMonitoringResources === 'function') {
// status tab: load monitoring resources
await this.viewModel.loadMonitoringResources();
} else { } else {
// status or firmware: refresh core node details // firmware: refresh core node details
if (nodeIp && typeof this.viewModel.loadNodeDetails === 'function') { if (nodeIp && typeof this.viewModel.loadNodeDetails === 'function') {
await this.viewModel.loadNodeDetails(nodeIp); await this.viewModel.loadNodeDetails(nodeIp);
} }
@@ -203,6 +199,153 @@ class NodeDetailsComponent extends Component {
}); });
} }
renderStatusTab(nodeStatus, monitoringResources) {
let html = '';
// Add gauges section if monitoring resources are available
if (monitoringResources) {
html += this.renderResourceGauges(monitoringResources);
}
html += `
<div class="detail-row">
<span class="detail-label">Chip ID:</span>
<span class="detail-value">${nodeStatus.chipId}</span>
</div>
<div class="detail-row">
<span class="detail-label">SDK Version:</span>
<span class="detail-value">${nodeStatus.sdkVersion}</span>
</div>
<div class="detail-row">
<span class="detail-label">CPU Frequency:</span>
<span class="detail-value">${nodeStatus.cpuFreqMHz}MHz</span>
</div>
<div class="detail-row">
<span class="detail-label">Flash Size:</span>
<span class="detail-value">${Math.round(nodeStatus.flashChipSize / 1024)}KB</span>
</div>
`;
// Add monitoring resources if available
if (monitoringResources) {
html += `
<div class="monitoring-section">
<div class="monitoring-header">Resources</div>
`;
// CPU Usage
if (monitoringResources.cpu) {
html += `
<div class="detail-row">
<span class="detail-label">CPU Usage (Avg):</span>
<span class="detail-value">${monitoringResources.cpu.average_usage ? monitoringResources.cpu.average_usage.toFixed(1) + '%' : 'N/A'}</span>
</div>
`;
}
// Memory Usage
if (monitoringResources.memory) {
const heapUsagePercent = monitoringResources.memory.heap_usage_percent || 0;
const totalHeap = monitoringResources.memory.total_heap || 0;
const usedHeap = totalHeap - (monitoringResources.memory.free_heap || 0);
const usedHeapKB = Math.round(usedHeap / 1024);
const totalHeapKB = Math.round(totalHeap / 1024);
html += `
<div class="detail-row">
<span class="detail-label">Heap Usage:</span>
<span class="detail-value">${heapUsagePercent.toFixed(1)}% (${usedHeapKB}KB / ${totalHeapKB}KB)</span>
</div>
`;
}
// Filesystem Usage
if (monitoringResources.filesystem) {
const usedKB = Math.round(monitoringResources.filesystem.used_bytes / 1024);
const totalKB = Math.round(monitoringResources.filesystem.total_bytes / 1024);
html += `
<div class="detail-row">
<span class="detail-label">Filesystem:</span>
<span class="detail-value">${monitoringResources.filesystem.usage_percent ? monitoringResources.filesystem.usage_percent.toFixed(1) + '%' : 'N/A'} (${usedKB}KB / ${totalKB}KB)</span>
</div>
`;
}
// System Uptime
if (monitoringResources.system) {
html += `
<div class="detail-row">
<span class="detail-label">Uptime:</span>
<span class="detail-value">${monitoringResources.system.uptime_formatted || 'N/A'}</span>
</div>
`;
}
html += `</div>`;
}
return html;
}
renderResourceGauges(monitoringResources) {
// Get values with fallbacks and ensure they are numbers
const cpuUsage = parseFloat(monitoringResources.cpu?.average_usage) || 0;
const heapUsage = parseFloat(monitoringResources.memory?.heap_usage_percent) || 0;
const filesystemUsage = parseFloat(monitoringResources.filesystem?.usage_percent) || 0;
const filesystemUsed = parseFloat(monitoringResources.filesystem?.used_bytes) || 0;
const filesystemTotal = parseFloat(monitoringResources.filesystem?.total_bytes) || 0;
// Convert filesystem bytes to KB
const filesystemUsedKB = Math.round(filesystemUsed / 1024);
const filesystemTotalKB = Math.round(filesystemTotal / 1024);
// Helper function to get color class based on percentage
const getColorClass = (percentage) => {
const numPercentage = parseFloat(percentage);
if (numPercentage === 0 || isNaN(numPercentage)) return 'gauge-empty';
if (numPercentage < 50) return 'gauge-green';
if (numPercentage < 80) return 'gauge-yellow';
return 'gauge-red';
};
return `
<div class="resource-gauges">
<div class="gauge-container">
<div class="gauge ${getColorClass(cpuUsage)}" data-percentage="${cpuUsage}" style="--percentage: ${cpuUsage}">
<div class="gauge-circle">
<div class="gauge-text">
<div class="gauge-value">${cpuUsage.toFixed(1)}%</div>
<div class="gauge-label">CPU</div>
</div>
</div>
</div>
</div>
<div class="gauge-container">
<div class="gauge ${getColorClass(heapUsage)}" data-percentage="${heapUsage}" style="--percentage: ${heapUsage}">
<div class="gauge-circle">
<div class="gauge-text">
<div class="gauge-value">${heapUsage.toFixed(1)}%</div>
<div class="gauge-label">Heap</div>
</div>
</div>
</div>
</div>
<div class="gauge-container">
<div class="gauge ${getColorClass(filesystemUsage)}" data-percentage="${filesystemUsage}" style="--percentage: ${filesystemUsage}">
<div class="gauge-circle">
<div class="gauge-text">
<div class="gauge-value">${filesystemUsage.toFixed(1)}%</div>
<div class="gauge-label">Storage</div>
<!-- <div class="gauge-detail">${filesystemUsedKB}KB / ${filesystemTotalKB}KB</div> -->
</div>
</div>
</div>
</div>
</div>
`;
}
renderEndpointsTab(endpoints) { renderEndpointsTab(endpoints) {
if (!endpoints || !Array.isArray(endpoints.endpoints) || endpoints.endpoints.length === 0) { if (!endpoints || !Array.isArray(endpoints.endpoints) || endpoints.endpoints.length === 0) {
return ` return `

View File

@@ -220,7 +220,8 @@ class NodeDetailsViewModel extends ViewModel {
activeTab: 'status', activeTab: 'status',
nodeIp: null, nodeIp: null,
endpoints: null, endpoints: null,
tasksSummary: null tasksSummary: null,
monitoringResources: null
}); });
} }
@@ -250,6 +251,9 @@ class NodeDetailsViewModel extends ViewModel {
// Load endpoints data // Load endpoints data
await this.loadEndpointsData(); await this.loadEndpointsData();
// Load monitoring resources data
await this.loadMonitoringResources();
} catch (error) { } catch (error) {
console.error('Failed to load node details:', error); console.error('Failed to load node details:', error);
this.set('error', error.message); this.set('error', error.message);
@@ -284,6 +288,20 @@ class NodeDetailsViewModel extends ViewModel {
} }
} }
// Load monitoring resources data with state preservation
async loadMonitoringResources() {
try {
const ip = this.get('nodeIp');
const response = await window.apiClient.getMonitoringResources(ip);
// The proxy call returns { data: {...} }, so we need to extract the data
const monitoringData = (response && response.data) ? response.data : null;
this.set('monitoringResources', monitoringData);
} catch (error) {
console.error('Failed to load monitoring resources:', error);
this.set('monitoringResources', null);
}
}
// Invoke an endpoint against this node // Invoke an endpoint against this node
async callEndpoint(method, uri, params) { async callEndpoint(method, uri, params) {
const ip = this.get('nodeIp'); const ip = this.get('nodeIp');

View File

@@ -391,6 +391,173 @@ p {
opacity: 1; opacity: 1;
} }
/* Monitoring Section Styles */
.monitoring-section {
margin-top: 1.5rem;
padding-top: 1rem;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.monitoring-header {
font-size: 1rem;
font-weight: 600;
color: var(--text-primary);
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Resource Gauges Styles */
.resource-gauges {
display: flex;
justify-content: space-around;
align-items: center;
margin-bottom: 2rem;
padding: 1rem;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.gauge-container {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
max-width: 110px;
}
.gauge {
position: relative;
width: 120px;
height: 120px;
margin-bottom: 0.3rem;
}
.gauge-circle {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
display: flex;
align-items: center;
justify-content: center;
position: relative;
transition: all 0.3s ease;
padding: 6px;
}
.gauge-circle::before {
content: '';
position: absolute;
top: 6px;
left: 6px;
width: calc(100% - 12px);
height: calc(100% - 12px);
border-radius: 50%;
background: var(--bg-primary);
z-index: 1;
}
.gauge-circle::after {
content: '';
position: absolute;
top: 6px;
left: 6px;
width: calc(100% - 12px);
height: calc(100% - 12px);
border-radius: 50%;
background: conic-gradient(
from -90deg,
transparent 0deg,
transparent calc(var(--percentage) * 3.6deg),
var(--bg-primary) calc(var(--percentage) * 3.6deg),
var(--bg-primary) 360deg
);
z-index: 2;
}
.gauge-text {
position: relative;
z-index: 3;
text-align: center;
color: var(--text-primary);
}
.gauge-value {
font-size: 1.2rem;
font-weight: 600;
line-height: 1;
margin-bottom: 0.1rem;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.gauge-label {
font-size: 0.75rem;
font-weight: 500;
opacity: 0.7;
text-transform: uppercase;
letter-spacing: 0.3px;
margin-bottom: 0.1rem;
}
.gauge-detail {
font-size: 0.65rem;
opacity: 0.5;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-weight: 400;
}
/* Dynamic gauge colors based on percentage */
.gauge-empty .gauge-circle {
background: rgba(255, 255, 255, 0.1);
}
.gauge-green .gauge-circle {
background: conic-gradient(
from -90deg,
var(--accent-success) 0deg,
var(--accent-success) calc(var(--percentage) * 3.6deg),
rgba(255, 255, 255, 0.1) calc(var(--percentage) * 3.6deg),
rgba(255, 255, 255, 0.1) 360deg
);
}
.gauge-yellow .gauge-circle {
background: conic-gradient(
from -90deg,
var(--accent-warning) 0deg,
var(--accent-warning) calc(var(--percentage) * 3.6deg),
rgba(255, 255, 255, 0.1) calc(var(--percentage) * 3.6deg),
rgba(255, 255, 255, 0.1) 360deg
);
}
.gauge-red .gauge-circle {
background: conic-gradient(
from -90deg,
var(--accent-error) 0deg,
var(--accent-error) calc(var(--percentage) * 3.6deg),
rgba(255, 255, 255, 0.1) calc(var(--percentage) * 3.6deg),
rgba(255, 255, 255, 0.1) 360deg
);
}
/* Gauge value color based on usage level */
.gauge[data-percentage] .gauge-value {
color: var(--text-primary);
}
/* Hover effects */
.gauge:hover .gauge-circle {
transform: scale(1.05);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.2);
}
.gauge:hover .gauge-value {
color: var(--accent-primary);
}
.api-endpoints { .api-endpoints {
margin-top: 1rem; margin-top: 1rem;
} }
@@ -2844,6 +3011,13 @@ p {
display: none; display: none;
} }
/* Hide expand icon on desktop screens */
@media (min-width: 1025px) {
.expand-icon {
display: none;
}
}
/* Ensure expanded state is visually clear */ /* Ensure expanded state is visually clear */
.member-overlay-body .member-card.expanded .member-details { .member-overlay-body .member-card.expanded .member-details {
display: block; display: block;
@@ -2899,7 +3073,7 @@ p {
top: 0; top: 0;
right: 0; right: 0;
height: 100vh; height: 100vh;
width: clamp(33.333vw, 520px, 90vw); width: clamp(33.333vw, 650px, 90vw);
background: var(--bg-primary); background: var(--bg-primary);
color: var(--text-primary); color: var(--text-primary);
border-left: 1px solid var(--border-primary); border-left: 1px solid var(--border-primary);