From 92eff5aad74738edcc2badd4e29c58546198cb4c Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Mon, 29 Dec 2025 21:57:12 +0100 Subject: [PATCH] refactor secubox app packaging and theme --- DOCS/DOCUMENTATION-INDEX.md | 2 +- DOCS/embedded/app-store.md | 18 +- docs/documentation-index.md | 2 +- luci-app-secubox/Makefile | 11 +- .../profiles}/README.md | 0 .../profiles}/gateway_dmz.json | 0 .../profiles}/hardened.json | 0 .../profiles}/home.json | 0 .../profiles}/lab.json | 0 .../resources/system-hub/theme-assets.js | 18 + .../resources/view/system-hub/backup.js | 7 +- .../resources/view/system-hub/components.js | 7 +- .../resources/view/system-hub/dev-status.js | 5 +- .../resources/view/system-hub/diagnostics.js | 5 +- .../resources/view/system-hub/health.js | 7 +- .../resources/view/system-hub/logs.js | 7 +- .../resources/view/system-hub/overview.js | 7 +- .../resources/view/system-hub/remote.js | 5 +- .../resources/view/system-hub/services.js | 7 +- .../resources/view/system-hub/settings.js | 5 +- .../secubox-theme/system-hub/backup.css | 100 +++ .../secubox-theme/system-hub/common.css | 636 +++++++++++++ .../secubox-theme/system-hub/components.css | 280 ++++++ .../secubox-theme/system-hub/dashboard.css | 836 ++++++++++++++++++ .../secubox-theme/system-hub/health.css | 68 ++ .../secubox-theme/system-hub/logs.css | 247 ++++++ .../secubox-theme/system-hub/overview.css | 268 ++++++ .../secubox-theme/system-hub/services.css | 219 +++++ .../secubox/secubox-app-domoticz}/Makefile | 0 .../files/etc/config/domoticz | 0 .../files/etc/init.d/domoticz | 0 .../files/usr/sbin/domoticzctl | 0 .../secubox/secubox-app-lyrion}/Makefile | 0 .../files/etc/config/lyrion | 0 .../files/etc/init.d/lyrion | 0 .../files/usr/sbin/lyrionctl | 0 .../secubox/secubox-app-zigbee2mqtt}/Makefile | 0 .../files/etc/config/zigbee2mqtt | 0 .../files/etc/init.d/zigbee2mqtt | 0 .../files/usr/sbin/zigbee2mqttctl | 0 package/secubox/secubox-app/Makefile | 37 + .../secubox-app/files/usr/sbin/secubox-app | 253 ++++++ .../secubox/plugins}/domoticz/manifest.json | 0 .../secubox/plugins}/lyrion/manifest.json | 0 .../plugins}/zigbee2mqtt/manifest.json | 0 plugins | 1 + profiles | 1 + secubox-app-domoticz | 1 + secubox-app-lyrion | 1 + secubox-app-zigbee2mqtt | 1 + secubox-tools/local-build.sh | 45 +- secubox-tools/quick-deploy.sh | 228 ++++- secubox-tools/secubox-app | 219 +---- 53 files changed, 3270 insertions(+), 284 deletions(-) rename {profiles => luci-app-secubox/profiles}/README.md (100%) rename {profiles => luci-app-secubox/profiles}/gateway_dmz.json (100%) rename {profiles => luci-app-secubox/profiles}/hardened.json (100%) rename {profiles => luci-app-secubox/profiles}/home.json (100%) rename {profiles => luci-app-secubox/profiles}/lab.json (100%) create mode 100644 luci-app-system-hub/htdocs/luci-static/resources/system-hub/theme-assets.js create mode 100644 luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/backup.css create mode 100644 luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/common.css create mode 100644 luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/components.css create mode 100644 luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/dashboard.css create mode 100644 luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/health.css create mode 100644 luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/logs.css create mode 100644 luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/overview.css create mode 100644 luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/services.css rename {secubox-app-domoticz => package/secubox/secubox-app-domoticz}/Makefile (100%) rename {secubox-app-domoticz => package/secubox/secubox-app-domoticz}/files/etc/config/domoticz (100%) rename {secubox-app-domoticz => package/secubox/secubox-app-domoticz}/files/etc/init.d/domoticz (100%) rename {secubox-app-domoticz => package/secubox/secubox-app-domoticz}/files/usr/sbin/domoticzctl (100%) rename {secubox-app-lyrion => package/secubox/secubox-app-lyrion}/Makefile (100%) rename {secubox-app-lyrion => package/secubox/secubox-app-lyrion}/files/etc/config/lyrion (100%) rename {secubox-app-lyrion => package/secubox/secubox-app-lyrion}/files/etc/init.d/lyrion (100%) rename {secubox-app-lyrion => package/secubox/secubox-app-lyrion}/files/usr/sbin/lyrionctl (100%) rename {secubox-app-zigbee2mqtt => package/secubox/secubox-app-zigbee2mqtt}/Makefile (100%) rename {secubox-app-zigbee2mqtt => package/secubox/secubox-app-zigbee2mqtt}/files/etc/config/zigbee2mqtt (100%) rename {secubox-app-zigbee2mqtt => package/secubox/secubox-app-zigbee2mqtt}/files/etc/init.d/zigbee2mqtt (100%) rename {secubox-app-zigbee2mqtt => package/secubox/secubox-app-zigbee2mqtt}/files/usr/sbin/zigbee2mqttctl (100%) create mode 100644 package/secubox/secubox-app/Makefile create mode 100755 package/secubox/secubox-app/files/usr/sbin/secubox-app rename {plugins => package/secubox/secubox-app/files/usr/share/secubox/plugins}/domoticz/manifest.json (100%) rename {plugins => package/secubox/secubox-app/files/usr/share/secubox/plugins}/lyrion/manifest.json (100%) rename {plugins => package/secubox/secubox-app/files/usr/share/secubox/plugins}/zigbee2mqtt/manifest.json (100%) create mode 120000 plugins create mode 120000 profiles create mode 120000 secubox-app-domoticz create mode 120000 secubox-app-lyrion create mode 120000 secubox-app-zigbee2mqtt mode change 100755 => 120000 secubox-tools/secubox-app diff --git a/DOCS/DOCUMENTATION-INDEX.md b/DOCS/DOCUMENTATION-INDEX.md index 9297d1a..3916884 100644 --- a/DOCS/DOCUMENTATION-INDEX.md +++ b/DOCS/DOCUMENTATION-INDEX.md @@ -219,7 +219,7 @@ Pointer: see `docs/embedded/docker-zigbee2mqtt.md` for the canonical version. Pointer: see `docs/embedded/vhost-manager.md` for the canonical version. #### **embedded/app-store.md** 🛒 -*Plugin manifest format and CLI for the SecuBox App Store.* +*Manifest schema, `secubox-app` CLI usage, and packaged SecuBox apps (Zigbee2MQTT, Lyrion, Domoticz).* Pointer: see `docs/embedded/app-store.md` for the canonical version. diff --git a/DOCS/embedded/app-store.md b/DOCS/embedded/app-store.md index b3fd2b4..445c585 100644 --- a/DOCS/embedded/app-store.md +++ b/DOCS/embedded/app-store.md @@ -4,7 +4,7 @@ **Last Updated:** 2025-12-28 **Status:** Active -This guide outlines the initial “SecuBox Apps” registry format and the `secubox-app` CLI helper. It currently ships with a single manifest (Zigbee2MQTT), but the workflow scales to other Docker/LXC/native services such as Lyrion. +This guide outlines the “SecuBox Apps” registry format and the `secubox-app` CLI helper. The App Store currently ships manifests for Zigbee2MQTT, Lyrion Media Server, and Domoticz, with the workflow ready for additional Docker/LXC/native services. --- @@ -65,7 +65,7 @@ Each plugin folder contains a `manifest.json`. Example (Zigbee2MQTT): ## CLI Usage (`secubox-app`) -`luci-app-secubox` installs the CLI as `/usr/sbin/secubox-app` (also available under `secubox-tools/` for development). Commands: +`secubox-app` is shipped as a standalone OpenWrt package (see `package/secubox/secubox-app`) and installs the CLI at `/usr/sbin/secubox-app`. Commands: ```bash # List manifests @@ -92,6 +92,20 @@ The CLI relies on `opkg` and `jsonfilter`, so run it on the router (or within th --- +## Packaged SecuBox Apps + +`secubox-app-*` packages provide the runtime pieces behind each manifest (init scripts, helpers, and default configs). They are copied automatically by `secubox-tools/local-build.sh` into both firmware builds and the SDK feed, so developers get the same artifacts as the LuCI wizard and CLI. + +| Package | Manifest ID | Purpose | +|---------|-------------|---------| +| `secubox-app-zigbee2mqtt` | `zigbee2mqtt` | Installs Docker runner + `zigbee2mqttctl`, exposes splash/log helpers, and ships default UCI config. | +| `secubox-app-lyrion` | `lyrion` | Deploys the Lyrion Media Server container, CLI (`lyrionctl`), and profile hooks for HTTPS publishing. | +| `secubox-app-domoticz` | `domoticz` | Provides Domoticz Docker automation (`domoticzctl`) and the base data/service layout consumed by the wizard. | + +All three packages declare their dependencies (Docker, vhost manager, etc.) so `secubox-app install ` only has to orchestrate actions, not guess at required feeds. + +--- + ## Future Integration - LuCI App Store page will consume the same manifest directory to render cards, filters, and install buttons. diff --git a/docs/documentation-index.md b/docs/documentation-index.md index ffe4fb9..1df8853 100644 --- a/docs/documentation-index.md +++ b/docs/documentation-index.md @@ -219,7 +219,7 @@ Pointer: see `docs/embedded/docker-zigbee2mqtt.md` for the canonical version. Pointer: see `docs/embedded/vhost-manager.md` for the canonical version. #### **embedded/app-store.md** 🛒 -*Plugin manifest format and CLI for the SecuBox App Store.* +*Manifest schema, `secubox-app` CLI usage, and packaged SecuBox apps (Zigbee2MQTT, Lyrion, Domoticz).* Pointer: see `docs/embedded/app-store.md` for the canonical version. diff --git a/luci-app-secubox/Makefile b/luci-app-secubox/Makefile index 884c602..d9f295a 100644 --- a/luci-app-secubox/Makefile +++ b/luci-app-secubox/Makefile @@ -24,19 +24,10 @@ include $(TOPDIR)/feeds/luci/luci.mk define Package/$(PKG_NAME)/install $(call Package/luci/install,$(1)) - $(INSTALL_DIR) $(1)/usr/share/secubox/plugins - for dir in $(CURDIR)/../plugins/*; do \ - [ -d $$dir ] || continue; \ - name=$$(basename $$dir); \ - $(INSTALL_DIR) $(1)/usr/share/secubox/plugins/$$name; \ - $(INSTALL_DATA) $$dir/manifest.json $(1)/usr/share/secubox/plugins/$$name/manifest.json; \ - done $(INSTALL_DIR) $(1)/usr/share/secubox/profiles - for file in $(CURDIR)/../profiles/*.json; do \ + for file in $(CURDIR)/profiles/*.json; do \ $(INSTALL_DATA) $$file $(1)/usr/share/secubox/profiles/$$(basename $$file); \ done - $(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/profiles/README.md b/luci-app-secubox/profiles/README.md similarity index 100% rename from profiles/README.md rename to luci-app-secubox/profiles/README.md diff --git a/profiles/gateway_dmz.json b/luci-app-secubox/profiles/gateway_dmz.json similarity index 100% rename from profiles/gateway_dmz.json rename to luci-app-secubox/profiles/gateway_dmz.json diff --git a/profiles/hardened.json b/luci-app-secubox/profiles/hardened.json similarity index 100% rename from profiles/hardened.json rename to luci-app-secubox/profiles/hardened.json diff --git a/profiles/home.json b/luci-app-secubox/profiles/home.json similarity index 100% rename from profiles/home.json rename to luci-app-secubox/profiles/home.json diff --git a/profiles/lab.json b/luci-app-secubox/profiles/lab.json similarity index 100% rename from profiles/lab.json rename to luci-app-secubox/profiles/lab.json diff --git a/luci-app-system-hub/htdocs/luci-static/resources/system-hub/theme-assets.js b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/theme-assets.js new file mode 100644 index 0000000..14f0381 --- /dev/null +++ b/luci-app-system-hub/htdocs/luci-static/resources/system-hub/theme-assets.js @@ -0,0 +1,18 @@ +'use strict'; + +return { + stylesheet: function(name) { + var primary = L.resource('secubox-theme/system-hub/' + name); + var fallback = L.resource('system-hub/' + name); + var link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = primary; + link.onerror = function() { + if (link.dataset.fallbackApplied) return; + link.dataset.fallbackApplied = '1'; + console.warn('System Hub theme asset missing:', primary, 'falling back to', fallback); + link.href = fallback; + }; + return link; + } +}; diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js index 37501d7..0e603e3 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/backup.js @@ -3,6 +3,7 @@ 'require ui'; 'require system-hub/api as API'; 'require secubox-theme/theme as Theme'; +'require system-hub/theme-assets as ThemeAssets'; 'require system-hub/nav as HubNav'; var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || @@ -23,9 +24,9 @@ return view.extend({ render: function() { return E('div', { 'class': 'system-hub-dashboard sh-backup-view' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/backup.css') }), + ThemeAssets.stylesheet('common.css'), + ThemeAssets.stylesheet('dashboard.css'), + ThemeAssets.stylesheet('backup.css'), HubNav.renderTabs('backup'), this.renderHeader(), this.renderHero(), diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js index 368fc79..4eac97a 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/components.js @@ -5,6 +5,7 @@ 'require poll'; 'require system-hub/api as API'; 'require secubox-theme/theme as Theme'; +'require system-hub/theme-assets as ThemeAssets'; 'require system-hub/nav as HubNav'; var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || @@ -27,9 +28,9 @@ return view.extend({ var view = E('div', { 'class': 'system-hub-dashboard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/components.css') }), + ThemeAssets.stylesheet('common.css'), + ThemeAssets.stylesheet('dashboard.css'), + ThemeAssets.stylesheet('components.css'), HubNav.renderTabs('components'), diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/dev-status.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/dev-status.js index 6a74875..f3bb2a7 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/dev-status.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/dev-status.js @@ -1,6 +1,7 @@ 'use strict'; 'require view'; 'require secubox-theme/theme as Theme'; +'require system-hub/theme-assets as ThemeAssets'; 'require system-hub/dev-status-widget as DevStatusWidget'; 'require system-hub/nav as HubNav'; @@ -24,8 +25,8 @@ return view.extend({ var widget = this.getWidget(); var container = E('div', { 'class': 'system-hub-dev-status' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), + ThemeAssets.stylesheet('common.css'), + ThemeAssets.stylesheet('dashboard.css'), HubNav.renderTabs('dev-status'), this.renderHeader(), this.renderSummaryGrid(), diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js index 8438cc4..be79d5b 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/diagnostics.js @@ -5,6 +5,7 @@ 'require fs'; 'require secubox-theme/theme as Theme'; 'require system-hub/api as API'; +'require system-hub/theme-assets as ThemeAssets'; 'require system-hub/nav as HubNav'; var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || @@ -23,8 +24,8 @@ return view.extend({ var view = E('div', { 'class': 'system-hub-dashboard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), + ThemeAssets.stylesheet('common.css'), + ThemeAssets.stylesheet('dashboard.css'), HubNav.renderTabs('diagnostics'), // Collect Diagnostics diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js index d9bbedf..a30dd6a 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/health.js @@ -5,6 +5,7 @@ 'require poll'; 'require system-hub/api as API'; 'require secubox-theme/theme as Theme'; +'require system-hub/theme-assets as ThemeAssets'; 'require system-hub/nav as HubNav'; var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || @@ -24,9 +25,9 @@ return view.extend({ var container = E('div', { 'class': 'system-hub-dashboard sh-health-view' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/health.css') }), + ThemeAssets.stylesheet('common.css'), + ThemeAssets.stylesheet('dashboard.css'), + ThemeAssets.stylesheet('health.css'), HubNav.renderTabs('health'), this.renderHero(), this.renderMetricGrid(), diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js index 12e34a0..2923b2a 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/logs.js @@ -5,6 +5,7 @@ 'require poll'; 'require system-hub/api as API'; 'require secubox-theme/theme as Theme'; +'require system-hub/theme-assets as ThemeAssets'; 'require system-hub/nav as HubNav'; var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || @@ -29,9 +30,9 @@ return view.extend({ var container = E('div', { 'class': 'sh-logs-view' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/logs.css') }), + ThemeAssets.stylesheet('common.css'), + ThemeAssets.stylesheet('dashboard.css'), + ThemeAssets.stylesheet('logs.css'), HubNav.renderTabs('logs'), this.renderHero(), this.renderControls(), diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js index 0cdd3af..db6c327 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js @@ -5,6 +5,7 @@ 'require poll'; 'require system-hub/api as API'; 'require secubox-theme/theme as Theme'; +'require system-hub/theme-assets as ThemeAssets'; 'require system-hub/nav as HubNav'; var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || @@ -29,9 +30,9 @@ return view.extend({ var container = E('div', { 'class': 'sh-overview' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/overview.css') }), + ThemeAssets.stylesheet('common.css'), + ThemeAssets.stylesheet('dashboard.css'), + ThemeAssets.stylesheet('overview.css'), HubNav.renderTabs('overview'), this.renderPageHeader(), this.renderInfoGrid(), diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js index ada8c66..de29034 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/remote.js @@ -4,6 +4,7 @@ 'require ui'; 'require secubox-theme/theme as Theme'; 'require system-hub/api as API'; +'require system-hub/theme-assets as ThemeAssets'; 'require system-hub/nav as HubNav'; var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || @@ -21,8 +22,8 @@ return view.extend({ var view = E('div', { 'class': 'system-hub-dashboard' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), + ThemeAssets.stylesheet('common.css'), + ThemeAssets.stylesheet('dashboard.css'), HubNav.renderTabs('remote'), // RustDesk Section diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js index fe225ae..9cb95d9 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/services.js @@ -5,6 +5,7 @@ 'require poll'; 'require system-hub/api as API'; 'require secubox-theme/theme as Theme'; +'require system-hub/theme-assets as ThemeAssets'; 'require system-hub/nav as HubNav'; var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || @@ -26,9 +27,9 @@ return view.extend({ var container = E('div', { 'class': 'sh-services-view' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/services.css') }), + ThemeAssets.stylesheet('common.css'), + ThemeAssets.stylesheet('dashboard.css'), + ThemeAssets.stylesheet('services.css'), HubNav.renderTabs('services'), this.renderHeader(), this.renderControls(), diff --git a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js index 1c311eb..9156320 100644 --- a/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js +++ b/luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/settings.js @@ -3,6 +3,7 @@ 'require ui'; 'require system-hub/api as API'; 'require secubox-theme/theme as Theme'; +'require system-hub/theme-assets as ThemeAssets'; 'require system-hub/nav as HubNav'; var shLang = (typeof L !== 'undefined' && L.env && L.env.lang) || @@ -24,8 +25,8 @@ return view.extend({ var container = E('div', { 'class': 'system-hub-dashboard sh-settings-view' }, [ E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('system-hub/dashboard.css') }), + ThemeAssets.stylesheet('common.css'), + ThemeAssets.stylesheet('dashboard.css'), HubNav.renderTabs('settings'), this.renderHeader(), this.renderGeneralSection(), diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/backup.css b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/backup.css new file mode 100644 index 0000000..a009e11 --- /dev/null +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/backup.css @@ -0,0 +1,100 @@ +.sh-backup-view { + padding: 28px; +} + +.sh-backup-hero { + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 16px; + padding: 24px; + border-radius: 20px; + background: var(--sh-gradient-soft); + border: 1px solid var(--sh-border); + box-shadow: var(--sh-shadow); + margin-bottom: 24px; +} + +.sh-hero-eyebrow { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.2em; + color: var(--sh-text-secondary); + margin-bottom: 6px; + display: inline-block; +} + +.sh-backup-hero h1 { + margin: 0 0 6px; + font-size: 26px; +} + +.sh-backup-hero p { + margin: 0; + color: var(--sh-text-secondary); +} + +.sh-hero-badges { + display: flex; + gap: 12px; + align-items: center; +} + +.sh-hero-badge { + padding: 12px 18px; + border-radius: 16px; + background: rgba(15,23,42,0.5); + border: 1px solid rgba(255,255,255,0.08); + min-width: 140px; +} + +.sh-hero-badge .label { + display: block; + font-size: 11px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--sh-text-secondary); +} + +.sh-hero-badge strong { + font-size: 18px; +} + +.sh-backup-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 20px; +} + +.sh-text-muted { + color: var(--sh-text-secondary); + font-size: 14px; + line-height: 1.6; +} + +.sh-upload { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 16px; + margin: 16px 0; + border: 1px dashed var(--sh-border); + border-radius: 14px; + color: var(--sh-text-secondary); + cursor: pointer; + transition: all 0.2s; +} + +.sh-upload:hover { + background: rgba(255,255,255,0.04); +} + +.sh-upload input { + display: none; +} + +.sh-action-row { + display: flex; + justify-content: flex-end; +} diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/common.css b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/common.css new file mode 100644 index 0000000..1eb5a8b --- /dev/null +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/common.css @@ -0,0 +1,636 @@ +/** + * System Hub - Common Styles (Demo-inspired) + * Shared styles across all System Hub pages + * Version: 0.3.0 - Matching https://cybermind.fr/apps/system-hub/demo.html + */ + +/* === Import Fonts === */ +@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap'); + +/* === Variables (Demo-inspired Dark Mode) === */ +:root, +[data-secubox-theme="light"] { + /* Light Mode (less used) */ + --sh-text-primary: #0f172a; + --sh-text-secondary: #475569; + --sh-bg-primary: #ffffff; + --sh-bg-secondary: #f8fafc; + --sh-bg-tertiary: #f1f5f9; + --sh-bg-card: #ffffff; + --sh-border: #e2e8f0; + --sh-hover-bg: #f8fafc; + --sh-hover-shadow: rgba(0, 0, 0, 0.1); + --sh-primary: #6366f1; + --sh-primary-end: #8b5cf6; + --sh-shadow: rgba(0, 0, 0, 0.08); + --sh-success: #22c55e; + --sh-danger: #ef4444; + --sh-warning: #f59e0b; +} + +[data-theme="dark"], +[data-secubox-theme="dark"] { + /* Demo-inspired Dark Palette */ + --sh-text-primary: #fafafa; + --sh-text-secondary: #a0a0b0; + --sh-bg-primary: #0a0a0f; + --sh-bg-secondary: #12121a; + --sh-bg-tertiary: #1a1a24; + --sh-bg-card: #12121a; + --sh-border: #2a2a35; + --sh-hover-bg: #1a1a24; + --sh-hover-shadow: rgba(0, 0, 0, 0.6); + --sh-primary: #6366f1; + --sh-primary-end: #8b5cf6; + --sh-shadow: rgba(0, 0, 0, 0.4); + --sh-success: #22c55e; + --sh-danger: #ef4444; + --sh-warning: #f59e0b; +} + +/* === Global Typography === */ +body, +.system-hub-dashboard, +.sh-page-header, +.sh-card { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; +} + +code, +.sh-mono, +.sh-id-display, +pre { + font-family: 'JetBrains Mono', 'Courier New', monospace; +} + +/* === Page Header === */ +.sh-page-header { + margin-bottom: 24px; + padding: 24px; + background: var(--sh-bg-card); + border-radius: 16px; + border: 1px solid var(--sh-border); + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 20px; +} + +.sh-page-title { + font-size: 20px; + font-weight: 700; + margin: 0; + background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + display: flex; + align-items: center; + gap: 12px; +} + +.sh-page-title-icon { + font-size: 24px; + line-height: 1; + -webkit-text-fill-color: initial; +} + +.sh-page-subtitle { + margin: 4px 0 0 0; + font-size: 14px; + color: var(--sh-text-secondary); + font-weight: 500; +} + +/* === Stats Badges (Compact Demo Style) === */ +.sh-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); + gap: 12px; + margin: 20px 0; +} + +.sh-stat-badge { + display: flex; + flex-direction: column; + align-items: center; + padding: 16px 12px; + background: var(--sh-bg-card); + border: 1px solid var(--sh-border); + border-radius: 12px; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.sh-stat-badge::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end)); + opacity: 0; + transition: opacity 0.3s ease; +} + +.sh-stat-badge:hover { + transform: translateY(-3px); + box-shadow: 0 8px 20px var(--sh-shadow); + border-color: var(--sh-primary); +} + +.sh-stat-badge:hover::before { + opacity: 1; +} + +.sh-stat-value { + font-size: 28px; + font-weight: 700; + line-height: 1; + margin-bottom: 6px; + color: var(--sh-text-primary); + font-family: 'JetBrains Mono', monospace; +} + +.sh-stat-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.8px; + color: var(--sh-text-secondary); +} + +/* === Navigation Tabs (Internal Demo Style) === */ +.sh-nav-tabs { + display: flex; + gap: 8px; + background: var(--sh-bg-secondary); + padding: 8px; + border-radius: 12px; + border: 1px solid var(--sh-border); + margin-bottom: 24px; + overflow-x: auto; + position: sticky; + top: 0; + z-index: 100; + backdrop-filter: blur(10px); +} + +.sh-nav-tab { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 18px; + background: transparent; + border: none; + border-radius: 8px; + cursor: pointer; + transition: all 0.2s ease; + font-size: 14px; + font-weight: 600; + color: var(--sh-text-secondary); + white-space: nowrap; + position: relative; +} + +.sh-nav-tab:hover { + background: var(--sh-hover-bg); + color: var(--sh-text-primary); +} + +/* Hide default LuCI tabs (we render SecuNav instead) */ +body[data-page^="admin-secubox-system-system-hub"] .tabs, +body[data-page^="admin-secubox-system-system-hub"] #tabmenu, +body[data-page^="admin-secubox-system-system-hub"] .cbi-tabmenu, +body[data-page^="admin-secubox-system-system-hub"] .nav-tabs { + display: none !important; +} + +.sh-nav-tab.active { + color: var(--sh-primary); + background: rgba(99, 102, 241, 0.1); +} + +.sh-nav-tab.active::after { + content: ''; + position: absolute; + bottom: 0; + left: 10%; + right: 10%; + height: 2px; + background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end)); + border-radius: 2px; +} + +/* === Filter Tabs === */ +.sh-service-tabs, +.sh-component-tabs { + margin-bottom: 24px; +} + +/* === Cards (with colored top border) === */ +.sh-card { + background: var(--sh-bg-card); + border-radius: 16px; + border: 1px solid var(--sh-border); + overflow: hidden; + transition: all 0.3s ease; + margin-bottom: 20px; + position: relative; +} + +.sh-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, var(--sh-primary), var(--sh-primary-end)); + opacity: 0; + transition: opacity 0.3s ease; +} + +.sh-card:hover { + transform: translateY(-3px); + box-shadow: 0 12px 28px var(--sh-hover-shadow); + border-color: var(--sh-border); +} + +.sh-card:hover::before { + opacity: 1; +} + +/* Colored borders for different card types */ +.sh-card-success::before { + background: var(--sh-success); + opacity: 1; +} + +.sh-card-danger::before { + background: var(--sh-danger); + opacity: 1; +} + +.sh-card-warning::before { + background: var(--sh-warning); + opacity: 1; +} + +.sh-card-header { + padding: 20px 24px; + background: var(--sh-bg-secondary); + border-bottom: 1px solid var(--sh-border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.sh-card-title { + font-size: 18px; + font-weight: 700; + color: var(--sh-text-primary); + display: flex; + align-items: center; + gap: 10px; + margin: 0; +} + +.sh-card-title-icon { + font-size: 22px; + line-height: 1; +} + +.sh-card-badge { + padding: 6px 14px; + background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end)); + color: #ffffff; + border-radius: 20px; + font-size: 12px; + font-weight: 700; +} + +.sh-card-body { + padding: 24px; +} + +/* === Empty State === */ +.sh-empty-state { + text-align: center; + padding: 60px 20px; + background: var(--sh-bg-secondary); + border-radius: 12px; + border: 2px dashed var(--sh-border); +} + +.sh-empty-icon { + font-size: 64px; + margin-bottom: 16px; + opacity: 0.5; +} + +.sh-empty-text { + font-size: 16px; + color: var(--sh-text-secondary); + font-weight: 600; +} + +/* === Buttons (Gradient Style) === */ +.sh-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 10px 20px; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + text-decoration: none; + border: none; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; + overflow: hidden; +} + +.sh-btn-primary { + background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end)); + color: #ffffff; + box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3); +} + +.sh-btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(99, 102, 241, 0.5); +} + +.sh-btn-success { + background: var(--sh-success); + color: #ffffff; + box-shadow: 0 4px 12px rgba(34, 197, 94, 0.3); +} + +.sh-btn-success:hover { + background: #16a34a; + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(34, 197, 94, 0.5); +} + +.sh-btn-danger { + background: var(--sh-danger); + color: #ffffff; + box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3); +} + +.sh-btn-danger:hover { + background: #dc2626; + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(239, 68, 68, 0.5); +} + +.sh-btn-warning { + background: var(--sh-warning); + color: #ffffff; + box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3); +} + +.sh-btn-warning:hover { + background: #d97706; + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(245, 158, 11, 0.5); +} + +.sh-btn-secondary { + background: var(--sh-bg-tertiary); + color: var(--sh-text-primary); + border: 1px solid var(--sh-border); +} + +.sh-btn-secondary:hover { + background: var(--sh-hover-bg); + border-color: var(--sh-primary); + transform: translateY(-2px); +} + +/* === ID Display (Monospace) === */ +.sh-id-display { + font-size: 24px; + font-weight: 700; + font-family: 'JetBrains Mono', monospace; + color: var(--sh-text-primary); + background: var(--sh-bg-secondary); + padding: 12px 20px; + border-radius: 8px; + border: 1px solid var(--sh-border); + text-align: center; + letter-spacing: 2px; +} + +/* === Gradient Text Utility === */ +.sh-gradient-text { + background: linear-gradient(135deg, var(--sh-primary), var(--sh-primary-end)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +/* === Responsive === */ +@media (max-width: 768px) { + .sh-page-title { + font-size: 22px; + } + + .sh-stats-grid { + grid-template-columns: repeat(2, 1fr); + } + + .sh-nav-tabs { + gap: 4px; + padding: 6px; + } + + .sh-nav-tab { + padding: 8px 14px; + font-size: 13px; + } + +/* === Development Status Bonus Tab === */ +.sh-page-insight { + display: flex; + flex-direction: column; + align-items: flex-end; + text-align: right; + background: var(--sh-bg-card); + border: 1px solid var(--sh-border); + padding: 16px 20px; + border-radius: 12px; + min-width: 220px; +} + +.sh-page-insight-label { + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.6px; + color: var(--sh-text-secondary); + font-weight: 600; +} + +.sh-page-insight-value { + font-size: 18px; + font-weight: 700; + color: var(--sh-text-primary); + margin: 6px 0 4px; +} + +.sh-page-insight-sub { + font-size: 13px; + color: var(--sh-primary); + font-family: 'JetBrains Mono', monospace; +} + +.sh-dev-status-grid .sh-stat-badge { + min-height: 110px; +} + +.sh-dev-status-widget-shell { + margin-top: 12px; +} + +.sh-dev-status-widget-shell #dev-status-widget { + margin-top: 8px; +} + +.sh-dev-status-note { + margin-top: 20px; + padding: 14px 16px; + border-radius: 12px; + background: var(--sh-bg-secondary); + border: 1px dashed var(--sh-border); + font-size: 13px; + color: var(--sh-text-secondary); + display: flex; + align-items: center; + gap: 8px; +} +} + +/* === Dark Mode Overrides === */ +[data-theme="dark"] .sh-card, +[data-theme="dark"] .sh-stat-badge, +[data-theme="dark"] .sh-nav-tabs, +[data-theme="dark"] .sh-page-header { + background: var(--sh-bg-card); + border-color: var(--sh-border); +} + +[data-theme="dark"] .sh-card-header, +[data-theme="dark"] .sh-nav-tabs { + background: var(--sh-bg-secondary); + border-color: var(--sh-border); +} + +[data-theme="dark"] .sh-nav-tab { + background: transparent; +} + +[data-theme="dark"] .sh-nav-tab:hover { + background: var(--sh-hover-bg); +} + +[data-theme="dark"] .sh-btn-secondary { + background: var(--sh-bg-tertiary); + border-color: var(--sh-border); + color: var(--sh-text-primary); +} + +[data-theme="dark"] .sh-btn-secondary:hover { + background: var(--sh-hover-bg); +} + +[data-theme="dark"] .sh-empty-state { + background: var(--sh-bg-secondary); + border-color: var(--sh-border); +} + +[data-theme="dark"] .sh-id-display { + background: var(--sh-bg-secondary); + border-color: var(--sh-border); +} + + +/* Slim header utility */ +.sh-page-header-lite { + background: #f7f9fc; + border: 1px solid rgba(148, 163, 184, 0.35); + border-radius: 18px; + padding: 14px 20px; + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; + flex-wrap: wrap; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05); +} + +.sh-header-meta { + display: flex; + gap: 10px; + flex-wrap: wrap; + align-items: center; +} + +.sh-header-chip { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + border-radius: 999px; + background: #ffffff; + border: 1px solid rgba(148, 163, 184, 0.3); + font-size: 13px; + font-weight: 600; + color: var(--sh-text-secondary); +} + +.sh-header-chip strong { + display: block; + color: var(--sh-text-primary); + font-size: 14px; +} + +.sh-chip-text { + line-height: 1.1; +} + +.sh-chip-label { + display: block; + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--sh-text-muted, #94a3b8); +} + +.sh-chip-icon { + font-size: 16px; +} + +.sh-header-chip.success { + background: rgba(34, 197, 94, 0.12); + border-color: rgba(34, 197, 94, 0.4); + color: #15803d; +} + +.sh-header-chip.danger { + background: rgba(239, 68, 68, 0.12); + border-color: rgba(239, 68, 68, 0.45); + color: #b91c1c; +} + +.sh-header-chip.warn { + background: rgba(245, 158, 11, 0.12); + border-color: rgba(245, 158, 11, 0.45); + color: #b45309; +} diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/components.css b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/components.css new file mode 100644 index 0000000..8bf422b --- /dev/null +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/components.css @@ -0,0 +1,280 @@ +/** + * System Hub - Components Page Styles + * Responsive card layout with theme support + * Version: 0.2.2 + */ + +/* === Header & Filters === */ +.sh-components-header { + margin-bottom: 24px; +} + +.sh-page-title { + font-size: 28px; + font-weight: 700; + margin: 0 0 20px 0; + color: var(--sh-text-primary, #1e293b); + display: flex; + align-items: center; + gap: 12px; +} + +.sh-title-icon { + font-size: 32px; + line-height: 1; +} + +.sh-component-tabs { + width: 100%; +} + +/* === Components Grid === */ +.sh-components-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); + gap: 20px; + margin-top: 24px; +} + +@media (max-width: 768px) { + .sh-components-grid { + grid-template-columns: 1fr; + } +} + +/* === Component Card === */ +.sh-component-card { + background: var(--sh-bg-card, #ffffff); + border-radius: 12px; + border: 1px solid var(--sh-border, #e2e8f0); + overflow: hidden; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + position: relative; +} + +.sh-component-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px var(--sh-hover-shadow, rgba(0, 0, 0, 0.12)); +} + +.sh-component-card-header { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 20px; + background: var(--sh-bg-secondary, #f8fafc); + border-bottom: 1px solid var(--sh-border, #e2e8f0); +} + +.sh-component-icon { + font-size: 36px; + line-height: 1; + flex-shrink: 0; +} + +.sh-component-info { + flex: 1; + min-width: 0; +} + +.sh-component-name { + font-size: 18px; + font-weight: 700; + margin: 0 0 8px 0; + color: var(--sh-text-primary, #1e293b); +} + +.sh-component-meta { + display: flex; + gap: 12px; + flex-wrap: wrap; + font-size: 13px; +} + +.sh-component-version, +.sh-component-category { + padding: 4px 10px; + border-radius: 6px; + font-weight: 600; +} + +.sh-component-version { + background: var(--sh-primary, #6366f1); + color: #ffffff; +} + +.sh-component-category { + background: var(--sh-bg-tertiary, #f1f5f9); + color: var(--sh-text-secondary, #64748b); + text-transform: capitalize; +} + +/* === Status Indicator === */ +.sh-status-indicator { + width: 14px; + height: 14px; + border-radius: 50%; + flex-shrink: 0; + margin-top: 4px; +} + +.sh-status-running { + background: #22c55e; + animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; +} + +.sh-status-stopped { + background: #f59e0b; +} + +.sh-status-not-installed { + background: #94a3b8; +} + +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } +} + +/* === Component Card Body === */ +.sh-component-card-body { + padding: 16px 20px; +} + +.sh-component-description { + margin: 0; + font-size: 14px; + line-height: 1.6; + color: var(--sh-text-secondary, #64748b); +} + +/* === Component Actions === */ +.sh-component-card-actions { + display: flex; + gap: 10px; + padding: 16px 20px; + background: var(--sh-bg-secondary, #f8fafc); + border-top: 1px solid var(--sh-border, #e2e8f0); + flex-wrap: wrap; +} + +.sh-action-btn { + flex: 1; + min-width: fit-content; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 16px; + border-radius: 8px; + font-size: 14px; + font-weight: 600; + text-decoration: none; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.sh-btn-success { + background: #22c55e; + color: #ffffff; +} + +.sh-btn-success:hover { + background: #16a34a; + transform: translateY(-2px); +} + +.sh-btn-danger { + background: #ef4444; + color: #ffffff; +} + +.sh-btn-danger:hover { + background: #dc2626; + transform: translateY(-2px); +} + +.sh-btn-warning { + background: #f59e0b; + color: #ffffff; +} + +.sh-btn-warning:hover { + background: #d97706; + transform: translateY(-2px); +} + +.sh-btn-primary { + background: var(--sh-primary, #6366f1); + color: #ffffff; +} + +.sh-btn-primary:hover { + background: #4f46e5; + transform: translateY(-2px); + text-decoration: none; +} + +.sh-btn-secondary { + background: var(--sh-bg-tertiary, #f1f5f9); + color: var(--sh-text-secondary, #64748b); + cursor: not-allowed; + opacity: 0.6; +} + +/* === Empty State === */ +.sh-empty-state { + grid-column: 1 / -1; + text-align: center; + padding: 60px 20px; + background: var(--sh-bg-secondary, #f8fafc); + border-radius: 12px; + border: 2px dashed var(--sh-border, #e2e8f0); +} + +.sh-empty-icon { + font-size: 64px; + margin-bottom: 16px; + opacity: 0.5; +} + +.sh-empty-text { + font-size: 16px; + color: var(--sh-text-secondary, #64748b); + font-weight: 600; +} + +/* === Dark Mode Support === */ +[data-theme="dark"] { + --sh-text-primary: #f1f5f9; + --sh-text-secondary: #cbd5e1; + --sh-bg-primary: #0f172a; + --sh-bg-secondary: #1e293b; + --sh-bg-tertiary: #334155; + --sh-bg-card: #1e293b; + --sh-border: #334155; + --sh-hover-bg: #334155; + --sh-hover-shadow: rgba(0, 0, 0, 0.4); +} + +[data-theme="dark"] .sh-component-card { + background: var(--sh-bg-card); + border-color: var(--sh-border); +} + +[data-theme="dark"] .sh-component-card-header, +[data-theme="dark"] .sh-component-card-actions { + background: var(--sh-bg-tertiary); + border-color: var(--sh-border); +} + +[data-theme="dark"] .sh-component-category { + background: var(--sh-bg-tertiary); + color: var(--sh-text-secondary); +} + +[data-theme="dark"] .sh-empty-state { + background: var(--sh-bg-secondary); + border-color: var(--sh-border); +} diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/dashboard.css b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/dashboard.css new file mode 100644 index 0000000..7bf84e5 --- /dev/null +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/dashboard.css @@ -0,0 +1,836 @@ +/* System Hub Dashboard - Central Control Theme * Version: 0.3.0 + */ +/* Copyright (C) 2024 CyberMind.fr - Gandalf * Version: 0.3.0 + */ +/* Theme-aware styles with dark/light mode support */ + +/* Common variables (theme-independent) */ +:root { + --sh-accent-indigo: #6366f1; + --sh-accent-violet: #8b5cf6; + --sh-accent-blue: #3b82f6; + --sh-accent-cyan: #06b6d4; + --sh-accent-green: #22c55e; + --sh-accent-amber: #f59e0b; + --sh-accent-red: #ef4444; + + --sh-success: #22c55e; + --sh-warning: #f59e0b; + --sh-danger: #ef4444; + --sh-info: #3b82f6; + + --sh-gradient: linear-gradient(135deg, #6366f1, #8b5cf6, #a855f7); + --sh-gradient-soft: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(139, 92, 246, 0.1)); + + --sh-font-mono: 'JetBrains Mono', 'Fira Code', monospace; + --sh-font-sans: 'Inter', -apple-system, sans-serif; + + --sh-radius: 8px; + --sh-radius-lg: 12px; +} + +/* Dark theme (default) */ +:root, +[data-theme="dark"], +[data-secubox-theme="dark"] { + --sh-bg-primary: #0a0a0f; + --sh-bg-secondary: #12121a; + --sh-bg-tertiary: #1a1a24; + --sh-border: #2a2a3a; + --sh-border-light: #3a3a4a; + + --sh-text-primary: #fafafa; + --sh-text-secondary: #a0a0b0; + --sh-text-muted: #707080; + + --sh-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); + --sh-shadow-glow: 0 0 30px rgba(99, 102, 241, 0.3); +} + +/* Light theme */ +[data-theme="light"], +[data-secubox-theme="light"] { + --sh-bg-primary: #f5f5f7; + --sh-bg-secondary: #ffffff; + --sh-bg-tertiary: #f9fafb; + --sh-border: #e5e7eb; + --sh-border-light: #d1d5db; + + --sh-text-primary: #0a0a0f; + --sh-text-secondary: #4b5563; + --sh-text-muted: #9ca3af; + + --sh-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + --sh-shadow-glow: 0 0 30px rgba(99, 102, 241, 0.2); +} + +/* Base */ +.system-hub-dashboard { + font-family: var(--sh-font-sans); + background: var(--sh-bg-primary); + color: var(--sh-text-primary); + min-height: 100vh; + padding: 16px; +} + +.system-hub-dashboard * { box-sizing: border-box; } + +/* Header */ +.sh-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0 20px; + border-bottom: 1px solid var(--sh-border); + margin-bottom: 20px; +} + +.sh-logo { + display: flex; + align-items: center; + gap: 14px; +} + +.sh-logo-icon { + width: 52px; + height: 52px; + background: var(--sh-gradient); + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + font-size: 26px; + box-shadow: var(--sh-shadow-glow); + position: relative; +} + +.sh-logo-icon::after { + content: ''; + position: absolute; + inset: -2px; + background: var(--sh-gradient); + border-radius: 16px; + z-index: -1; + opacity: 0.4; + filter: blur(12px); +} + +.sh-logo-text { + font-size: 26px; + font-weight: 700; +} + +.sh-logo-text span { + background: var(--sh-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +/* Health Score */ +.sh-health-score { + display: flex; + align-items: center; + gap: 16px; + padding: 12px 20px; + background: var(--sh-bg-secondary); + border-radius: var(--sh-radius-lg); + border: 1px solid var(--sh-border); +} + +.sh-score-circle { + width: 60px; + height: 60px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-family: var(--sh-font-mono); + font-size: 20px; + font-weight: 800; + position: relative; +} + +.sh-score-circle::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 50%; + border: 4px solid var(--sh-border); +} + +.sh-score-circle.healthy { background: rgba(34, 197, 94, 0.15); color: var(--sh-success); border-color: var(--sh-success); } +.sh-score-circle.warning { background: rgba(245, 158, 11, 0.15); color: var(--sh-warning); border-color: var(--sh-warning); } +.sh-score-circle.critical { background: rgba(239, 68, 68, 0.15); color: var(--sh-danger); border-color: var(--sh-danger); } + +.sh-score-circle::before { + border-color: currentColor; + opacity: 0.3; +} + +.sh-score-info { + display: flex; + flex-direction: column; + gap: 2px; +} + +.sh-score-label { + font-size: 14px; + font-weight: 600; +} + +.sh-score-status { + font-size: 12px; + color: var(--sh-text-muted); +} + +/* Stats Grid */ +.sh-stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 14px; + margin-bottom: 24px; +} + +.sh-stat-card { + background: var(--sh-bg-secondary); + border: 1px solid var(--sh-border); + border-radius: var(--sh-radius-lg); + padding: 18px; + text-align: center; + transition: all 0.3s; +} + +.sh-stat-card:hover { + transform: translateY(-3px); + box-shadow: var(--sh-shadow); + border-color: var(--sh-accent-indigo); +} + +.sh-stat-icon { font-size: 26px; margin-bottom: 8px; } +.sh-stat-value { + font-size: 28px; + font-weight: 800; + font-family: var(--sh-font-mono); + background: var(--sh-gradient); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.sh-stat-label { + font-size: 11px; + color: var(--sh-text-muted); + text-transform: uppercase; + letter-spacing: 0.5px; + margin-top: 4px; +} + +/* Card */ +.sh-card { + background: var(--sh-bg-secondary); + border: 1px solid var(--sh-border); + border-radius: var(--sh-radius-lg); + overflow: hidden; + margin-bottom: 20px; +} + +.sh-card-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--sh-border); + background: rgba(0, 0, 0, 0.3); +} + +.sh-card-title { + display: flex; + align-items: center; + gap: 12px; + font-size: 16px; + font-weight: 600; +} + +.sh-card-title-icon { font-size: 22px; } + +.sh-card-badge { + font-family: var(--sh-font-mono); + font-size: 12px; + font-weight: 600; + padding: 5px 12px; + border-radius: 16px; + background: var(--sh-gradient); + color: white; +} + +.sh-card-body { padding: 20px; } + +/* Settings + inputs */ +.sh-settings-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 16px; +} + +.sh-settings-grid--compact { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); +} + +.sh-input-group { + display: flex; + flex-direction: column; + gap: 6px; +} + +.sh-input-label { + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--sh-text-secondary); +} + +.sh-input { + width: 100%; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid var(--sh-border); + background: var(--sh-bg-secondary); + color: var(--sh-text-primary); + font-family: var(--sh-font-sans); + font-size: 13px; + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.sh-input:focus { + outline: none; + border-color: var(--sh-primary); + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.15); +} + +.sh-threshold-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 16px; +} + +.sh-threshold-row { + background: var(--sh-bg-secondary); + border: 1px solid var(--sh-border); + border-radius: 12px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.sh-threshold-label { + font-weight: 600; + color: var(--sh-text-primary); +} + +.sh-threshold-inputs { + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.sh-threshold-inputs label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 11px; + text-transform: uppercase; + color: var(--sh-text-secondary); + flex: 1; + min-width: 120px; +} + +.sh-support-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 14px; +} + +.sh-support-card { + background: var(--sh-bg-secondary); + border: 1px solid var(--sh-border); + border-radius: 12px; + padding: 14px; + display: flex; + flex-direction: column; + gap: 6px; +} + +.sh-support-label { + font-size: 11px; + text-transform: uppercase; + color: var(--sh-text-secondary); + letter-spacing: 0.06em; +} + +/* Component Grid */ +.sh-components-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; +} + +.sh-component-card { + background: var(--sh-bg-tertiary); + border: 1px solid var(--sh-border); + border-radius: var(--sh-radius-lg); + padding: 20px; + position: relative; + overflow: hidden; + transition: all 0.3s; +} + +.sh-component-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--component-color); +} + +.sh-component-card:hover { + transform: translateY(-3px); + box-shadow: var(--sh-shadow); +} + +.sh-component-card.planned { + opacity: 0.6; + border-style: dashed; +} + +.sh-component-header { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 12px; +} + +.sh-component-info { + display: flex; + align-items: center; + gap: 12px; +} + +.sh-component-icon { + width: 44px; + height: 44px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--component-color); + color: var(--component-color); +} + +.sh-component-name { + font-size: 15px; + font-weight: 700; +} + +.sh-component-desc { + font-size: 11px; + color: var(--sh-text-muted); +} + +.sh-component-status { + padding: 4px 10px; + border-radius: 12px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; +} + +.sh-component-status.running { + background: rgba(34, 197, 94, 0.15); + color: var(--sh-success); +} + +.sh-component-status.stopped { + background: rgba(239, 68, 68, 0.15); + color: var(--sh-danger); +} + +.sh-component-status.planned { + background: rgba(99, 102, 241, 0.15); + color: var(--sh-accent-indigo); +} + +.sh-component-actions { + display: flex; + gap: 8px; + margin-top: 14px; + padding-top: 14px; + border-top: 1px solid var(--sh-border); +} + +.sh-component-action { + flex: 1; + padding: 8px 12px; + border-radius: 6px; + border: 1px solid var(--sh-border); + background: var(--sh-bg-secondary); + color: var(--sh-text-secondary); + font-size: 11px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + text-align: center; +} + +.sh-component-action:hover { + border-color: var(--sh-accent-indigo); + color: var(--sh-text-primary); +} + +/* Health Metrics */ +.sh-health-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; +} + +.sh-health-metric { + background: var(--sh-bg-tertiary); + border: 1px solid var(--sh-border); + border-radius: var(--sh-radius); + padding: 16px; +} + +.sh-metric-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12px; +} + +.sh-metric-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 600; +} + +.sh-metric-icon { font-size: 18px; } + +.sh-metric-value { + font-family: var(--sh-font-mono); + font-size: 14px; + font-weight: 600; +} + +.sh-metric-value.ok { color: var(--sh-success); } +.sh-metric-value.warning { color: var(--sh-warning); } +.sh-metric-value.critical { color: var(--sh-danger); } + +.sh-progress-bar { + height: 8px; + background: var(--sh-bg-primary); + border-radius: 4px; + overflow: hidden; +} + +.sh-progress-fill { + height: 100%; + border-radius: 4px; + transition: width 0.5s; +} + +.sh-progress-fill.ok { background: var(--sh-success); } +.sh-progress-fill.warning { background: var(--sh-warning); } +.sh-progress-fill.critical { background: var(--sh-danger); } + +/* Remote Section */ +.sh-remote-card { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.1), rgba(139, 92, 246, 0.05)); + border-color: rgba(99, 102, 241, 0.3); +} + +.sh-remote-id { + display: flex; + align-items: center; + gap: 16px; + padding: 20px; + background: var(--sh-bg-tertiary); + border-radius: var(--sh-radius); + margin-bottom: 16px; +} + +.sh-remote-id-icon { + width: 60px; + height: 60px; + background: var(--sh-gradient); + border-radius: 12px; + display: flex; + align-items: center; + justify-content: center; + font-size: 28px; +} + +.sh-remote-id-value { + font-family: var(--sh-font-mono); + font-size: 28px; + font-weight: 800; + letter-spacing: 2px; +} + +.sh-remote-id-label { + font-size: 12px; + color: var(--sh-text-muted); +} + +/* Logs */ +.sh-log-list { + max-height: 400px; + overflow-y: auto; +} + +.sh-log-item { + display: flex; + gap: 12px; + padding: 10px; + border-bottom: 1px solid var(--sh-border); + font-size: 12px; +} + +.sh-log-item:last-child { border-bottom: none; } + +.sh-log-time { + font-family: var(--sh-font-mono); + font-size: 10px; + color: var(--sh-text-muted); + min-width: 140px; +} + +.sh-log-source { + padding: 2px 8px; + border-radius: 4px; + font-size: 9px; + font-weight: 600; + background: var(--sh-bg-tertiary); + color: var(--sh-accent-indigo); + min-width: 80px; + text-align: center; +} + +.sh-log-level { + padding: 2px 8px; + border-radius: 4px; + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + min-width: 55px; + text-align: center; +} + +.sh-log-level.info { background: rgba(59, 130, 246, 0.15); color: var(--sh-info); } +.sh-log-level.warning { background: rgba(245, 158, 11, 0.15); color: var(--sh-warning); } +.sh-log-level.error { background: rgba(239, 68, 68, 0.15); color: var(--sh-danger); } + +.sh-log-message { flex: 1; color: var(--sh-text-secondary); } + +/* Toggle */ +.sh-toggle { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + background: var(--sh-bg-tertiary); + border-radius: var(--sh-radius); + margin-bottom: 10px; +} + +.sh-toggle-info { display: flex; align-items: center; gap: 12px; } +.sh-toggle-icon { font-size: 20px; } +.sh-toggle-label { font-size: 14px; font-weight: 500; } +.sh-toggle-desc { font-size: 11px; color: var(--sh-text-muted); } + +.sh-toggle-switch { + width: 48px; + height: 26px; + background: var(--sh-bg-primary); + border-radius: 13px; + position: relative; + cursor: pointer; + transition: background 0.3s; +} + +.sh-toggle-switch.active { background: var(--sh-accent-indigo); } + +.sh-toggle-switch::after { + content: ''; + position: absolute; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + top: 3px; + left: 3px; + transition: transform 0.3s; +} + +.sh-toggle-switch.active::after { transform: translateX(22px); } + +/* Form */ +.sh-form-group { margin-bottom: 16px; } +.sh-form-label { display: block; font-size: 13px; font-weight: 600; margin-bottom: 8px; color: var(--sh-text-secondary); } +.sh-form-hint { font-size: 11px; color: var(--sh-text-muted); margin-top: 6px; } + +.sh-input, +.sh-select { + width: 100%; + padding: 12px 16px; + background: var(--sh-bg-primary); + border: 1px solid var(--sh-border); + border-radius: var(--sh-radius); + color: var(--sh-text-primary); + font-size: 14px; + font-family: var(--sh-font-mono); + transition: border-color 0.2s; +} + +.sh-input:focus, +.sh-select:focus { + outline: none; + border-color: var(--sh-accent-indigo); +} + +/* Buttons */ +.sh-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + border: 1px solid var(--sh-border); + border-radius: var(--sh-radius); + background: var(--sh-bg-tertiary); + color: var(--sh-text-primary); + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.sh-btn:hover { border-color: var(--sh-accent-indigo); } + +.sh-btn-primary { + background: var(--sh-gradient); + border: none; + color: white; +} + +.sh-btn-primary:hover { + box-shadow: var(--sh-shadow-glow); + transform: translateY(-2px); +} + +.sh-btn-success { background: var(--sh-success); border: none; color: white; } +.sh-btn-danger { background: var(--sh-danger); border: none; color: white; } + +.sh-btn-group { display: flex; gap: 12px; flex-wrap: wrap; } + +/* Roadmap */ +.sh-roadmap-item { + display: flex; + align-items: center; + gap: 16px; + padding: 14px; + background: var(--sh-bg-tertiary); + border: 1px dashed var(--sh-border); + border-radius: var(--sh-radius); + margin-bottom: 10px; +} + +.sh-roadmap-icon { + width: 40px; + height: 40px; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 20px; + background: rgba(99, 102, 241, 0.1); + border: 1px solid var(--sh-accent-indigo); + color: var(--sh-accent-indigo); +} + +.sh-roadmap-info { flex: 1; } +.sh-roadmap-name { font-size: 14px; font-weight: 600; } +.sh-roadmap-desc { font-size: 11px; color: var(--sh-text-muted); } + +.sh-roadmap-date { + padding: 6px 12px; + background: var(--sh-bg-primary); + border-radius: 8px; + font-family: var(--sh-font-mono); + font-size: 12px; + color: var(--sh-accent-indigo); +} + +/* System Info */ +.sh-sysinfo-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 10px; +} + +.sh-sysinfo-item { + display: flex; + justify-content: space-between; + padding: 10px 14px; + background: var(--sh-bg-tertiary); + border-radius: 6px; +} + +.sh-sysinfo-label { + font-size: 12px; + color: var(--sh-text-muted); +} + +.sh-sysinfo-value { + font-family: var(--sh-font-mono); + font-size: 12px; + font-weight: 600; +} + +/* Responsive */ +@media (max-width: 768px) { + .sh-header { flex-direction: column; gap: 16px; align-items: flex-start; } + .sh-stats-grid { grid-template-columns: repeat(2, 1fr); } + .sh-components-grid { grid-template-columns: 1fr; } + .sh-health-grid { grid-template-columns: 1fr; } + .sh-btn-group { flex-direction: column; } + .sh-btn { width: 100%; justify-content: center; } +} + +/* Scrollbar */ +.system-hub-dashboard ::-webkit-scrollbar { width: 8px; height: 8px; } +.system-hub-dashboard ::-webkit-scrollbar-track { background: var(--sh-bg-tertiary); } +.system-hub-dashboard ::-webkit-scrollbar-thumb { background: var(--sh-border); border-radius: 4px; } +.system-hub-dashboard ::-webkit-scrollbar-thumb:hover { background: var(--sh-text-muted); } + +/* Animations */ +@keyframes pulse-sh { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.6; } +} + +.sh-status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + animation: pulse-sh 1.5s ease-in-out infinite; +} + +.sh-status-dot.running { background: var(--sh-success); } +.sh-status-dot.stopped { background: var(--sh-danger); } +.sh-status-dot.warning { background: var(--sh-warning); } diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/health.css b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/health.css new file mode 100644 index 0000000..384485f --- /dev/null +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/health.css @@ -0,0 +1,68 @@ +.sh-health-view { padding: 28px; } +.sh-health-hero { + display: flex; + justify-content: space-between; + align-items: center; + padding: 24px; + background: var(--sh-gradient-soft); + border-radius: 20px; + border: 1px solid var(--sh-border); + box-shadow: var(--sh-shadow); + margin-bottom: 24px; +} +.sh-health-score { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 140px; + height: 140px; + border-radius: 50%; + border: 8px solid rgba(255,255,255,0.08); + font-size: 32px; + font-weight: 700; +} +.sh-health-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} +.sh-health-card { + background: var(--sh-bg-secondary); + border-radius: 16px; + border: 1px solid var(--sh-border); + padding: 16px; + box-shadow: var(--sh-shadow); +} +.sh-health-value { font-size: 28px; font-weight: 700; } +.sh-health-bar { + margin-top: 12px; + height: 6px; + background: rgba(255,255,255,0.08); + border-radius: 999px; + overflow: hidden; +} +.sh-health-bar-fill { + height: 100%; + background: linear-gradient(90deg,#22c55e,#3b82f6); + transition: width 0.3s ease; +} +.sh-summary-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 16px; + margin-bottom: 24px; +} +.sh-summary-card { + background: var(--sh-bg-secondary); + border: 1px solid var(--sh-border); + border-radius: 16px; + padding: 18px; + box-shadow: var(--sh-shadow); +} +.sh-reco-list { list-style: none; padding: 0; margin: 0; } +.sh-reco-list li { padding: 10px; border-bottom: 1px solid var(--sh-border); } +@media (max-width: 720px) { + .sh-health-hero { flex-direction: column; gap: 16px; } +} diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/logs.css b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/logs.css new file mode 100644 index 0000000..6b3dd19 --- /dev/null +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/logs.css @@ -0,0 +1,247 @@ +.sh-logs-view { + padding: 28px; + background: radial-gradient(circle at top, rgba(248,113,113,0.08), transparent), + radial-gradient(circle at bottom, rgba(14,165,233,0.08), transparent), + var(--sh-bg); + border-radius: 22px; +} + +.sh-logs-hero { + background: linear-gradient(135deg, rgba(248,113,113,0.16), rgba(248,159,99,0.16)); + border-radius: 22px; + padding: 24px; + border: 1px solid var(--sh-border); + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 18px; + box-shadow: 0 25px 40px rgba(0,0,0,0.35); +} + +.sh-logs-hero h1 { + margin: 0; + font-size: 26px; +} + +.sh-logs-hero p { + margin: 6px 0 0; + color: var(--sh-text-secondary); +} + +.sh-log-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; + min-width: 280px; +} + +.sh-log-stat { + background: rgba(15,23,42,0.45); + border-radius: 16px; + padding: 12px; + border: 1px solid rgba(255,255,255,0.08); + text-align: center; +} + +.sh-log-stat .label { + display: block; + font-size: 12px; + text-transform: uppercase; + color: var(--sh-text-secondary); + letter-spacing: 0.12em; +} + +.sh-log-stat .value { + font-size: 28px; + font-weight: 700; + line-height: 1; + font-family: 'JetBrains Mono', monospace; +} + +.sh-log-stat.danger .value { + color: #f87171; +} + +.sh-log-stat.warn .value { + color: #fbbf24; +} + +.sh-log-controls { + margin: 26px 0 20px; + display: flex; + flex-wrap: wrap; + gap: 16px; + align-items: center; +} + +.sh-log-search { + flex: 1; + min-width: 260px; +} + +.sh-log-search input { + width: 100%; + border-radius: 16px; + border: 1px solid rgba(255,255,255,0.12); + background: rgba(255,255,255,0.02); + padding: 12px 16px; + color: var(--sh-text-primary); + font-size: 15px; +} + +.sh-log-selectors { + display: flex; + gap: 12px; + align-items: center; +} + +.sh-log-selectors select { + border-radius: 12px; + border: 1px solid rgba(255,255,255,0.12); + background: rgba(15,23,42,0.8); + color: var(--sh-text-primary); + padding: 10px 14px; +} + +.sh-toggle { + display: flex; + gap: 6px; + align-items: center; + font-size: 13px; + color: var(--sh-text-secondary); +} + +.sh-toggle input { + width: 16px; + height: 16px; +} + +.sh-btn { + border-radius: 12px; + padding: 10px 18px; + border: 1px solid rgba(255,255,255,0.12); + background: rgba(99,102,241,0.18); + color: white; + font-weight: 600; + cursor: pointer; +} + +.sh-btn-primary { + background: linear-gradient(135deg,#6366f1,#a855f7); + border-color: transparent; +} + +.sh-logs-body { + display: grid; + grid-template-columns: 3fr 1fr; + gap: 20px; +} + +.sh-log-panel { + background: var(--sh-bg-card); + border-radius: 20px; + padding: 16px; + border: 1px solid var(--sh-border); + box-shadow: 0 20px 40px rgba(0,0,0,0.3); + display: flex; + flex-direction: column; + gap: 12px; +} + +.sh-log-filters { + margin-bottom: 8px; +} + +.sh-log-stream { + background: rgba(0,0,0,0.35); + border-radius: 16px; + border: 1px solid rgba(255,255,255,0.08); + padding: 14px; + height: 520px; + overflow-y: auto; + font-family: 'JetBrains Mono', monospace; + font-size: 13px; + color: var(--sh-text-primary); +} + +.sh-log-line { + display: flex; + gap: 14px; + padding: 6px 4px; + border-bottom: 1px solid rgba(255,255,255,0.03); +} + +.sh-log-line.error { + background: rgba(248,113,113,0.08); +} + +.sh-log-line.warning { + background: rgba(251,191,36,0.08); +} + +.sh-log-index { + color: var(--sh-text-muted); + min-width: 40px; + text-align: right; +} + +.sh-log-message { + white-space: pre-wrap; +} + +.sh-log-side { + background: rgba(15,23,42,0.85); + border-radius: 20px; + padding: 18px; + border: 1px solid rgba(255,255,255,0.08); + box-shadow: 0 20px 40px rgba(0,0,0,0.25); +} + +.sh-log-side h3 { + margin: 0 0 12px; + font-size: 18px; +} + +.sh-log-side ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 10px; +} + +.sh-log-side li { + display: flex; + justify-content: space-between; + font-size: 14px; + border-bottom: 1px dashed rgba(255,255,255,0.1); + padding-bottom: 6px; +} + +.sh-log-side strong { + font-family: 'JetBrains Mono', monospace; +} + +@media (max-width: 1024px) { + .sh-logs-body { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .sh-logs-view { + padding: 16px; + } + + .sh-log-controls { + flex-direction: column; + align-items: stretch; + } + + .sh-log-selectors { + flex-direction: column; + align-items: stretch; + } +} diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/overview.css b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/overview.css new file mode 100644 index 0000000..579c17b --- /dev/null +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/overview.css @@ -0,0 +1,268 @@ +.sh-overview { + padding: 28px; + background: radial-gradient(circle at top, rgba(99,102,241,0.15), transparent), + radial-gradient(circle at bottom, rgba(14,165,233,0.12), transparent), + var(--sh-bg); + border-radius: 22px; +} + +.sh-hero { + background: linear-gradient(135deg, rgba(99,102,241,0.18), rgba(14,165,233,0.18)); + border-radius: 20px; + padding: 28px; + border: 1px solid var(--sh-border); + display: grid; + grid-template-columns: 2fr 2fr 1fr; + gap: 20px; + align-items: center; + box-shadow: 0 20px 40px rgba(0,0,0,0.35); +} + +.sh-hero-title { + display: flex; + align-items: center; + gap: 18px; +} + +.sh-hero-icon { + width: 64px; + height: 64px; + border-radius: 18px; + background: rgba(15,23,42,0.4); + display: flex; + align-items: center; + justify-content: center; + font-size: 32px; +} + +.sh-hero-title h1 { + margin: 0; + font-size: 28px; + color: var(--sh-text-primary); +} + +.sh-hero-title p { + margin: 4px 0 0; + color: var(--sh-text-secondary); +} + +.sh-hero-meta { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.sh-badge { + padding: 8px 14px; + border-radius: 999px; + background: rgba(15,23,42,0.6); + border: 1px solid rgba(255,255,255,0.1); + font-weight: 600; + cursor: default; +} + +.sh-badge.ghost { + background: rgba(255,255,255,0.1); +} + +.sh-badge-copy { + cursor: pointer; +} + +.sh-hero-score { + text-align: center; + background: rgba(15,23,42,0.6); + padding: 16px; + border-radius: 18px; + border: 1px solid rgba(255,255,255,0.08); +} + +.sh-score-value { + font-size: 42px; + font-weight: 700; +} + +.sh-score-label { + text-transform: uppercase; + font-size: 12px; + letter-spacing: 0.18em; + color: var(--sh-text-secondary); +} + +.sh-info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; + margin: 26px 0; +} + +.sh-info-card { + background: var(--sh-bg-card); + border-radius: 18px; + border: 1px solid var(--sh-border); + padding: 18px; + box-shadow: 0 10px 25px rgba(0,0,0,0.25); + position: relative; +} + +.sh-info-label { + font-size: 12px; + text-transform: uppercase; + color: var(--sh-text-secondary); + letter-spacing: 0.15em; + margin-bottom: 10px; +} + +.sh-info-value { + font-size: 22px; + font-weight: 600; + color: var(--sh-text-primary); +} + +.sh-info-value.mono { + font-family: 'JetBrains Mono', monospace; +} + +.sh-info-action { + position: absolute; + top: 14px; + right: 14px; + border: none; + background: rgba(99,102,241,0.18); + color: var(--sh-text-primary); + font-weight: 600; + padding: 6px 12px; + border-radius: 999px; + cursor: pointer; +} + +.sh-monitor-panel, +.sh-status-panel { + background: var(--sh-bg-card); + border-radius: 20px; + border: 1px solid var(--sh-border); + box-shadow: 0 15px 35px rgba(0,0,0,0.25); + padding: 22px; + margin-bottom: 26px; +} + +.sh-section-header { + margin-bottom: 16px; +} + +.sh-section-header h2 { + margin: 0; + font-size: 20px; +} + +.sh-section-header p { + margin: 4px 0 0; + color: var(--sh-text-secondary); +} + +.sh-monitor-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); + gap: 18px; +} + +.sh-monitor-card { + background: rgba(255,255,255,0.02); + border-radius: 18px; + border: 1px solid rgba(255,255,255,0.05); + padding: 18px; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 12px; + align-items: center; +} + +.sh-monitor-icon { + font-size: 28px; +} + +.sh-monitor-info { + display: flex; + flex-direction: column; +} + +.sh-monitor-label { + font-weight: 600; +} + +.sh-monitor-detail { + color: var(--sh-text-secondary); + font-size: 14px; +} + +.sh-monitor-progress { + grid-column: 2 / span 2; + height: 7px; + background: rgba(255,255,255,0.08); + border-radius: 999px; + overflow: hidden; +} + +.sh-monitor-bar { + height: 100%; + background: linear-gradient(90deg, #22d3ee, #a855f7); + transition: width 0.25s ease; +} + +.sh-monitor-percent { + font-family: 'JetBrains Mono', monospace; +} + +.sh-status-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 16px; +} + +.sh-status-card { + border-radius: 18px; + padding: 16px; + border: 1px solid rgba(255,255,255,0.08); + display: flex; + gap: 14px; + background: rgba(255,255,255,0.025); +} + +.sh-status-icon { + font-size: 20px; +} + +.sh-status-card.ok { + border-left: 3px solid #22c55e; +} + +.sh-status-card.warn { + border-left: 3px solid #f97316; +} + +.sh-status-card.unknown { + border-left: 3px solid #9ca3af; +} + +.sh-status-value { + font-weight: 600; +} + +.sh-status-extra { + display: block; + font-size: 13px; + color: var(--sh-text-secondary); + margin-top: 4px; +} + +@media (max-width: 960px) { + .sh-hero { + grid-template-columns: 1fr; + } +} + +@media (max-width: 600px) { + .sh-overview { + padding: 16px; + } +} diff --git a/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/services.css b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/services.css new file mode 100644 index 0000000..5e4d064 --- /dev/null +++ b/luci-theme-secubox/htdocs/luci-static/resources/secubox-theme/system-hub/services.css @@ -0,0 +1,219 @@ +.sh-services-view { + padding: 28px; + background: radial-gradient(circle at top, rgba(15,118,255,0.12), transparent), + radial-gradient(circle at bottom, rgba(99,102,241,0.1), transparent), + var(--sh-bg); + border-radius: 22px; +} + +.sh-services-hero { + background: linear-gradient(135deg, rgba(99,102,241,0.18), rgba(6,182,212,0.18)); + border-radius: 22px; + padding: 24px; + border: 1px solid var(--sh-border); + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + gap: 16px; + box-shadow: 0 20px 40px rgba(0,0,0,0.35); +} + +.sh-services-hero h1 { + margin: 0; + font-size: 26px; +} + +.sh-services-hero p { + margin: 4px 0 0; + color: var(--sh-text-secondary); +} + +.sh-services-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); + gap: 12px; + min-width: 300px; +} + +.sh-service-stat { + background: rgba(15,23,42,0.45); + border-radius: 16px; + padding: 12px; + border: 1px solid rgba(255,255,255,0.08); + text-align: center; +} + +.sh-service-stat .label { + display: block; + font-size: 12px; + text-transform: uppercase; + color: var(--sh-text-secondary); + letter-spacing: 0.15em; +} + +.sh-service-stat .value { + font-size: 28px; + font-weight: 700; + line-height: 1; +} + +.sh-service-stat.success .value { + color: #22c55e; +} + +.sh-service-stat.danger .value { + color: #ef4444; +} + +.sh-service-controls { + margin: 24px 0; + display: flex; + flex-wrap: wrap; + gap: 16px; + align-items: center; + justify-content: space-between; +} + +.sh-service-tabs { + flex: 1; + min-width: 220px; +} + +.sh-service-search input { + width: 260px; + max-width: 100%; + border-radius: 16px; + border: 1px solid rgba(255,255,255,0.12); + background: rgba(255,255,255,0.02); + padding: 10px 16px; + color: var(--sh-text-primary); +} + +.sh-services-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 18px; +} + +.sh-service-card { + background: var(--sh-bg-card); + border-radius: 18px; + border: 1px solid rgba(255,255,255,0.08); + padding: 18px; + box-shadow: 0 12px 30px rgba(0,0,0,0.25); + display: flex; + flex-direction: column; + gap: 16px; +} + +.sh-service-card.running { + border-left: 4px solid #22c55e; +} + +.sh-service-card.stopped { + border-left: 4px solid #ef4444; +} + +.sh-service-head { + display: flex; + justify-content: space-between; + gap: 10px; +} + +.sh-service-head h3 { + margin: 0; + font-size: 20px; +} + +.sh-service-tag { + font-size: 12px; + color: var(--sh-text-secondary); +} + +.sh-service-status { + padding: 6px 12px; + border-radius: 999px; + font-weight: 600; +} + +.sh-service-status.running { + background: rgba(34,197,94,0.18); + color: #22c55e; +} + +.sh-service-status.stopped { + background: rgba(239,68,68,0.18); + color: #ef4444; +} + +.sh-service-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; +} + +.sh-btn { + border-radius: 12px; + padding: 10px; + border: 1px solid rgba(255,255,255,0.08); + background: rgba(255,255,255,0.03); + color: var(--sh-text-primary); + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.sh-btn:hover { + background: rgba(255,255,255,0.09); +} + +.sh-btn-ghost { + grid-column: span 2; + background: rgba(99,102,241,0.15); + border-color: rgba(99,102,241,0.35); +} + +.sh-empty-state { + grid-column: 1 / -1; + text-align: center; + padding: 40px; + color: var(--sh-text-secondary); + border: 1px dashed rgba(255,255,255,0.15); + border-radius: 18px; +} + +.sh-empty-icon { + font-size: 40px; + margin-bottom: 8px; +} + +.sh-service-detail { + display: flex; + flex-direction: column; + gap: 12px; + margin-bottom: 18px; +} + +.sh-service-detail-row { + display: flex; + justify-content: space-between; + border-bottom: 1px solid rgba(148,163,184,0.2); + padding-bottom: 6px; + font-size: 14px; +} + +@media (max-width: 720px) { + .sh-services-view { + padding: 16px; + } + + .sh-service-controls { + flex-direction: column; + align-items: stretch; + } + + .sh-service-search input { + width: 100%; + } +} diff --git a/secubox-app-domoticz/Makefile b/package/secubox/secubox-app-domoticz/Makefile similarity index 100% rename from secubox-app-domoticz/Makefile rename to package/secubox/secubox-app-domoticz/Makefile diff --git a/secubox-app-domoticz/files/etc/config/domoticz b/package/secubox/secubox-app-domoticz/files/etc/config/domoticz similarity index 100% rename from secubox-app-domoticz/files/etc/config/domoticz rename to package/secubox/secubox-app-domoticz/files/etc/config/domoticz diff --git a/secubox-app-domoticz/files/etc/init.d/domoticz b/package/secubox/secubox-app-domoticz/files/etc/init.d/domoticz similarity index 100% rename from secubox-app-domoticz/files/etc/init.d/domoticz rename to package/secubox/secubox-app-domoticz/files/etc/init.d/domoticz diff --git a/secubox-app-domoticz/files/usr/sbin/domoticzctl b/package/secubox/secubox-app-domoticz/files/usr/sbin/domoticzctl similarity index 100% rename from secubox-app-domoticz/files/usr/sbin/domoticzctl rename to package/secubox/secubox-app-domoticz/files/usr/sbin/domoticzctl diff --git a/secubox-app-lyrion/Makefile b/package/secubox/secubox-app-lyrion/Makefile similarity index 100% rename from secubox-app-lyrion/Makefile rename to package/secubox/secubox-app-lyrion/Makefile diff --git a/secubox-app-lyrion/files/etc/config/lyrion b/package/secubox/secubox-app-lyrion/files/etc/config/lyrion similarity index 100% rename from secubox-app-lyrion/files/etc/config/lyrion rename to package/secubox/secubox-app-lyrion/files/etc/config/lyrion diff --git a/secubox-app-lyrion/files/etc/init.d/lyrion b/package/secubox/secubox-app-lyrion/files/etc/init.d/lyrion similarity index 100% rename from secubox-app-lyrion/files/etc/init.d/lyrion rename to package/secubox/secubox-app-lyrion/files/etc/init.d/lyrion diff --git a/secubox-app-lyrion/files/usr/sbin/lyrionctl b/package/secubox/secubox-app-lyrion/files/usr/sbin/lyrionctl similarity index 100% rename from secubox-app-lyrion/files/usr/sbin/lyrionctl rename to package/secubox/secubox-app-lyrion/files/usr/sbin/lyrionctl diff --git a/secubox-app-zigbee2mqtt/Makefile b/package/secubox/secubox-app-zigbee2mqtt/Makefile similarity index 100% rename from secubox-app-zigbee2mqtt/Makefile rename to package/secubox/secubox-app-zigbee2mqtt/Makefile diff --git a/secubox-app-zigbee2mqtt/files/etc/config/zigbee2mqtt b/package/secubox/secubox-app-zigbee2mqtt/files/etc/config/zigbee2mqtt similarity index 100% rename from secubox-app-zigbee2mqtt/files/etc/config/zigbee2mqtt rename to package/secubox/secubox-app-zigbee2mqtt/files/etc/config/zigbee2mqtt diff --git a/secubox-app-zigbee2mqtt/files/etc/init.d/zigbee2mqtt b/package/secubox/secubox-app-zigbee2mqtt/files/etc/init.d/zigbee2mqtt similarity index 100% rename from secubox-app-zigbee2mqtt/files/etc/init.d/zigbee2mqtt rename to package/secubox/secubox-app-zigbee2mqtt/files/etc/init.d/zigbee2mqtt diff --git a/secubox-app-zigbee2mqtt/files/usr/sbin/zigbee2mqttctl b/package/secubox/secubox-app-zigbee2mqtt/files/usr/sbin/zigbee2mqttctl similarity index 100% rename from secubox-app-zigbee2mqtt/files/usr/sbin/zigbee2mqttctl rename to package/secubox/secubox-app-zigbee2mqtt/files/usr/sbin/zigbee2mqttctl diff --git a/package/secubox/secubox-app/Makefile b/package/secubox/secubox-app/Makefile new file mode 100644 index 0000000..c00acff --- /dev/null +++ b/package/secubox/secubox-app/Makefile @@ -0,0 +1,37 @@ +include $(TOPDIR)/rules.mk + +PKG_NAME:=secubox-app +PKG_VERSION:=1.0.0 +PKG_RELEASE:=1 + +PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) + +include $(INCLUDE_DIR)/package.mk + +define Package/secubox-app + SECTION:=utils + CATEGORY:=Utilities + TITLE:=SecuBox App Store CLI + DEPENDS:=+jsonfilter +endef + +define Package/secubox-app/description +Command line helper for SecuBox App Store manifests. Installs /usr/sbin/secubox-app +and ships the default manifests under /usr/share/secubox/plugins/. +endef + +define Build/Prepare + $(CP) ./files $(PKG_BUILD_DIR)/ +endef + +define Build/Configure +endef + +define Build/Compile +endef + +define Package/secubox-app/install + $(CP) $(PKG_BUILD_DIR)/files/* $(1)/ +endef + +$(eval $(call BuildPackage,secubox-app)) diff --git a/package/secubox/secubox-app/files/usr/sbin/secubox-app b/package/secubox/secubox-app/files/usr/sbin/secubox-app new file mode 100755 index 0000000..954b28e --- /dev/null +++ b/package/secubox/secubox-app/files/usr/sbin/secubox-app @@ -0,0 +1,253 @@ +#!/bin/sh +# SecuBox Apps CLI +# Lists/installs/removes plugin manifests declared under plugins/*/manifest.json + +set -eu + +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 +PKG_MGR="" +PKG_UPDATED=0 + +info() { printf '[INFO] %s\n' "$*"; } +warn() { printf '[WARN] %s\n' "$*" >&2; } +err() { printf '[ERROR] %s\n' "$*" >&2; } + +usage() { + cat <<'USAGE' +SecuBox Apps CLI +Usage: secubox-app [arguments] + +Commands: + list Show available app manifests + show Display manifest details + install Install required packages + run install action + remove Remove packages listed in manifest + status Show install state and run status action if defined + update Run plugin update action or opkg upgrade + +Environment: + SECUBOX_PLUGINS_DIR Override manifest directory (default: /usr/share/secubox/plugins) +USAGE +} + +require_tool() { + command -v "$1" >/dev/null 2>&1 || { err "Missing dependency: $1"; exit 1; } +} + +require_jsonfilter() { require_tool jsonfilter; } + +ensure_pkg_mgr() { + if [ -n "$PKG_MGR" ]; then + return + fi + if command -v opkg >/dev/null 2>&1; then + PKG_MGR="opkg" + elif command -v apk >/dev/null 2>&1; then + PKG_MGR="apk" + else + err "Missing dependency: require opkg or apk" + exit 1 + fi +} + +pkg_update_once() { + ensure_pkg_mgr + [ "$PKG_UPDATED" -eq 1 ] && return + case "$PKG_MGR" in + opkg) opkg update >/dev/null ;; + apk) apk update >/dev/null ;; + esac + PKG_UPDATED=1 +} + +pkg_is_installed() { + ensure_pkg_mgr + case "$PKG_MGR" in + opkg) opkg status "$1" >/dev/null 2>&1 ;; + apk) apk info -e "$1" >/dev/null 2>&1 ;; + esac +} + +pkg_install() { + local pkg="$1" + if pkg_is_installed "$pkg"; then + info "$pkg already installed" + return + fi + info "Installing $pkg" + pkg_update_once + case "$PKG_MGR" in + opkg) opkg install "$pkg" ;; + apk) apk add "$pkg" ;; + esac +} + +pkg_remove() { + local pkg="$1" + if ! pkg_is_installed "$pkg"; then + info "$pkg not installed" + return + fi + info "Removing $pkg" + case "$PKG_MGR" in + opkg) opkg remove "$pkg" ;; + apk) apk del "$pkg" ;; + esac +} + +pkg_upgrade() { + case "$PKG_MGR" in + opkg) opkg upgrade "$1" ;; + apk) apk upgrade "$1" ;; + esac +} + +manifest_path() { + local id="$1" + local file="$PLUGINS_DIR/$id/manifest.json" + [ -f "$file" ] || { err "Manifest not found for '$id' ($file)"; exit 1; } + printf '%s' "$file" +} + +manifest_field() { + local file="$1"; shift + jsonfilter -f "$file" -e "$1" 2>/dev/null || true +} + +manifest_packages() { + local file="$1" + jsonfilter -f "$file" -e '@.packages[*]' 2>/dev/null || true +} + +manifest_action() { + local file="$1"; shift + jsonfilter -f "$file" -e "@.actions.$1" 2>/dev/null || true +} + +plugin_state() { + local file="$1" + local pkgs pkg missing=0 installed=0 total=0 + pkgs=$(manifest_packages "$file") + for pkg in $pkgs; do + total=$((total+1)) + if pkg_is_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 +} + +list_plugins() { + require_jsonfilter + ensure_pkg_mgr + printf '%-16s %-22s %-10s %-10s\n' "ID" "Name" "Type" "State" + printf '%-16s %-22s %-10s %-10s\n' "--" "----" "----" "-----" + find "$PLUGINS_DIR" -mindepth 2 -maxdepth 2 -name manifest.json | sort | while read -r file; do + local id name type state + id=$(manifest_field "$file" '@.id') + name=$(manifest_field "$file" '@.name') + type=$(manifest_field "$file" '@.type') + state=$(plugin_state "$file") + printf '%-16s %-22s %-10s %-10s\n' "$id" "${name:-Unknown}" "${type:-?}" "$state" + done +} + +show_manifest() { + require_jsonfilter + local file=$(manifest_path "$1") + cat "$file" +} + +install_plugin() { + require_jsonfilter + ensure_pkg_mgr + local file=$(manifest_path "$1") + local pkgs pkg + pkgs=$(manifest_packages "$file") + if [ -z "$pkgs" ]; then + warn "Manifest has no packages; nothing to install." + else + for pkg in $pkgs; do + pkg_install "$pkg" + done + fi + local install_cmd + install_cmd=$(manifest_action "$file" install) + if [ -n "$install_cmd" ]; then + info "Running install action: $install_cmd" + sh -c "$install_cmd" + fi +} + +remove_plugin() { + require_jsonfilter + ensure_pkg_mgr + local file=$(manifest_path "$1") + local pkgs pkg + pkgs=$(manifest_packages "$file") + for pkg in $pkgs; do + pkg_remove "$pkg" + done +} + +plugin_status_cmd() { + require_jsonfilter + local file=$(manifest_path "$1") + local status_cmd + status_cmd=$(manifest_action "$file" status) + local state + state=$(plugin_state "$file") + printf 'App: %s\nState: %s\n' "$1" "$state" + if [ -n "$status_cmd" ]; then + printf 'Status command output:\n' + sh -c "$status_cmd" || true + fi +} + +update_plugin() { + require_jsonfilter + ensure_pkg_mgr + local file=$(manifest_path "$1") + local update_cmd pkgs pkg + update_cmd=$(manifest_action "$file" update) + if [ -n "$update_cmd" ]; then + info "Running update action: $update_cmd" + sh -c "$update_cmd" + else + pkgs=$(manifest_packages "$file") + for pkg in $pkgs; do + info "Upgrading $pkg" + pkg_update_once + pkg_upgrade "$pkg" || true + done + fi +} + +case "${1:-}" in + list) shift; list_plugins ;; + show) shift; [ $# -ge 1 ] || { err "show requires an app id"; exit 1; }; show_manifest "$1" ;; + install) shift; [ $# -ge 1 ] || { err "install requires an app id"; exit 1; }; install_plugin "$1" ;; + remove) shift; [ $# -ge 1 ] || { err "remove requires an app id"; exit 1; }; remove_plugin "$1" ;; + status) shift; [ $# -ge 1 ] || { err "status requires an app id"; exit 1; }; plugin_status_cmd "$1" ;; + update) shift; [ $# -ge 1 ] || { err "update requires an app id"; exit 1; }; update_plugin "$1" ;; + help|--help|-h|'') usage ;; + *) err "Unknown command: $1"; usage; exit 1 ;; +esac diff --git a/plugins/domoticz/manifest.json b/package/secubox/secubox-app/files/usr/share/secubox/plugins/domoticz/manifest.json similarity index 100% rename from plugins/domoticz/manifest.json rename to package/secubox/secubox-app/files/usr/share/secubox/plugins/domoticz/manifest.json diff --git a/plugins/lyrion/manifest.json b/package/secubox/secubox-app/files/usr/share/secubox/plugins/lyrion/manifest.json similarity index 100% rename from plugins/lyrion/manifest.json rename to package/secubox/secubox-app/files/usr/share/secubox/plugins/lyrion/manifest.json diff --git a/plugins/zigbee2mqtt/manifest.json b/package/secubox/secubox-app/files/usr/share/secubox/plugins/zigbee2mqtt/manifest.json similarity index 100% rename from plugins/zigbee2mqtt/manifest.json rename to package/secubox/secubox-app/files/usr/share/secubox/plugins/zigbee2mqtt/manifest.json diff --git a/plugins b/plugins new file mode 120000 index 0000000..d396c44 --- /dev/null +++ b/plugins @@ -0,0 +1 @@ +package/secubox/secubox-app/files/usr/share/secubox/plugins \ No newline at end of file diff --git a/profiles b/profiles new file mode 120000 index 0000000..5151a7c --- /dev/null +++ b/profiles @@ -0,0 +1 @@ +../luci-app-secubox/profiles \ No newline at end of file diff --git a/secubox-app-domoticz b/secubox-app-domoticz new file mode 120000 index 0000000..de26972 --- /dev/null +++ b/secubox-app-domoticz @@ -0,0 +1 @@ +package/secubox/secubox-app-domoticz \ No newline at end of file diff --git a/secubox-app-lyrion b/secubox-app-lyrion new file mode 120000 index 0000000..312183a --- /dev/null +++ b/secubox-app-lyrion @@ -0,0 +1 @@ +package/secubox/secubox-app-lyrion \ No newline at end of file diff --git a/secubox-app-zigbee2mqtt b/secubox-app-zigbee2mqtt new file mode 120000 index 0000000..9dd8ed3 --- /dev/null +++ b/secubox-app-zigbee2mqtt @@ -0,0 +1 @@ +package/secubox/secubox-app-zigbee2mqtt \ No newline at end of file diff --git a/secubox-tools/local-build.sh b/secubox-tools/local-build.sh index 7789aab..ab6991d 100755 --- a/secubox-tools/local-build.sh +++ b/secubox-tools/local-build.sh @@ -549,6 +549,7 @@ copy_packages() { # Use the local feed directory (outside SDK) local feed_dir="../local-feed" mkdir -p "$feed_dir" + local -a core_pkg_names=() if [[ -n "$single_package" ]]; then print_info "Copying single package: $single_package" @@ -594,14 +595,24 @@ copy_packages() { fi done - # Copy core packages (non-LuCI) - for pkg in ../../package/secubox/nodogsplash/; do + # Copy secubox-app-* helper packages + for pkg in ../../package/secubox/secubox-app-*/; do if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then local pkg_name=$(basename "$pkg") echo " 📁 $pkg_name" cp -r "$pkg" "$feed_dir/" fi done + + # Copy core packages (non-LuCI) + for pkg in ../../package/secubox/*/; do + if [[ -d "$pkg" && -f "${pkg}Makefile" ]]; then + local pkg_name=$(basename "$pkg") + echo " 📁 $pkg_name" + cp -r "$pkg" "$feed_dir/" + core_pkg_names+=("$pkg_name") + fi + done fi echo "" @@ -639,10 +650,10 @@ copy_packages() { fi done - # Install core packages (non-LuCI) - for pkg in "$feed_dir"/nodogsplash/; do - if [[ -d "$pkg" ]]; then - local pkg_name=$(basename "$pkg") + # Install secubox core packages + for pkg_name in "${core_pkg_names[@]}"; do + local pkg_path="$feed_dir/$pkg_name" + if [[ -d "$pkg_path" ]]; then echo " Installing $pkg_name..." ./scripts/feeds install "$pkg_name" 2>&1 | grep -v "WARNING:" || true fi @@ -764,6 +775,14 @@ build_packages() { for pkg in feeds/secubox/luci-theme-*/; do [[ -d "$pkg" ]] && packages_to_build+=("$(basename "$pkg")") done + + # Build core secubox packages (secubox-app, nodogsplash, etc.) + for pkg in feeds/secubox/secubox-*/; do + [[ -d "$pkg" ]] && packages_to_build+=("$(basename "$pkg")") + done + for pkg in feeds/secubox/nodogsplash/; do + [[ -d "$pkg" ]] && packages_to_build+=("$(basename "$pkg")") + done fi # Build packages @@ -1048,8 +1067,18 @@ copy_secubox_to_openwrt() { fi done - # Copy additional core packages (non-LuCI) - for pkg in ../../package/secubox/nodogsplash/; do + # Copy secubox-app-* helper packages + for pkg in ../../package/secubox/secubox-app-*/; do + if [[ -d "$pkg" ]]; then + local pkg_name=$(basename "$pkg") + echo " ✅ $pkg_name" + cp -r "$pkg" package/secubox/ + pkg_count=$((pkg_count + 1)) + fi + done + + # Copy additional core packages (non-LuCI / non-app store) + for pkg in ../../package/secubox/*/; do if [[ -d "$pkg" ]]; then local pkg_name=$(basename "$pkg") echo " ✅ $pkg_name" diff --git a/secubox-tools/quick-deploy.sh b/secubox-tools/quick-deploy.sh index 3fdf08a..b544d32 100755 --- a/secubox-tools/quick-deploy.sh +++ b/secubox-tools/quick-deploy.sh @@ -23,6 +23,12 @@ SRC_PATH="" GIT_URL="" GIT_BRANCH="" POST_CMD="" +BACKUP_DIR="${BACKUP_DIR:-/tmp/quickdeploy-backups}" +HISTORY_DIR="${HOME}/.secubox" +HISTORY_FILE="$HISTORY_DIR/quickdeploy-history.log" +LAST_BACKUP_FILE="$HISTORY_DIR/quickdeploy-last" +LAST_BACKUP="" +UNINSTALL_TARGET="" usage() { cat <<'USAGE' @@ -34,6 +40,7 @@ Options (choose one source): --ipk Upload + install an IPK via opkg. --apk Upload + install an APK via apk add. --src Tar + upload a local directory to --target-path. + --src-clean Remove files previously deployed from a local directory (no upload). --git Clone repo (optionally --branch) then upload. --profile Use a predefined deployment profile (e.g. theme, luci-app). --app Shortcut for --profile luci-app; auto-resolves `luci-app-` @@ -48,6 +55,7 @@ Common flags: --no-verify Skip post-deploy file verification. --force-root Allow --src to write directly under /. Use with caution. --no-auto-profile Disable automatic LuCI app detection when using --src. + --uninstall [backup] Restore the latest (or specific) quick-deploy backup. --post Extra remote command to run after deploy. -h, --help Show this message. @@ -79,6 +87,87 @@ join_path() { fi } +gather_deploy_files() { + local dir="$1" + local -n out_ref="$2" + out_ref=() + [[ ! -d "$dir" ]] && return 0 + if [[ ${#INCLUDE_PATHS[@]} -gt 0 ]]; then + for inc in "${INCLUDE_PATHS[@]}"; do + local path="$dir/$inc" + if [[ -d "$path" ]]; then + while IFS= read -r f; do out_ref+=("$f"); done < <(find "$path" -type f -not -path '*/.git/*' | sort) + elif [[ -f "$path" ]]; then + out_ref+=("$path") + fi + done + else + while IFS= read -r f; do out_ref+=("$f"); done < <(find "$dir" -type f -not -path '*/.git/*' | sort) + fi +} + +record_backup_metadata() { + local backup_file="$1" + [[ -z "$backup_file" ]] && return + mkdir -p "$HISTORY_DIR" + printf '%s | %s | %s\n' "$(date -Iseconds)" "$ROUTER" "$backup_file" >> "$HISTORY_FILE" + printf '%s\n' "$backup_file" > "$LAST_BACKUP_FILE" + LAST_BACKUP="$backup_file" + log "🗂 Backup saved to $backup_file (restore with --uninstall $(basename "$backup_file" .tar.gz))" +} + +backup_remote_list() { + local -a remote_paths=("$@") + if [[ ${#remote_paths[@]} -eq 0 ]]; then + log "No remote paths to backup; skipping snapshot." + return 0 + fi + local unique_paths + unique_paths=$(printf '%s\n' "${remote_paths[@]}" | awk 'length && !seen[$0]++') + local trimmed_list="" + while IFS= read -r path; do + local trimmed="${path#/}" + [[ -z "$trimmed" ]] && continue + trimmed_list+="$trimmed"$'\n' + done <<< "$unique_paths" + if [[ -z "$trimmed_list" ]]; then + log "No existing remote files detected to backup." + return 0 + fi + local backup_id + backup_id=$(date +%Y%m%d_%H%M%S) + local backup_file="$BACKUP_DIR/$backup_id.tar.gz" + local list_file="$BACKUP_DIR/$backup_id.list" + remote_exec "mkdir -p '$BACKUP_DIR'" + remote_exec "cat <<'EOF' > '$list_file' +$trimmed_list +EOF" + if remote_exec "cd / && tar --ignore-failed-read -czf '$backup_file' -T '$list_file' >/dev/null"; then + record_backup_metadata "$backup_file" + else + log "⚠️ Failed to create backup archive." + remote_exec "rm -f '$backup_file' '$list_file'" + fi +} + +backup_remote_paths() { + local dir="$1" + local base="$2" + local -a files=() + gather_deploy_files "$dir" files + if [[ ${#files[@]} -eq 0 ]]; then + log "No local files detected for $dir; skipping backup." + return 0 + fi + local -a remote_paths=() + for file in "${files[@]}"; do + local rel=${file#$dir/} + [[ -z "$rel" ]] && continue + remote_paths+=("$(join_path "$base" "$rel")") + done + backup_remote_list "${remote_paths[@]}" +} + ensure_remote_hash() { if [[ -n "$REMOTE_HASH_CMD" ]]; then return 0 @@ -99,23 +188,8 @@ verify_remote() { local base="$TARGET_PATH" [[ "$FORCE_ROOT" == "true" ]] && base="/" ensure_remote_hash || return - local -a candidates - if [[ ${#INCLUDE_PATHS[@]} -gt 0 ]]; then - for inc in "${INCLUDE_PATHS[@]}"; do - local path="$dir/$inc" - if [[ -d "$path" ]]; then - while IFS= read -r f; do - candidates+=("$f") - done < <(find "$path" -type f -not -path '*/.git/*' | sort) - elif [[ -f "$path" ]]; then - candidates+=("$path") - fi - done - else - while IFS= read -r f; do - candidates+=("$f") - done < <(find "$dir" -type f -not -path '*/.git/*' | sort) - fi + local -a candidates=() + gather_deploy_files "$dir" candidates local -a samples for f in "${candidates[@]}"; do samples+=("$f") @@ -269,12 +343,34 @@ deploy_profile_theme() { "luci-app-system-hub/htdocs/luci-static/resources/system-hub/dashboard.css:/www/luci-static/resources/system-hub/" "luci-app-system-hub/htdocs/luci-static/resources/view/system-hub/overview.js:/www/luci-static/resources/view/system-hub/" ) + remote_exec "mkdir -p /usr/libexec/rpcd /usr/share/rpcd/acl.d /www/luci-static/resources/secubox /www/luci-static/resources/view/secubox /www/luci-static/resources/system-hub /www/luci-static/resources/view/system-hub /www/luci-static/resources/secubox-theme" + local -a backup_targets=() + for entry in "${files[@]}"; do + local src=${entry%%:*} + local dest=${entry##*:} + local remote_file=$(join_path "$dest" "$(basename "$src")") + backup_targets+=("$remote_file") + done + backup_remote_list "${backup_targets[@]}" for entry in "${files[@]}"; do local src=${entry%%:*} local dest=${entry##*:} log "Copying $src -> $dest" copy_file "$src" "$dest" done + + local theme_src_dir="luci-theme-secubox/htdocs/luci-static/resources" + if [[ -d "$theme_src_dir/secubox-theme" ]]; then + backup_remote_paths "$theme_src_dir/secubox-theme" "/www/luci-static/resources/secubox-theme" + local theme_archive=$(mktemp /tmp/secubox-theme-XXXX.tar.gz) + ( cd "$theme_src_dir" && tar -czf "$theme_archive" secubox-theme/ ) + local remote_theme="/tmp/secubox-theme.tar.gz" + log "Copying theme bundle to /www/luci-static/resources/secubox-theme" + copy_file "$theme_archive" "$remote_theme" + remote_exec "mkdir -p /www/luci-static/resources && tar -xzf '$remote_theme' -C /www/luci-static/resources && rm -f '$remote_theme'" + rm -f "$theme_archive" + fi + log "Setting permissions + restarting rpcd" remote_exec "chmod +x /usr/libexec/rpcd/luci.secubox && \\ chmod 644 /www/luci-static/resources/secubox/*.{js,css} 2>/dev/null || true && \\ @@ -331,6 +427,8 @@ while [[ $# -gt 0 ]]; do MODE="apk"; PKG_PATH="$2"; shift 2 ;; --src) MODE="src"; SRC_PATH="$2"; shift 2 ;; + --src-clean) + MODE="src-clean"; SRC_PATH="$2"; shift 2 ;; --src-select) MODE="src"; SRC_PATH=""; shift ;; --git) @@ -353,6 +451,13 @@ while [[ $# -gt 0 ]]; do FORCE_ROOT="true"; shift ;; --no-auto-profile) AUTO_PROFILE=0; shift ;; + --uninstall) + MODE="uninstall" + if [[ $# -gt 1 && "$2" != --* ]]; then + UNINSTALL_TARGET="$2"; shift 2 + else + UNINSTALL_TARGET="latest"; shift + fi ;; -h|--help) usage ;; *) @@ -366,6 +471,10 @@ if [[ "$APP_NAME" == "list" ]]; then APP_NAME="" fi +if [[ "$MODE" == "uninstall" ]]; then + perform_uninstall "$UNINSTALL_TARGET" +fi + if [[ $LIST_APPS -eq 1 ]]; then list_luci_apps exit 0 @@ -423,6 +532,13 @@ if [[ -z "$PROFILE" && "$MODE" == "src" && "$AUTO_PROFILE" -eq 1 && -n "$SRC_PAT log "Auto-detected LuCI app at $SRC_PATH (use --no-auto-profile to disable)." fi +if [[ "$MODE" == "src-clean" ]]; then + if [[ -z "$SRC_PATH" || ! -d "$SRC_PATH" ]]; then + echo "Error: --src-clean requires a valid --src path" >&2 + exit 1 + fi +fi + if [[ -z "$MODE" && -z "$PROFILE" ]]; then echo "Error: specify one of --ipk/--apk/--src/--git or --profile" >&2 usage @@ -433,7 +549,7 @@ if [[ -n "$MODE" && -n "$PROFILE" ]]; then exit 1 fi -if [[ "$FORCE_ROOT" == "true" && "$MODE" != "src" && "$PROFILE" != "luci-app" ]]; then +if [[ "$FORCE_ROOT" == "true" && "$MODE" != "src" && "$MODE" != "src-clean" && "$PROFILE" != "luci-app" ]]; then echo "Error: --force-root is only valid with --src" >&2 exit 1 fi @@ -489,6 +605,7 @@ upload_source_dir() { if [[ "$FORCE_ROOT" == "true" ]]; then extract_target="/" fi + backup_remote_paths "$dir" "$extract_target" log "Extracting to $extract_target" remote_exec "mkdir -p $extract_target && tar -xzf $remote -C $extract_target && rm -f $remote" if [[ "$CACHE_BUST" -eq 1 ]]; then @@ -511,6 +628,79 @@ clone_and_upload() { upload_source_dir "$cleanup_tmp" } +clean_source_dir() { + local dir="$1" + if [[ -z "$dir" || ! -d "$dir" ]]; then + echo "Error: --src-clean requires a valid local directory" >&2 + exit 1 + fi + local base="$TARGET_PATH" + [[ "$FORCE_ROOT" == "true" ]] && base="/" + log "🧹 Removing files deployed from $dir (target base: $base)" + local -a files=() + gather_deploy_files "$dir" files + if [[ ${#files[@]} -eq 0 ]]; then + log "No files detected within $dir; nothing to remove." + return 0 + fi + local list="" + for file in "${files[@]}"; do + local rel=${file#$dir/} + [[ -z "$rel" ]] && continue + local remote=$(join_path "$base" "$rel") + list+="$remote"$'\n' + done + remote_exec "cat <<'EOF' > /tmp/quickdeploy-clean.list +$list +EOF" + remote_exec "while IFS= read -r f || [ -n \"\$f\" ]; do [ -z \"\$f\" ] && continue; rm -f \"\$f\" 2>/dev/null || true; done < /tmp/quickdeploy-clean.list; rm -f /tmp/quickdeploy-clean.list" + if [[ "$CACHE_BUST" -eq 1 ]]; then + remote_exec "rm -rf /tmp/luci-*" + fi + log "Cleanup complete." +} + +resolve_backup_file() { + local target="$1" + local backup_file="" + if [[ "$target" == "latest" || -z "$target" ]]; then + backup_file=$(remote_exec "ls -1t '$BACKUP_DIR'/*.tar.gz 2>/dev/null | head -n1") || true + else + if [[ "$target" == /* ]]; then + backup_file="$target" + else + backup_file="$BACKUP_DIR/$target" + [[ "$target" != *.tar.gz ]] && backup_file="$backup_file.tar.gz" + fi + fi + echo "$backup_file" +} + +perform_uninstall() { + local target="$1" + local backup_file + backup_file=$(resolve_backup_file "$target") + if [[ -z "$backup_file" ]]; then + echo "No backup archives found in $BACKUP_DIR" >&2 + exit 1 + fi + local list_file="${backup_file%.tar.gz}.list" + log "Restoring from backup: $backup_file" + if ! remote_exec "if [ ! -f '$backup_file' ]; then echo 'Backup $backup_file not found' >&2; exit 1; fi"; then + exit 1 + fi + if ! remote_exec "if [ ! -f '$list_file' ]; then echo 'Warning: manifest $list_file missing; proceeding without cleanup.' >&2; fi"; then + true + fi + remote_exec "if [ -f '$list_file' ]; then while IFS= read -r rel || [ -n \"\$rel\" ]; do [ -z \"\$rel\" ] && continue; rm -f \"/\$rel\" 2>/dev/null || true; done < '$list_file'; fi" + remote_exec "cd / && tar -xzf '$backup_file'" + if [[ "$CACHE_BUST" -eq 1 ]]; then + remote_exec "rm -rf /tmp/luci-*" + fi + log "Rollback complete." + exit 0 +} + if [[ -n "$PROFILE" ]]; then case "$PROFILE" in theme|theme-system) @@ -541,6 +731,8 @@ else install_apk "$PKG_PATH" ;; src) upload_source_dir "$SRC_PATH" ;; + src-clean) + clean_source_dir "$SRC_PATH" ;; git) clone_and_upload ;; *) diff --git a/secubox-tools/secubox-app b/secubox-tools/secubox-app deleted file mode 100755 index ef2ea90..0000000 --- a/secubox-tools/secubox-app +++ /dev/null @@ -1,218 +0,0 @@ -#!/bin/sh -# SecuBox Apps CLI -# Lists/installs/removes plugin manifests declared under plugins/*/manifest.json - -set -eu - -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' "$*"; } -warn() { printf '[WARN] %s\n' "$*" >&2; } -err() { printf '[ERROR] %s\n' "$*" >&2; } - -usage() { - cat <<'USAGE' -SecuBox Apps CLI -Usage: secubox-app [arguments] - -Commands: - list Show available app manifests - show Display manifest details - install Install required packages + run install action - remove Remove packages listed in manifest - status Show install state and run status action if defined - update Run plugin update action or opkg upgrade - -Environment: - SECUBOX_PLUGINS_DIR Override manifest directory (default: /usr/share/secubox/plugins) -USAGE -} - -require_tool() { - command -v "$1" >/dev/null 2>&1 || { err "Missing dependency: $1"; exit 1; } -} - -require_opkg() { require_tool opkg; } -require_jsonfilter() { require_tool jsonfilter; } - -manifest_path() { - local id="$1" - local file="$PLUGINS_DIR/$id/manifest.json" - [ -f "$file" ] || { err "Manifest not found for '$id' ($file)"; exit 1; } - printf '%s' "$file" -} - -manifest_field() { - local file="$1"; shift - jsonfilter -s "$file" -e "$1" 2>/dev/null || true -} - -manifest_packages() { - local file="$1" - jsonfilter -s "$file" -e '@.packages[*]' 2>/dev/null || true -} - -manifest_action() { - local file="$1"; shift - jsonfilter -s "$file" -e "@.actions.$1" 2>/dev/null || true -} - -ensure_opkg_updated() { - [ "$OPKG_UPDATED" -eq 1 ] && return - opkg update >/dev/null - OPKG_UPDATED=1 -} - -packages_installed() { - local all_installed=0 - local partial=0 - local pkg - for pkg in $1; do - if opkg status "$pkg" >/dev/null 2>&1; then - continue - else - all_installed=1 - fi - [ -n "$partial" ] - done -} - -plugin_state() { - local file="$1" - local pkgs pkg missing=0 installed=0 total=0 - pkgs=$(manifest_packages "$file") - for pkg in $pkgs; do - total=$((total+1)) - if opkg status "$pkg" >/dev/null 2>&1; 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 -} - -list_plugins() { - require_jsonfilter - require_opkg - printf '%-16s %-22s %-10s %-10s\n' "ID" "Name" "Type" "State" - printf '%-16s %-22s %-10s %-10s\n' "--" "----" "----" "-----" - find "$PLUGINS_DIR" -mindepth 2 -maxdepth 2 -name manifest.json | sort | while read -r file; do - local id name type state - id=$(manifest_field "$file" '@.id') - name=$(manifest_field "$file" '@.name') - type=$(manifest_field "$file" '@.type') - state=$(plugin_state "$file") - printf '%-16s %-22s %-10s %-10s\n' "$id" "${name:-Unknown}" "${type:-?}" "$state" - done -} - -show_manifest() { - require_jsonfilter - local file=$(manifest_path "$1") - cat "$file" -} - -install_plugin() { - require_jsonfilter - require_opkg - local file=$(manifest_path "$1") - local pkgs pkg - pkgs=$(manifest_packages "$file") - if [ -z "$pkgs" ]; then - warn "Manifest has no packages; nothing to install." - else - for pkg in $pkgs; do - if opkg status "$pkg" >/dev/null 2>&1; then - info "$pkg already installed" - else - info "Installing $pkg" - ensure_opkg_updated - opkg install "$pkg" - fi - done - fi - local install_cmd - install_cmd=$(manifest_action "$file" install) - if [ -n "$install_cmd" ]; then - info "Running install action: $install_cmd" - sh -c "$install_cmd" - fi -} - -remove_plugin() { - require_jsonfilter - require_opkg - local file=$(manifest_path "$1") - local pkgs pkg - pkgs=$(manifest_packages "$file") - for pkg in $pkgs; do - if opkg status "$pkg" >/dev/null 2>&1; then - info "Removing $pkg" - opkg remove "$pkg" - else - info "$pkg not installed" - fi - done -} - -plugin_status_cmd() { - require_jsonfilter - local file=$(manifest_path "$1") - local status_cmd - status_cmd=$(manifest_action "$file" status) - local state - state=$(plugin_state "$file") - printf 'App: %s\nState: %s\n' "$1" "$state" - if [ -n "$status_cmd" ]; then - printf 'Status command output:\n' - sh -c "$status_cmd" || true - fi -} - -update_plugin() { - require_jsonfilter - require_opkg - local file=$(manifest_path "$1") - local update_cmd pkgs pkg - update_cmd=$(manifest_action "$file" update) - if [ -n "$update_cmd" ]; then - info "Running update action: $update_cmd" - sh -c "$update_cmd" - else - pkgs=$(manifest_packages "$file") - for pkg in $pkgs; do - info "Upgrading $pkg" - ensure_opkg_updated - opkg upgrade "$pkg" || true - done - fi -} - -case "${1:-}" in - list) shift; list_plugins ;; - show) shift; [ $# -ge 1 ] || { err "show requires an app id"; exit 1; }; show_manifest "$1" ;; - install) shift; [ $# -ge 1 ] || { err "install requires an app id"; exit 1; }; install_plugin "$1" ;; - remove) shift; [ $# -ge 1 ] || { err "remove requires an app id"; exit 1; }; remove_plugin "$1" ;; - status) shift; [ $# -ge 1 ] || { err "status requires an app id"; exit 1; }; plugin_status_cmd "$1" ;; - update) shift; [ $# -ge 1 ] || { err "update requires an app id"; exit 1; }; update_plugin "$1" ;; - help|--help|-h|'') usage ;; - *) err "Unknown command: $1"; usage; exit 1 ;; -esac diff --git a/secubox-tools/secubox-app b/secubox-tools/secubox-app new file mode 120000 index 0000000..329c0fa --- /dev/null +++ b/secubox-tools/secubox-app @@ -0,0 +1 @@ +../package/secubox/secubox-app/files/usr/sbin/secubox-app \ No newline at end of file