secubox: add first-run and app wizards

This commit is contained in:
CyberMind-FR 2025-12-29 17:14:04 +01:00
parent 7c5ad8e53d
commit eab24f9609
15 changed files with 803 additions and 55 deletions

View File

@ -17,7 +17,7 @@ These notes summarize the repository structure, conventions, and supporting tool
- `luci-app-system-hub`: system health/remote assistance. - `luci-app-system-hub`: system health/remote assistance.
- `luci-app-network-modes`: prebuilt router/sniffer modes (bridges, AP, relay, etc.). - `luci-app-network-modes`: prebuilt router/sniffer modes (bridges, AP, relay, etc.).
- `luci-app-vhost-manager`: reverse-proxy/vhost configuration (existing baseline for future work). - `luci-app-vhost-manager`: reverse-proxy/vhost configuration (existing baseline for future work).
- **Tooling (`secubox-tools/`)** Bash/POSIX scripts for validation, building, deployment, permission repair, etc. The README documents workflows such as `validate-modules.sh`, `local-build.sh`, `fix-permissions.sh`, and `deploy-*.sh`. - **Tooling (`secubox-tools/`)** Bash/POSIX scripts for validation, building, deployment, permission repair, etc. The README documents workflows such as `validate-modules.sh`, `local-build.sh`, `fix-permissions.sh`, and `deploy-*.sh`. The newer `secubox-app` helper consumes manifests under `/usr/share/secubox/plugins/` to install and configure “apps”.
- **Automation & Docs** - **Automation & Docs**
- `DOCS/` + `docs/`: mirrored, versioned documentation tree (design system, prompts, module templates, validation, permissions, etc.). - `DOCS/` + `docs/`: mirrored, versioned documentation tree (design system, prompts, module templates, validation, permissions, etc.).
- `EXAMPLES/` and `templates/`: snippets and scaffolding. - `EXAMPLES/` and `templates/`: snippets and scaffolding.

View File

