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>
297 lines
11 KiB
JavaScript
297 lines
11 KiB
JavaScript
'use strict';
|
|
'require view';
|
|
'require dom';
|
|
'require ui';
|
|
'require poll';
|
|
'require zigbee2mqtt/api as API';
|
|
'require secubox-theme/theme as Theme';
|
|
|
|
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 API.getStatus();
|
|
},
|
|
|
|
render: function(data) {
|
|
var config = data || {};
|
|
var container = E('div', { 'class': 'z2m-dashboard' }, [
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }),
|
|
E('link', { 'rel': 'stylesheet', 'href': L.resource('zigbee2mqtt/common.css') }),
|
|
this.renderHeader(config),
|
|
this.renderSetup(config),
|
|
this.renderForm(config),
|
|
this.renderLogs()
|
|
]);
|
|
|
|
poll.add(L.bind(function() {
|
|
return API.getStatus().then(L.bind(function(newData) {
|
|
config = newData;
|
|
this.updateHeader(config);
|
|
this.updateDiagnostics(config.diagnostics);
|
|
}, this));
|
|
}, this), 10);
|
|
|
|
return container;
|
|
},
|
|
|
|
renderHeader: function(cfg) {
|
|
var header = E('div', { 'class': 'z2m-card', 'id': 'z2m-status-card' }, [
|
|
E('div', { 'class': 'z2m-card-header' }, [
|
|
E('div', { 'class': 'sh-page-title' }, [
|
|
E('span', { 'class': 'sh-page-title-icon' }, '🧩'),
|
|
_('Zigbee2MQTT')
|
|
]),
|
|
E('div', { 'class': 'z2m-status-badges' }, [
|
|
E('div', { 'class': 'z2m-badge ' + ((cfg.service && cfg.service.running) ? 'on' : 'off'), 'id': 'z2m-badge-running' },
|
|
cfg.service && cfg.service.running ? _('Running') : _('Stopped')),
|
|
E('div', { 'class': 'z2m-badge ' + ((cfg.service && cfg.service.enabled) ? 'on' : 'off'), 'id': 'z2m-badge-enabled' },
|
|
cfg.service && cfg.service.enabled ? _('Enabled') : _('Disabled'))
|
|
])
|
|
]),
|
|
E('div', { 'class': 'z2m-actions' }, [
|
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleCheck.bind(this) }, _('Run checks')),
|
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleInstall.bind(this) }, _('Install prerequisites')),
|
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleLogs.bind(this) }, _('Refresh logs')),
|
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleUpdate.bind(this) }, _('Update Image')),
|
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleControl.bind(this, 'restart') }, _('Restart')),
|
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleControl.bind(this, 'start') }, _('Start')),
|
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleControl.bind(this, 'stop') }, _('Stop'))
|
|
])
|
|
]);
|
|
return header;
|
|
},
|
|
|
|
updateHeader: function(cfg) {
|
|
var runBadge = document.getElementById('z2m-badge-running');
|
|
var enBadge = document.getElementById('z2m-badge-enabled');
|
|
if (runBadge) {
|
|
runBadge.className = 'z2m-badge ' + ((cfg.service && cfg.service.running) ? 'on' : 'off');
|
|
runBadge.textContent = (cfg.service && cfg.service.running) ? _('Running') : _('Stopped');
|
|
}
|
|
if (enBadge) {
|
|
enBadge.className = 'z2m-badge ' + ((cfg.service && cfg.service.enabled) ? 'on' : 'off');
|
|
enBadge.textContent = (cfg.service && cfg.service.enabled) ? _('Enabled') : _('Disabled');
|
|
}
|
|
},
|
|
|
|
renderSetup: function(cfg) {
|
|
var diag = cfg.diagnostics || {};
|
|
return E('div', { 'class': 'z2m-card' }, [
|
|
E('div', { 'class': 'z2m-card-header' }, [
|
|
E('div', { 'class': 'sh-card-title' }, _('Prerequisites & Health'))
|
|
]),
|
|
this.renderDiagnostics(diag)
|
|
]);
|
|
},
|
|
|
|
renderDiagnostics: function(diag) {
|
|
var items = [
|
|
{ key: 'cgroups', label: _('cgroups mounted') },
|
|
{ key: 'docker', label: _('Docker daemon') },
|
|
{ key: 'usb_module', label: _('cdc_acm module') },
|
|
{ key: 'serial_device', label: _('Serial device') },
|
|
{ key: 'service_file', label: _('Service script') }
|
|
];
|
|
return E('div', { 'class': 'z2m-diag-list' }, items.map(function(item) {
|
|
var ok = diag[item.key];
|
|
return E('div', {
|
|
'class': 'z2m-diag-chip ' + (ok ? 'ok' : 'bad'),
|
|
'id': 'z2m-diag-' + item.key
|
|
}, [
|
|
E('span', { 'class': 'z2m-diag-label' }, item.label),
|
|
E('span', { 'class': 'z2m-diag-value' }, ok ? _('OK') : _('Missing'))
|
|
]);
|
|
}));
|
|
},
|
|
|
|
updateDiagnostics: function(diag) {
|
|
var keys = ['cgroups', 'docker', 'usb_module', 'serial_device', 'service_file'];
|
|
diag = diag || {};
|
|
keys.forEach(function(key) {
|
|
var el = document.getElementById('z2m-diag-' + key);
|
|
if (el) {
|
|
var ok = diag[key];
|
|
el.className = 'z2m-diag-chip ' + (ok ? 'ok' : 'bad');
|
|
var valueEl = el.querySelector('.z2m-diag-value');
|
|
if (valueEl)
|
|
valueEl.textContent = ok ? _('OK') : _('Missing');
|
|
}
|
|
});
|
|
},
|
|
|
|
renderForm: function(cfg) {
|
|
var self = this;
|
|
var inputs = [
|
|
self.input('enabled', _('Enable service'), cfg.enabled ? '1' : '0', 'checkbox'),
|
|
self.input('serial_port', _('Serial device'), cfg.serial_port || '/dev/ttyACM0'),
|
|
self.input('mqtt_host', _('MQTT host URL'), cfg.mqtt_host || 'mqtt://127.0.0.1:1883'),
|
|
self.input('mqtt_username', _('MQTT username'), cfg.mqtt_username || ''),
|
|
self.input('mqtt_password', _('MQTT password'), cfg.mqtt_password || '', 'password'),
|
|
self.input('base_topic', _('Base topic'), cfg.base_topic || 'zigbee2mqtt'),
|
|
self.input('frontend_port', _('Frontend port'), cfg.frontend_port || '8080', 'number'),
|
|
self.input('channel', _('Zigbee channel'), cfg.channel || '11', 'number'),
|
|
self.input('data_path', _('Data path'), cfg.data_path || '/srv/zigbee2mqtt'),
|
|
self.input('image', _('Docker image'), cfg.image || 'ghcr.io/koenkk/zigbee2mqtt:latest'),
|
|
self.input('timezone', _('Timezone'), cfg.timezone || 'UTC')
|
|
];
|
|
|
|
return E('div', { 'class': 'z2m-card' }, [
|
|
E('div', { 'class': 'z2m-card-header' }, [
|
|
E('div', { 'class': 'sh-card-title' }, _('Configuration'))
|
|
]),
|
|
E('div', { 'class': 'z2m-form-grid', 'id': 'z2m-form-grid' }, inputs),
|
|
E('div', { 'class': 'z2m-actions' }, [
|
|
E('button', { 'class': 'sh-btn-primary', 'click': this.handleSave.bind(this) }, _('Save & Apply'))
|
|
])
|
|
]);
|
|
},
|
|
|
|
input: function(id, label, value, type) {
|
|
type = type || 'text';
|
|
var attrs = { 'class': 'z2m-input', 'id': id, 'value': value };
|
|
if (type === 'checkbox') {
|
|
attrs.type = 'checkbox';
|
|
if (value === '1' || value === 1 || value === true) attrs.checked = true;
|
|
} else {
|
|
attrs.type = type;
|
|
}
|
|
if (id === 'mqtt_password')
|
|
attrs.autocomplete = 'new-password';
|
|
return E('div', { 'class': 'z2m-input-group' }, [
|
|
E('label', { 'for': id }, label),
|
|
E('input', attrs)
|
|
]);
|
|
},
|
|
|
|
renderLogs: function() {
|
|
return E('div', { 'class': 'z2m-card' }, [
|
|
E('div', { 'class': 'z2m-card-header' }, [
|
|
E('div', { 'class': 'sh-card-title' }, _('Logs')),
|
|
E('div', { 'class': 'z2m-actions' }, [
|
|
E('input', { 'class': 'z2m-input', 'type': 'number', 'id': 'z2m-log-tail', 'value': '200', 'style': 'width:90px;' }),
|
|
E('button', { 'class': 'sh-btn-secondary', 'click': this.handleLogs.bind(this) }, _('Refresh'))
|
|
])
|
|
]),
|
|
E('pre', { 'class': 'z2m-log', 'id': 'z2m-log-output' }, _('Logs will appear here.'))
|
|
]);
|
|
},
|
|
|
|
handleSave: function() {
|
|
var payload = {
|
|
enabled: document.getElementById('enabled').checked ? '1' : '0',
|
|
serial_port: document.getElementById('serial_port').value,
|
|
mqtt_host: document.getElementById('mqtt_host').value,
|
|
mqtt_username: document.getElementById('mqtt_username').value,
|
|
mqtt_password: document.getElementById('mqtt_password').value,
|
|
base_topic: document.getElementById('base_topic').value,
|
|
frontend_port: document.getElementById('frontend_port').value,
|
|
channel: document.getElementById('channel').value,
|
|
data_path: document.getElementById('data_path').value,
|
|
image: document.getElementById('image').value,
|
|
timezone: document.getElementById('timezone').value
|
|
};
|
|
ui.showModal(_('Applying configuration'), [
|
|
E('p', {}, _('Saving settings and restarting service…')),
|
|
E('div', { 'class': 'spinning' })
|
|
]);
|
|
API.applyConfig(payload).then(function() {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, _('Configuration applied.')), 'info');
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
handleLogs: function() {
|
|
var tail = parseInt(document.getElementById('z2m-log-tail').value, 10) || 200;
|
|
API.getLogs(tail).then(function(result) {
|
|
var box = document.getElementById('z2m-log-output');
|
|
if (box && result && result.lines) {
|
|
box.textContent = result.lines.join('\n');
|
|
}
|
|
});
|
|
},
|
|
|
|
handleControl: function(action) {
|
|
ui.showModal(_('Executing action'), [
|
|
E('p', {}, _('Performing %s…').format(action)),
|
|
E('div', { 'class': 'spinning' })
|
|
]);
|
|
API.control(action).then(function(result) {
|
|
ui.hideModal();
|
|
if (result && result.success) {
|
|
ui.addNotification(null, E('p', {}, _('Action completed: %s').format(action)), 'info');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, _('Action failed')), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
handleUpdate: function() {
|
|
ui.showModal(_('Updating image'), [
|
|
E('p', {}, _('Pulling latest Zigbee2MQTT image…')),
|
|
E('div', { 'class': 'spinning' })
|
|
]);
|
|
API.update().then(function(result) {
|
|
ui.hideModal();
|
|
if (result && result.success) {
|
|
ui.addNotification(null, E('p', {}, _('Image updated. Service restarted.')), 'info');
|
|
} else {
|
|
ui.addNotification(null, E('p', {}, _('Update failed')), 'error');
|
|
}
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
ui.addNotification(null, E('p', {}, err.message || err), 'error');
|
|
});
|
|
},
|
|
|
|
handleInstall: function() {
|
|
this.runCommand(_('Installing prerequisites…'), API.install);
|
|
},
|
|
|
|
handleCheck: function() {
|
|
this.runCommand(_('Running prerequisite checks…'), API.runCheck);
|
|
},
|
|
|
|
runCommand: function(title, fn) {
|
|
var self = this;
|
|
ui.showModal(title, [
|
|
E('p', {}, title),
|
|
E('div', { 'class': 'spinning' })
|
|
]);
|
|
fn().then(function(result) {
|
|
ui.hideModal();
|
|
self.showCommandOutput(result, title);
|
|
self.refreshStatus();
|
|
}).catch(function(err) {
|
|
ui.hideModal();
|
|
self.showCommandOutput({ success: 0, output: err && err.message ? err.message : err }, title);
|
|
});
|
|
},
|
|
|
|
showCommandOutput: function(result, title) {
|
|
var output = (result && result.output) ? result.output : _('Command finished.');
|
|
var tone = (result && result.success) ? 'info' : 'error';
|
|
ui.addNotification(null, E('div', {}, [
|
|
E('strong', {}, title),
|
|
E('pre', { 'style': 'white-space:pre-wrap;margin-top:8px;' }, output)
|
|
]), tone);
|
|
},
|
|
|
|
refreshStatus: function() {
|
|
var self = this;
|
|
return API.getStatus().then(function(newData) {
|
|
self.updateHeader(newData);
|
|
self.updateDiagnostics(newData.diagnostics);
|
|
});
|
|
}
|
|
});
|