Add unified SecuBox header navigation to all 10 System Hub views for consistent portal integration when accessed from SecuBox Portal: - overview.js, health.js, services.js, diagnostics.js - logs.js, backup.js, components.js, settings.js - dev-status.js, remote.js Pattern: Wrap view content with secubox-page-wrapper and prepend SbHeader.render() to hide LuCI sidebar when in portal context. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
191 lines
6.4 KiB
JavaScript
191 lines
6.4 KiB
JavaScript
'use strict';
|
||
'require view';
|
||
'require dom';
|
||
'require ui';
|
||
'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';
|
||
'require secubox-portal/header as SbHeader';
|
||
|
||
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({
|
||
healthData: null,
|
||
|
||
load: function() {
|
||
return API.getHealth();
|
||
},
|
||
|
||
render: function(data) {
|
||
this.healthData = data || {};
|
||
|
||
var container = E('div', { 'class': 'system-hub-dashboard sh-health-view' }, [
|
||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||
ThemeAssets.stylesheet('common.css'),
|
||
ThemeAssets.stylesheet('dashboard.css'),
|
||
ThemeAssets.stylesheet('health.css'),
|
||
HubNav.renderTabs('health'),
|
||
this.renderHero(),
|
||
this.renderMetricGrid(),
|
||
this.renderSummaryPanels(),
|
||
this.renderRecommendations(),
|
||
this.renderActions()
|
||
]);
|
||
|
||
var self = this;
|
||
poll.add(function() {
|
||
return API.getHealth().then(function(fresh) {
|
||
self.healthData = fresh || {};
|
||
self.updateWidgets();
|
||
});
|
||
}, 30);
|
||
|
||
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||
wrapper.appendChild(SbHeader.render());
|
||
wrapper.appendChild(container);
|
||
return wrapper;
|
||
},
|
||
|
||
renderHero: function() {
|
||
var score = this.healthData.score || 0;
|
||
var state = score >= 80 ? 'healthy' : score >= 60 ? 'good' : score >= 40 ? 'warning' : 'critical';
|
||
|
||
return E('section', { 'class': 'sh-health-hero' }, [
|
||
E('div', {}, [
|
||
E('span', { 'class': 'sh-hero-eyebrow' }, _('System Health Monitor')),
|
||
E('h1', {}, _('Real-time health score') + ': ' + score + '/100'),
|
||
E('p', {}, _('CPU, RAM, storage, temperature, network, and services health summarized.'))
|
||
]),
|
||
E('div', { 'class': 'sh-health-score sh-' + state }, [
|
||
E('div', { 'id': 'sh-health-score' }, score),
|
||
E('span', {}, state.toUpperCase())
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderMetricGrid: function() {
|
||
var cpu = this.healthData.cpu || {};
|
||
var mem = this.healthData.memory || {};
|
||
var disk = this.healthData.disk || {};
|
||
var temp = this.healthData.temperature || {};
|
||
|
||
return E('section', { 'class': 'sh-health-grid', 'id': 'sh-health-grid' }, [
|
||
this.renderMetricCard('cpu', '🔥', _('CPU Usage'), (cpu.usage || 0) + '%', cpu),
|
||
this.renderMetricCard('mem', '💾', _('Memory'), (mem.usage || 0) + '%', mem),
|
||
this.renderMetricCard('disk', '💿', _('Storage'), (disk.usage || 0) + '%', disk),
|
||
this.renderMetricCard('temp', '🌡️', _('Temperature'), (temp.value || 0) + '°C', temp)
|
||
]);
|
||
},
|
||
|
||
renderMetricCard: function(id, icon, label, value, data) {
|
||
var percent = data.usage || data.value || 0;
|
||
var status = data.status || 'ok';
|
||
return E('div', { 'class': 'sh-health-card sh-' + status, 'data-id': id }, [
|
||
E('div', { 'class': 'sh-card-header' }, [
|
||
E('span', { 'class': 'sh-card-title-icon' }, icon),
|
||
E('div', { 'class': 'sh-card-title' }, label)
|
||
]),
|
||
E('div', { 'class': 'sh-health-value', 'id': 'sh-health-value-' + id }, value),
|
||
E('div', { 'class': 'sh-health-bar' }, [
|
||
E('div', {
|
||
'class': 'sh-health-bar-fill',
|
||
'id': 'sh-health-bar-' + id,
|
||
'style': 'width:' + Math.min(100, percent) + '%'
|
||
})
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderSummaryPanels: function() {
|
||
return E('section', { 'class': 'sh-summary-grid' }, [
|
||
this.renderNetworkPanel(),
|
||
this.renderServicesPanel()
|
||
]);
|
||
},
|
||
|
||
renderNetworkPanel: function() {
|
||
var net = this.healthData.network || {};
|
||
var status = net.wan_up ? _('Connected') : _('Disconnected');
|
||
return E('div', { 'class': 'sh-summary-card' }, [
|
||
E('h3', {}, _('Network Summary')),
|
||
E('ul', {}, [
|
||
E('li', {}, _('WAN: ') + status),
|
||
E('li', {}, _('RX: ') + API.formatBytes(net.rx_bytes || 0)),
|
||
E('li', {}, _('TX: ') + API.formatBytes(net.tx_bytes || 0))
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderServicesPanel: function() {
|
||
var svc = this.healthData.services || {};
|
||
return E('div', { 'class': 'sh-summary-card' }, [
|
||
E('h3', {}, _('Services Health')),
|
||
E('ul', {}, [
|
||
E('li', {}, _('Running: ') + (svc.running || 0)),
|
||
E('li', {}, _('Failed: ') + (svc.failed || 0))
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderRecommendations: function() {
|
||
var recos = this.healthData.recommendations || [];
|
||
if (!recos.length) return E('section', { 'class': 'sh-card' }, []);
|
||
return E('section', { 'class': 'sh-card' }, [
|
||
E('div', { 'class': 'sh-card-header' }, [
|
||
E('div', { 'class': 'sh-card-title' }, [E('span', { 'class': 'sh-card-title-icon' }, '📋'), _('Recommendations')])
|
||
]),
|
||
E('ul', { 'class': 'sh-reco-list', 'id': 'sh-reco-list' },
|
||
recos.map(function(text) {
|
||
return E('li', {}, text);
|
||
})
|
||
)
|
||
]);
|
||
},
|
||
|
||
renderActions: function() {
|
||
return E('section', { 'class': 'sh-card' }, [
|
||
E('div', { 'class': 'sh-card-header' }, [
|
||
E('div', { 'class': 'sh-card-title' }, [E('span', { 'class': 'sh-card-title-icon' }, '🔧'), _('Maintenance Actions')])
|
||
]),
|
||
E('div', { 'class': 'sh-card-body sh-btn-group' }, [
|
||
E('button', {
|
||
'class': 'sh-btn sh-btn-primary',
|
||
'click': ui.createHandlerFn(this, 'runHealthCheck')
|
||
}, _('Run full health check')),
|
||
E('button', {
|
||
'class': 'sh-btn',
|
||
'click': function() { window.location.hash = '#admin/secubox/network/modes/overview'; }
|
||
}, _('Open Network Modes'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
updateWidgets: function() {
|
||
var scoreNode = document.getElementById('sh-health-score');
|
||
if (scoreNode) scoreNode.textContent = this.healthData.score || 0;
|
||
['cpu','mem','disk','temp'].forEach(function(id) {
|
||
var bar = document.getElementById('sh-health-bar-' + id);
|
||
var data = this.healthData[id === 'cpu' ? 'cpu' : id === 'mem' ? 'memory' : id === 'disk' ? 'disk' : 'temperature'] || {};
|
||
var percent = data.usage || data.value || 0;
|
||
if (bar) bar.style.width = Math.min(100, percent) + '%';
|
||
var valNode = document.getElementById('sh-health-value-' + id);
|
||
if (valNode) valNode.textContent = (id === 'temp') ? (data.value || 0) + '°C' : (percent || 0) + '%';
|
||
}, this);
|
||
var list = document.getElementById('sh-reco-list');
|
||
if (list) {
|
||
dom.content(list, (this.healthData.recommendations || []).map(function(text) {
|
||
return E('li', {}, text);
|
||
}));
|
||
}
|
||
},
|
||
|
||
runHealthCheck: function() {
|
||
ui.addNotification(null, E('p', {}, _('Full health check started (see alerts).')), 'info');
|
||
}
|
||
});
|