@ -23,8 +23,15 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
"volumes": ["/srv/zigbee2mqtt"], "volumes": ["/srv/zigbee2mqtt"],
"network": { "default_mode": "lan", "dmz_supported": true }, "network": { "default_mode": "lan", "dmz_supported": true },
"wizard": { "wizard": {
"uci": "/etc/config/zigbee2mqtt", "uci": { "config": "zigbee2mqtt", "section": "main" },
"steps": ["serial_port", "mqtt_host", "credentials", "frontend_port"] "fields": [
{ "id": "serial_port", "label": "Serial Port", "type": "text", "uci_option": "serial_port" },
{ "id": "mqtt_host", "label": "MQTT Host", "type": "text", "uci_option": "mqtt_host" },
{ "id": "mqtt_username", "label": "MQTT Username", "type": "text", "uci_option": "mqtt_username" },
{ "id": "mqtt_password", "label": "MQTT Password", "type": "password", "uci_option": "mqtt_password" },
{ "id": "base_topic", "label": "Base Topic", "type": "text", "uci_option": "base_topic" },
{ "id": "frontend_port", "label": "Frontend Port", "type": "number", "uci_option": "frontend_port" }
]
}, },
"profiles": ["home", "lab"], "profiles": ["home", "lab"],
"actions": { "actions": {
@ -51,14 +58,14 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
- `ports`: Document exposed services for the App Store UI. - `ports`: Document exposed services for the App Store UI.
- `volumes`: Persistent directories (e.g., `/srv/zigbee2mqtt`). - `volumes`: Persistent directories (e.g., `/srv/zigbee2mqtt`).
- `network`: Defaults + whether DMZ mode is supported. - `network`: Defaults + whether DMZ mode is supported.
- `wizard`: UCI file and logical steps for the future wizard UI. - `wizard`: UCI target plus the declarative field list consumed by the LuCI wizard.
- `profiles`: Tags to pre-load when applying OS-like profiles. - `profiles`: Tags to pre-load when applying OS-like profiles.
--- ---
## CLI Usage (`secubox-tools/secubox-app`) ## CLI Usage (`secubox-app`)
Copy or install `secubox-tools/secubox-app` on the router (ensure its executable). Commands: `luci-app-secubox` installs the CLI as `/usr/sbin/secubox-app` (also available under `secubox-tools/` for development). Commands:
```bash ```bash
# List manifests # List manifests
@ -92,4 +99,3 @@ The CLI relies on `opkg` and `jsonfilter`, so run it on the router (or within th
- Profiles can bundle manifests with specific network modes (e.g., DMZ + Zigbee2MQTT + Lyrion). - Profiles can bundle manifests with specific network modes (e.g., DMZ + Zigbee2MQTT + Lyrion).
For now, Zigbee2MQTT demonstrates the format. Additional manifests should follow the same schema to ensure the CLI and future UIs remain consistent. For now, Zigbee2MQTT demonstrates the format. Additional manifests should follow the same schema to ensure the CLI and future UIs remain consistent.

View File

@ -20,7 +20,7 @@ These notes capture the current repository structure, conventions, and supportin
`luci-theme-secubox` carries the design system (dark palette, `sh-*`/`sb-*` classes, Inter + JetBrains Mono). Every LuCI view imports `secubox-theme/secubox-theme.css` plus module-specific CSS as needed. `luci-theme-secubox` carries the design system (dark palette, `sh-*`/`sb-*` classes, Inter + JetBrains Mono). Every LuCI view imports `secubox-theme/secubox-theme.css` plus module-specific CSS as needed.
- **Tooling (`secubox-tools/`)** - **Tooling (`secubox-tools/`)**
Contains validation (`validate-modules.sh`), build automation (`local-build.sh`), permission repair, deployment helpers, and debug loggers. These scripts mirror GitHub Actions workflows and should be reused for any new installer/test tooling. Contains validation (`validate-modules.sh`), build automation (`local-build.sh`), permission repair, deployment helpers, and debug loggers. These scripts mirror GitHub Actions workflows and should be reused for new installer/test tooling. The `secubox-app` CLI now lives here as well, consuming manifests under `/usr/share/secubox/plugins/` to install and configure “apps”.
- **Automation/scripts (`scripts/`)** - **Automation/scripts (`scripts/`)**
Hosts documentation publishing helpers plus existing diagnostics/smoke scripts. New repo-wide diagnostics should live alongside them. Hosts documentation publishing helpers plus existing diagnostics/smoke scripts. New repo-wide diagnostics should live alongside them.

View File

@ -23,8 +23,15 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
"volumes": ["/srv/zigbee2mqtt"], "volumes": ["/srv/zigbee2mqtt"],
"network": { "default_mode": "lan", "dmz_supported": true }, "network": { "default_mode": "lan", "dmz_supported": true },
"wizard": { "wizard": {
"uci": "/etc/config/zigbee2mqtt", "uci": { "config": "zigbee2mqtt", "section": "main" },
"steps": ["serial_port", "mqtt_host", "credentials", "frontend_port"] "fields": [
{ "id": "serial_port", "label": "Serial Port", "type": "text", "uci_option": "serial_port", "placeholder": "/dev/ttyACM0" },
{ "id": "mqtt_host", "label": "MQTT Host", "type": "text", "uci_option": "mqtt_host", "placeholder": "mqtt://127.0.0.1:1883" },
{ "id": "mqtt_username", "label": "MQTT Username", "type": "text", "uci_option": "mqtt_username" },
{ "id": "mqtt_password", "label": "MQTT Password", "type": "password", "uci_option": "mqtt_password" },
{ "id": "base_topic", "label": "Base Topic", "type": "text", "uci_option": "base_topic" },
{ "id": "frontend_port", "label": "Frontend Port", "type": "number", "uci_option": "frontend_port" }
]
}, },
"profiles": ["home", "lab"], "profiles": ["home", "lab"],
"actions": { "actions": {
@ -51,14 +58,14 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
- `ports`: Document exposed services for the App Store UI. - `ports`: Document exposed services for the App Store UI.
- `volumes`: Persistent directories (e.g., `/srv/zigbee2mqtt`). - `volumes`: Persistent directories (e.g., `/srv/zigbee2mqtt`).
- `network`: Defaults + whether DMZ mode is supported. - `network`: Defaults + whether DMZ mode is supported.
- `wizard`: UCI file and logical steps for the future wizard UI. - `wizard`: Declarative form metadata (`uci.config`, `uci.section`, `fields[*].uci_option`).
- `profiles`: Tags to pre-load when applying OS-like profiles. - `profiles`: Tags to pre-load when applying OS-like profiles.
--- ---
## CLI Usage (`secubox-tools/secubox-app`) ## CLI Usage (`secubox-app`)
Copy or install `secubox-tools/secubox-app` on the router (ensure its executable). Commands: `luci-app-secubox` installs the CLI as `/usr/sbin/secubox-app` (also available under `secubox-tools/` for development). Commands:
```bash ```bash
# List manifests # List manifests
@ -92,4 +99,3 @@ The CLI relies on `opkg` and `jsonfilter`, so run it on the router (or within th
- Profiles can bundle manifests with specific network modes (e.g., DMZ + Zigbee2MQTT + Lyrion). - Profiles can bundle manifests with specific network modes (e.g., DMZ + Zigbee2MQTT + Lyrion).
For now, Zigbee2MQTT demonstrates the format. Additional manifests should follow the same schema to ensure the CLI and future UIs remain consistent. For now, Zigbee2MQTT demonstrates the format. Additional manifests should follow the same schema to ensure the CLI and future UIs remain consistent.

View File

@ -22,4 +22,12 @@ PKG_FILE_MODES:=/usr/libexec/rpcd/luci.secubox:root:root:755 \
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk
define Package/$(PKG_NAME)/install
$(call Package/luci/install,$(1))
$(INSTALL_DIR) $(1)/usr/share/secubox/plugins/zigbee2mqtt
$(INSTALL_DATA) $(CURDIR)/../plugins/zigbee2mqtt/manifest.json $(1)/usr/share/secubox/plugins/zigbee2mqtt/manifest.json
$(INSTALL_DIR) $(1)/usr/sbin
$(INSTALL_BIN) $(CURDIR)/../secubox-tools/secubox-app $(1)/usr/sbin/secubox-app
endef
# call BuildPackage - OpenWrt buildroot # call BuildPackage - OpenWrt buildroot

View File

@ -52,6 +52,12 @@ Auto-detection and status monitoring for all SecuBox modules:
- **CDN Cache** - Local caching proxy - **CDN Cache** - Local caching proxy
- **Virtual Host Manager** - Virtual host configuration - **Virtual Host Manager** - Virtual host configuration
### Wizard & App Store Integration
- First-run assistant to verify password, timezone, storage, and preferred network mode
- Manifest-driven app wizards (e.g., Zigbee2MQTT) surfaced directly inside SecuBox
- `secubox-app` CLI (installed under `/usr/sbin/`) for scripted installs/updates via manifests
- Plugins stored under `/usr/share/secubox/plugins/<app>/manifest.json` for easy expansion
## LuCI Menu Structure ## LuCI Menu Structure
The SecuBox hub organizes all modules into a hierarchical menu structure in LuCI: The SecuBox hub organizes all modules into a hierarchical menu structure in LuCI:

View File

@ -145,6 +145,35 @@ var callFixPermissions = rpc.declare({
expect: { success: false, message: '', output: '' } expect: { success: false, message: '', output: '' }
}); });
var callFirstRunStatus = rpc.declare({
object: 'luci.secubox',
method: 'first_run_status',
expect: { }
});
var callApplyFirstRun = rpc.declare({
object: 'luci.secubox',
method: 'apply_first_run'
});
var callListApps = rpc.declare({
object: 'luci.secubox',
method: 'list_apps',
expect: { apps: [] }
});
var callGetAppManifest = rpc.declare({
object: 'luci.secubox',
method: 'get_app_manifest',
params: ['app_id']
});
var callApplyAppWizard = rpc.declare({
object: 'luci.secubox',
method: 'apply_app_wizard',
params: ['app_id', 'values']
});
function formatUptime(seconds) { function formatUptime(seconds) {
if (!seconds) return '0s'; if (!seconds) return '0s';
var d = Math.floor(seconds / 86400); var d = Math.floor(seconds / 86400);
@ -188,6 +217,11 @@ return baseclass.extend({
dismissAlert: callDismissAlert, dismissAlert: callDismissAlert,
clearAlerts: callClearAlerts, clearAlerts: callClearAlerts,
fixPermissions: callFixPermissions, fixPermissions: callFixPermissions,
getFirstRunStatus: callFirstRunStatus,
applyFirstRun: callApplyFirstRun,
listApps: callListApps,
getAppManifest: callGetAppManifest,
applyAppWizard: callApplyAppWizard,
// Utilities // Utilities
formatUptime: formatUptime, formatUptime: formatUptime,
formatBytes: formatBytes formatBytes: formatBytes

View File

@ -577,3 +577,158 @@ pre {
box-shadow: 0 14px 28px rgba(239, 68, 68, 0.5); box-shadow: 0 14px 28px rgba(239, 68, 68, 0.5);
transform: translateY(-2px); transform: translateY(-2px);
} }
/* === Wizard Styles === */
.secubox-wizard-page {
display: flex;
flex-direction: column;
gap: 20px;
}
.sb-wizard-card {
background: var(--sh-bg-card);
border-radius: 16px;
border: 1px solid var(--sh-border);
padding: 20px;
box-shadow: 0 1px 3px var(--sh-shadow);
}
.sb-wizard-title {
font-weight: 600;
margin-bottom: 16px;
font-size: 18px;
}
.sb-wizard-steps {
display: flex;
flex-direction: column;
gap: 12px;
}
.sb-wizard-step {
border: 1px solid var(--sh-border);
border-radius: 12px;
padding: 12px 16px;
background: var(--sh-bg-secondary);
}
.sb-wizard-step.complete {
border-color: rgba(34, 197, 94, 0.4);
background: rgba(34, 197, 94, 0.08);
}
.sb-wizard-step-header {
display: flex;
gap: 12px;
align-items: center;
}
.sb-wizard-step-icon {
font-size: 20px;
}
.sb-wizard-step-label {
font-weight: 600;
}
.sb-wizard-step-desc {
font-size: 13px;
color: var(--sh-text-secondary);
}
.sb-wizard-step-body {
margin-top: 10px;
}
.sb-wizard-inline {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.sb-wizard-input,
.sb-wizard-select {
border-radius: 10px;
border: 1px solid var(--sh-border);
background: var(--sh-bg-primary);
color: var(--sh-text-primary);
padding: 8px 12px;
}
.sb-wizard-status {
padding: 6px 12px;
border-radius: 999px;
font-size: 11px;
text-transform: uppercase;
}
.sb-wizard-status.ok {
background: rgba(34, 197, 94, 0.12);
color: #22c55e;
}
.sb-wizard-status.warn {
background: rgba(245, 158, 11, 0.12);
color: #f59e0b;
}
.sb-app-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 12px;
}
.sb-app-card {
border: 1px solid var(--sh-border);
border-radius: 12px;
padding: 14px 16px;
background: var(--sh-bg-secondary);
display: flex;
flex-direction: column;
gap: 10px;
}
.sb-app-name {
font-weight: 600;
display: flex;
gap: 8px;
align-items: center;
}
.sb-app-version {
font-size: 12px;
color: var(--sh-text-secondary);
}
.sb-app-desc {
font-size: 13px;
color: var(--sh-text-secondary);
}
.sb-app-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.sb-app-state {
font-size: 12px;
text-transform: uppercase;
color: var(--sh-warning);
}
.sb-app-state.ok {
color: var(--sh-success);
}
.sb-form-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.sb-app-wizard-form label {
font-size: 13px;
color: var(--sh-text-secondary);
}

View File

@ -5,6 +5,7 @@
var tabs = [ var tabs = [
{ id: 'dashboard', icon: '🚀', label: _('Dashboard'), path: ['admin', 'secubox', 'dashboard'] }, { id: 'dashboard', icon: '🚀', label: _('Dashboard'), path: ['admin', 'secubox', 'dashboard'] },
{ id: 'modules', icon: '🧩', label: _('Modules'), path: ['admin', 'secubox', 'modules'] }, { id: 'modules', icon: '🧩', label: _('Modules'), path: ['admin', 'secubox', 'modules'] },
{ id: 'wizard', icon: '✨', label: _('Wizard'), path: ['admin', 'secubox', 'wizard'] },
{ id: 'monitoring', icon: '📡', label: _('Monitoring'), path: ['admin', 'secubox', 'monitoring'] }, { id: 'monitoring', icon: '📡', label: _('Monitoring'), path: ['admin', 'secubox', 'monitoring'] },
{ id: 'alerts', icon: '⚠️', label: _('Alerts'), path: ['admin', 'secubox', 'alerts'] }, { id: 'alerts', icon: '⚠️', label: _('Alerts'), path: ['admin', 'secubox', 'alerts'] },
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'settings'] }, { id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'settings'] },

View File

@ -0,0 +1,245 @@
'use strict';
'require view';
'require ui';
'require secubox/api as API';
'require secubox/nav as SecuNav';
var TIMEZONES = [
{ id: 'UTC', label: 'UTC' },
{ id: 'Europe/Paris', label: 'Europe/Paris' },
{ id: 'Europe/Berlin', label: 'Europe/Berlin' },
{ id: 'America/New_York', label: 'America/New_York' },
{ id: 'America/Los_Angeles', label: 'America/Los_Angeles' },
{ id: 'Asia/Singapore', label: 'Asia/Singapore' }
];
var NETWORK_MODES = [
{ id: 'router', label: _('Router (default)') },
{ id: 'dmz', label: _('Router + DMZ') }
];
return view.extend({
load: function() {
return Promise.all([
API.getFirstRunStatus(),
API.listApps()
]);
},
render: function(payload) {
this.firstRun = payload[0] || {};
this.appList = (payload[1] && payload[1].apps) || [];
var container = E('div', { 'class': 'secubox-wizard-page' }, [
E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/common.css') }),
SecuNav.renderTabs('wizard'),
this.renderHeader(),
this.renderFirstRunCard(),
this.renderAppsCard()
]);
return container;
},
renderHeader: function() {
return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [
E('div', {}, [
E('h2', { 'class': 'sh-page-title' }, [
E('span', { 'class': 'sh-page-title-icon' }, '🧭'),
_('Setup Wizard')
]),
E('p', { 'class': 'sh-page-subtitle' }, _('Guide the first-run experience and configure apps with manifest-driven wizards.'))
])
]);
},
renderFirstRunCard: function() {
var data = this.firstRun || {};
var steps = [
{ icon: '🔐', label: _('Secure Admin Account'), description: _('Set a LuCI/root password to protect the router.'), complete: !!data.password_set, content: this.renderPasswordStep(data) },
{ icon: '🌍', label: _('Timezone & Locale'), description: _('Align system time with your region.'), complete: false, content: this.renderTimezoneStep(data) },
{ icon: '💾', label: _('Storage Path'), description: _('Choose where SecuBox apps store data (USB/NAS recommended).'), complete: !!data.storage_ready, content: this.renderStorageStep(data) },
{ icon: '🛡️', label: _('Network Mode'), description: _('Pick a default SecuBox network mode (router or DMZ).'), complete: false, content: this.renderModeStep(data) }
];
return E('div', { 'class': 'sb-wizard-card' }, [
E('div', { 'class': 'sb-wizard-title' }, ['🧩 ', _('First-run Checklist')]),
E('div', { 'class': 'sb-wizard-steps' }, steps.map(function(step) {
return E('div', { 'class': 'sb-wizard-step' + (step.complete ? ' complete' : '') }, [
E('div', { 'class': 'sb-wizard-step-header' }, [
E('span', { 'class': 'sb-wizard-step-icon' }, step.icon),
E('div', {}, [
E('div', { 'class': 'sb-wizard-step-label' }, step.label),
E('div', { 'class': 'sb-wizard-step-desc' }, step.description)
])
]),
E('div', { 'class': 'sb-wizard-step-body' }, [step.content])
]);
}, this))
]);
},
renderPasswordStep: function(data) {
return E('div', { 'class': 'sb-wizard-inline' }, [
E('div', { 'class': 'sb-wizard-status ' + (data.password_set ? 'ok' : 'warn') }, data.password_set ? _('Password set') : _('Password missing')),
E('a', {
'class': 'cbi-button cbi-button-action',
'href': L.url('admin', 'system', 'admin')
}, _('Open password page'))
]);
},
renderTimezoneStep: function(data) {
var selected = data.timezone || 'UTC';
var select = E('select', { 'class': 'sb-wizard-select', 'id': 'wizard-timezone' }, TIMEZONES.map(function(zone) {
return E('option', { 'value': zone.id, 'selected': zone.id === selected }, zone.label);
}));
return E('div', { 'class': 'sb-wizard-inline' }, [
select,
E('button', {
'class': 'cbi-button cbi-button-action',
'click': this.applyTimezone.bind(this)
}, _('Apply'))
]);
},
renderStorageStep: function(data) {
var input = E('input', {
'class': 'sb-wizard-input',
'id': 'wizard-storage',
'value': data.storage_path || '/srv/secubox'
});
return E('div', { 'class': 'sb-wizard-inline' }, [
input,
E('button', {
'class': 'cbi-button cbi-button-action',
'click': this.prepareStorage.bind(this)
}, _('Prepare'))
]);
},
renderModeStep: function(data) {
var select = E('select', { 'class': 'sb-wizard-select', 'id': 'wizard-network-mode' }, NETWORK_MODES.map(function(mode) {
return E('option', { 'value': mode.id, 'selected': mode.id === data.network_mode }, mode.label);
}));
return E('div', { 'class': 'sb-wizard-inline' }, [
select,
E('button', {
'class': 'cbi-button cbi-button-action',
'click': this.applyNetworkMode.bind(this)
}, _('Switch'))
]);
},
renderAppsCard: function() {
var apps = this.appList || [];
return E('div', { 'class': 'sb-wizard-card' }, [
E('div', { 'class': 'sb-wizard-title' }, ['📦 ', _('App Wizards')]),
apps.length ? E('div', { 'class': 'sb-app-grid' }, apps.map(this.renderAppCard, this)) :
E('div', { 'class': 'secubox-empty-state' }, [
E('div', { 'class': 'secubox-empty-icon' }, '📭'),
E('div', { 'class': 'secubox-empty-title' }, _('No manifests detected')),
E('div', { 'class': 'secubox-empty-text' }, _('Install manifests under /usr/share/secubox/plugins/.'))
])
]);
},
renderAppCard: function(app) {
return E('div', { 'class': 'sb-app-card' }, [
E('div', { 'class': 'sb-app-card-info' }, [
E('div', { 'class': 'sb-app-name' }, [app.name || app.id, app.version ? E('span', { 'class': 'sb-app-version' }, 'v' + app.version) : '']),
E('div', { 'class': 'sb-app-desc' }, app.description || '')
]),
E('div', { 'class': 'sb-app-actions' }, [
E('span', { 'class': 'sb-app-state' + (app.state === 'installed' ? ' ok' : '') }, app.state || 'n/a'),
(app.has_wizard ? E('button', { 'class': 'cbi-button cbi-button-action', 'click': this.openAppWizard.bind(this, app) }, _('Configure')) : '')
])
]);
},
applyTimezone: function(ev) {
var tz = document.getElementById('wizard-timezone').value;
API.applyFirstRun({ timezone: tz }).then(this.reloadPage).catch(this.showError);
},
prepareStorage: function(ev) {
var path = document.getElementById('wizard-storage').value.trim();
if (!path) {
ui.addNotification(null, E('p', {}, _('Storage path is required')), 'error');
return;
}
API.applyFirstRun({ storage_path: path }).then(this.reloadPage).catch(this.showError);
},
applyNetworkMode: function(ev) {
var mode = document.getElementById('wizard-network-mode').value;
API.applyFirstRun({ network_mode: mode }).then(this.reloadPage).catch(this.showError);
},
reloadPage: function() {
ui.hideModal();
window.location.reload();
},
showError: function(err) {
ui.hideModal();
ui.addNotification(null, E('p', {}, err && err.message ? err.message : err), 'error');
},
openAppWizard: function(app, ev) {
var self = this;
ui.showModal(_('Loading %s wizard…').format(app.name || app.id), [E('div', { 'class': 'spinning' })]);
API.getAppManifest(app.id).then(function(manifest) {
ui.hideModal();
manifest = manifest || {};
var wizard = manifest.wizard || {};
var fields = wizard.fields || [];
if (!fields.length) {
ui.addNotification(null, E('p', {}, _('No wizard metadata for this app.')), 'warn');
return;
}
var form = E('div', { 'class': 'sb-app-wizard-form' }, fields.map(function(field) {
return E('div', { 'class': 'sb-form-group' }, [
E('label', {}, field.label || field.id),
E('input', {
'class': 'sb-wizard-input',
'name': field.id,
'type': field.type || 'text',
'placeholder': field.placeholder || ''
})
]);
}));
ui.showModal(_('Configure %s').format(app.name || app.id), [
form,
E('div', { 'class': 'right', 'style': 'margin-top:16px;' }, [
E('button', {
'class': 'cbi-button cbi-button-cancel',
'click': ui.hideModal
}, _('Cancel')),
E('button', {
'class': 'cbi-button cbi-button-action',
'click': function() {
self.submitAppWizard(app.id, form, fields);
}
}, _('Apply'))
])
]);
}).catch(this.showError);
},
submitAppWizard: function(appId, form, fields) {
var values = {};
fields.forEach(function(field) {
var input = form.querySelector('[name="' + field.id + '"]');
if (input && input.value !== '')
values[field.id] = input.value;
});
ui.showModal(_('Saving…'), [E('div', { 'class': 'spinning' })]);
API.applyAppWizard(appId, values).then(function(result) {
ui.hideModal();
if (result && result.success) {
ui.addNotification(null, E('p', {}, _('Wizard applied.')), 'info');
} else {
ui.addNotification(null, E('p', {}, _('Failed to apply wizard.')), 'error');
}
}).catch(this.showError);
}
});

View File

@ -54,6 +54,9 @@ get_pkg_version() {
PKG_VERSION="$(get_pkg_version)" PKG_VERSION="$(get_pkg_version)"
PLUGIN_DIR="/usr/share/secubox/plugins"
DEFAULT_STORAGE_PATH="/srv/secubox"
# Module registry - auto-detected from /usr/libexec/rpcd/ # Module registry - auto-detected from /usr/libexec/rpcd/
detect_modules() { detect_modules() {
local modules="" local modules=""
@ -72,6 +75,58 @@ detect_modules() {
MODULES=$(detect_modules) MODULES=$(detect_modules)
is_root_password_set() {
local hash
hash=$(grep '^root:' /etc/shadow 2>/dev/null | cut -d: -f2)
[ -n "$hash" ] && [ "$hash" != "!" ] && [ "$hash" != "*" ]
}
get_storage_path() {
local path
path=$(uci -q get secubox.main.storage_path)
if [ -z "$path" ]; then
path="$DEFAULT_STORAGE_PATH"
fi
echo "$path"
}
package_installed() {
local pkg="$1"
if command -v opkg >/dev/null 2>&1; then
opkg status "$pkg" >/dev/null 2>&1 && return 0
elif command -v apk >/dev/null 2>&1; then
apk info -e "$pkg" >/dev/null 2>&1 && return 0
fi
return 1
}
packages_state() {
local manifest="$1"
local total=0 installed=0 missing=0 pkg
for pkg in $(jsonfilter -s "$manifest" -e '@.packages[*]' 2>/dev/null); do
total=$((total + 1))
if package_installed "$pkg"; then
installed=$((installed + 1))
else
missing=$((missing + 1))
fi
done
if [ "$total" -eq 0 ]; then
echo "n/a"
elif [ "$missing" -eq 0 ]; then
echo "installed"
elif [ "$installed" -eq 0 ]; then
echo "missing"
else
echo "partial"
fi
}
ensure_directory() {
local dir="$1"
[ -d "$dir" ] || mkdir -p "$dir"
}
# Check if a module is installed (supports both opkg and apk) # Check if a module is installed (supports both opkg and apk)
check_module_installed() { check_module_installed() {
local module="$1" local module="$1"
@ -1090,6 +1145,185 @@ fix_permissions() {
} }
# Main dispatcher # Main dispatcher
first_run_status() {
local timezone storage network_json mode_name mode_id storage_path
storage_path=$(get_storage_path)
local storage_ready=0
[ -d "$storage_path" ] && storage_ready=1
local password_state=0
if is_root_password_set; then
password_state=1
fi
timezone=$(uci -q get system.@system[0].timezone || echo "UTC")
mode_id="unknown"
mode_name="N/A"
if command -v ubus >/dev/null 2>&1; then
network_json=$(ubus call luci.network-modes status 2>/dev/null || printf '')
if [ -n "$network_json" ]; then
mode_id=$(jsonfilter -s "$network_json" -e '@.current_mode' 2>/dev/null || echo "$mode_id")
mode_name=$(jsonfilter -s "$network_json" -e '@.mode_name' 2>/dev/null || echo "$mode_name")
fi
fi
json_init
json_add_boolean "password_set" "$password_state"
json_add_string "timezone" "$timezone"
json_add_string "storage_path" "$storage_path"
json_add_boolean "storage_ready" "$storage_ready"
json_add_string "network_mode" "$mode_id"
json_add_string "network_mode_name" "$mode_name"
json_add_array "recommended_modes"
json_add_string "" "router"
json_add_string "" "dmz"
json_close_array
json_dump
}
apply_first_run() {
local input
read input
json_load "$input"
json_get_var timezone timezone
json_get_var storage_path storage_path
json_get_var network_mode network_mode
json_init
json_add_array "messages"
if [ -n "$timezone" ]; then
uci set system.@system[0].timezone="$timezone"
uci set system.@system[0].zonename="$timezone"
uci commit system
if [ -f "/usr/share/zoneinfo/$timezone" ]; then
ln -sf "/usr/share/zoneinfo/$timezone" /etc/localtime
fi
json_add_string "" "Timezone updated"
fi
if [ -n "$storage_path" ]; then
mkdir -p "$storage_path"
chmod 755 "$storage_path"
uci set secubox.main.storage_path="$storage_path"
uci commit secubox
json_add_string "" "Storage prepared at $storage_path"
fi
if [ -n "$network_mode" ]; then
if command -v ubus >/dev/null 2>&1; then
if ubus call luci.network-modes set_mode "{\"mode\":\"$network_mode\"}" >/dev/null 2>&1; then
if ubus call luci.network-modes apply_mode >/dev/null 2>&1; then
json_add_string "" "Network mode $network_mode applied"
else
json_add_string "" "Failed to apply network mode"
fi
else
json_add_string "" "Mode $network_mode unsupported"
fi
else
json_add_string "" "ubus unavailable; cannot change network mode"
fi
fi
json_close_array
json_add_boolean "success" 1
json_dump
}
list_apps() {
ensure_directory "$PLUGIN_DIR"
json_init
json_add_array "apps"
local manifest_file
for manifest_file in "$PLUGIN_DIR"/*/manifest.json; do
[ -f "$manifest_file" ] || continue
local manifest
manifest=$(cat "$manifest_file")
local id name type version description state wizard_field
id=$(jsonfilter -s "$manifest" -e '@.id' 2>/dev/null)
name=$(jsonfilter -s "$manifest" -e '@.name' 2>/dev/null)
type=$(jsonfilter -s "$manifest" -e '@.type' 2>/dev/null)
version=$(jsonfilter -s "$manifest" -e '@.version' 2>/dev/null)
description=$(jsonfilter -s "$manifest" -e '@.description' 2>/dev/null)
[ -n "$id" ] || continue
state=$(packages_state "$manifest")
wizard_field=$(jsonfilter -s "$manifest" -e '@.wizard.fields[0].id' 2>/dev/null)
json_add_object
json_add_string "id" "$id"
json_add_string "name" "$name"
json_add_string "type" "$type"
json_add_string "version" "$version"
json_add_string "description" "$description"
json_add_string "state" "$state"
json_add_boolean "has_wizard" "$([ -n "$wizard_field" ] && echo 1 || echo 0)"
json_close_object
done
json_close_array
json_dump
}
get_app_manifest() {
local input app_id file manifest state
read input
app_id=$(jsonfilter -s "$input" -e '@.app_id' 2>/dev/null)
if [ -z "$app_id" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "app_id required"
json_dump
return
fi
file="$PLUGIN_DIR/$app_id/manifest.json"
if [ ! -f "$file" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Manifest not found"
json_dump
return
fi
manifest=$(cat "$file")
state=$(packages_state "$manifest")
json_load "$manifest"
json_add_string "state" "$state"
json_dump
}
apply_app_wizard() {
local input app_id file manifest config section fields field option value
read input
app_id=$(jsonfilter -s "$input" -e '@.app_id' 2>/dev/null)
if [ -z "$app_id" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "app_id required"
json_dump
return
fi
file="$PLUGIN_DIR/$app_id/manifest.json"
if [ ! -f "$file" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Manifest not found"
json_dump
return
fi
manifest=$(cat "$file")
config=$(jsonfilter -s "$manifest" -e '@.wizard.uci.config' 2>/dev/null)
section=$(jsonfilter -s "$manifest" -e '@.wizard.uci.section' 2>/dev/null)
[ -n "$section" ] || section="main"
if [ -z "$config" ]; then
json_init
json_add_boolean "success" 0
json_add_string "error" "Wizard missing UCI config"
json_dump
return
fi
fields=$(jsonfilter -s "$manifest" -e '@.wizard.fields[*].id' 2>/dev/null)
for field in $fields; do
option=$(jsonfilter -s "$manifest" -e "@.wizard.fields[@.id='$field'].uci_option" 2>/dev/null)
[ -n "$option" ] || continue
value=$(jsonfilter -s "$input" -e "@.values.$field" 2>/dev/null)
[ -n "$value" ] || continue
uci set ${config}.${section}.${option}="$value"
done
uci commit "$config"
json_init
json_add_boolean "success" 1
json_dump
}
case "$1" in case "$1" in
list) list)
json_init json_init
@ -1146,6 +1380,18 @@ case "$1" in
json_close_object json_close_object
json_add_object "fix_permissions" json_add_object "fix_permissions"
json_close_object json_close_object
json_add_object "first_run_status"
json_close_object
json_add_object "apply_first_run"
json_close_object
json_add_object "list_apps"
json_close_object
json_add_object "get_app_manifest"
json_add_string "app_id" "string"
json_close_object
json_add_object "apply_app_wizard"
json_add_string "app_id" "string"
json_close_object
json_dump json_dump
;; ;;
call) call)
@ -1243,9 +1489,24 @@ case "$1" in
clear_alerts) clear_alerts)
clear_alerts clear_alerts
;; ;;
fix_permissions) fix_permissions)
fix_permissions fix_permissions
;; ;;
first_run_status)
first_run_status
;;
apply_first_run)
apply_first_run
;;
list_apps)
list_apps
;;
get_app_manifest)
get_app_manifest
;;
apply_app_wizard)
apply_app_wizard
;;
*) *)
echo '{"error":"Unknown method"}' echo '{"error":"Unknown method"}'
;; ;;

View File

@ -14,6 +14,14 @@
"path": "secubox/dashboard" "path": "secubox/dashboard"
} }
}, },
"admin/secubox/wizard": {
"title": "First-Run Wizard",
"order": 15,
"action": {
"type": "view",
"path": "secubox/wizard"
}
},
"admin/secubox/modules": { "admin/secubox/modules": {
"title": "Modules", "title": "Modules",
"order": 20, "order": 20,
@ -242,4 +250,4 @@
"path": "network-modes/settings" "path": "network-modes/settings"
} }
} }
} }

View File

@ -1,21 +1,24 @@
{ {
"luci-app-secubox": { "luci-app-secubox": {
"description": "SecuBox Dashboard", "description": "SecuBox Dashboard",
"read": { "read": {
"ubus": { "ubus": {
"luci.secubox": [ "luci.secubox": [
"status", "status",
"modules", "modules",
"modules_by_category", "modules_by_category",
"module_info", "module_info",
"check_module_enabled", "check_module_enabled",
"health", "health",
"diagnostics", "diagnostics",
"get_system_health", "get_system_health",
"get_alerts", "get_alerts",
"get_dashboard_data", "get_dashboard_data",
"get_theme" "get_theme",
], "first_run_status",
"list_apps",
"get_app_manifest"
],
"uci": [ "uci": [
"get", "get",
"state" "state"
@ -25,20 +28,22 @@
"secubox" "secubox"
] ]
}, },
"write": { "write": {
"ubus": { "ubus": {
"luci.secubox": [ "luci.secubox": [
"start_module", "start_module",
"stop_module", "stop_module",
"restart_module", "restart_module",
"enable_module", "enable_module",
"disable_module", "disable_module",
"quick_action", "quick_action",
"set_theme", "set_theme",
"dismiss_alert", "dismiss_alert",
"clear_alerts", "clear_alerts",
"fix_permissions" "fix_permissions",
], "apply_first_run",
"apply_app_wizard"
],
"uci": [ "uci": [
"set", "set",
"delete", "delete",

View File

@ -18,12 +18,17 @@
"dmz_supported": true "dmz_supported": true
], ],
"wizard": { "wizard": {
"uci": "/etc/config/zigbee2mqtt", "uci": {
"steps": [ "config": "zigbee2mqtt",
"serial_port", "section": "main"
"mqtt_host", },
"credentials", "fields": [
"frontend_port" { "id": "serial_port", "label": "Serial Port", "type": "text", "uci_option": "serial_port", "placeholder": "/dev/ttyACM0" },
{ "id": "mqtt_host", "label": "MQTT Host", "type": "text", "uci_option": "mqtt_host", "placeholder": "mqtt://127.0.0.1:1883" },
{ "id": "mqtt_username", "label": "MQTT Username", "type": "text", "uci_option": "mqtt_username" },
{ "id": "mqtt_password", "label": "MQTT Password", "type": "password", "uci_option": "mqtt_password" },
{ "id": "base_topic", "label": "Base Topic", "type": "text", "uci_option": "base_topic", "placeholder": "zigbee2mqtt" },
{ "id": "frontend_port", "label": "Frontend Port", "type": "number", "uci_option": "frontend_port", "placeholder": "8080" }
] ]
}, },
"profiles": ["home", "lab"], "profiles": ["home", "lab"],

View File

@ -4,7 +4,15 @@
set -eu set -eu
PLUGINS_DIR="${SECUBOX_PLUGINS_DIR:-$(cd "$(dirname "$0")/../plugins" && pwd)}" SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
DEFAULT_PLUGINS_DIR="/usr/share/secubox/plugins"
if [ -n "${SECUBOX_PLUGINS_DIR:-}" ]; then
PLUGINS_DIR="$SECUBOX_PLUGINS_DIR"
elif [ -d "$SCRIPT_DIR/../plugins" ]; then
PLUGINS_DIR=$(cd "$SCRIPT_DIR/../plugins" && pwd)
else
PLUGINS_DIR="$DEFAULT_PLUGINS_DIR"
fi
OPKG_UPDATED=0 OPKG_UPDATED=0
info() { printf '[INFO] %s\n' "$*"; } info() { printf '[INFO] %s\n' "$*"; }
@ -25,7 +33,7 @@ Commands:
update <app-id> Run plugin update action or opkg upgrade update <app-id> Run plugin update action or opkg upgrade
Environment: Environment:
SECUBOX_PLUGINS_DIR Override plugin manifest directory (default: ../plugins) SECUBOX_PLUGINS_DIR Override manifest directory (default: /usr/share/secubox/plugins)
USAGE USAGE
} }