feat: system-hub overview - Modern dashboard with widgets

Complete redesign of Overview page with modern dashboard layout:

Features:
• Health Score circle with visual status (excellent/good/warning/critical)
• Metric cards with progress bars (CPU, Memory, Disk, Temperature)
• Color-coded status indicators
• System Information card with icons
• Network Status card with connection state
• Services card with quick actions link
• Auto-refresh every 30 seconds
• Responsive grid layout
• Full dark mode support

Design:
• Gradient score circle (120px)
• Modern metric cards with hover effects
• Info cards with organized data rows
• Status badges (ok/warning/error)
• Smooth transitions and animations
• Cohesive with SecuBox design language

Replaces old cbi-based layout with modern component-based design.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
CyberMind-FR 2025-12-26 17:49:28 +01:00
parent ebedc5f9d8
commit 4e2763190d
2 changed files with 610 additions and 201 deletions

View File

@ -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));
}

View File

@ -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