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:
parent
ebedc5f9d8
commit
4e2763190d
@ -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));
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user