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>
155 lines
5.6 KiB
JavaScript
155 lines
5.6 KiB
JavaScript
'use strict';
|
||
'require view';
|
||
'require ui';
|
||
'require mqtt-bridge/api as API';
|
||
'require secubox-theme/theme as Theme';
|
||
'require mqtt-bridge/nav as Nav';
|
||
|
||
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([
|
||
API.listDevices(),
|
||
API.getStatus()
|
||
]);
|
||
},
|
||
|
||
render: function(payload) {
|
||
var devices = (payload[0] && payload[0].devices) || [];
|
||
var status = payload[1] || {};
|
||
return E('div', { 'class': 'mqtt-bridge-dashboard' }, [
|
||
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
||
E('link', { 'rel': 'stylesheet', 'href': L.resource('mqtt-bridge/common.css') }),
|
||
Nav.renderTabs('devices'),
|
||
this.renderStats(status),
|
||
this.renderProfiles(status.adapters || [], status.profiles || []),
|
||
E('div', { 'class': 'mb-card' }, [
|
||
E('div', { 'class': 'mb-card-header' }, [
|
||
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '🔌'), _('USB & Sensors')]),
|
||
E('button', {
|
||
'class': 'mb-btn mb-btn-primary',
|
||
'click': ui.createHandlerFn(this, 'startPairing')
|
||
}, ['➕ ', _('Pair new device')])
|
||
]),
|
||
devices.length ? devices.map(this.renderDeviceRow.bind(this)) :
|
||
E('p', { 'style': 'color:var(--mb-muted);' }, _('No devices detected yet. Plug a USB bridge or trigger pairing.'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderProfiles: function(adapters, liveProfiles) {
|
||
var primary = (adapters && adapters.length) ? adapters : [];
|
||
var fallback = (liveProfiles && liveProfiles.length) ? liveProfiles : [];
|
||
var items = primary.length ? primary : fallback;
|
||
var cards = items.length ? items.map(this.renderProfile.bind(this)) :
|
||
[E('p', { 'style': 'color:var(--mb-muted);' },
|
||
_('No presets detected yet. Connect a Zigbee adapter or review the documentation below.'))];
|
||
|
||
return E('div', { 'class': 'mb-card' }, [
|
||
E('div', { 'class': 'mb-card-header' }, [
|
||
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '🛰️'), _('Zigbee & serial presets')])
|
||
]),
|
||
E('div', { 'class': 'mb-profile-grid' }, cards),
|
||
E('div', { 'class': 'mb-profile-hint' }, [
|
||
E('strong', {}, _('dmesg/logcat hints:')),
|
||
E('pre', {}, '[ 6456.735692] usb 3-1.1: USB disconnect, device number 3\n' +
|
||
'[ 6459.021458] usb 3-1.1: new full-speed USB device number 4 using xhci-hcd'),
|
||
E('p', {}, _('Match the Bus/Device numbers to /dev/tty* and update the MQTT bridge config if needed.'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
renderProfile: function(profile) {
|
||
var detected = profile.detected === true || profile.detected === 1 || profile.detected === '1';
|
||
var meta = [
|
||
(profile.vendor && profile.product) ? _('VID:PID ') + profile.vendor + ':' + profile.product : null,
|
||
profile.bus ? _('Bus ') + profile.bus : null,
|
||
profile.device ? _('Device ') + profile.device : null,
|
||
profile.port ? _('Port ') + profile.port : null
|
||
].filter(Boolean);
|
||
var statusParts = [];
|
||
if (detected)
|
||
statusParts.push(_('Detected'));
|
||
else
|
||
statusParts.push(_('Waiting'));
|
||
if (profile.health)
|
||
statusParts.push(profile.health);
|
||
if (profile.last_seen)
|
||
statusParts.push(_('Last seen ') + profile.last_seen);
|
||
|
||
return E('div', { 'class': 'mb-profile-card' }, [
|
||
E('div', { 'class': 'mb-profile-header' }, [
|
||
E('div', {}, [
|
||
E('strong', {}, profile.label || _('USB profile')),
|
||
E('div', { 'class': 'mb-profile-meta' }, meta.map(function(entry) {
|
||
return E('span', {}, entry);
|
||
}))
|
||
]),
|
||
E('span', {
|
||
'class': 'mb-profile-status' + (detected ? ' online' : '')
|
||
}, statusParts.join(' • '))
|
||
]),
|
||
profile.notes ? E('p', { 'class': 'mb-profile-notes' }, profile.notes) : null
|
||
]);
|
||
},
|
||
|
||
renderStats: function(status) {
|
||
var stats = status.device_stats || {};
|
||
return E('div', { 'class': 'mb-card' }, [
|
||
E('div', { 'class': 'mb-card-header' }, [
|
||
E('div', { 'class': 'mb-card-title' }, [E('span', {}, '📊'), _('Device stats')])
|
||
]),
|
||
E('div', { 'class': 'mb-grid' }, [
|
||
this.stat(_('Paired devices'), stats.total || 0),
|
||
this.stat(_('Online'), stats.online || 0),
|
||
this.stat(_('USB adapters'), stats.usb || 0)
|
||
])
|
||
]);
|
||
},
|
||
|
||
stat: function(label, value) {
|
||
return E('div', { 'class': 'mb-stat' }, [
|
||
E('span', { 'class': 'mb-stat-label' }, label),
|
||
E('div', { 'class': 'mb-stat-value' }, value)
|
||
]);
|
||
},
|
||
|
||
renderDeviceRow: function(device) {
|
||
return E('div', { 'class': 'mb-device-row' }, [
|
||
E('div', { 'class': 'mb-device-info' }, [
|
||
E('strong', {}, device.name || device.serial || _('Unknown device')),
|
||
E('span', { 'style': 'color:var(--mb-muted);font-size:12px;' },
|
||
(device.protocol || 'USB') + ' • ' + (device.port || 'N/A'))
|
||
]),
|
||
E('div', { 'style': 'display:flex;gap:8px;' }, [
|
||
E('span', {
|
||
'style': 'font-size:12px;text-transform:uppercase;color:' +
|
||
(device.online ? '#22c55e' : '#f97316')
|
||
}, device.online ? _('Online') : _('Paired / offline'))
|
||
])
|
||
]);
|
||
},
|
||
|
||
startPairing: function() {
|
||
ui.showModal(_('Pairing'), [
|
||
E('p', {}, _('Listening for device join requests…')),
|
||
E('div', { 'class': 'spinning' })
|
||
]);
|
||
return API.triggerPairing().then(function(result) {
|
||
ui.hideModal();
|
||
if (result && result.success) {
|
||
ui.addNotification(null, E('p', {}, _('Pairing window opened for 2 minutes.')), 'info');
|
||
} else {
|
||
ui.addNotification(null, E('p', {}, _('Unable to start pairing')), 'error');
|
||
}
|
||
}).catch(function(err) {
|
||
ui.hideModal();
|
||
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
||
});
|
||
}
|
||
});
|