359 lines
11 KiB
JavaScript
359 lines
11 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'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({
|
|
settings: null,
|
|
fieldRefs: null,
|
|
|
|
load: function() {
|
|
return API.getSettings();
|
|
},
|
|
|
|
render: function(data) {
|
|
this.settings = data || {};
|
|
this.fieldRefs = {};
|
|
|
|
var container = E('div', { 'class': 'system-hub-dashboard sh-settings-view' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
ThemeAssets.stylesheet('common.css'),
|
|
ThemeAssets.stylesheet('dashboard.css'),
|
|
HubNav.renderTabs('settings'),
|
|
this.renderHeader(),
|
|
this.renderGeneralSection(),
|
|
this.renderThresholdSection(),
|
|
this.renderSupportSection(),
|
|
this.renderActions()
|
|
]);
|
|
|
|
return container;
|
|
},
|
|
|
|
renderHeader: function() {
|
|
var general = this.settings.general || {};
|
|
var autoRefresh = this.boolValue(general.auto_refresh, true);
|
|
var healthCheck = this.boolValue(general.health_check, true);
|
|
|
|
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 Hub Preferences')
|
|
]),
|
|
E('p', { 'class': 'sh-page-subtitle' },
|
|
_('Control health checks, refresh cadence, and alert thresholds for every System Hub widget.'))
|
|
]),
|
|
E('div', { 'class': 'sh-header-meta' }, [
|
|
this.renderChip('⏱️', _('Auto refresh'), autoRefresh ? _('Enabled') : _('Disabled')),
|
|
this.renderChip('🩺', _('Health monitor'), healthCheck ? _('Active') : _('Paused')),
|
|
this.renderChip('🧪', _('Diagnostics'), _('Manual triggers'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderChip: function(icon, label, value) {
|
|
return E('div', { 'class': 'sh-header-chip' }, [
|
|
E('span', { 'class': 'sh-chip-icon' }, icon),
|
|
E('div', { 'class': 'sh-chip-text' }, [
|
|
E('span', { 'class': 'sh-chip-label' }, label),
|
|
E('strong', {}, value || '—')
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderGeneralSection: function() {
|
|
var general = this.settings.general || {};
|
|
var refresh = (general.refresh_interval != null) ? String(general.refresh_interval) : '30';
|
|
|
|
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' }, '🛠️'),
|
|
_('Automation & Refresh')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'sh-card-body' }, [
|
|
E('div', { 'class': 'sh-settings-grid' }, [
|
|
this.renderToggle('auto_refresh', _('Auto refresh'), _('Poll services & metrics every few seconds'), this.boolValue(general.auto_refresh, true), '♻️'),
|
|
this.renderToggle('health_check', _('Health monitor'), _('Run background probes to populate the Health tab'), this.boolValue(general.health_check, true), '🩺'),
|
|
this.renderToggle('debug_mode', _('Debug mode'), _('Surface extra logs and RPC payloads (development only)'), this.boolValue(general.debug_mode, false), '🐛')
|
|
]),
|
|
E('div', { 'class': 'sh-settings-grid sh-settings-grid--compact', 'style': 'margin-top: 20px;' }, [
|
|
this.renderSelect('refresh_interval', _('Refresh cadence'), [
|
|
{ value: '15', label: _('Every 15 seconds') },
|
|
{ value: '30', label: _('Every 30 seconds') },
|
|
{ value: '60', label: _('Every minute') },
|
|
{ value: '120', label: _('Every 2 minutes') },
|
|
{ value: '0', label: _('Manual refresh only') }
|
|
], refresh),
|
|
this.renderNumber('log_retention', _('Log retention (days)'), general.log_retention || 30, 1, 365)
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderThresholdSection: function() {
|
|
var th = this.settings.thresholds || {};
|
|
|
|
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' }, '🚨'),
|
|
_('Alert thresholds')
|
|
]),
|
|
E('div', { 'class': 'sh-card-subtitle' }, _('Define warning/critical limits used by Health dashboards.'))
|
|
]),
|
|
E('div', { 'class': 'sh-card-body' }, [
|
|
E('div', { 'class': 'sh-threshold-grid' }, [
|
|
this.renderThresholdRow('cpu', _('CPU usage (%)'), th.cpu_warning || 80, th.cpu_critical || 95),
|
|
this.renderThresholdRow('mem', _('Memory usage (%)'), th.mem_warning || 80, th.mem_critical || 95),
|
|
this.renderThresholdRow('disk', _('Disk usage (%)'), th.disk_warning || 80, th.disk_critical || 95),
|
|
this.renderThresholdRow('temp', _('Temperature (°C)'), th.temp_warning || 70, th.temp_critical || 85)
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderSupportSection: function() {
|
|
var support = this.settings.support || {};
|
|
var upload = this.settings.upload || {};
|
|
|
|
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' }, '🤝'),
|
|
_('Support & export')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'sh-card-body' }, [
|
|
E('div', { 'class': 'sh-support-grid' }, [
|
|
E('div', { 'class': 'sh-support-card' }, [
|
|
E('div', { 'class': 'sh-support-label' }, _('Provider')),
|
|
E('strong', {}, support.provider || _('Unknown')),
|
|
E('div', { 'class': 'sh-support-desc' }, support.email || '')
|
|
]),
|
|
E('div', { 'class': 'sh-support-card' }, [
|
|
E('div', { 'class': 'sh-support-label' }, _('Documentation')),
|
|
E('a', { 'href': support.docs || '#', 'target': '_blank', 'rel': 'noreferrer' }, support.docs || _('Unavailable'))
|
|
]),
|
|
E('div', { 'class': 'sh-support-card' }, [
|
|
E('div', { 'class': 'sh-support-label' }, _('Auto upload')),
|
|
E('strong', {}, this.boolValue(upload.auto_upload, false) ? _('Enabled') : _('Disabled')),
|
|
E('div', { 'class': 'sh-support-desc' }, upload.url || _('No endpoint configured'))
|
|
])
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
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' }, '💾'),
|
|
_('Apply changes')
|
|
])
|
|
]),
|
|
E('div', { 'class': 'sh-card-body sh-btn-group' }, [
|
|
E('button', {
|
|
'class': 'sh-btn sh-btn-primary',
|
|
'click': L.bind(this.saveSettings, this)
|
|
}, [ '✅ ', _('Save preferences') ]),
|
|
E('button', {
|
|
'class': 'sh-btn sh-btn-secondary',
|
|
'click': L.bind(this.resetView, this)
|
|
}, [ '↺ ', _('Reset') ])
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderToggle: function(key, label, desc, active, icon) {
|
|
var self = this;
|
|
var switchEl = E('div', {
|
|
'class': 'sh-toggle-switch' + (active ? ' active' : ''),
|
|
'data-key': key,
|
|
'click': function(ev) {
|
|
ev.target.classList.toggle('active');
|
|
}
|
|
});
|
|
this.fieldRefs[key] = { type: 'toggle', node: switchEl };
|
|
|
|
return E('div', { 'class': 'sh-toggle' }, [
|
|
E('div', { 'class': 'sh-toggle-info' }, [
|
|
E('span', { 'class': 'sh-toggle-icon' }, icon || '•'),
|
|
E('div', {}, [
|
|
E('div', { 'class': 'sh-toggle-label' }, label),
|
|
E('div', { 'class': 'sh-toggle-desc' }, desc)
|
|
])
|
|
]),
|
|
switchEl
|
|
]);
|
|
},
|
|
|
|
renderSelect: function(key, label, options, current) {
|
|
var select = E('select', {
|
|
'class': 'sh-input',
|
|
'change': function(ev) {
|
|
ev.target.setAttribute('data-value', ev.target.value);
|
|
}
|
|
}, options.map(function(opt) {
|
|
return E('option', {
|
|
'value': opt.value,
|
|
'selected': opt.value === current
|
|
}, opt.label);
|
|
}));
|
|
select.setAttribute('data-value', current);
|
|
this.fieldRefs[key] = { type: 'select', node: select };
|
|
|
|
return E('div', { 'class': 'sh-input-group' }, [
|
|
E('label', { 'class': 'sh-input-label' }, label),
|
|
select
|
|
]);
|
|
},
|
|
|
|
renderNumber: function(key, label, value, min, max) {
|
|
var input = E('input', {
|
|
'type': 'number',
|
|
'class': 'sh-input',
|
|
'value': value,
|
|
'min': min,
|
|
'max': max
|
|
});
|
|
this.fieldRefs[key] = { type: 'number', node: input };
|
|
|
|
return E('div', { 'class': 'sh-input-group' }, [
|
|
E('label', { 'class': 'sh-input-label' }, label),
|
|
input
|
|
]);
|
|
},
|
|
|
|
renderThresholdRow: function(prefix, label, warning, critical) {
|
|
var warnKey = prefix + '_warning';
|
|
var critKey = prefix + '_critical';
|
|
|
|
var warnInput = E('input', {
|
|
'type': 'number',
|
|
'class': 'sh-input',
|
|
'value': warning,
|
|
'min': 0,
|
|
'max': 200
|
|
});
|
|
var critInput = E('input', {
|
|
'type': 'number',
|
|
'class': 'sh-input',
|
|
'value': critical,
|
|
'min': 0,
|
|
'max': 200
|
|
});
|
|
|
|
this.fieldRefs[warnKey] = { type: 'number', node: warnInput };
|
|
this.fieldRefs[critKey] = { type: 'number', node: critInput };
|
|
|
|
return E('div', { 'class': 'sh-threshold-row' }, [
|
|
E('div', { 'class': 'sh-threshold-label' }, label),
|
|
E('div', { 'class': 'sh-threshold-inputs' }, [
|
|
E('label', {}, [
|
|
_('Warning'),
|
|
warnInput
|
|
]),
|
|
E('label', {}, [
|
|
_('Critical'),
|
|
critInput
|
|
])
|
|
])
|
|
]);
|
|
},
|
|
|
|
boolValue: function(value, fallback) {
|
|
if (value === 0 || value === '0')
|
|
return false;
|
|
if (value === 1 || value === '1')
|
|
return true;
|
|
return !!fallback;
|
|
},
|
|
|
|
collectPayload: function() {
|
|
var payload = {};
|
|
var self = this;
|
|
|
|
function readBool(key) {
|
|
var ref = self.fieldRefs[key];
|
|
return ref && ref.node.classList.contains('active') ? 1 : 0;
|
|
}
|
|
|
|
function readNumber(key) {
|
|
var ref = self.fieldRefs[key];
|
|
return ref ? parseInt(ref.node.value, 10) || 0 : 0;
|
|
}
|
|
|
|
function readSelect(key) {
|
|
var ref = self.fieldRefs[key];
|
|
return ref ? ref.node.getAttribute('data-value') || ref.node.value : '';
|
|
}
|
|
|
|
payload.auto_refresh = readBool('auto_refresh');
|
|
payload.health_check = readBool('health_check');
|
|
payload.debug_mode = readBool('debug_mode');
|
|
payload.refresh_interval = readSelect('refresh_interval');
|
|
payload.log_retention = readNumber('log_retention');
|
|
|
|
['cpu', 'mem', 'disk', 'temp'].forEach(function(prefix) {
|
|
payload[prefix + '_warning'] = readNumber(prefix + '_warning');
|
|
payload[prefix + '_critical'] = readNumber(prefix + '_critical');
|
|
});
|
|
|
|
return payload;
|
|
},
|
|
|
|
saveSettings: function(ev) {
|
|
ev && ev.preventDefault();
|
|
var payload = this.collectPayload();
|
|
|
|
ui.showModal(_('Saving preferences…'), [
|
|
E('p', {}, _('Applying thresholds and refresh cadence')),
|
|
E('div', { 'class': 'spinning' })
|
|
]);
|
|
|
|
API.saveSettings(payload).then(L.bind(function(result) {
|
|
ui.hideModal();
|
|
if (result && result.success) {
|
|
ui.addNotification(null, E('p', {}, _('Preferences saved.')), 'info');
|
|
this.reloadView();
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, (result && result.error) || _('Unable to save settings')), 'error');
|
|
}
|
|
}, this)).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
resetView: function(ev) {
|
|
ev && ev.preventDefault();
|
|
this.reloadView();
|
|
},
|
|
|
|
reloadView: function() {
|
|
this.load().then(L.bind(function(data) {
|
|
var node = this.render(data);
|
|
var root = document.querySelector('.system-hub-dashboard');
|
|
if (root && root.parentNode) {
|
|
root.parentNode.replaceChild(node, root);
|
|
}
|
|
}, this));
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|