feature/monitoring-overview #10
@@ -20,7 +20,7 @@
|
||||
<div class="nav-left">
|
||||
<button class="nav-tab active" data-view="cluster">🌐 Cluster</button>
|
||||
<button class="nav-tab" data-view="topology">🔗 Topology</button>
|
||||
<button class="nav-tab" data-view="monitoring">📊 Monitoring</button>
|
||||
<button class="nav-tab" data-view="monitoring">📡 Monitoring</button>
|
||||
<button class="nav-tab" data-view="firmware">📦 Firmware</button>
|
||||
</div>
|
||||
<div class="nav-right">
|
||||
@@ -143,7 +143,7 @@
|
||||
<div id="monitoring-view" class="view-content">
|
||||
<div class="monitoring-view-section">
|
||||
<div class="monitoring-header">
|
||||
<h2>📊 Monitoring</h2>
|
||||
<h2>📡 Monitoring</h2>
|
||||
<button class="refresh-btn" id="refresh-monitoring-btn">
|
||||
<svg class="refresh-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M1 4v6h6M23 20v-6h-6" />
|
||||
|
||||
@@ -132,6 +132,46 @@ class MonitoringViewComponent extends Component {
|
||||
return 'utilization-red';
|
||||
}
|
||||
|
||||
// Format lastSeen timestamp to human readable format
|
||||
formatLastSeen(lastSeen) {
|
||||
if (!lastSeen) return 'Unknown';
|
||||
|
||||
// lastSeen appears to be in milliseconds
|
||||
const now = Date.now();
|
||||
const diff = now - lastSeen;
|
||||
|
||||
if (diff < 60000) { // Less than 1 minute
|
||||
return 'Just now';
|
||||
} else if (diff < 3600000) { // Less than 1 hour
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
return `${minutes}m ago`;
|
||||
} else if (diff < 86400000) { // Less than 1 day
|
||||
const hours = Math.floor(diff / 3600000);
|
||||
const minutes = Math.floor((diff % 3600000) / 60000);
|
||||
return `${hours}h ${minutes}m ago`;
|
||||
} else { // More than 1 day
|
||||
const days = Math.floor(diff / 86400000);
|
||||
const hours = Math.floor((diff % 86400000) / 3600000);
|
||||
return `${days}d ${hours}h ago`;
|
||||
}
|
||||
}
|
||||
|
||||
// Format flash size in human readable format
|
||||
formatFlashSize(bytes) {
|
||||
if (!bytes || bytes === 0) return 'Unknown';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
// Clean up resources
|
||||
cleanup() {
|
||||
if (this.drawer) {
|
||||
@@ -323,6 +363,7 @@ class MonitoringViewComponent extends Component {
|
||||
renderNodeCard(nodeData) {
|
||||
const { ip, hostname, resources, hasResources, error, resourceSource } = nodeData;
|
||||
|
||||
|
||||
if (!hasResources) {
|
||||
return `
|
||||
<div class="node-card error" data-node-ip="${ip}">
|
||||
@@ -336,6 +377,12 @@ class MonitoringViewComponent extends Component {
|
||||
<div class="node-error">
|
||||
${error || 'Monitoring endpoint not available'}
|
||||
</div>
|
||||
${nodeData.lastSeen ? `
|
||||
<div class="node-uptime">
|
||||
<div class="uptime-label">⏱️ Last Seen</div>
|
||||
<div class="uptime-value">${this.formatLastSeen(nodeData.lastSeen)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -344,6 +391,7 @@ class MonitoringViewComponent extends Component {
|
||||
const cpu = resources?.cpu || {};
|
||||
const memory = resources?.memory || {};
|
||||
const storage = resources?.filesystem || resources?.storage || {};
|
||||
const system = resources?.system || {};
|
||||
|
||||
let cpuTotal, cpuAvailable, cpuUsed, cpuUtilization;
|
||||
let memoryTotal, memoryAvailable, memoryUsed, memoryUtilization;
|
||||
@@ -398,6 +446,26 @@ class MonitoringViewComponent extends Component {
|
||||
<span class="status-badge ${resourceSource === 'monitoring' ? 'success' : 'warning'}">${resourceSourceText}</span>
|
||||
</div>
|
||||
|
||||
${system.uptime_formatted ? `
|
||||
<div class="node-uptime">
|
||||
<div class="uptime-label">⏱️ Uptime</div>
|
||||
<div class="uptime-value">${system.uptime_formatted}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="node-latency">
|
||||
<div class="latency-label">🐢 Latency</div>
|
||||
<div class="latency-value">${nodeData.latency ? `${nodeData.latency}ms` : 'N/A'}</div>
|
||||
</div>
|
||||
<div class="latency-divider"></div>
|
||||
|
||||
${(nodeData.basic?.flashChipSize || nodeData.resources?.flashChipSize) ? `
|
||||
<div class="node-flash">
|
||||
<div class="flash-label">💾 Flash</div>
|
||||
<div class="flash-value">${this.formatFlashSize(nodeData.basic?.flashChipSize || nodeData.resources?.flashChipSize)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div class="node-resources">
|
||||
<div class="resource-item">
|
||||
<div class="resource-label">⚡ CPU</div>
|
||||
|
||||
@@ -25,6 +25,22 @@ class NodeDetailsComponent extends Component {
|
||||
return (r << 16) + (g << 8) + b;
|
||||
}
|
||||
|
||||
// Format flash size in human readable format
|
||||
formatFlashSize(bytes) {
|
||||
if (!bytes || bytes === 0) return 'Unknown';
|
||||
|
||||
const units = ['B', 'KB', 'MB', 'GB'];
|
||||
let size = bytes;
|
||||
let unitIndex = 0;
|
||||
|
||||
while (size >= 1024 && unitIndex < units.length - 1) {
|
||||
size /= 1024;
|
||||
unitIndex++;
|
||||
}
|
||||
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
// Parameter component renderers
|
||||
renderSelectComponent(p, formId, pidx) {
|
||||
return `<select id="${formId}-field-${pidx}" data-param-name="${p.name}" data-param-location="${p.location || 'body'}" data-param-type="${p.type || 'string'}" data-param-required="${p.required ? '1' : '0'}" class="param-input">${p.values.map(v => `<option value="${v}">${v}</option>`).join('')}</select>`;
|
||||
@@ -333,7 +349,7 @@ class NodeDetailsComponent extends Component {
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">Flash Size:</span>
|
||||
<span class="detail-value">${Math.round(nodeStatus.flashChipSize / 1024)}KB</span>
|
||||
<span class="detail-value">${this.formatFlashSize(nodeStatus.flashChipSize)}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
||||
@@ -725,6 +725,7 @@ class MonitoringViewModel extends ViewModel {
|
||||
nodeResources.set(member.ip, {
|
||||
...member,
|
||||
resources: basicResources,
|
||||
basic: member.resources, // Preserve original basic data
|
||||
hasResources: !!basicResources,
|
||||
resourceSource: basicResources ? 'basic' : 'none',
|
||||
error: basicResources ? null : error.message
|
||||
|
||||
@@ -782,10 +782,6 @@ p {
|
||||
.latency-value {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
background: var(--bg-secondary);
|
||||
padding: 0.2rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
/* Tab Styles */
|
||||
@@ -4220,6 +4216,83 @@ html {
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.node-uptime {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.uptime-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.uptime-value {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.node-latency {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.5rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.latency-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.latency-value {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.latency-divider {
|
||||
height: 1px;
|
||||
background: var(--border-primary);
|
||||
margin: 0.5rem 0;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.node-flash {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-primary);
|
||||
}
|
||||
|
||||
.flash-label {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.flash-value {
|
||||
color: var(--text-primary);
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
Reference in New Issue
Block a user