Major structural reorganization and feature additions: ## Folder Reorganization - Move 17 luci-app-* packages to package/secubox/ (except luci-app-secubox core hub) - Update all tooling to support new structure: - secubox-tools/quick-deploy.sh: search both locations - secubox-tools/validate-modules.sh: validate both directories - secubox-tools/fix-permissions.sh: fix permissions in both locations - .github/workflows/test-validate.yml: build from both paths - Update README.md links to new package/secubox/ paths ## AppStore Migration (Complete) - Add catalog entries for all remaining luci-app packages: - network-tweaks.json: Network optimization tools - secubox-bonus.json: Documentation & demos hub - Total: 24 apps in AppStore catalog (22 existing + 2 new) - New category: 'documentation' for docs/demos/tutorials ## VHost Manager v2.0 Enhancements - Add profile activation system for Internal Services and Redirects - Implement createVHost() API wrapper for template-based deployment - Fix Virtual Hosts view rendering with proper LuCI patterns - Fix RPCD backend shell script errors (remove invalid local declarations) - Extend backend validation for nginx return directives (redirect support) - Add section_id parameter for named VHost profiles - Add Remove button to Redirects page for feature parity - Update README to v2.0 with comprehensive feature documentation ## Network Tweaks Dashboard - Close button added to component details modal Files changed: 340+ (336 renames with preserved git history) Packages affected: 19 luci-app, 2 secubox-app, 1 theme, 4 tools 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
194 lines
6.3 KiB
JavaScript
194 lines
6.3 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require secubox-theme/theme as Theme';
|
|
'require ui';
|
|
'require dom';
|
|
'require traffic-shaper/api as API';
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return API.listPresets();
|
|
},
|
|
|
|
render: function(data) {
|
|
var presets = data.presets || [];
|
|
|
|
var container = E('div', { 'class': 'traffic-shaper-dashboard' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
E('h2', {}, _('Traffic Shaping Presets')),
|
|
|
|
E('div', { 'class': 'cbi-section' }, [
|
|
E('p', {}, _('Apply pre-configured traffic shaping profiles optimized for common use cases. Applying a preset will replace your current configuration.')),
|
|
E('p', { 'style': 'background: #fff3cd; padding: 0.75em; border-left: 4px solid #ffc107;' }, [
|
|
E('strong', {}, _('Warning:')),
|
|
' ',
|
|
_('Applying a preset will overwrite all existing traffic classes and rules.')
|
|
])
|
|
]),
|
|
|
|
E('div', { 'class': 'ts-preset-grid' },
|
|
presets.length > 0 ?
|
|
presets.map(L.bind(this.renderPresetCard, this)) :
|
|
this.renderDefaultPresets()
|
|
)
|
|
]);
|
|
|
|
return container;
|
|
},
|
|
|
|
renderPresetCard: function(preset) {
|
|
return E('div', { 'class': 'ts-preset-card' }, [
|
|
E('div', { 'class': 'ts-preset-title' }, preset.name || preset.id),
|
|
E('div', { 'class': 'ts-preset-description' }, preset.description || ''),
|
|
|
|
E('div', { 'style': 'margin: 1em 0; padding: 1em; background: #f8f9fa; border-radius: 4px;' }, [
|
|
E('strong', {}, _('Configuration:')),
|
|
E('ul', { 'style': 'margin: 0.5em 0 0 0; padding-left: 1.5em;' },
|
|
(preset.classes || []).map(function(cls) {
|
|
return E('li', {}, '%s: %s (priority %s)'.format(
|
|
cls.name,
|
|
cls.rate + ' → ' + cls.ceil,
|
|
cls.priority
|
|
));
|
|
})
|
|
)
|
|
]),
|
|
|
|
E('div', { 'class': 'ts-preset-apply' }, [
|
|
E('button', {
|
|
'class': 'ts-btn ts-btn-success',
|
|
'click': L.bind(function(ev) {
|
|
this.handleApplyPreset(ev, preset.id);
|
|
}, this)
|
|
}, _('Apply This Preset'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderDefaultPresets: function() {
|
|
// Fallback presets if backend doesn't provide them
|
|
var defaultPresets = [
|
|
{
|
|
id: 'gaming',
|
|
name: _('Gaming & Low-Latency'),
|
|
description: _('Optimized for online gaming, VoIP, and real-time applications. Prioritizes UDP and low-latency traffic.'),
|
|
classes: [
|
|
{ name: _('Gaming/VoIP'), priority: '1', rate: '5mbit', ceil: '50mbit' },
|
|
{ name: _('Interactive'), priority: '3', rate: '10mbit', ceil: '80mbit' },
|
|
{ name: _('Streaming'), priority: '5', rate: '8mbit', ceil: '60mbit' },
|
|
{ name: _('Bulk Downloads'), priority: '8', rate: '2mbit', ceil: '30mbit' }
|
|
]
|
|
},
|
|
{
|
|
id: 'streaming',
|
|
name: _('Video Streaming'),
|
|
description: _('Optimized for video streaming services like Netflix, YouTube, and video conferencing. Ensures smooth playback.'),
|
|
classes: [
|
|
{ name: _('Video Conference'), priority: '2', rate: '10mbit', ceil: '60mbit' },
|
|
{ name: _('Video Streaming'), priority: '3', rate: '15mbit', ceil: '80mbit' },
|
|
{ name: _('Web Browsing'), priority: '5', rate: '5mbit', ceil: '50mbit' },
|
|
{ name: _('Background'), priority: '7', rate: '2mbit', ceil: '30mbit' }
|
|
]
|
|
},
|
|
{
|
|
id: 'work_from_home',
|
|
name: _('Work From Home'),
|
|
description: _('Balanced configuration for remote work. Prioritizes video calls, VPN, and web applications while allowing downloads.'),
|
|
classes: [
|
|
{ name: _('Video Calls'), priority: '1', rate: '8mbit', ceil: '50mbit' },
|
|
{ name: _('VPN Traffic'), priority: '2', rate: '10mbit', ceil: '60mbit' },
|
|
{ name: _('Web/Email'), priority: '4', rate: '8mbit', ceil: '70mbit' },
|
|
{ name: _('Downloads'), priority: '6', rate: '5mbit', ceil: '40mbit' }
|
|
]
|
|
},
|
|
{
|
|
id: 'balanced',
|
|
name: _('Balanced (Default)'),
|
|
description: _('General-purpose configuration that works well for most households. Equal priority for common applications.'),
|
|
classes: [
|
|
{ name: _('High Priority'), priority: '2', rate: '10mbit', ceil: '60mbit' },
|
|
{ name: _('Normal Traffic'), priority: '5', rate: '15mbit', ceil: '80mbit' },
|
|
{ name: _('Bulk Transfer'), priority: '7', rate: '5mbit', ceil: '50mbit' }
|
|
]
|
|
}
|
|
];
|
|
|
|
return defaultPresets.map(L.bind(this.renderPresetCard, this));
|
|
},
|
|
|
|
handleApplyPreset: function(ev, presetId) {
|
|
var btn = ev.target;
|
|
|
|
ui.showModal(_('Apply Preset'), [
|
|
E('p', {}, _('Are you sure you want to apply this preset? This will:')),
|
|
E('ul', {}, [
|
|
E('li', {}, _('Delete all existing traffic classes')),
|
|
E('li', {}, _('Delete all existing classification rules')),
|
|
E('li', {}, _('Create new classes from the preset')),
|
|
E('li', {}, _('Apply the new configuration immediately'))
|
|
]),
|
|
E('p', { 'style': 'font-weight: bold; color: #dc3545;' },
|
|
_('This action cannot be undone!')),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', {
|
|
'class': 'btn',
|
|
'click': ui.hideModal
|
|
}, _('Cancel')),
|
|
' ',
|
|
E('button', {
|
|
'class': 'btn cbi-button-action',
|
|
'click': L.bind(function() {
|
|
ui.hideModal();
|
|
this.applyPreset(presetId, btn);
|
|
}, this)
|
|
}, _('Yes, Apply Preset'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
applyPreset: function(presetId, btn) {
|
|
var originalText = btn.textContent;
|
|
btn.disabled = true;
|
|
btn.textContent = _('Applying...');
|
|
|
|
ui.showModal(_('Applying Preset'), [
|
|
E('p', { 'class': 'spinning' }, _('Applying preset configuration...'))
|
|
], null, null, { 'spinning': true });
|
|
|
|
API.applyPreset(presetId).then(L.bind(function(result) {
|
|
ui.hideModal();
|
|
|
|
if (result.success) {
|
|
ui.addNotification(null,
|
|
E('p', _('Preset applied successfully! Traffic shaping has been reconfigured.')),
|
|
'info'
|
|
);
|
|
|
|
// Redirect to overview after 2 seconds
|
|
setTimeout(function() {
|
|
window.location.href = L.url('admin/network/traffic-shaper/overview');
|
|
}, 2000);
|
|
} else {
|
|
ui.addNotification(null,
|
|
E('p', _('Failed to apply preset: %s').format(result.message || _('Unknown error'))),
|
|
'error'
|
|
);
|
|
btn.disabled = false;
|
|
btn.textContent = originalText;
|
|
}
|
|
}, this)).catch(L.bind(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null,
|
|
E('p', _('Error applying preset: %s').format(err.message || err)),
|
|
'error'
|
|
);
|
|
btn.disabled = false;
|
|
btn.textContent = originalText;
|
|
}, this));
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|