Fixed updateDashboard() to properly convert childNodes to array before passing to dom.content(). Was showing '[object HTMLCollection]' instead of rendering the updated metrics. Changed from: - dom.content(el, element.children) To: - Array.prototype.slice.call(element.childNodes) This ensures proper DOM manipulation and fixes the refresh cycle.
688 lines
25 KiB
JavaScript
688 lines
25 KiB
JavaScript
'use strict';
|
||
'require view';
|
||
'require ui';
|
||
'require dom';
|
||
'require poll';
|
||
'require system-hub/api as API';
|
||
'require system-hub/theme as Theme';
|
||
|
||
return view.extend({
|
||
healthData: null,
|
||
sysInfo: null,
|
||
|
||
load: function() {
|
||
return Promise.all([
|
||
API.getSystemInfo(),
|
||
API.getHealth(),
|
||
Theme.getTheme()
|
||
]);
|
||
},
|
||
|
||
render: function(data) {
|
||
var self = this;
|
||
this.sysInfo = data[0] || {};
|
||
this.healthData = data[1] || {};
|
||
var theme = data[2];
|
||
|
||
var container = E('div', { 'class': 'system-hub-dashboard' }, [
|
||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
|
||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
|
||
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/overview.css') }),
|
||
|
||
// Header
|
||
this.renderHeader(),
|
||
|
||
// Stats Overview (like SecuBox)
|
||
this.renderStatsOverview(),
|
||
|
||
// System Info Grid (4 columns per prompt)
|
||
this.renderSystemInfoGrid(),
|
||
|
||
// Real-Time Performance Metrics (v0.3.2 - NEW: modern histograms)
|
||
E('h3', { 'class': 'sh-section-title' }, '📈 Real-Time Performance Metrics'),
|
||
this.renderRealtimeMetrics(),
|
||
|
||
// Quick Status Indicators (per prompt)
|
||
E('h3', { 'class': 'sh-section-title' }, '⚡ System Status'),
|
||
this.renderQuickStatusIndicators()
|
||
]);
|
||
|
||
// Setup auto-refresh
|
||
poll.add(L.bind(function() {
|
||
return Promise.all([
|
||
API.getSystemInfo(),
|
||
API.getHealth()
|
||
]).then(L.bind(function(refreshData) {
|
||
this.sysInfo = refreshData[0] || {};
|
||
this.healthData = refreshData[1] || {};
|
||
this.updateDashboard();
|
||
}, this));
|
||
}, this), 30);
|
||
|
||
return container;
|
||
},
|
||
|
||
renderHeader: function() {
|
||
var score = this.healthData.score || 0;
|
||
var scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical'));
|
||
|
||
return E('div', { 'class': 'sh-dashboard-header' }, [
|
||
E('div', { 'class': 'sh-dashboard-header-content' }, [
|
||
E('div', {}, [
|
||
E('h2', {}, '⚙️ System Control Center'),
|
||
E('p', { 'class': 'sh-dashboard-subtitle' }, 'System Monitoring & Management Center')
|
||
]),
|
||
E('div', { 'class': 'sh-dashboard-header-info' }, [
|
||
E('span', { 'class': 'sh-dashboard-badge sh-dashboard-badge-version' },
|
||
'v0.3.2'),
|
||
E('span', { 'class': 'sh-dashboard-badge' },
|
||
'⏱️ ' + (this.sysInfo.uptime_formatted || '0d 0h 0m')),
|
||
E('span', { 'class': 'sh-dashboard-badge' },
|
||
'🖥️ ' + (this.sysInfo.hostname || 'OpenWrt'))
|
||
])
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderStatsOverview: function() {
|
||
var score = this.healthData.score || 0;
|
||
var scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical'));
|
||
var scoreLabel = score >= 80 ? 'Excellent' : (score >= 60 ? 'Good' : (score >= 40 ? 'Warning' : 'Critical'));
|
||
|
||
// Enhanced stats with status indicators (v0.3.2)
|
||
var cpu = this.healthData.cpu || {};
|
||
var memory = this.healthData.memory || {};
|
||
var disk = this.healthData.disk || {};
|
||
var network = this.healthData.network || {};
|
||
|
||
// Process count (v0.3.2)
|
||
var processes = (cpu.processes_running || 0) + '/' + (cpu.processes_total || 0);
|
||
|
||
// Network throughput (v0.3.2) - format bytes
|
||
var rxGB = ((network.rx_bytes || 0) / 1024 / 1024 / 1024).toFixed(2);
|
||
var txGB = ((network.tx_bytes || 0) / 1024 / 1024 / 1024).toFixed(2);
|
||
|
||
// Status icons (v0.3.2)
|
||
var getStatusIcon = function(status) {
|
||
if (status === 'critical') return '⚠️';
|
||
if (status === 'warning') return '⚡';
|
||
return '✓';
|
||
};
|
||
|
||
return E('div', { 'class': 'sh-stats-overview-grid' }, [
|
||
// Health Score Card
|
||
E('div', { 'class': 'sh-stat-overview-card sh-stat-' + scoreClass }, [
|
||
E('div', { 'class': 'sh-stat-overview-value' }, score),
|
||
E('div', { 'class': 'sh-stat-overview-label' }, 'Health Score'),
|
||
E('div', { 'class': 'sh-stat-overview-status' }, scoreLabel)
|
||
]),
|
||
|
||
// CPU Card with enhanced info
|
||
E('div', {
|
||
'class': 'sh-stat-overview-card sh-stat-cpu sh-stat-' + (cpu.status || 'ok'),
|
||
'title': 'Load: ' + (cpu.load_1m || '0') + ' | ' + (cpu.cores || 0) + ' cores | ' + processes + ' processes'
|
||
}, [
|
||
E('div', { 'class': 'sh-stat-overview-icon' }, '🔥'),
|
||
E('div', { 'class': 'sh-stat-overview-value' }, [
|
||
E('span', {}, (cpu.usage || 0) + '%'),
|
||
E('span', { 'class': 'sh-stat-status-icon' }, getStatusIcon(cpu.status))
|
||
]),
|
||
E('div', { 'class': 'sh-stat-overview-label' }, 'CPU Usage'),
|
||
E('div', { 'class': 'sh-stat-overview-detail' },
|
||
'Load: ' + (cpu.load_1m || '0') + ' • ' + processes + ' proc')
|
||
]),
|
||
|
||
// Memory Card with swap info
|
||
E('div', {
|
||
'class': 'sh-stat-overview-card sh-stat-memory sh-stat-' + (memory.status || 'ok'),
|
||
'title': ((memory.used_kb || 0) / 1024).toFixed(0) + ' MB / ' + ((memory.total_kb || 0) / 1024).toFixed(0) + ' MB' +
|
||
(memory.swap_total_kb > 0 ? ' | Swap: ' + (memory.swap_usage || 0) + '%' : '')
|
||
}, [
|
||
E('div', { 'class': 'sh-stat-overview-icon' }, '💾'),
|
||
E('div', { 'class': 'sh-stat-overview-value' }, [
|
||
E('span', {}, (memory.usage || 0) + '%'),
|
||
E('span', { 'class': 'sh-stat-status-icon' }, getStatusIcon(memory.status))
|
||
]),
|
||
E('div', { 'class': 'sh-stat-overview-label' }, 'Memory'),
|
||
E('div', { 'class': 'sh-stat-overview-detail' },
|
||
((memory.used_kb || 0) / 1024).toFixed(0) + 'MB / ' +
|
||
((memory.total_kb || 0) / 1024).toFixed(0) + 'MB' +
|
||
(memory.swap_total_kb > 0 ? ' • Swap: ' + (memory.swap_usage || 0) + '%' : ''))
|
||
]),
|
||
|
||
// Disk Card
|
||
E('div', {
|
||
'class': 'sh-stat-overview-card sh-stat-disk sh-stat-' + (disk.status || 'ok'),
|
||
'title': ((disk.used_kb || 0) / 1024 / 1024).toFixed(1) + ' GB / ' + ((disk.total_kb || 0) / 1024 / 1024).toFixed(1) + ' GB'
|
||
}, [
|
||
E('div', { 'class': 'sh-stat-overview-icon' }, '💿'),
|
||
E('div', { 'class': 'sh-stat-overview-value' }, [
|
||
E('span', {}, (disk.usage || 0) + '%'),
|
||
E('span', { 'class': 'sh-stat-status-icon' }, getStatusIcon(disk.status))
|
||
]),
|
||
E('div', { 'class': 'sh-stat-overview-label' }, 'Disk Usage'),
|
||
E('div', { 'class': 'sh-stat-overview-detail' },
|
||
((disk.used_kb || 0) / 1024 / 1024).toFixed(1) + 'GB / ' +
|
||
((disk.total_kb || 0) / 1024 / 1024).toFixed(1) + 'GB')
|
||
]),
|
||
|
||
// Network Card (v0.3.2 - NEW)
|
||
E('div', {
|
||
'class': 'sh-stat-overview-card sh-stat-network sh-stat-' + (network.wan_up ? 'ok' : 'error'),
|
||
'title': 'RX: ' + rxGB + ' GB | TX: ' + txGB + ' GB'
|
||
}, [
|
||
E('div', { 'class': 'sh-stat-overview-icon' }, '🌐'),
|
||
E('div', { 'class': 'sh-stat-overview-value' }, [
|
||
E('span', {}, network.wan_up ? 'Online' : 'Offline'),
|
||
E('span', { 'class': 'sh-stat-status-icon' }, network.wan_up ? '✓' : '✗')
|
||
]),
|
||
E('div', { 'class': 'sh-stat-overview-label' }, 'Network'),
|
||
E('div', { 'class': 'sh-stat-overview-detail' },
|
||
'↓ ' + rxGB + 'GB • ↑ ' + txGB + 'GB')
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderSystemInfoGrid: function() {
|
||
var self = this;
|
||
var cpu = this.healthData.cpu || {};
|
||
|
||
return E('div', {}, [
|
||
E('h3', { 'class': 'sh-section-title' }, 'System Information'),
|
||
E('div', { 'class': 'sh-system-info-grid' }, [
|
||
// Hostname card with edit button
|
||
E('div', { 'class': 'sh-info-grid-card' }, [
|
||
E('div', { 'class': 'sh-info-grid-header' }, [
|
||
E('span', { 'class': 'sh-info-grid-icon' }, '🏷️'),
|
||
E('span', { 'class': 'sh-info-grid-title' }, 'Hostname')
|
||
]),
|
||
E('div', { 'class': 'sh-info-grid-value' }, this.sysInfo.hostname || 'unknown'),
|
||
E('button', {
|
||
'class': 'sh-info-grid-action',
|
||
'click': function() {
|
||
ui.addNotification(null, E('p', 'Edit hostname feature coming soon'), 'info');
|
||
}
|
||
}, '✏️ Edit')
|
||
]),
|
||
|
||
// Uptime card
|
||
E('div', { 'class': 'sh-info-grid-card' }, [
|
||
E('div', { 'class': 'sh-info-grid-header' }, [
|
||
E('span', { 'class': 'sh-info-grid-icon' }, '⏱️'),
|
||
E('span', { 'class': 'sh-info-grid-title' }, 'Uptime')
|
||
]),
|
||
E('div', { 'class': 'sh-info-grid-value' }, this.sysInfo.uptime_formatted || '0d 0h 0m'),
|
||
E('div', { 'class': 'sh-info-grid-detail' }, 'System runtime')
|
||
]),
|
||
|
||
// Load Average card (monospace per prompt)
|
||
E('div', { 'class': 'sh-info-grid-card' }, [
|
||
E('div', { 'class': 'sh-info-grid-header' }, [
|
||
E('span', { 'class': 'sh-info-grid-icon' }, '📊'),
|
||
E('span', { 'class': 'sh-info-grid-title' }, 'Load Average')
|
||
]),
|
||
E('div', { 'class': 'sh-info-grid-value sh-monospace' },
|
||
(cpu.load_1m || '0.00') + ' / ' +
|
||
(cpu.load_5m || '0.00') + ' / ' +
|
||
(cpu.load_15m || '0.00')
|
||
),
|
||
E('div', { 'class': 'sh-info-grid-detail' }, '1m / 5m / 15m')
|
||
]),
|
||
|
||
// Kernel Version card with copy icon
|
||
E('div', { 'class': 'sh-info-grid-card' }, [
|
||
E('div', { 'class': 'sh-info-grid-header' }, [
|
||
E('span', { 'class': 'sh-info-grid-icon' }, '⚙️'),
|
||
E('span', { 'class': 'sh-info-grid-title' }, 'Kernel Version')
|
||
]),
|
||
E('div', { 'class': 'sh-info-grid-value sh-monospace' }, this.sysInfo.kernel || 'unknown'),
|
||
E('button', {
|
||
'class': 'sh-info-grid-action',
|
||
'click': function() {
|
||
var kernel = self.sysInfo.kernel || 'unknown';
|
||
navigator.clipboard.writeText(kernel).then(function() {
|
||
ui.addNotification(null, E('p', '✓ Copied to clipboard: ' + kernel), 'info');
|
||
});
|
||
}
|
||
}, '📋 Copy')
|
||
])
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderQuickStatusIndicators: function() {
|
||
var network = this.healthData.network || {};
|
||
var services = this.healthData.services || {};
|
||
|
||
// Determine status colors and icons
|
||
var internetStatus = network.wan_up ? 'ok' : 'error';
|
||
var internetIcon = network.wan_up ? '✓' : '✗';
|
||
var internetText = network.wan_up ? 'Connected' : 'Disconnected';
|
||
|
||
var dnsStatus = network.dns_ok !== false ? 'ok' : 'error';
|
||
var dnsIcon = network.dns_ok !== false ? '✓' : '✗';
|
||
var dnsText = network.dns_ok !== false ? 'Resolving' : 'Failed';
|
||
|
||
var ntpStatus = 'ok'; // Placeholder, would need backend support
|
||
var ntpIcon = '✓';
|
||
var ntpText = 'Synced';
|
||
|
||
var fwStatus = 'ok';
|
||
var fwIcon = '✓';
|
||
var fwText = (network.firewall_rules || 0) + ' rules active';
|
||
|
||
return E('div', { 'class': 'sh-status-indicators-grid' }, [
|
||
// Internet connectivity
|
||
E('div', { 'class': 'sh-status-indicator sh-status-' + internetStatus }, [
|
||
E('div', { 'class': 'sh-status-indicator-icon' }, '🌐'),
|
||
E('div', { 'class': 'sh-status-indicator-content' }, [
|
||
E('div', { 'class': 'sh-status-indicator-label' }, 'Internet Connectivity'),
|
||
E('div', { 'class': 'sh-status-indicator-value' }, [
|
||
E('span', { 'class': 'sh-status-badge sh-status-badge-' + internetStatus }, internetIcon),
|
||
E('span', {}, internetText)
|
||
])
|
||
])
|
||
]),
|
||
|
||
// DNS resolution
|
||
E('div', { 'class': 'sh-status-indicator sh-status-' + dnsStatus }, [
|
||
E('div', { 'class': 'sh-status-indicator-icon' }, '🔍'),
|
||
E('div', { 'class': 'sh-status-indicator-content' }, [
|
||
E('div', { 'class': 'sh-status-indicator-label' }, 'DNS Resolution'),
|
||
E('div', { 'class': 'sh-status-indicator-value' }, [
|
||
E('span', { 'class': 'sh-status-badge sh-status-badge-' + dnsStatus }, dnsIcon),
|
||
E('span', {}, dnsText)
|
||
])
|
||
])
|
||
]),
|
||
|
||
// NTP sync
|
||
E('div', { 'class': 'sh-status-indicator sh-status-' + ntpStatus }, [
|
||
E('div', { 'class': 'sh-status-indicator-icon' }, '🕐'),
|
||
E('div', { 'class': 'sh-status-indicator-content' }, [
|
||
E('div', { 'class': 'sh-status-indicator-label' }, 'NTP Sync'),
|
||
E('div', { 'class': 'sh-status-indicator-value' }, [
|
||
E('span', { 'class': 'sh-status-badge sh-status-badge-' + ntpStatus }, ntpIcon),
|
||
E('span', {}, ntpText)
|
||
])
|
||
])
|
||
]),
|
||
|
||
// Firewall status
|
||
E('div', { 'class': 'sh-status-indicator sh-status-' + fwStatus }, [
|
||
E('div', { 'class': 'sh-status-indicator-icon' }, '🛡️'),
|
||
E('div', { 'class': 'sh-status-indicator-content' }, [
|
||
E('div', { 'class': 'sh-status-indicator-label' }, 'Firewall'),
|
||
E('div', { 'class': 'sh-status-indicator-value' }, [
|
||
E('span', { 'class': 'sh-status-badge sh-status-badge-' + fwStatus }, fwIcon),
|
||
E('span', {}, fwText)
|
||
])
|
||
])
|
||
])
|
||
]);
|
||
},
|
||
|
||
// v0.3.2 - Modern real-time metrics with histograms
|
||
renderRealtimeMetrics: function() {
|
||
var cpu = this.healthData.cpu || {};
|
||
var memory = this.healthData.memory || {};
|
||
var disk = this.healthData.disk || {};
|
||
var temp = this.healthData.temperature || {};
|
||
|
||
return E('div', { 'class': 'sh-realtime-metrics' }, [
|
||
// CPU with load trend bars
|
||
E('div', { 'class': 'sh-rt-metric sh-rt-metric-cpu' }, [
|
||
E('div', { 'class': 'sh-rt-header' }, [
|
||
E('div', { 'class': 'sh-rt-title-group' }, [
|
||
E('span', { 'class': 'sh-rt-icon' }, '🔥'),
|
||
E('span', { 'class': 'sh-rt-title' }, 'CPU Performance')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-value-group' }, [
|
||
E('span', { 'class': 'sh-rt-value' }, (cpu.usage || 0) + '%'),
|
||
E('span', { 'class': 'sh-rt-badge sh-rt-badge-' + (cpu.status || 'ok') }, cpu.status || 'ok')
|
||
])
|
||
]),
|
||
E('div', { 'class': 'sh-rt-progress-modern' }, [
|
||
E('div', {
|
||
'class': 'sh-rt-progress-fill sh-rt-gradient-cpu',
|
||
'style': 'width: ' + (cpu.usage || 0) + '%',
|
||
'data-value': (cpu.usage || 0)
|
||
})
|
||
]),
|
||
E('div', { 'class': 'sh-rt-details-grid' }, [
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Load Average'),
|
||
E('span', { 'class': 'sh-rt-detail-value' }, (cpu.load_1m || '0.00'))
|
||
]),
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Cores'),
|
||
E('span', { 'class': 'sh-rt-detail-value' }, (cpu.cores || 0))
|
||
]),
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Processes'),
|
||
E('span', { 'class': 'sh-rt-detail-value' }, (cpu.processes_running || 0) + '/' + (cpu.processes_total || 0))
|
||
])
|
||
]),
|
||
// Mini histogram for load average
|
||
E('div', { 'class': 'sh-rt-histogram' },
|
||
this.renderLoadHistogram([cpu.load_1m || 0, cpu.load_5m || 0, cpu.load_15m || 0]))
|
||
]),
|
||
|
||
// Memory with swap visualization
|
||
E('div', { 'class': 'sh-rt-metric sh-rt-metric-memory' }, [
|
||
E('div', { 'class': 'sh-rt-header' }, [
|
||
E('div', { 'class': 'sh-rt-title-group' }, [
|
||
E('span', { 'class': 'sh-rt-icon' }, '💾'),
|
||
E('span', { 'class': 'sh-rt-title' }, 'Memory Usage')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-value-group' }, [
|
||
E('span', { 'class': 'sh-rt-value' }, (memory.usage || 0) + '%'),
|
||
E('span', { 'class': 'sh-rt-badge sh-rt-badge-' + (memory.status || 'ok') }, memory.status || 'ok')
|
||
])
|
||
]),
|
||
E('div', { 'class': 'sh-rt-progress-modern' }, [
|
||
E('div', {
|
||
'class': 'sh-rt-progress-fill sh-rt-gradient-memory',
|
||
'style': 'width: ' + (memory.usage || 0) + '%',
|
||
'data-value': (memory.usage || 0)
|
||
})
|
||
]),
|
||
E('div', { 'class': 'sh-rt-details-grid' }, [
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Used'),
|
||
E('span', { 'class': 'sh-rt-detail-value' }, ((memory.used_kb || 0) / 1024).toFixed(0) + ' MB')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Total'),
|
||
E('span', { 'class': 'sh-rt-detail-value' }, ((memory.total_kb || 0) / 1024).toFixed(0) + ' MB')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Swap'),
|
||
E('span', { 'class': 'sh-rt-detail-value' },
|
||
memory.swap_total_kb > 0 ? (memory.swap_usage || 0) + '%' : 'N/A')
|
||
])
|
||
]),
|
||
// Memory breakdown visualization
|
||
memory.swap_total_kb > 0 ? E('div', { 'class': 'sh-rt-multi-bar' }, [
|
||
E('div', { 'class': 'sh-rt-bar-segment sh-rt-bar-used', 'style': 'width: ' + (memory.usage || 0) + '%' }),
|
||
E('div', { 'class': 'sh-rt-bar-segment sh-rt-bar-swap', 'style': 'width: ' + (memory.swap_usage || 0) + '%' })
|
||
]) : null
|
||
]),
|
||
|
||
// Disk with storage breakdown
|
||
E('div', { 'class': 'sh-rt-metric sh-rt-metric-disk' }, [
|
||
E('div', { 'class': 'sh-rt-header' }, [
|
||
E('div', { 'class': 'sh-rt-title-group' }, [
|
||
E('span', { 'class': 'sh-rt-icon' }, '💿'),
|
||
E('span', { 'class': 'sh-rt-title' }, 'Disk Space')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-value-group' }, [
|
||
E('span', { 'class': 'sh-rt-value' }, (disk.usage || 0) + '%'),
|
||
E('span', { 'class': 'sh-rt-badge sh-rt-badge-' + (disk.status || 'ok') }, disk.status || 'ok')
|
||
])
|
||
]),
|
||
E('div', { 'class': 'sh-rt-progress-modern' }, [
|
||
E('div', {
|
||
'class': 'sh-rt-progress-fill sh-rt-gradient-disk',
|
||
'style': 'width: ' + (disk.usage || 0) + '%',
|
||
'data-value': (disk.usage || 0)
|
||
})
|
||
]),
|
||
E('div', { 'class': 'sh-rt-details-grid' }, [
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Used'),
|
||
E('span', { 'class': 'sh-rt-detail-value' }, ((disk.used_kb || 0) / 1024 / 1024).toFixed(1) + ' GB')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Total'),
|
||
E('span', { 'class': 'sh-rt-detail-value' }, ((disk.total_kb || 0) / 1024 / 1024).toFixed(1) + ' GB')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Free'),
|
||
E('span', { 'class': 'sh-rt-detail-value' },
|
||
((disk.total_kb - disk.used_kb || 0) / 1024 / 1024).toFixed(1) + ' GB')
|
||
])
|
||
])
|
||
]),
|
||
|
||
// Temperature gauge
|
||
E('div', { 'class': 'sh-rt-metric sh-rt-metric-temp' }, [
|
||
E('div', { 'class': 'sh-rt-header' }, [
|
||
E('div', { 'class': 'sh-rt-title-group' }, [
|
||
E('span', { 'class': 'sh-rt-icon' }, '🌡️'),
|
||
E('span', { 'class': 'sh-rt-title' }, 'Temperature')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-value-group' }, [
|
||
E('span', { 'class': 'sh-rt-value' }, (temp.value || 0) + '°C'),
|
||
E('span', { 'class': 'sh-rt-badge sh-rt-badge-' + (temp.status || 'ok') }, temp.status || 'ok')
|
||
])
|
||
]),
|
||
E('div', { 'class': 'sh-rt-progress-modern' }, [
|
||
E('div', {
|
||
'class': 'sh-rt-progress-fill sh-rt-gradient-temp',
|
||
'style': 'width: ' + Math.min((temp.value || 0), 100) + '%',
|
||
'data-value': (temp.value || 0)
|
||
})
|
||
]),
|
||
E('div', { 'class': 'sh-rt-details-grid' }, [
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Status'),
|
||
E('span', { 'class': 'sh-rt-detail-value' }, temp.status || 'ok')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Warning at'),
|
||
E('span', { 'class': 'sh-rt-detail-value' }, '70°C')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-detail' }, [
|
||
E('span', { 'class': 'sh-rt-detail-label' }, 'Critical at'),
|
||
E('span', { 'class': 'sh-rt-detail-value' }, '85°C')
|
||
])
|
||
])
|
||
])
|
||
]);
|
||
},
|
||
|
||
// Render mini histogram for load average
|
||
renderLoadHistogram: function(loads) {
|
||
var maxLoad = Math.max(...loads, 1);
|
||
return E('div', { 'class': 'sh-rt-histogram-bars' }, [
|
||
E('div', { 'class': 'sh-rt-histogram-bar-group' }, [
|
||
E('div', {
|
||
'class': 'sh-rt-histogram-bar',
|
||
'style': 'height: ' + ((loads[0] / maxLoad) * 100) + '%',
|
||
'title': '1m: ' + loads[0]
|
||
}),
|
||
E('span', { 'class': 'sh-rt-histogram-label' }, '1m')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-histogram-bar-group' }, [
|
||
E('div', {
|
||
'class': 'sh-rt-histogram-bar',
|
||
'style': 'height: ' + ((loads[1] / maxLoad) * 100) + '%',
|
||
'title': '5m: ' + loads[1]
|
||
}),
|
||
E('span', { 'class': 'sh-rt-histogram-label' }, '5m')
|
||
]),
|
||
E('div', { 'class': 'sh-rt-histogram-bar-group' }, [
|
||
E('div', {
|
||
'class': 'sh-rt-histogram-bar',
|
||
'style': 'height: ' + ((loads[2] / maxLoad) * 100) + '%',
|
||
'title': '15m: ' + loads[2]
|
||
}),
|
||
E('span', { 'class': 'sh-rt-histogram-label' }, '15m')
|
||
])
|
||
]);
|
||
},
|
||
|
||
getMetricConfig: function(type, data) {
|
||
switch(type) {
|
||
case 'CPU':
|
||
return {
|
||
icon: '🔥',
|
||
title: 'CPU Usage',
|
||
value: (data.usage || 0) + '%',
|
||
percentage: data.usage || 0,
|
||
status: data.status || 'ok',
|
||
color: this.getStatusColor(data.usage || 0),
|
||
details: 'Load: ' + (data.load_1m || '0') + ' • ' + (data.cores || 0) + ' cores'
|
||
};
|
||
case 'Memory':
|
||
var usedMB = ((data.used_kb || 0) / 1024).toFixed(0);
|
||
var totalMB = ((data.total_kb || 0) / 1024).toFixed(0);
|
||
return {
|
||
icon: '💾',
|
||
title: 'Memory',
|
||
value: (data.usage || 0) + '%',
|
||
percentage: data.usage || 0,
|
||
status: data.status || 'ok',
|
||
color: this.getStatusColor(data.usage || 0),
|
||
details: usedMB + ' MB / ' + totalMB + ' MB used'
|
||
};
|
||
case 'Disk':
|
||
var usedGB = ((data.used_kb || 0) / 1024 / 1024).toFixed(1);
|
||
var totalGB = ((data.total_kb || 0) / 1024 / 1024).toFixed(1);
|
||
return {
|
||
icon: '💿',
|
||
title: 'Disk Space',
|
||
value: (data.usage || 0) + '%',
|
||
percentage: data.usage || 0,
|
||
status: data.status || 'ok',
|
||
color: this.getStatusColor(data.usage || 0),
|
||
details: usedGB + ' GB / ' + totalGB + ' GB used'
|
||
};
|
||
case 'Temperature':
|
||
return {
|
||
icon: '🌡️',
|
||
title: 'Temperature',
|
||
value: (data.value || 0) + '°C',
|
||
percentage: Math.min((data.value || 0), 100),
|
||
status: data.status || 'ok',
|
||
color: this.getTempColor(data.value || 0),
|
||
details: 'Status: ' + (data.status || 'unknown')
|
||
};
|
||
default:
|
||
return {
|
||
icon: '📊',
|
||
title: type,
|
||
value: 'N/A',
|
||
percentage: 0,
|
||
status: 'unknown',
|
||
color: '#64748b',
|
||
details: 'No data'
|
||
};
|
||
}
|
||
},
|
||
|
||
getStatusColor: function(usage) {
|
||
if (usage >= 90) return '#ef4444';
|
||
if (usage >= 75) return '#f59e0b';
|
||
if (usage >= 50) return '#3b82f6';
|
||
return '#22c55e';
|
||
},
|
||
|
||
getTempColor: function(temp) {
|
||
if (temp >= 80) return '#ef4444';
|
||
if (temp >= 70) return '#f59e0b';
|
||
if (temp >= 60) return '#3b82f6';
|
||
return '#22c55e';
|
||
},
|
||
|
||
renderInfoCard: function(title, content) {
|
||
return E('div', { 'class': 'sh-info-card' }, [
|
||
E('div', { 'class': 'sh-info-card-header' }, [
|
||
E('h3', {}, title)
|
||
]),
|
||
E('div', { 'class': 'sh-info-card-body' }, content)
|
||
]);
|
||
},
|
||
|
||
renderSystemInfo: function() {
|
||
return E('div', { 'class': 'sh-info-list' }, [
|
||
this.renderInfoRow('🏷️', 'Hostname', this.sysInfo.hostname || 'unknown'),
|
||
this.renderInfoRow('🖥️', 'Model', this.sysInfo.model || 'Unknown'),
|
||
this.renderInfoRow('📦', 'OpenWrt', this.sysInfo.openwrt_version || 'Unknown'),
|
||
this.renderInfoRow('⚙️', 'Kernel', this.sysInfo.kernel || 'unknown'),
|
||
this.renderInfoRow('⏱️', 'Uptime', this.sysInfo.uptime_formatted || '0d 0h 0m'),
|
||
this.renderInfoRow('🕐', 'Local Time', this.sysInfo.local_time || 'unknown')
|
||
]);
|
||
},
|
||
|
||
renderNetworkInfo: function() {
|
||
var wan_status = this.healthData.network ? this.healthData.network.wan_up : false;
|
||
return E('div', { 'class': 'sh-info-list' }, [
|
||
this.renderInfoRow('🌐', 'WAN Status',
|
||
E('span', {
|
||
'class': 'sh-status-badge sh-status-' + (wan_status ? 'ok' : 'error')
|
||
}, wan_status ? 'Connected' : 'Disconnected')
|
||
),
|
||
this.renderInfoRow('📡', 'Network', this.healthData.network ? this.healthData.network.status : 'unknown')
|
||
]);
|
||
},
|
||
|
||
renderServicesInfo: function() {
|
||
var running = this.healthData.services ? this.healthData.services.running : 0;
|
||
var failed = this.healthData.services ? this.healthData.services.failed : 0;
|
||
|
||
return E('div', { 'class': 'sh-info-list' }, [
|
||
this.renderInfoRow('▶️', 'Running Services',
|
||
E('span', { 'class': 'sh-status-badge sh-status-ok' }, running + ' services')
|
||
),
|
||
this.renderInfoRow('⏹️', 'Failed Services',
|
||
failed > 0
|
||
? E('span', { 'class': 'sh-status-badge sh-status-error' }, failed + ' services')
|
||
: E('span', { 'class': 'sh-status-badge sh-status-ok' }, 'None')
|
||
),
|
||
this.renderInfoRow('🔗', 'Quick Actions',
|
||
E('a', {
|
||
'class': 'sh-link-button',
|
||
'href': '/cgi-bin/luci/admin/secubox/system/system-hub/services'
|
||
}, 'Manage Services →')
|
||
)
|
||
]);
|
||
},
|
||
|
||
renderInfoRow: function(icon, label, value) {
|
||
return E('div', { 'class': 'sh-info-row' }, [
|
||
E('span', { 'class': 'sh-info-icon' }, icon),
|
||
E('span', { 'class': 'sh-info-label' }, label),
|
||
E('span', { 'class': 'sh-info-value' }, value)
|
||
]);
|
||
},
|
||
|
||
updateDashboard: function() {
|
||
// Update real-time metrics (v0.3.2)
|
||
var realtimeMetrics = document.querySelector('.sh-realtime-metrics');
|
||
if (realtimeMetrics) {
|
||
var newMetrics = this.renderRealtimeMetrics();
|
||
dom.content(realtimeMetrics, Array.prototype.slice.call(newMetrics.childNodes));
|
||
}
|
||
|
||
// Update stats overview
|
||
var statsOverview = document.querySelector('.sh-stats-overview-grid');
|
||
if (statsOverview) {
|
||
var newStats = this.renderStatsOverview();
|
||
dom.content(statsOverview, Array.prototype.slice.call(newStats.childNodes));
|
||
}
|
||
|
||
// Update system info grid
|
||
var systemInfoGrid = document.querySelector('.sh-system-info-grid');
|
||
if (systemInfoGrid) {
|
||
var newInfoGrid = this.renderSystemInfoGrid();
|
||
var gridElement = newInfoGrid.querySelector('.sh-system-info-grid');
|
||
if (gridElement) {
|
||
dom.content(systemInfoGrid, Array.prototype.slice.call(gridElement.childNodes));
|
||
}
|
||
}
|
||
|
||
// Update quick status indicators
|
||
var statusIndicators = document.querySelector('.sh-status-indicators-grid');
|
||
if (statusIndicators) {
|
||
var newIndicators = this.renderQuickStatusIndicators();
|
||
dom.content(statusIndicators, Array.prototype.slice.call(newIndicators.childNodes));
|
||
}
|
||
},
|
||
|
||
handleSaveApply: null,
|
||
handleSave: null,
|
||
handleReset: null
|
||
});
|