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-network-modes`: prebuilt router/sniffer modes (bridges, AP, relay, etc.).
- `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**
- `DOCS/` + `docs/`: mirrored, versioned documentation tree (design system, prompts, module templates, validation, permissions, etc.).
- `EXAMPLES/` and `templates/`: snippets and scaffolding.

View File

@ -23,8 +23,15 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
"volumes": ["/srv/zigbee2mqtt"],
"network": { "default_mode": "lan", "dmz_supported": true },
"wizard": {
"uci": "/etc/config/zigbee2mqtt",
"steps": ["serial_port", "mqtt_host", "credentials", "frontend_port"]
"uci": { "config": "zigbee2mqtt", "section": "main" },
"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"],
"actions": {
@ -51,14 +58,14 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
- `ports`: Document exposed services for the App Store UI.
- `volumes`: Persistent directories (e.g., `/srv/zigbee2mqtt`).
- `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.
---
## 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
# 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).
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.
- **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/`)**
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"],
"network": { "default_mode": "lan", "dmz_supported": true },
"wizard": {
"uci": "/etc/config/zigbee2mqtt",
"steps": ["serial_port", "mqtt_host", "credentials", "frontend_port"]
"uci": { "config": "zigbee2mqtt", "section": "main" },
"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"],
"actions": {
@ -51,14 +58,14 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT):
- `ports`: Document exposed services for the App Store UI.
- `volumes`: Persistent directories (e.g., `/srv/zigbee2mqtt`).
- `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.
---
## 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
# 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).
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
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

View File

@ -52,6 +52,12 @@ Auto-detection and status monitoring for all SecuBox modules:
- **CDN Cache** - Local caching proxy
- **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
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: '' }
});
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) {
if (!seconds) return '0s';
var d = Math.floor(seconds / 86400);
@ -188,6 +217,11 @@ return baseclass.extend({
dismissAlert: callDismissAlert,
clearAlerts: callClearAlerts,
fixPermissions: callFixPermissions,
getFirstRunStatus: callFirstRunStatus,
applyFirstRun: callApplyFirstRun,
listApps: callListApps,
getAppManifest: callGetAppManifest,
applyAppWizard: callApplyAppWizard,
// Utilities
formatUptime: formatUptime,
formatBytes: formatBytes

View File

@ -577,3 +577,158 @@ pre {
box-shadow: 0 14px 28px rgba(239, 68, 68, 0.5);
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 = [
{ id: 'dashboard', icon: '🚀', label: _('Dashboard'), path: ['admin', 'secubox', 'dashboard'] },
{ 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: 'alerts', icon: '⚠️', label: _('Alerts'), path: ['admin', 'secubox', 'alerts'] },
{ 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)"
PLUGIN_DIR="/usr/share/secubox/plugins"
DEFAULT_STORAGE_PATH="/srv/secubox"
# Module registry - auto-detected from /usr/libexec/rpcd/
detect_modules() {
local modules=""
@ -72,6 +75,58 @@ 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_module_installed() {
local module="$1"
@ -1090,6 +1145,185 @@ fix_permissions() {
}
# 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
list)
json_init
@ -1146,6 +1380,18 @@ case "$1" in
json_close_object
json_add_object "fix_permissions"
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
;;
call)
@ -1243,9 +1489,24 @@ case "$1" in
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"}'
;;

View File

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

View File

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

View File

@ -18,12 +18,17 @@
"dmz_supported": true
],
"wizard": {
"uci": "/etc/config/zigbee2mqtt",
"steps": [
"serial_port",
"mqtt_host",
"credentials",
"frontend_port"
"uci": {
"config": "zigbee2mqtt",
"section": "main"
},
"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", "placeholder": "zigbee2mqtt" },
{ "id": "frontend_port", "label": "Frontend Port", "type": "number", "uci_option": "frontend_port", "placeholder": "8080" }
]
},
"profiles": ["home", "lab"],

View File

@ -4,7 +4,15 @@
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
info() { printf '[INFO] %s\n' "$*"; }
@ -25,7 +33,7 @@ Commands:
update <app-id> Run plugin update action or opkg upgrade
Environment:
SECUBOX_PLUGINS_DIR Override plugin manifest directory (default: ../plugins)
SECUBOX_PLUGINS_DIR Override manifest directory (default: /usr/share/secubox/plugins)
USAGE
}