diff --git a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/overview.css b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/overview.css new file mode 100644 index 00000000..9caf1e6d --- /dev/null +++ b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/overview.css @@ -0,0 +1,366 @@ +/** + * System Hub - Overview Page Styles + * Modern dashboard with widgets and metrics + * Version: 0.2.0 + */ + +/* === Header === */ +.sh-overview-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 32px; + padding: 24px; + background: var(--sh-bg-card, #ffffff); + border-radius: 16px; + border: 1px solid var(--sh-border, #e2e8f0); +} + +.sh-overview-title h2 { + font-size: 32px; + font-weight: 700; + margin: 0 0 8px 0; + color: var(--sh-text-primary, #1e293b); + display: flex; + align-items: center; + gap: 12px; +} + +.sh-title-icon { + font-size: 36px; +} + +.sh-overview-subtitle { + margin: 0; + font-size: 16px; + color: var(--sh-text-secondary, #64748b); + font-weight: 500; +} + +/* === Health Score Circle === */ +.sh-score-circle { + width: 120px; + height: 120px; + border-radius: 50%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border: 6px solid; + transition: all 0.3s ease; +} + +.sh-score-excellent { + border-color: #22c55e; + background: linear-gradient(135deg, rgba(34, 197, 94, 0.1), rgba(34, 197, 94, 0.05)); +} + +.sh-score-good { + border-color: #3b82f6; + background: linear-gradient(135deg, rgba(59, 130, 246, 0.1), rgba(59, 130, 246, 0.05)); +} + +.sh-score-warning { + border-color: #f59e0b; + background: linear-gradient(135deg, rgba(245, 158, 11, 0.1), rgba(245, 158, 11, 0.05)); +} + +.sh-score-critical { + border-color: #ef4444; + background: linear-gradient(135deg, rgba(239, 68, 68, 0.1), rgba(239, 68, 68, 0.05)); +} + +.sh-score-value { + font-size: 42px; + font-weight: 700; + line-height: 1; + color: var(--sh-text-primary, #1e293b); +} + +.sh-score-label { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: var(--sh-text-secondary, #64748b); + margin-top: 4px; +} + +/* === Metrics Grid === */ +.sh-metrics-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 20px; + margin-bottom: 32px; +} + +.sh-metric-card { + background: var(--sh-bg-card, #ffffff); + border-radius: 16px; + padding: 24px; + border: 1px solid var(--sh-border, #e2e8f0); + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.sh-metric-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #6366f1, #8b5cf6); + opacity: 0; + transition: opacity 0.3s ease; +} + +.sh-metric-card:hover { + transform: translateY(-4px); + box-shadow: 0 12px 28px var(--sh-hover-shadow, rgba(0, 0, 0, 0.12)); +} + +.sh-metric-card:hover::before { + opacity: 1; +} + +.sh-metric-header { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 16px; +} + +.sh-metric-icon { + font-size: 32px; + line-height: 1; +} + +.sh-metric-title { + font-size: 16px; + font-weight: 600; + color: var(--sh-text-primary, #1e293b); +} + +.sh-metric-value { + font-size: 48px; + font-weight: 700; + line-height: 1; + margin-bottom: 16px; + color: var(--sh-text-primary, #1e293b); +} + +.sh-metric-progress { + height: 8px; + background: var(--sh-bg-tertiary, #f1f5f9); + border-radius: 4px; + overflow: hidden; + margin-bottom: 12px; +} + +.sh-metric-progress-bar { + height: 100%; + border-radius: 4px; + transition: width 0.5s ease; +} + +.sh-metric-details { + font-size: 14px; + color: var(--sh-text-secondary, #64748b); + font-weight: 500; +} + +/* === Info Grid === */ +.sh-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 20px; +} + +.sh-info-card { + background: var(--sh-bg-card, #ffffff); + border-radius: 16px; + border: 1px solid var(--sh-border, #e2e8f0); + overflow: hidden; + transition: all 0.3s ease; +} + +.sh-info-card:hover { + box-shadow: 0 4px 12px var(--sh-hover-shadow, rgba(0, 0, 0, 0.08)); +} + +.sh-info-card-header { + padding: 20px 24px; + background: var(--sh-bg-secondary, #f8fafc); + border-bottom: 1px solid var(--sh-border, #e2e8f0); +} + +.sh-info-card-header h3 { + margin: 0; + font-size: 18px; + font-weight: 700; + color: var(--sh-text-primary, #1e293b); +} + +.sh-info-card-body { + padding: 8px; +} + +.sh-info-list { + display: flex; + flex-direction: column; + gap: 0; +} + +.sh-info-row { + display: grid; + grid-template-columns: 40px 1fr auto; + align-items: center; + gap: 12px; + padding: 12px 16px; + transition: background 0.2s ease; + border-radius: 8px; +} + +.sh-info-row:hover { + background: var(--sh-hover-bg, #f8fafc); +} + +.sh-info-icon { + font-size: 20px; + text-align: center; +} + +.sh-info-label { + font-size: 14px; + font-weight: 600; + color: var(--sh-text-secondary, #64748b); +} + +.sh-info-value { + font-size: 14px; + font-weight: 600; + color: var(--sh-text-primary, #1e293b); + text-align: right; +} + +/* === Status Badges === */ +.sh-status-badge { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 13px; + font-weight: 600; + line-height: 1.4; +} + +.sh-status-ok { + background: rgba(34, 197, 94, 0.15); + color: #16a34a; +} + +.sh-status-error { + background: rgba(239, 68, 68, 0.15); + color: #dc2626; +} + +.sh-status-warning { + background: rgba(245, 158, 11, 0.15); + color: #d97706; +} + +/* === Link Button === */ +.sh-link-button { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 16px; + background: var(--sh-primary, #6366f1); + color: #ffffff; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + text-decoration: none; + transition: all 0.2s ease; +} + +.sh-link-button:hover { + background: #4f46e5; + transform: translateX(4px); + text-decoration: none; +} + +/* === Responsive === */ +@media (max-width: 768px) { + .sh-overview-header { + flex-direction: column; + gap: 20px; + text-align: center; + } + + .sh-overview-title h2 { + justify-content: center; + } + + .sh-metrics-grid { + grid-template-columns: 1fr; + } + + .sh-info-grid { + grid-template-columns: 1fr; + } + + .sh-metric-value { + font-size: 36px; + } +} + +/* === Dark Mode === */ +[data-theme="dark"] { + --sh-text-primary: #f1f5f9; + --sh-text-secondary: #cbd5e1; + --sh-bg-primary: #0f172a; + --sh-bg-secondary: #1e293b; + --sh-bg-tertiary: #334155; + --sh-bg-card: #1e293b; + --sh-border: #334155; + --sh-hover-bg: #334155; + --sh-hover-shadow: rgba(0, 0, 0, 0.4); +} + +[data-theme="dark"] .sh-metric-card, +[data-theme="dark"] .sh-info-card, +[data-theme="dark"] .sh-overview-header { + background: var(--sh-bg-card); + border-color: var(--sh-border); +} + +[data-theme="dark"] .sh-info-card-header { + background: var(--sh-bg-tertiary); + border-color: var(--sh-border); +} + +[data-theme="dark"] .sh-metric-progress { + background: var(--sh-bg-tertiary); +} + +[data-theme="dark"] .sh-info-row:hover { + background: var(--sh-hover-bg); +} + +[data-theme="dark"] .sh-score-excellent { + background: linear-gradient(135deg, rgba(34, 197, 94, 0.2), rgba(34, 197, 94, 0.1)); +} + +[data-theme="dark"] .sh-score-good { + background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(59, 130, 246, 0.1)); +} + +[data-theme="dark"] .sh-score-warning { + background: linear-gradient(135deg, rgba(245, 158, 11, 0.2), rgba(245, 158, 11, 0.1)); +} + +[data-theme="dark"] .sh-score-critical { + background: linear-gradient(135deg, rgba(239, 68, 68, 0.2), rgba(239, 68, 68, 0.1)); +} diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js index 428acd59..92b4eaee 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js @@ -1,233 +1,276 @@ 'use strict'; 'require view'; +'require ui'; +'require dom'; 'require poll'; 'require system-hub/api as API'; 'require system-hub/theme as Theme'; -// Load CSS -document.head.appendChild(E('link', { - 'rel': 'stylesheet', - 'type': 'text/css', - 'href': L.resource('system-hub/dashboard.css') -})); +return view.extend({ + healthData: null, + sysInfo: null, -// Initialize theme -Theme.init(); - -return L.view.extend({ load: function() { return Promise.all([ API.getSystemInfo(), API.getHealth(), - API.getStatus() + Theme.getTheme() ]); }, render: function(data) { - var sysInfo = data[0] || {}; - var health = data[1] || {}; - var status = data[2] || {}; + var self = this; + this.sysInfo = data[0] || {}; + this.healthData = data[1] || {}; + var theme = data[2]; - var v = E('div', { 'class': 'cbi-map' }, [ - E('h2', {}, _('System Hub - Overview')), - E('div', { 'class': 'cbi-map-descr' }, _('Central system control and monitoring')) + var container = E('div', { 'class': 'system-hub-dashboard' }, [ + 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(), + + // Health Metrics Cards + E('div', { 'class': 'sh-metrics-grid' }, [ + this.renderMetricCard('CPU', this.healthData.cpu), + this.renderMetricCard('Memory', this.healthData.memory), + this.renderMetricCard('Disk', this.healthData.disk), + this.renderMetricCard('Temperature', this.healthData.temperature) + ]), + + // System Info Grid + E('div', { 'class': 'sh-info-grid' }, [ + this.renderInfoCard('System Information', this.renderSystemInfo()), + this.renderInfoCard('Network Status', this.renderNetworkInfo()), + this.renderInfoCard('Services', this.renderServicesInfo()) + ]) ]); - // System Information Card - var infoSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('System Information')), - E('div', { 'class': 'table' }, [ - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('Hostname: ')), - E('span', {}, sysInfo.hostname || 'unknown') - ]), - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('Model: ')), - E('span', {}, sysInfo.model || 'Unknown') - ]) + // 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-overview-header' }, [ + E('div', { 'class': 'sh-overview-title' }, [ + E('h2', {}, [ + E('span', { 'class': 'sh-title-icon' }, 'đŸ–Ĩī¸'), + ' System Overview' ]), - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('OpenWrt: ')), - E('span', {}, sysInfo.openwrt_version || 'Unknown') - ]), - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('Kernel: ')), - E('span', {}, sysInfo.kernel || 'unknown') - ]) - ]), - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('Uptime: ')), - E('span', {}, sysInfo.uptime_formatted || '0d 0h 0m') - ]), - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('Local Time: ')), - E('span', {}, sysInfo.local_time || 'unknown') - ]) + E('p', { 'class': 'sh-overview-subtitle' }, + this.sysInfo.hostname + ' â€ĸ ' + this.sysInfo.model) + ]), + E('div', { 'class': 'sh-overview-score' }, [ + E('div', { 'class': 'sh-score-circle sh-score-' + scoreClass }, [ + E('div', { 'class': 'sh-score-value' }, score), + E('div', { 'class': 'sh-score-label' }, 'Health Score') ]) ]) ]); - v.appendChild(infoSection); - - // Health Metrics with Gauges - var healthSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('System Health')) - ]); - - var gaugesContainer = E('div', { 'style': 'display: flex; justify-content: space-around; flex-wrap: wrap; margin: 20px 0;' }); - - // CPU Load Gauge - var cpuLoad = parseFloat(health.cpu ? health.cpu.load_1m : '0'); - var cpuPercent = health.cpu ? health.cpu.usage : 0; - gaugesContainer.appendChild(this.createGauge('CPU Load', cpuPercent, cpuLoad.toFixed(2))); - - // Memory Gauge - var memPercent = health.memory ? health.memory.usage : 0; - var memUsed = health.memory ? (health.memory.used_kb / 1024).toFixed(0) : 0; - var memTotal = health.memory ? (health.memory.total_kb / 1024).toFixed(0) : 0; - gaugesContainer.appendChild(this.createGauge('Memory', memPercent, memUsed + ' / ' + memTotal + ' MB')); - - // Disk Gauge - var diskPercent = health.disk ? health.disk.usage : 0; - var diskUsed = health.disk ? (health.disk.used_kb / 1024).toFixed(0) : 0; - var diskTotal = health.disk ? (health.disk.total_kb / 1024).toFixed(0) : 0; - var diskInfo = diskUsed + ' / ' + diskTotal + ' MB'; - gaugesContainer.appendChild(this.createGauge('Disk Usage', diskPercent, diskInfo)); - - healthSection.appendChild(gaugesContainer); - v.appendChild(healthSection); - - // CPU Info - if (health.cpu) { - var cpuSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('CPU Information')), - E('div', { 'class': 'table' }, [ - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('Cores: ')), - E('span', {}, String(health.cpu.cores)) - ]), - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('Usage: ')), - E('span', {}, health.cpu.usage + '%') - ]) - ]), - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left' }, [ - E('strong', {}, _('Load Average: ')), - E('span', {}, (health.cpu.load_1m + ' / ' + health.cpu.load_5m + ' / ' + health.cpu.load_15m)) - ]) - ]) - ]) - ]); - v.appendChild(cpuSection); - } - - // Temperature - if (health.temperature && health.temperature.value > 0) { - var tempValue = health.temperature.value; - var tempColor = tempValue > 80 ? 'red' : (tempValue > 60 ? 'orange' : 'green'); - var tempSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Temperature')), - E('div', { 'class': 'table' }, [ - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left' }, [ - E('strong', {}, _('System Temperature: ')), - E('span', { 'style': 'color: ' + tempColor + '; font-weight: bold;' }, tempValue + '°C') - ]) - ]) - ]) - ]); - v.appendChild(tempSection); - } - - // Storage (Root Filesystem) - if (health.disk) { - var diskColor = health.disk.usage > 90 ? 'red' : (health.disk.usage > 75 ? 'orange' : 'green'); - var storageSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Storage (Root Filesystem)')), - E('div', { 'class': 'table' }, [ - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('Total: ')), - E('span', {}, (health.disk.total_kb / 1024).toFixed(0) + ' MB') - ]), - E('div', { 'class': 'td left', 'width': '50%' }, [ - E('strong', {}, _('Used: ')), - E('span', {}, (health.disk.used_kb / 1024).toFixed(0) + ' MB') - ]) - ]), - E('div', { 'class': 'tr' }, [ - E('div', { 'class': 'td left' }, [ - E('strong', {}, _('Usage: ')), - E('div', { 'style': 'display: inline-flex; align-items: center; width: 200px;' }, [ - E('div', { 'style': 'flex: 1; background: #eee; height: 10px; border-radius: 5px; margin-right: 10px;' }, [ - E('div', { - 'style': 'background: ' + diskColor + '; width: ' + health.disk.usage + '%; height: 100%; border-radius: 5px;' - }) - ]), - E('span', { 'style': 'font-weight: bold; color: ' + diskColor }, health.disk.usage + '%') - ]) - ]) - ]) - ]) - ]); - v.appendChild(storageSection); - } - - // Auto-refresh every 5 seconds - poll.add(L.bind(function() { - return Promise.all([ - API.getHealth(), - API.getStatus() - ]).then(L.bind(function(refreshData) { - // Update would go here in a production implementation - }, this)); - }, this), 5); - - return v; }, - createGauge: function(label, percent, detail) { - var color = percent > 90 ? '#dc3545' : (percent > 75 ? '#fd7e14' : '#28a745'); - var size = 120; - var strokeWidth = 10; - var radius = (size - strokeWidth) / 2; - var circumference = 2 * Math.PI * radius; - var offset = circumference - (percent / 100 * circumference); + renderMetricCard: function(type, data) { + if (!data) return E('div'); - return E('div', { 'style': 'text-align: center; margin: 10px;' }, [ - E('div', {}, [ - E('svg', { 'width': size, 'height': size, 'style': 'transform: rotate(-90deg);' }, [ - E('circle', { - 'cx': size/2, - 'cy': size/2, - 'r': radius, - 'fill': 'none', - 'stroke': '#eee', - 'stroke-width': strokeWidth - }), - E('circle', { - 'cx': size/2, - 'cy': size/2, - 'r': radius, - 'fill': 'none', - 'stroke': color, - 'stroke-width': strokeWidth, - 'stroke-dasharray': circumference, - 'stroke-dashoffset': offset, - 'stroke-linecap': 'round' - }) - ]) + var config = this.getMetricConfig(type, data); + + return E('div', { 'class': 'sh-metric-card sh-metric-' + config.status }, [ + E('div', { 'class': 'sh-metric-header' }, [ + E('span', { 'class': 'sh-metric-icon' }, config.icon), + E('span', { 'class': 'sh-metric-title' }, config.title) ]), - E('div', { 'style': 'margin-top: -' + (size/2 + 10) + 'px; font-size: 20px; font-weight: bold; color: ' + color + ';' }, Math.round(percent) + '%'), - E('div', { 'style': 'margin-top: ' + (size/2 - 10) + 'px; font-weight: bold;' }, label), - E('div', { 'style': 'font-size: 12px; color: #666;' }, detail) + E('div', { 'class': 'sh-metric-value' }, config.value), + E('div', { 'class': 'sh-metric-progress' }, [ + E('div', { + 'class': 'sh-metric-progress-bar', + 'style': 'width: ' + config.percentage + '%; background: ' + config.color + }) + ]), + E('div', { 'class': 'sh-metric-details' }, config.details) ]); }, + 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() { + var metricsGrid = document.querySelector('.sh-metrics-grid'); + if (metricsGrid) { + dom.content(metricsGrid, [ + this.renderMetricCard('CPU', this.healthData.cpu), + this.renderMetricCard('Memory', this.healthData.memory), + this.renderMetricCard('Disk', this.healthData.disk), + this.renderMetricCard('Temperature', this.healthData.temperature) + ]); + } + + var infoGrid = document.querySelector('.sh-info-grid'); + if (infoGrid) { + dom.content(infoGrid, [ + this.renderInfoCard('System Information', this.renderSystemInfo()), + this.renderInfoCard('Network Status', this.renderNetworkInfo()), + this.renderInfoCard('Services', this.renderServicesInfo()) + ]); + } + + // Update health score + var scoreValue = document.querySelector('.sh-score-value'); + var scoreCircle = document.querySelector('.sh-score-circle'); + if (scoreValue && scoreCircle) { + var score = this.healthData.score || 0; + var scoreClass = score >= 80 ? 'excellent' : (score >= 60 ? 'good' : (score >= 40 ? 'warning' : 'critical')); + scoreValue.textContent = score; + scoreCircle.className = 'sh-score-circle sh-score-' + scoreClass; + } + }, + handleSaveApply: null, handleSave: null, handleReset: null