From eab24f9609487f43697bf0d8a5d0896c547f6d18 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 29 Dec 2025 17:14:04 +0100 Subject: [PATCH] secubox: add first-run and app wizards --- DOCS/ARCHITECTURE_NOTES.md | 2 +- DOCS/embedded/app-store.md | 18 +- docs/ARCHITECTURE_NOTES.md | 2 +- docs/embedded/app-store.md | 18 +- luci-app-secubox/Makefile | 8 + luci-app-secubox/README.md | 6 + .../luci-static/resources/secubox/api.js | 34 +++ .../luci-static/resources/secubox/common.css | 155 ++++++++++ .../luci-static/resources/secubox/nav.js | 1 + .../resources/view/secubox/wizard.js | 245 ++++++++++++++++ .../root/usr/libexec/rpcd/luci.secubox | 267 +++++++++++++++++- .../share/luci/menu.d/luci-app-secubox.json | 10 +- .../share/rpcd/acl.d/luci-app-secubox.json | 63 +++-- plugins/zigbee2mqtt/manifest.json | 17 +- secubox-tools/secubox-app | 12 +- 15 files changed, 803 insertions(+), 55 deletions(-) create mode 100644 luci-app-secubox/htdocs/luci-static/resources/view/secubox/wizard.js diff --git a/DOCS/ARCHITECTURE_NOTES.md b/DOCS/ARCHITECTURE_NOTES.md index c32df3f2..baebdc5d 100644 --- a/DOCS/ARCHITECTURE_NOTES.md +++ b/DOCS/ARCHITECTURE_NOTES.md @@ -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. diff --git a/DOCS/embedded/app-store.md b/DOCS/embedded/app-store.md index 866f6c96..b3fd2b42 100644 --- a/DOCS/embedded/app-store.md +++ b/DOCS/embedded/app-store.md @@ -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 it’s 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. - diff --git a/docs/ARCHITECTURE_NOTES.md b/docs/ARCHITECTURE_NOTES.md index d1434ae1..75eedaa6 100644 --- a/docs/ARCHITECTURE_NOTES.md +++ b/docs/ARCHITECTURE_NOTES.md @@ -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. diff --git a/docs/embedded/app-store.md b/docs/embedded/app-store.md index 866f6c96..bbb15a24 100644 --- a/docs/embedded/app-store.md +++ b/docs/embedded/app-store.md @@ -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 it’s 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. - diff --git a/luci-app-secubox/Makefile b/luci-app-secubox/Makefile index 8adc7a16..b05f1f7a 100644 --- a/luci-app-secubox/Makefile +++ b/luci-app-secubox/Makefile @@ -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 diff --git a/luci-app-secubox/README.md b/luci-app-secubox/README.md index 1dbe478e..fe061e3d 100644 --- a/luci-app-secubox/README.md +++ b/luci-app-secubox/README.md @@ -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//manifest.json` for easy expansion + ## LuCI Menu Structure The SecuBox hub organizes all modules into a hierarchical menu structure in LuCI: diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js b/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js index 31614058..83915da1 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js @@ -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 diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/common.css b/luci-app-secubox/htdocs/luci-static/resources/secubox/common.css index 8d6d346f..8c4c5293 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/common.css +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/common.css @@ -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); +} diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js b/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js index cff54134..617f51c4 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js @@ -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'] }, diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/wizard.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/wizard.js new file mode 100644 index 00000000..ef7ddfb2 --- /dev/null +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/wizard.js @@ -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); + } +}); diff --git a/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox b/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox index f2178d85..2c0c23b0 100755 --- a/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox +++ b/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox @@ -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"}' ;; diff --git a/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json b/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json index e27e652b..02868356 100644 --- a/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json +++ b/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json @@ -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" } } -} \ No newline at end of file +} diff --git a/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json b/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json index 11def90e..4cfdd0c8 100644 --- a/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json +++ b/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json @@ -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", diff --git a/plugins/zigbee2mqtt/manifest.json b/plugins/zigbee2mqtt/manifest.json index 4708c166..3cb59f49 100644 --- a/plugins/zigbee2mqtt/manifest.json +++ b/plugins/zigbee2mqtt/manifest.json @@ -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"], diff --git a/secubox-tools/secubox-app b/secubox-tools/secubox-app index 9cb4a5ed..ef2ea90d 100755 --- a/secubox-tools/secubox-app +++ b/secubox-tools/secubox-app @@ -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 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 }