System Hub enhancements: - Add cron-based scheduled backup configuration (daily/weekly/monthly) - Add backup schedule RPCD methods (get_backup_schedule, set_backup_schedule) - Add live streaming logs with LIVE badge, play/pause, 2s refresh - Add real component installation detection from secubox state field - Add service running status detection for components - Add category-based icons for components (security, network, monitoring) - Fix status emoji display (✅ ⚠️ ❓) for Quick Status Indicators UI improvements: - New Scheduled Backups card in backup page with enable/disable toggle - Time picker for backup schedule (hour/minute selectors) - Day of week/month selectors for weekly/monthly backups - Live indicator badge with pulse animation for logs - Play/Pause button for log streaming control - New log highlighting with fade-in animation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
317 lines
11 KiB
JavaScript
317 lines
11 KiB
JavaScript
'use strict';
|
||
'require view';
|
||
'require ui';
|
||
'require dom';
|
||
'require poll';
|
||
'require system-hub/api as API';
|
||
'require secubox-theme/theme as Theme';
|
||
'require system-hub/theme-assets as ThemeAssets';
|
||
'require system-hub/nav as HubNav';
|
||
|
||
var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||
Theme.init({ language: shLang });
|
||
|
||
return view.extend({
|
||
sysInfo: null,
|
||
healthData: null,
|
||
|
||
load: function() {
|
||
return Promise.all([
|
||
API.getSystemInfo(),
|
||
API.getHealth()
|
||
]);
|
||
},
|
||
|
||
render: function(data) {
|
||
this.sysInfo = data[0] || {};
|
||
this.healthData = data[1] || {};
|
||
|
||
var container = E('div', { 'class': 'sh-overview' }, [
|
||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||
ThemeAssets.stylesheet('common.css'),
|
||
ThemeAssets.stylesheet('dashboard.css'),
|
||
ThemeAssets.stylesheet('overview.css'),
|
||
HubNav.renderTabs('overview'),
|
||
this.renderPageHeader(),
|
||
this.renderInfoGrid(),
|
||
this.renderResourceMonitors(),
|
||
this.renderQuickStatus()
|
||
]);
|
||
|
||
var self = this;
|
||
poll.add(function() {
|
||
return Promise.all([
|
||
API.getSystemInfo(),
|
||
API.getHealth()
|
||
]).then(function(refresh) {
|
||
self.sysInfo = refresh[0] || {};
|
||
self.healthData = refresh[1] || {};
|
||
self.updateDynamicSections();
|
||
});
|
||
}, 30);
|
||
|
||
return container;
|
||
},
|
||
|
||
renderPageHeader: function() {
|
||
var uptime = this.sysInfo.uptime_formatted || '0d 0h 0m';
|
||
var hostname = this.sysInfo.hostname || 'OpenWrt';
|
||
var kernel = this.sysInfo.kernel || '-';
|
||
var score = (this.healthData.score || 0);
|
||
var version = this.sysInfo.version || _('Unknown');
|
||
|
||
var stats = [
|
||
{ label: _('Version'), value: version, icon: '🏷️' },
|
||
{ label: _('Uptime'), value: uptime, icon: '⏱' },
|
||
{ label: _('Hostname'), value: hostname, icon: '🖥' },
|
||
{ label: _('Kernel'), value: kernel, copy: kernel, icon: '🧬' },
|
||
{ label: _('Health'), value: score + '/100', icon: '❤️' }
|
||
];
|
||
|
||
return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [
|
||
E('div', {}, [
|
||
E('h2', { 'class': 'sh-page-title' }, [
|
||
E('span', { 'class': 'sh-page-title-icon' }, '⚙️'),
|
||
_('System Control Center')
|
||
]),
|
||
E('p', { 'class': 'sh-page-subtitle' }, _('Unified telemetry & orchestration'))
|
||
]),
|
||
E('div', { 'class': 'sh-header-meta' }, stats.map(this.renderHeaderChip, this))
|
||
]);
|
||
},
|
||
|
||
renderHeaderChip: function(stat) {
|
||
var chip = E('div', { 'class': 'sh-header-chip' }, [
|
||
E('span', { 'class': 'sh-chip-icon' }, stat.icon || '•'),
|
||
E('div', { 'class': 'sh-chip-text' }, [
|
||
E('span', { 'class': 'sh-chip-label' }, stat.label),
|
||
E('strong', {}, stat.value || '-')
|
||
])
|
||
]);
|
||
|
||
if (stat.copy && navigator.clipboard) {
|
||
chip.style.cursor = 'pointer';
|
||
chip.addEventListener('click', function() {
|
||
navigator.clipboard.writeText(stat.copy).then(function() {
|
||
ui.addNotification(null, E('p', {}, _('Copied to clipboard')), 'info');
|
||
});
|
||
});
|
||
}
|
||
|
||
return chip;
|
||
},
|
||
|
||
renderInfoGrid: function() {
|
||
var cards = [
|
||
{ id: 'hostname', label: _('Hostname'), value: this.sysInfo.hostname || 'OpenWrt', action: _('Edit'), handler: this.openSystemSettings },
|
||
{ id: 'uptime', label: _('Uptime'), value: this.sysInfo.uptime_formatted || '0d 0h 0m' },
|
||
{ id: 'load', label: _('Load Avg (1/5/15)'), value: (this.sysInfo.load || []).join(' · ') || '0.00 · 0.00 · 0.00', monospace: true },
|
||
{ id: 'kernel', label: _('Kernel Version'), value: this.sysInfo.kernel || '-', action: _('Copy'), handler: this.copyKernel.bind(this) }
|
||
];
|
||
|
||
return E('section', { 'class': 'sh-info-grid', 'id': 'sh-info-grid' },
|
||
cards.map(function(card) {
|
||
return E('div', { 'class': 'sh-info-card' }, [
|
||
E('div', { 'class': 'sh-info-label' }, card.label),
|
||
E('div', {
|
||
'class': 'sh-info-value' + (card.monospace ? ' mono' : ''),
|
||
'id': 'sh-info-' + card.id
|
||
}, card.value),
|
||
card.action ? E('button', {
|
||
'class': 'sh-info-action',
|
||
'type': 'button',
|
||
'click': card.handler
|
||
}, card.action) : ''
|
||
]);
|
||
}, this)
|
||
);
|
||
},
|
||
|
||
renderResourceMonitors: function() {
|
||
var monitors = [
|
||
this.createMonitor('cpu', _('CPU Usage'), this.healthData.cpu || {}, '🔥'),
|
||
this.createMonitor('memory', _('Memory'), this.healthData.memory || {}, '💾'),
|
||
this.createMonitor('storage', _('Storage'), this.healthData.disk || {}, '💿'),
|
||
this.createMonitor('network', _('Network'), this.healthData.network || {}, '🌐')
|
||
];
|
||
|
||
return E('section', { 'class': 'sh-monitor-panel' }, [
|
||
E('div', { 'class': 'sh-section-header' }, [
|
||
E('h2', {}, _('Resource Monitors')),
|
||
E('p', {}, _('Live usage for CPU, RAM, Storage, Network'))
|
||
]),
|
||
E('div', { 'class': 'sh-monitor-grid', 'id': 'sh-monitor-grid' }, monitors)
|
||
]);
|
||
},
|
||
|
||
createMonitor: function(id, label, data, icon) {
|
||
var percent = Math.round(data.percent || data.usage || 0);
|
||
var detail = '';
|
||
if (id === 'memory' || id === 'storage') {
|
||
var used = API.formatBytes((data.used_kb || 0) * 1024);
|
||
var total = API.formatBytes((data.total_kb || 0) * 1024);
|
||
detail = used + ' / ' + total;
|
||
} else if (id === 'network') {
|
||
detail = data.wan_up ? _('Online') : _('Offline');
|
||
percent = data.wan_up ? 100 : 0;
|
||
} else {
|
||
detail = _('Load: ') + (data.load_1m || data.load || '0.00');
|
||
}
|
||
|
||
return E('div', { 'class': 'sh-monitor-card' }, [
|
||
E('div', { 'class': 'sh-monitor-icon' }, icon),
|
||
E('div', { 'class': 'sh-monitor-info' }, [
|
||
E('div', { 'class': 'sh-monitor-label' }, label),
|
||
E('div', { 'class': 'sh-monitor-detail', 'id': 'sh-monitor-detail-' + id }, detail)
|
||
]),
|
||
E('div', { 'class': 'sh-monitor-progress' }, [
|
||
E('div', {
|
||
'class': 'sh-monitor-bar',
|
||
'id': 'sh-monitor-bar-' + id,
|
||
'style': 'width:' + percent + '%'
|
||
})
|
||
]),
|
||
E('div', { 'class': 'sh-monitor-percent', 'id': 'sh-monitor-percent-' + id }, percent + '%')
|
||
]);
|
||
},
|
||
|
||
renderQuickStatus: function() {
|
||
var status = this.sysInfo.status || {};
|
||
|
||
var indicators = [
|
||
{ id: 'internet', label: _('Internet Connectivity'), state: status.internet, icon: '🌍' },
|
||
{ id: 'dns', label: _('DNS Resolution'), state: status.dns, icon: '🧭' },
|
||
{ id: 'ntp', label: _('NTP Sync'), state: status.ntp, icon: '⏱' },
|
||
{ id: 'firewall', label: _('Firewall Rules'), state: status.firewall, icon: '🛡', extra: status.firewall_rules ? status.firewall_rules + _(' rules') : '' }
|
||
];
|
||
|
||
return E('section', { 'class': 'sh-status-panel' }, [
|
||
E('div', { 'class': 'sh-section-header' }, [
|
||
E('h2', {}, _('Quick Status Indicators')),
|
||
E('p', {}, _('Checks for connectivity, DNS, NTP, firewall'))
|
||
]),
|
||
E('div', { 'class': 'sh-status-grid', 'id': 'sh-status-grid' },
|
||
indicators.map(function(item) {
|
||
return E('div', {
|
||
'class': 'sh-status-card ' + this.getStatusClass(item.state),
|
||
'id': 'sh-status-' + item.id
|
||
}, [
|
||
E('div', { 'class': 'sh-status-icon' }, item.icon),
|
||
E('div', { 'class': 'sh-status-body' }, [
|
||
E('strong', {}, item.label),
|
||
E('span', { 'class': 'sh-status-value', 'id': 'sh-status-label-' + item.id },
|
||
this.getStatusLabel(item.state)),
|
||
item.extra ? E('span', { 'class': 'sh-status-extra', 'id': 'sh-status-extra-' + item.id }, item.extra) : ''
|
||
])
|
||
]);
|
||
}, this))
|
||
]);
|
||
},
|
||
|
||
updateDynamicSections: function() {
|
||
this.updateInfoGrid();
|
||
this.updateMonitors();
|
||
this.updateStatuses();
|
||
},
|
||
|
||
updateInfoGrid: function() {
|
||
var mappings = {
|
||
hostname: this.sysInfo.hostname || 'OpenWrt',
|
||
uptime: this.sysInfo.uptime_formatted || '0d 0h 0m',
|
||
load: (this.sysInfo.load || []).join(' · ') || '0.00 · 0.00 · 0.00',
|
||
kernel: this.sysInfo.kernel || '-'
|
||
};
|
||
|
||
Object.keys(mappings).forEach(function(key) {
|
||
var node = document.getElementById('sh-info-' + key);
|
||
if (node) node.textContent = mappings[key];
|
||
});
|
||
|
||
var score = this.healthData.score || 0;
|
||
var scoreNode = document.getElementById('sh-score-value');
|
||
var labelNode = document.getElementById('sh-score-label');
|
||
if (scoreNode) scoreNode.textContent = score;
|
||
if (labelNode) labelNode.textContent = this.getScoreLabel(score);
|
||
},
|
||
|
||
updateMonitors: function() {
|
||
var health = this.healthData || {};
|
||
var map = {
|
||
cpu: { percent: Math.round((health.cpu && (health.cpu.percent || health.cpu.usage)) || 0), detail: _('Load: ') + ((health.cpu && (health.cpu.load_1m || health.cpu.load)) || '0.00') },
|
||
memory: {
|
||
percent: Math.round((health.memory && (health.memory.percent || health.memory.usage)) || 0),
|
||
detail: API.formatBytes(((health.memory && health.memory.used_kb) || 0) * 1024) + ' / ' +
|
||
API.formatBytes(((health.memory && health.memory.total_kb) || 0) * 1024)
|
||
},
|
||
storage: {
|
||
percent: Math.round((health.disk && (health.disk.percent || health.disk.usage)) || 0),
|
||
detail: API.formatBytes(((health.disk && health.disk.used_kb) || 0) * 1024) + ' / ' +
|
||
API.formatBytes(((health.disk && health.disk.total_kb) || 0) * 1024)
|
||
},
|
||
network: {
|
||
percent: (health.network && health.network.wan_up) ? 100 : 0,
|
||
detail: (health.network && health.network.wan_up) ? _('Online') : _('Offline')
|
||
}
|
||
};
|
||
|
||
Object.keys(map).forEach(function(key) {
|
||
var percent = Math.min(100, Math.max(0, map[key].percent));
|
||
var bar = document.getElementById('sh-monitor-bar-' + key);
|
||
var val = document.getElementById('sh-monitor-percent-' + key);
|
||
var detail = document.getElementById('sh-monitor-detail-' + key);
|
||
if (bar) bar.style.width = percent + '%';
|
||
if (val) val.textContent = percent + '%';
|
||
if (detail) detail.textContent = map[key].detail;
|
||
});
|
||
},
|
||
|
||
updateStatuses: function() {
|
||
var status = this.sysInfo.status || {};
|
||
var ids = ['internet', 'dns', 'ntp', 'firewall'];
|
||
ids.forEach(function(id) {
|
||
var node = document.getElementById('sh-status-' + id);
|
||
var label = document.getElementById('sh-status-label-' + id);
|
||
var extra = document.getElementById('sh-status-extra-' + id);
|
||
var state = status[id];
|
||
if (node) {
|
||
node.className = 'sh-status-card ' + this.getStatusClass(state);
|
||
}
|
||
if (label) label.textContent = this.getStatusLabel(state);
|
||
if (extra && id === 'firewall') {
|
||
var rules = status.firewall_rules ? status.firewall_rules + _(' rules') : '';
|
||
extra.textContent = rules;
|
||
}
|
||
}, this);
|
||
},
|
||
|
||
getStatusClass: function(state) {
|
||
if (state === undefined || state === null) return 'unknown';
|
||
return state ? 'ok' : 'warn';
|
||
},
|
||
|
||
getStatusLabel: function(state) {
|
||
if (state === undefined || state === null) return '❓';
|
||
return state ? '✅' : '⚠️';
|
||
},
|
||
|
||
getScoreLabel: function(score) {
|
||
if (score >= 80) return _('Excellent');
|
||
if (score >= 60) return _('Good');
|
||
if (score >= 40) return _('Warning');
|
||
return _('Critical');
|
||
},
|
||
|
||
openSystemSettings: function() {
|
||
window.location.href = L.url('admin/secubox/system/system-hub/settings');
|
||
},
|
||
|
||
copyKernel: function() {
|
||
if (navigator.clipboard && this.sysInfo.kernel) {
|
||
navigator.clipboard.writeText(this.sysInfo.kernel);
|
||
ui.addNotification(null, E('p', {}, _('Kernel version copied')), 'info');
|
||
}
|
||
}
|
||
});
|