Major Enhancements: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Network Modes Module: - Added 3 new network modes: * Double NAT mode (doublenat.js) - Cascaded router configuration * Multi-WAN mode (multiwan.js) - Load balancing and failover * VPN Relay mode (vpnrelay.js) - VPN gateway configuration - Enhanced existing modes: * Access Point improvements * Travel mode refinements * Router mode enhancements * Relay mode updates * Sniffer mode optimizations - Updated wizard with new mode options - Enhanced API with new mode support - Improved dashboard CSS styling - Updated helpers for new modes - Extended RPCD backend functionality - Updated menu structure for new modes - Enhanced UCI configuration System Hub Module: - Added dedicated logs.css stylesheet - Enhanced logs.js view with better styling - Improved overview.css responsive design - Enhanced services.css for better UX - Updated overview.js with theme integration - Improved services.js layout SecuBox Dashboard: - Enhanced dashboard.css with theme variables - Improved dashboard.js responsiveness - Better integration with global theme Files Changed: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Network Modes (17 files): Modified: api.js, dashboard.css, helpers.js, menu, config, RPCD backend Modified Views: accesspoint, overview, relay, router, sniffer, travel, wizard New Views: doublenat, multiwan, vpnrelay System Hub (6 files): New: logs.css Modified: overview.css, services.css, logs.js, overview.js, services.js SecuBox (2 files): Modified: dashboard.css, dashboard.js Total: 25 files changed (21 modified, 4 new) Technical Improvements: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Global theme CSS variable usage - Responsive design enhancements - Improved error handling - Better mode validation - Enhanced user feedback - Optimized CSS performance - Improved accessibility Network Mode Capabilities: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1. Router Mode - Standard routing 2. Access Point Mode - WiFi AP with bridge 3. Relay Mode - WiFi repeater/extender 4. Travel Mode - Portable router configuration 5. Sniffer Mode - Network monitoring 6. Double NAT Mode - Cascaded NAT for network isolation (NEW) 7. Multi-WAN Mode - Multiple uplinks with load balancing (NEW) 8. VPN Relay Mode - VPN gateway and tunnel endpoint (NEW) Breaking Changes: ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ None - All changes are backward compatible 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
270 lines
8.2 KiB
JavaScript
270 lines
8.2 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require ui';
|
|
'require dom';
|
|
'require poll';
|
|
'require system-hub/api as API';
|
|
'require secubox-theme/theme as Theme';
|
|
|
|
Theme.init({ theme: 'dark', language: 'en' });
|
|
|
|
return view.extend({
|
|
services: [],
|
|
activeFilter: 'all',
|
|
searchQuery: '',
|
|
|
|
load: function() {
|
|
return API.listServices();
|
|
},
|
|
|
|
render: function(data) {
|
|
this.services = this.normalizeServices(data);
|
|
|
|
var container = E('div', { 'class': 'sh-services-view' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/services.css') }),
|
|
this.renderHeader(),
|
|
this.renderControls(),
|
|
E('div', { 'class': 'sh-services-grid', 'id': 'sh-services-grid' },
|
|
this.getFilteredServices().map(this.renderServiceCard, this))
|
|
]);
|
|
|
|
var self = this;
|
|
poll.add(function() {
|
|
return API.listServices().then(function(fresh) {
|
|
self.services = self.normalizeServices(fresh);
|
|
self.updateStats();
|
|
self.updateServicesGrid();
|
|
});
|
|
}, 30);
|
|
|
|
return container;
|
|
},
|
|
|
|
normalizeServices: function(data) {
|
|
if (!data) return [];
|
|
if (Array.isArray(data)) return data;
|
|
if (Array.isArray(data.services)) return data.services;
|
|
return [];
|
|
},
|
|
|
|
renderHeader: function() {
|
|
var stats = this.getStats();
|
|
|
|
return E('section', { 'class': 'sh-services-hero' }, [
|
|
E('div', {}, [
|
|
E('h1', {}, _('Service Control Center')),
|
|
E('p', {}, _('Start, stop, enable, and inspect all init.d services'))
|
|
]),
|
|
E('div', { 'class': 'sh-services-stats', 'id': 'sh-services-stats' }, [
|
|
this.createStatCard('sh-stat-total', _('Total'), stats.total),
|
|
this.createStatCard('sh-stat-running', _('Running'), stats.running, 'success'),
|
|
this.createStatCard('sh-stat-stopped', _('Stopped'), stats.stopped, 'danger'),
|
|
this.createStatCard('sh-stat-enabled', _('Enabled'), stats.enabled, 'info')
|
|
])
|
|
]);
|
|
},
|
|
|
|
createStatCard: function(id, label, value, tone) {
|
|
return E('div', { 'class': 'sh-service-stat ' + (tone || ''), 'id': id }, [
|
|
E('span', { 'class': 'label' }, label),
|
|
E('span', { 'class': 'value' }, value)
|
|
]);
|
|
},
|
|
|
|
renderControls: function() {
|
|
var self = this;
|
|
var filters = [
|
|
{ id: 'all', label: _('All') },
|
|
{ id: 'running', label: _('Running') },
|
|
{ id: 'stopped', label: _('Stopped') },
|
|
{ id: 'enabled', label: _('Enabled') },
|
|
{ id: 'disabled', label: _('Disabled') }
|
|
];
|
|
|
|
return E('div', { 'class': 'sh-service-controls' }, [
|
|
E('div', { 'class': 'sh-filter-tabs' },
|
|
filters.map(function(filter) {
|
|
return E('button', {
|
|
'class': 'sh-filter-tab' + (self.activeFilter === filter.id ? ' active' : ''),
|
|
'type': 'button',
|
|
'click': function() {
|
|
self.activeFilter = filter.id;
|
|
self.updateServicesGrid();
|
|
self.refreshFilterTabs();
|
|
}
|
|
}, filter.label);
|
|
})),
|
|
E('div', { 'class': 'sh-service-search' }, [
|
|
E('input', {
|
|
'type': 'text',
|
|
'class': 'cbi-input-text',
|
|
'placeholder': _('🔍 Search services...'),
|
|
'input': function(ev) {
|
|
self.searchQuery = (ev.target.value || '').toLowerCase();
|
|
self.updateServicesGrid();
|
|
}
|
|
})
|
|
])
|
|
]);
|
|
},
|
|
|
|
refreshFilterTabs: function() {
|
|
var tabs = document.querySelectorAll('.sh-filter-tab');
|
|
var filters = ['all', 'running', 'stopped', 'enabled', 'disabled'];
|
|
tabs.forEach(function(tab, idx) {
|
|
if (filters[idx] === this.activeFilter) tab.classList.add('active');
|
|
else tab.classList.remove('active');
|
|
}, this);
|
|
},
|
|
|
|
getStats: function() {
|
|
var total = this.services.length;
|
|
var running = this.services.filter(function(s) { return s.running; }).length;
|
|
var enabled = this.services.filter(function(s) { return s.enabled; }).length;
|
|
var stopped = total - running;
|
|
return { total: total, running: running, stopped: stopped, enabled: enabled, disabled: total - enabled };
|
|
},
|
|
|
|
getFilteredServices: function() {
|
|
return this.services.filter(function(service) {
|
|
var matchesFilter = true;
|
|
switch (this.activeFilter) {
|
|
case 'running': matchesFilter = service.running; break;
|
|
case 'stopped': matchesFilter = !service.running; break;
|
|
case 'enabled': matchesFilter = service.enabled; break;
|
|
case 'disabled': matchesFilter = !service.enabled; break;
|
|
}
|
|
var matchesSearch = !this.searchQuery ||
|
|
service.name.toLowerCase().includes(this.searchQuery);
|
|
return matchesFilter && matchesSearch;
|
|
}, this);
|
|
},
|
|
|
|
renderServiceCard: function(service) {
|
|
var statusClass = service.running ? 'running' : 'stopped';
|
|
var enabledLabel = service.enabled ? _('Enabled at boot') : _('Disabled');
|
|
|
|
return E('div', { 'class': 'sh-service-card ' + statusClass }, [
|
|
E('div', { 'class': 'sh-service-head' }, [
|
|
E('div', {}, [
|
|
E('h3', {}, service.name),
|
|
E('span', { 'class': 'sh-service-tag' }, enabledLabel)
|
|
]),
|
|
E('span', {
|
|
'class': 'sh-service-status ' + statusClass
|
|
}, service.running ? _('Running') : _('Stopped'))
|
|
]),
|
|
E('div', { 'class': 'sh-service-actions' }, [
|
|
this.renderActionButton(service, service.running ? 'stop' : 'start'),
|
|
this.renderActionButton(service, 'restart'),
|
|
this.renderActionButton(service, service.enabled ? 'disable' : 'enable'),
|
|
E('button', {
|
|
'class': 'sh-btn sh-btn-ghost',
|
|
'type': 'button',
|
|
'click': ui.createHandlerFn(this, 'showServiceDetails', service)
|
|
}, _('Details'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderActionButton: function(service, action) {
|
|
var labels = {
|
|
start: _('Start'),
|
|
stop: _('Stop'),
|
|
restart: _('Restart'),
|
|
enable: _('Enable'),
|
|
disable: _('Disable')
|
|
};
|
|
|
|
return E('button', {
|
|
'class': 'sh-btn sh-btn-action',
|
|
'type': 'button',
|
|
'click': ui.createHandlerFn(this, 'handleServiceAction', service, action)
|
|
}, labels[action] || action);
|
|
},
|
|
|
|
handleServiceAction: function(service, action) {
|
|
ui.showModal(_('Service action'), [
|
|
E('p', { 'class': 'spinning' }, _('Executing ') + action + ' ' + service.name + ' ...')
|
|
]);
|
|
|
|
return API.serviceAction({ service: service.name, action: action }).then(L.bind(function(result) {
|
|
ui.hideModal();
|
|
if (result && result.success === false) {
|
|
ui.addNotification(null, E('p', {}, result.message || _('Action failed')), 'error');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, result.message || _('Action completed')), 'info');
|
|
return API.listServices().then(L.bind(function(fresh) {
|
|
this.services = this.normalizeServices(fresh);
|
|
this.updateStats();
|
|
this.updateServicesGrid();
|
|
}, this));
|
|
}
|
|
}, this)).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
showServiceDetails: function(service) {
|
|
var details = [
|
|
{ label: _('Name'), value: service.name },
|
|
{ label: _('Enabled'), value: service.enabled ? _('Yes') : _('No') },
|
|
{ label: _('Running'), value: service.running ? _('Yes') : _('No') }
|
|
];
|
|
|
|
ui.showModal(_('Service Details'), [
|
|
E('div', { 'class': 'sh-service-detail' },
|
|
details.map(function(item) {
|
|
return E('div', { 'class': 'sh-service-detail-row' }, [
|
|
E('span', {}, item.label),
|
|
E('strong', {}, item.value)
|
|
]);
|
|
})),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'cbi-button cbi-button-positive',
|
|
'click': ui.hideModal
|
|
}, _('Close'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
updateStats: function() {
|
|
var stats = this.getStats();
|
|
var ids = {
|
|
'sh-stat-total': stats.total,
|
|
'sh-stat-running': stats.running,
|
|
'sh-stat-stopped': stats.stopped,
|
|
'sh-stat-enabled': stats.enabled
|
|
};
|
|
|
|
Object.keys(ids).forEach(function(id) {
|
|
var node = document.getElementById(id);
|
|
if (node) {
|
|
var value = node.querySelector('.value');
|
|
if (value) value.textContent = ids[id];
|
|
}
|
|
});
|
|
},
|
|
|
|
updateServicesGrid: function() {
|
|
var grid = document.getElementById('sh-services-grid');
|
|
if (!grid) return;
|
|
var filtered = this.getFilteredServices();
|
|
if (!filtered.length) {
|
|
dom.content(grid, [
|
|
E('div', { 'class': 'sh-empty-state' }, [
|
|
E('div', { 'class': 'sh-empty-icon' }, '📭'),
|
|
E('p', {}, _('No services match the current filter'))
|
|
])
|
|
]);
|
|
return;
|
|
}
|
|
dom.content(grid, filtered.map(this.renderServiceCard, this));
|
|
}
|
|
});
|