secubox-openwrt/package/secubox/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js
CyberMind-FR 66aa12d6b6 feat: Add SecuBox portal header to all System Hub views
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>
2026-01-09 15:46:48 +01:00

488 lines
15 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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';
'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({
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.renderThemeSection(),
this.renderThresholdSection(),
this.renderSupportSection(),
this.renderActions()
]);
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
wrapper.appendChild(SbHeader.render());
wrapper.appendChild(container);
return wrapper;
},
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
]);
},
renderThemeSection: function() {
var available = Theme.availableThemes || [];
var preferred = this.readStoredTheme() || Theme.currentTheme || available[0] || 'dark';
var selectEl = E('select', {
'id': 'sh-theme-select',
'class': 'sh-select',
'change': L.bind(function(ev) {
this.previewTheme(ev.target.value);
}, this)
});
if (available.length === 0) {
available = ['dark'];
}
available.forEach(function(themeName) {
selectEl.appendChild(E('option', {
'value': themeName,
'selected': themeName === preferred
}, themeName));
});
this.themeSelect = selectEl;
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' }, '🎨'),
_('SecuBox Theme Preferences')
]),
E('div', { 'class': 'sh-card-subtitle' },
_('Pick a theme, store it locally, and ensure the UI respects your choice.'))
]),
E('div', { 'class': 'sh-card-body' }, [
E('div', { 'class': 'sh-settings-grid' }, [
E('div', {}, [
E('label', { 'for': 'sh-theme-select' }, _('Preferred theme')),
selectEl,
E('small', { 'class': 'sh-muted' },
_('Current applied theme: {theme}', { theme: Theme.currentTheme || 'dark' }))
]),
E('div', { 'class': 'sh-btn-group' }, [
E('button', {
'class': 'sh-btn sh-btn-primary',
'click': L.bind(this.applyThemePreference, this)
}, [ '💾 ', _('Save to browser') ]),
E('button', {
'class': 'sh-btn sh-btn-secondary',
'click': L.bind(this.resetThemePreference, this)
}, [ '🗑 ', _('Reset preference') ])
])
]),
E('div', { 'class': 'sh-support-grid', 'style': 'margin-top: 18px;' }, [
E('div', { 'class': 'sh-support-card' }, [
E('div', { 'class': 'sh-support-label' }, _('localStorage value')),
E('code', {}, this.readStoredTheme() || _('(none)'))
]),
E('div', { 'class': 'sh-support-card' }, [
E('div', { 'class': 'sh-support-label' }, _('Available themes')),
E('span', {}, available.join(', '))
])
]),
E('div', { 'class': 'sh-support-card sh-theme-help' }, [
E('strong', {}, _('Troubleshooting (theme not persisting)')),
E('ol', { 'class': 'sh-theme-checklist' }, [
E('li', {}, _('Check localStorage: `localStorage.getItem(\'secubox.theme\')`')),
E('li', {}, _('Verify the theme exists in Theme.availableThemes')),
E('li', {}, _('Confirm the browser allows localStorage / site data'))
]),
E('p', { 'class': 'sh-muted' },
_('Use the buttons above to re-generate your preference if the UI resets.'))
])
])
]);
},
readStoredTheme: function() {
try {
return window.localStorage.getItem('secubox.theme');
} catch (err) {
return null;
}
},
previewTheme: function(themeName) {
if (Theme && Theme.apply) {
Theme.apply(themeName);
}
},
applyThemePreference: function() {
var theme = this.themeSelect ? this.themeSelect.value : Theme.currentTheme;
if (!theme)
return;
if (Theme && Theme.setPreferredTheme) {
Theme.setPreferredTheme(theme);
} else {
try {
window.localStorage.setItem('secubox.theme', theme);
} catch (err) {
console.warn('Unable to store theme preference:', err);
}
}
ui.addNotification(null,
E('p', {}, _('Theme preference saved to this browser.') + ' [' + theme + ']'),
'info');
},
resetThemePreference: function() {
try {
window.localStorage.removeItem('secubox.theme');
} catch (err) { /* ignore */ }
if (this.themeSelect) {
this.themeSelect.value = Theme.currentTheme || Theme.availableThemes[0] || 'dark';
}
ui.addNotification(null,
E('p', {}, _('Stored preference cleared. The default detection logic will run on next load.')),
'warning');
},
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
});