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>
179 lines
5.3 KiB
JavaScript
179 lines
5.3 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require rpc';
|
|
'require ui';
|
|
'require secubox-theme/theme as Theme';
|
|
'require cdn-cache/nav as CdnNav';
|
|
|
|
var callCacheList = rpc.declare({
|
|
object: 'luci.cdn-cache',
|
|
method: 'cache_list',
|
|
expect: { items: [] }
|
|
});
|
|
|
|
var callTopDomains = rpc.declare({
|
|
object: 'luci.cdn-cache',
|
|
method: 'top_domains',
|
|
expect: { domains: [] }
|
|
});
|
|
|
|
var callPurgeDomain = rpc.declare({
|
|
object: 'luci.cdn-cache',
|
|
method: 'purge_domain',
|
|
params: ['domain']
|
|
});
|
|
|
|
function formatBytes(bytes) {
|
|
if (!bytes)
|
|
return '0 B';
|
|
var units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
var i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
|
}
|
|
|
|
function formatAge(seconds) {
|
|
if (!seconds)
|
|
return '—';
|
|
if (seconds < 60) return seconds + 's';
|
|
if (seconds < 3600) return Math.floor(seconds / 60) + 'm';
|
|
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h';
|
|
return Math.floor(seconds / 86400) + 'd';
|
|
}
|
|
|
|
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
|
Theme.init({ language: lang });
|
|
|
|
return view.extend({
|
|
load: function() {
|
|
return Promise.all([
|
|
callTopDomains(),
|
|
callCacheList()
|
|
]);
|
|
},
|
|
|
|
render: function(data) {
|
|
var domains = (data[0] && data[0].domains) || [];
|
|
var items = (data[1] && data[1].items) || [];
|
|
|
|
return E('div', { 'class': 'cdn-dashboard' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/common.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('cdn-cache/dashboard.css') }),
|
|
CdnNav.renderTabs('cache'),
|
|
this.renderHero(items, domains),
|
|
this.renderDomains(domains),
|
|
this.renderCacheTable(items)
|
|
]);
|
|
},
|
|
|
|
renderHero: function(items, domains) {
|
|
return E('section', { 'class': 'cdn-hero' }, [
|
|
E('div', {}, [
|
|
E('h2', {}, '💾 CDN Cache Inventory'),
|
|
E('p', {}, _('Inspect cached objects, purge domains, and diagnose cache behaviours.'))
|
|
]),
|
|
E('div', { 'class': 'cdn-hero-meta' }, [
|
|
E('span', {}, _('Objects cached: ') + items.length),
|
|
E('span', {}, _('Active domains: ') + domains.length),
|
|
E('span', {}, _('Largest file: ') + (items[0] ? formatBytes(items[0].size || 0) : '0 B'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
renderDomains: function(domains) {
|
|
if (!domains.length) {
|
|
return E('section', { 'class': 'cdn-section' },
|
|
E('div', { 'class': 'secubox-empty-state' }, _('No cached domains yet.')));
|
|
}
|
|
|
|
var self = this;
|
|
return E('section', { 'class': 'cdn-section' }, [
|
|
E('div', { 'class': 'cdn-section-header' }, [
|
|
E('div', { 'class': 'cdn-section-title' }, ['🌐', ' ', _('Cached Domains')]),
|
|
E('span', { 'class': 'sb-badge sb-badge-ghost' }, _('Top utilisation'))
|
|
]),
|
|
E('div', { 'class': 'cdn-domain-grid' }, domains.slice(0, 12).map(function(domain) {
|
|
return E('div', { 'class': 'cdn-domain-card' }, [
|
|
E('div', { 'class': 'cdn-domain-name' }, [
|
|
E('span', {}, domain.domain || _('Unknown')),
|
|
E('button', {
|
|
'class': 'cdn-btn cdn-btn-sm cdn-btn-danger',
|
|
'click': function() {
|
|
self.handleDomainPurge(domain.domain);
|
|
}
|
|
}, _('Purge'))
|
|
]),
|
|
E('div', { 'class': 'cdn-domain-stats' }, [
|
|
_('Files: ') + (domain.count || 0),
|
|
_('Size: ') + formatBytes(domain.size_bytes || 0)
|
|
])
|
|
]);
|
|
}))
|
|
]);
|
|
},
|
|
|
|
renderCacheTable: function(items) {
|
|
if (!items.length)
|
|
return E('section', { 'class': 'cdn-section' }, E('div', { 'class': 'secubox-empty-state' }, _('Cache is empty.')));
|
|
|
|
return E('section', { 'class': 'cdn-section' }, [
|
|
E('div', { 'class': 'cdn-section-header' }, [
|
|
E('div', { 'class': 'cdn-section-title' }, ['🗃', ' ', _('Cached Objects')]),
|
|
E('span', { 'class': 'sb-badge sb-badge-ghost' }, _('Most recent 50'))
|
|
]),
|
|
E('table', { 'class': 'cdn-table' }, [
|
|
E('thead', {}, E('tr', {}, [
|
|
E('th', {}, _('File')),
|
|
E('th', {}, _('Domain')),
|
|
E('th', {}, _('Size')),
|
|
E('th', {}, _('Age'))
|
|
])),
|
|
E('tbody', {},
|
|
items.slice(0, 50).map(function(item) {
|
|
return E('tr', {}, [
|
|
E('td', { 'class': 'cdn-file-info' }, [
|
|
E('span', { 'class': 'cdn-file-icon' }, '📄'),
|
|
E('span', { 'class': 'cdn-file-name' }, item.path || _('Unnamed'))
|
|
]),
|
|
E('td', {}, E('span', { 'class': 'cdn-domain-badge' }, item.domain || _('Unknown'))),
|
|
E('td', { 'class': 'cdn-size' }, formatBytes(item.size || 0)),
|
|
E('td', { 'class': 'cdn-age' }, formatAge(item.age || 0))
|
|
]);
|
|
})
|
|
)
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleDomainPurge: function(domain) {
|
|
if (!domain)
|
|
return;
|
|
|
|
var self = this;
|
|
ui.showModal(_('Purge Domain'), [
|
|
E('p', {}, _('Remove all cached objects for ') + domain + '?'),
|
|
E('div', { 'class': 'right' }, [
|
|
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
|
E('button', {
|
|
'class': 'btn cbi-button-negative',
|
|
'click': function() {
|
|
callPurgeDomain(domain).then(function() {
|
|
ui.hideModal();
|
|
location.reload();
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
}
|
|
}, _('Purge'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
handleSaveApply: null,
|
|
handleSave: null,
|
|
handleReset: null
|
|
});
|