From 280dd9179823db4491bf5fa43e17c5f1df3d3511 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Tue, 30 Dec 2025 14:42:45 +0100 Subject: [PATCH] release: bump secubox hub to 0.6.1-0 --- .claude/settings.local.json | 5 +- .codex/SECUBOX_APP_STORE.md | 3 +- .codex/WIP.md | 37 +- luci-app-secubox/.appstore/apps.json | 138 +++++ luci-app-secubox/Makefile | 6 +- .../luci-static/resources/secubox/api.js | 33 ++ .../luci-static/resources/secubox/apps.css | 363 ++++++++++++++ .../luci-static/resources/secubox/nav.js | 2 +- .../resources/view/secubox/apps.js | 379 ++++++++++++++ .../resources/view/secubox/appstore.js | 470 ------------------ .../root/usr/libexec/rpcd/luci.secubox | 318 ++++++++++++ .../share/luci/menu.d/luci-app-secubox.json | 4 +- .../share/rpcd/acl.d/luci-app-secubox.json | 8 +- .../resources/system-hub/theme-assets.js | 5 +- package/secubox/.appstore/apps.json | 10 +- package/secubox/secubox-app-crowdsec/Makefile | 20 +- 16 files changed, 1298 insertions(+), 503 deletions(-) create mode 100644 luci-app-secubox/.appstore/apps.json create mode 100644 luci-app-secubox/htdocs/luci-static/resources/secubox/apps.css create mode 100644 luci-app-secubox/htdocs/luci-static/resources/view/secubox/apps.js delete mode 100644 luci-app-secubox/htdocs/luci-static/resources/view/secubox/appstore.js diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 2cc2697..6b236d2 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -173,7 +173,10 @@ "Bash(feeds/packages/net/crowdsec/Makefile)", "Bash(feeds/packages/net/crowdsec/patches/002-fix_go_version.patch)", "Bash(tail:*)", - "Bash(pkill -f \"local-build.sh build secubox-app-crowdsec\")" + "Bash(pkill -f \"local-build.sh build secubox-app-crowdsec\")", + "Bash(go version:*)", + "Bash(timeout 600 ./secubox-tools/local-build.sh:*)", + "Bash(timeout 300 ./secubox-tools/local-build.sh:*)" ] } } diff --git a/.codex/SECUBOX_APP_STORE.md b/.codex/SECUBOX_APP_STORE.md index 788250f..c2cb524 100644 --- a/.codex/SECUBOX_APP_STORE.md +++ b/.codex/SECUBOX_APP_STORE.md @@ -87,9 +87,10 @@ Metadata for all 5 SecuBox applications: 1. **secubox-app-crowdsec** (v1.7.4) - Category: Security πŸ›‘οΈ - - Status: Beta (requires Go 1.25+) + - Status: Development (requires full OpenWrt build environment) - LuCI App: `luci-app-crowdsec-dashboard` - Dependencies: `iptables-nft` + - Build: Go 1.23+ (available in OpenWrt 24.10), full build environment required 2. **secubox-app-nodogsplash** (v5.0.2) - Category: Network 🌐 diff --git a/.codex/WIP.md b/.codex/WIP.md index 75ac016..8a81691 100644 --- a/.codex/WIP.md +++ b/.codex/WIP.md @@ -2,17 +2,38 @@ ## Completed Today -- Introduced SecuBox cascade layout helper (CSS + JS) and migrated SecuNav + MQTT tabs to the new layered system. -- MQTT Bridge now exposes Zigbee/SMSC USB2134B presets with dmesg hints, tty detection, and documentation updates. -- New `mqtt-bridge` daemon keeps adapter metadata (port/bus/health) synced, updates stats, and runs automation rules/templates. -- Unified Monitoring + Modules filters and Help view with SecuNav styling. -- Added Bonus tab to navbar, refreshed alerts action buttons, removed legacy hero blocks. +**SecuBox App Store Integration (luci-app-secubox v0.6.1):** +- βœ… Added 4 new RPC backend methods (get_appstore_apps, get_appstore_app, install_appstore_app, remove_appstore_app) +- βœ… Created frontend App Store view (apps.js) with category filters and install/remove functionality +- βœ… Added apps.css with SecuBox theme integration and responsive design +- βœ… Updated API module with app store method declarations +- βœ… Added "App Store" menu entry at admin/secubox/apps +- βœ… Updated ACL permissions for app store operations +- βœ… Integrated .appstore/apps.json metadata into package installation +- βœ… Package builds successfully (luci-app-secubox_0.6.1-r1_all.ipk - 65KB) +- βœ… Total: 58 packages now building + +**Earlier Today - Build System Enhancement:** +- Enhanced local-build.sh to support secubox-app-* packages (6 locations updated) +- Renamed nodogsplash β†’ secubox-app-nodogsplash with proper Makefile structure +- Created SecuBox App Store metadata structure (.appstore/apps.json with 5 apps, 4 categories) +- Fixed golang package build issues (include paths, Go version detection, install paths) +- All script-based secubox-app packages building successfully +- Documented build system in .codex/SECUBOX_APP_STORE.md +- Updated apps.json: CrowdSec marked as "dev" status (requires full build environment) + +**Previous Work:** +- Introduced SecuBox cascade layout helper (CSS + JS) and migrated SecuNav + MQTT tabs to the new layered system. +- MQTT Bridge now exposes Zigbee/SMSC USB2134B presets with dmesg hints, tty detection, and documentation updates. +- New `mqtt-bridge` daemon keeps adapter metadata (port/bus/health) synced, updates stats, and runs automation rules/templates. +- Unified Monitoring + Modules filters and Help view with SecuNav styling. +- Added Bonus tab to navbar, refreshed alerts action buttons, removed legacy hero blocks. - Verified on router (scp + cache reset) and tagged release v0.5.0-A. - Settings now surface dark/light/system/cyberpunk themes with live preview + RPC persistence. - Built `secubox-tools/quick-deploy.sh` with interactive `--src-select`, LuCI profiles, verification, and cache-bust helpers. -- System Hub ACL now lists diagnostics + remote RPC methods so those tabs load under proper permissions. -- Validator now resolves cross-module menu paths and JS/CSS permissions normalized to 644 so checks pass repo-wide. -- Quick deploy prompt now writes menus to stderr so capturing the choice works again for `--src-select`. +- System Hub ACL now lists diagnostics + remote RPC methods so those tabs load under proper permissions. +- Validator now resolves cross-module menu paths and JS/CSS permissions normalized to 644 so checks pass repo-wide. +- Quick deploy prompt now writes menus to stderr so capturing the choice works again for `--src-select`. - System Hub views now import SecuBox theme CSS, hide default LuCI tabs, and respect `data-secubox-theme` for consistent styling. ## In Progress diff --git a/luci-app-secubox/.appstore/apps.json b/luci-app-secubox/.appstore/apps.json new file mode 100644 index 0000000..79e4acb --- /dev/null +++ b/luci-app-secubox/.appstore/apps.json @@ -0,0 +1,138 @@ +{ + "apps": [ + { + "id": "secubox-app-crowdsec", + "name": "CrowdSec", + "version": "1.7.4", + "category": "security", + "description": "CrowdSec is an open-source, lightweight security engine that detects and responds to malicious behaviors", + "icon": "πŸ›‘οΈ", + "author": "CyberMind.fr", + "license": "MIT", + "url": "https://github.com/crowdsecurity/crowdsec", + "tags": ["security", "ids", "ips", "firewall", "threat-detection"], + "requires": { + "go": "1.23+", + "memory": "128MB", + "storage": "50MB", + "build": "full" + }, + "status": "dev", + "luci_app": "luci-app-crowdsec-dashboard", + "dependencies": ["iptables-nft"], + "conflicts": [], + "notes": "Requires full OpenWrt build environment (not SDK). Go 1.23.12 available in OpenWrt 24.10." + }, + { + "id": "secubox-app-nodogsplash", + "name": "NoDogSplash", + "version": "5.0.2", + "category": "network", + "description": "Captive portal solution that intercepts HTTP traffic and serves a customizable splash page before granting network access", + "icon": "🌐", + "author": "CyberMind.fr", + "license": "GPL-2.0-or-later", + "url": "https://github.com/nodogsplash/nodogsplash", + "tags": ["captive-portal", "hotspot", "guest-network", "access-control"], + "requires": { + "memory": "32MB", + "storage": "5MB" + }, + "status": "stable", + "luci_app": null, + "dependencies": ["libmicrohttpd", "libjson-c", "iptables-nft"], + "conflicts": [] + }, + { + "id": "secubox-app-domoticz", + "name": "Domoticz", + "version": "1.0.0", + "category": "iot", + "description": "Home automation system with support for various devices and protocols", + "icon": "🏠", + "author": "CyberMind.fr", + "license": "GPL-3.0", + "url": "https://www.domoticz.com/", + "tags": ["home-automation", "iot", "smart-home", "docker"], + "requires": { + "docker": true, + "memory": "256MB", + "storage": "100MB" + }, + "status": "stable", + "luci_app": null, + "dependencies": ["docker", "dockerd"], + "conflicts": [] + }, + { + "id": "secubox-app-lyrion", + "name": "Lyrion Music Server", + "version": "1.0.0", + "category": "media", + "description": "Multi-room audio streaming server (formerly Logitech Media Server)", + "icon": "🎡", + "author": "CyberMind.fr", + "license": "GPL-2.0", + "url": "https://lyrion.org/", + "tags": ["music", "streaming", "multi-room", "audio", "docker"], + "requires": { + "docker": true, + "memory": "128MB", + "storage": "50MB" + }, + "status": "stable", + "luci_app": null, + "dependencies": ["docker", "dockerd"], + "conflicts": [] + }, + { + "id": "secubox-app-zigbee2mqtt", + "name": "Zigbee2MQTT", + "version": "1.0.0", + "category": "iot", + "description": "Zigbee to MQTT bridge allowing you to use Zigbee devices without proprietary hubs", + "icon": "πŸ“‘", + "author": "CyberMind.fr", + "license": "GPL-3.0", + "url": "https://www.zigbee2mqtt.io/", + "tags": ["zigbee", "mqtt", "iot", "smart-home", "docker"], + "requires": { + "docker": true, + "zigbee_adapter": true, + "memory": "128MB", + "storage": "50MB" + }, + "status": "stable", + "luci_app": "luci-app-zigbee2mqtt", + "dependencies": ["docker", "dockerd", "mqtt-broker"], + "conflicts": [] + } + ], + "categories": { + "security": { + "name": "Security", + "icon": "πŸ”’", + "description": "Security and threat detection applications" + }, + "network": { + "name": "Network", + "icon": "🌐", + "description": "Network services and utilities" + }, + "iot": { + "name": "IoT & Home Automation", + "icon": "🏠", + "description": "Internet of Things and home automation" + }, + "media": { + "name": "Media", + "icon": "🎬", + "description": "Media streaming and entertainment" + } + }, + "metadata": { + "version": "1.0", + "last_updated": "2024-12-30", + "repository": "https://github.com/cybermind-studio/secubox-openwrt" + } +} diff --git a/luci-app-secubox/Makefile b/luci-app-secubox/Makefile index 5e3f6c7..e78abfe 100644 --- a/luci-app-secubox/Makefile +++ b/luci-app-secubox/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-secubox -PKG_VERSION:=0.6.0 -PKG_RELEASE:=1 +PKG_VERSION:=0.6.1 +PKG_RELEASE:=0 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=CyberMind @@ -28,6 +28,8 @@ define Package/$(PKG_NAME)/install for file in $(CURDIR)/profiles/*.json; do \ $(INSTALL_DATA) $$file $(1)/usr/share/secubox/profiles/$$(basename $$file); \ done + $(INSTALL_DIR) $(1)/usr/share/secubox/.appstore + $(INSTALL_DATA) $(CURDIR)/.appstore/apps.json $(1)/usr/share/secubox/.appstore/apps.json endef # call BuildPackage - OpenWrt buildroot diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js b/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js index f228e3c..07b126a 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/api.js @@ -191,6 +191,34 @@ var callRollbackProfile = rpc.declare({ method: 'rollback_profile' }); +// App Store methods +var callGetAppstoreApps = rpc.declare({ + object: 'luci.secubox', + method: 'get_appstore_apps', + expect: { apps: [], categories: {} } +}); + +var callGetAppstoreApp = rpc.declare({ + object: 'luci.secubox', + method: 'get_appstore_app', + params: ['app_id'], + expect: { } +}); + +var callInstallAppstoreApp = rpc.declare({ + object: 'luci.secubox', + method: 'install_appstore_app', + params: ['app_id'], + expect: { success: false } +}); + +var callRemoveAppstoreApp = rpc.declare({ + object: 'luci.secubox', + method: 'remove_appstore_app', + params: ['app_id'], + expect: { success: false } +}); + function formatUptime(seconds) { if (!seconds) return '0s'; var d = Math.floor(seconds / 86400); @@ -242,6 +270,11 @@ return baseclass.extend({ listProfiles: callListProfiles, applyProfile: callApplyProfile, rollbackProfile: callRollbackProfile, + // App Store + getAppstoreApps: callGetAppstoreApps, + getAppstoreApp: callGetAppstoreApp, + installAppstoreApp: callInstallAppstoreApp, + removeAppstoreApp: callRemoveAppstoreApp, // Utilities formatUptime: formatUptime, formatBytes: formatBytes diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/apps.css b/luci-app-secubox/htdocs/luci-static/resources/secubox/apps.css new file mode 100644 index 0000000..01b5833 --- /dev/null +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/apps.css @@ -0,0 +1,363 @@ +/* SecuBox App Store Styles */ + +.secubox-apps-page { + min-height: 100vh; + padding: 1rem; +} + +.secubox-apps-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1.5rem; + margin-top: 1.5rem; + padding: 0 1rem; +} + +.app-card { + background: var(--card-bg, #1a1a2e); + border: 1px solid var(--card-border, rgba(255, 255, 255, 0.1)); + border-radius: 12px; + padding: 1.5rem; + transition: all 0.3s ease; + position: relative; +} + +.app-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); + border-color: var(--primary-color, #00d9ff); +} + +.app-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; + position: relative; +} + +.app-icon { + font-size: 2.5rem; + width: 3rem; + height: 3rem; + display: flex; + align-items: center; + justify-content: center; + background: var(--primary-color-transparent, rgba(0, 217, 255, 0.1)); + border-radius: 10px; +} + +.app-title { + flex: 1; +} + +.app-title h3 { + margin: 0; + font-size: 1.25rem; + color: var(--text-primary, #ffffff); + font-weight: 600; +} + +.app-version { + font-size: 0.875rem; + color: var(--text-secondary, rgba(255, 255, 255, 0.6)); + font-weight: 400; +} + +.app-status { + position: absolute; + top: 0; + right: 0; + padding: 0.25rem 0.75rem; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.app-status.status-stable { + background: rgba(34, 197, 94, 0.2); + color: #22c55e; +} + +.app-status.status-beta { + background: rgba(59, 130, 246, 0.2); + color: #3b82f6; +} + +.app-status.status-alpha { + background: rgba(251, 146, 60, 0.2); + color: #fb923c; +} + +.app-status.status-dev { + background: rgba(168, 85, 247, 0.2); + color: #a855f7; +} + +.app-description { + color: var(--text-secondary, rgba(255, 255, 255, 0.7)); + line-height: 1.6; + margin-bottom: 1rem; + font-size: 0.9375rem; +} + +.app-notes { + background: rgba(251, 191, 36, 0.1); + border-left: 3px solid #fbbf24; + padding: 0.75rem; + margin: 1rem 0; + border-radius: 4px; + font-size: 0.875rem; + color: var(--text-secondary, rgba(255, 255, 255, 0.8)); +} + +.app-notes strong { + color: #fbbf24; +} + +.app-luci { + display: flex; + align-items: center; + gap: 0.5rem; + margin: 0.75rem 0; + font-size: 0.875rem; + color: var(--primary-color, #00d9ff); +} + +.luci-icon { + font-size: 1rem; +} + +.app-actions { + display: flex; + gap: 0.75rem; + margin-top: 1.5rem; + padding-top: 1rem; + border-top: 1px solid var(--card-border, rgba(255, 255, 255, 0.1)); +} + +.app-actions .btn { + flex: 1; + padding: 0.625rem 1rem; + border-radius: 8px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + border: none; + font-size: 0.9375rem; +} + +.app-actions .btn-primary { + background: var(--primary-color, #00d9ff); + color: #000; +} + +.app-actions .btn-primary:hover { + background: var(--primary-hover, #00bce6); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 217, 255, 0.3); +} + +.app-actions .btn-secondary { + background: rgba(239, 68, 68, 0.2); + color: #ef4444; + border: 1px solid #ef4444; +} + +.app-actions .btn-secondary:hover { + background: rgba(239, 68, 68, 0.3); +} + +.app-actions .btn-link { + background: transparent; + color: var(--text-secondary, rgba(255, 255, 255, 0.7)); + border: 1px solid var(--card-border, rgba(255, 255, 255, 0.2)); +} + +.app-actions .btn-link:hover { + background: rgba(255, 255, 255, 0.05); + color: var(--text-primary, #ffffff); +} + +.app-actions .btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Empty State */ +.empty-state { + grid-column: 1 / -1; + text-align: center; + padding: 4rem 2rem; + color: var(--text-secondary, rgba(255, 255, 255, 0.6)); +} + +.empty-icon { + font-size: 4rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +.empty-state h3 { + color: var(--text-primary, #ffffff); + margin-bottom: 0.5rem; +} + +/* App Details Modal */ +.app-details-modal { + max-width: 600px; +} + +.modal-header { + display: flex; + align-items: center; + gap: 1.5rem; + margin-bottom: 1.5rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--card-border, rgba(255, 255, 255, 0.1)); +} + +.app-icon-large { + font-size: 4rem; + width: 5rem; + height: 5rem; + display: flex; + align-items: center; + justify-content: center; + background: var(--primary-color-transparent, rgba(0, 217, 255, 0.1)); + border-radius: 16px; +} + +.modal-body { + color: var(--text-secondary, rgba(255, 255, 255, 0.8)); +} + +.app-description-full { + line-height: 1.6; + margin-bottom: 1.5rem; +} + +.app-notes-box { + background: rgba(251, 191, 36, 0.1); + border-left: 3px solid #fbbf24; + padding: 1rem; + margin: 1.5rem 0; + border-radius: 4px; +} + +.app-notes-box strong { + color: #fbbf24; + display: block; + margin-bottom: 0.5rem; +} + +.app-meta { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; + margin: 1.5rem 0; + padding: 1rem; + background: rgba(255, 255, 255, 0.03); + border-radius: 8px; +} + +.meta-item { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.meta-item strong { + font-size: 0.75rem; + text-transform: uppercase; + color: var(--text-tertiary, rgba(255, 255, 255, 0.5)); +} + +.meta-item span { + color: var(--text-primary, #ffffff); +} + +.app-dependencies { + margin: 1.5rem 0; +} + +.app-dependencies strong { + display: block; + margin-bottom: 0.75rem; + color: var(--text-primary, #ffffff); +} + +.app-dependencies ul { + list-style: none; + padding: 0; + margin: 0; +} + +.app-dependencies li { + padding: 0.5rem; + background: rgba(255, 255, 255, 0.03); + margin-bottom: 0.25rem; + border-radius: 4px; + font-family: monospace; + font-size: 0.875rem; +} + +.app-tags { + margin: 1.5rem 0; +} + +.app-tags strong { + display: block; + margin-bottom: 0.75rem; + color: var(--text-primary, #ffffff); +} + +.tags-list { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +.tag { + padding: 0.375rem 0.75rem; + background: rgba(0, 217, 255, 0.1); + color: var(--primary-color, #00d9ff); + border-radius: 16px; + font-size: 0.8125rem; + font-weight: 500; +} + +.app-links { + margin-top: 1.5rem; + padding-top: 1.5rem; + border-top: 1px solid var(--card-border, rgba(255, 255, 255, 0.1)); +} + +.app-links .btn-link { + display: inline-block; + padding: 0.625rem 1.25rem; + background: var(--primary-color, #00d9ff); + color: #000; + text-decoration: none; + border-radius: 8px; + font-weight: 500; + transition: all 0.2s ease; +} + +.app-links .btn-link:hover { + background: var(--primary-hover, #00bce6); + transform: translateY(-1px); +} + +/* Responsive */ +@media (max-width: 768px) { + .secubox-apps-grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .app-meta { + grid-template-columns: 1fr; + } +} diff --git a/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js b/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js index b53c39d..c4eee23 100644 --- a/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js +++ b/luci-app-secubox/htdocs/luci-static/resources/secubox/nav.js @@ -6,7 +6,7 @@ var tabs = [ { id: 'dashboard', icon: 'πŸš€', label: _('Dashboard'), path: ['admin', 'secubox', 'dashboard'] }, { id: 'modules', icon: '🧩', label: _('Modules'), path: ['admin', 'secubox', 'modules'] }, { id: 'wizard', icon: '✨', label: _('Wizard'), path: ['admin', 'secubox', 'wizard'] }, - { id: 'appstore', icon: 'πŸ›’', label: _('App Store'), path: ['admin', 'secubox', 'appstore'] }, + { id: 'apps', icon: 'πŸ›’', label: _('App Store'), path: ['admin', 'secubox', 'apps'] }, { id: 'monitoring', icon: 'πŸ“‘', label: _('Monitoring'), path: ['admin', 'secubox', 'monitoring'] }, { id: 'alerts', icon: '⚠️', label: _('Alerts'), path: ['admin', 'secubox', 'alerts'] }, { id: 'settings', icon: 'βš™οΈ', label: _('Settings'), path: ['admin', 'secubox', 'settings'] }, diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/apps.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/apps.js new file mode 100644 index 0000000..34e7b9a --- /dev/null +++ b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/apps.js @@ -0,0 +1,379 @@ +'use strict'; +'require view'; +'require ui'; +'require dom'; +'require secubox/api as API'; +'require secubox-theme/theme as Theme'; +'require secubox/nav as SecuNav'; +'require secubox-theme/cascade as Cascade'; +'require poll'; + +// Load global theme CSS +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('secubox-theme/secubox-theme.css') +})); +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('secubox-theme/themes/cyberpunk.css') +})); +document.head.appendChild(E('link', { + 'rel': 'stylesheet', + 'type': 'text/css', + 'href': L.resource('secubox/apps.css') +})); + +// Initialize global theme +var secuLang = (typeof L !== 'undefined' && L.env && L.env.lang) || + (document.documentElement && document.documentElement.getAttribute('lang')) || + (navigator.language ? navigator.language.split('-')[0] : 'en'); +Theme.init({ language: secuLang }); + +return view.extend({ + appsData: [], + categoriesData: {}, + currentFilter: 'all', + filterLayer: null, + + load: function() { + return this.refreshData(); + }, + + refreshData: function() { + var self = this; + return API.getAppstoreApps().then(function(data) { + self.appsData = data.apps || []; + self.categoriesData = data.categories || {}; + return data; + }); + }, + + render: function(data) { + var self = this; + var apps = (data && data.apps) || this.appsData || []; + var categories = (data && data.categories) || this.categoriesData || {}; + + var defaultFilter = this.currentFilter || 'all'; + var container = E('div', { + 'class': 'secubox-apps-page', + 'data-cascade-root': 'apps' + }, [ + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/core/variables.css') }), + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/common.css') }), + E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/secubox.css') }), + SecuNav.renderTabs('apps'), + this.renderHeader(apps), + this.renderFilterTabs(categories), + E('div', { + 'id': 'apps-grid', + 'class': 'secubox-apps-grid sb-cascade-layer', + 'data-cascade-layer': 'view', + 'data-cascade-role': 'apps', + 'data-cascade-depth': '3', + 'data-cascade-filter': defaultFilter + }, this.renderAppCards(apps, defaultFilter)) + ]); + + // Auto-refresh every 10 seconds + poll.add(function() { + return self.refreshData().then(function() { + self.updateAppsGrid(); + }); + }, 10); + + return container; + }, + + renderHeader: function(apps) { + var installedCount = apps.filter(function(app) { + return app.installed; + }).length; + + return E('div', { 'class': 'secubox-page-header' }, [ + E('div', { 'class': 'header-content' }, [ + E('div', { 'class': 'header-title' }, [ + E('h1', {}, _('App Store')), + E('p', { 'class': 'subtitle' }, _('Browse and install SecuBox applications')) + ]), + E('div', { 'class': 'header-stats' }, [ + E('div', { 'class': 'stat-item' }, [ + E('span', { 'class': 'stat-value' }, String(apps.length)), + E('span', { 'class': 'stat-label' }, _('Available Apps')) + ]), + E('div', { 'class': 'stat-item' }, [ + E('span', { 'class': 'stat-value' }, String(installedCount)), + E('span', { 'class': 'stat-label' }, _('Installed')) + ]) + ]) + ]) + ]); + }, + + renderFilterTabs: function(categories) { + var self = this; + var filters = [ + { id: 'all', label: _('All Apps'), icon: 'πŸ“¦' } + ]; + + // Add category filters + Object.keys(categories).forEach(function(catId) { + var cat = categories[catId]; + filters.push({ + id: catId, + label: cat.name, + icon: cat.icon + }); + }); + + filters.push({ id: 'installed', label: _('Installed'), icon: 'βœ“' }); + + var tabs = filters.map(function(filter) { + var isActive = filter.id === self.currentFilter; + return E('button', { + 'class': isActive ? 'filter-tab active' : 'filter-tab', + 'data-filter': filter.id, + 'click': function(ev) { + self.switchFilter(filter.id); + } + }, [ + E('span', { 'class': 'tab-icon' }, filter.icon), + E('span', { 'class': 'tab-label' }, filter.label) + ]); + }); + + return E('div', { + 'class': 'secubox-filter-tabs sb-cascade-layer', + 'data-cascade-layer': 'nav', + 'data-cascade-depth': '2' + }, tabs); + }, + + renderAppCards: function(apps, filter) { + var self = this; + var filteredApps = apps.filter(function(app) { + if (filter === 'all') return true; + if (filter === 'installed') return app.installed; + return app.category === filter; + }); + + if (filteredApps.length === 0) { + return E('div', { 'class': 'empty-state' }, [ + E('div', { 'class': 'empty-icon' }, 'πŸ“¦'), + E('h3', {}, _('No apps found')), + E('p', {}, _('No applications match the selected filter')) + ]); + } + + return filteredApps.map(function(app) { + return self.renderAppCard(app); + }); + }, + + renderAppCard: function(app) { + var self = this; + var statusClass = 'status-' + app.status; + var statusLabel = { + 'stable': _('Stable'), + 'beta': _('Beta'), + 'alpha': _('Alpha'), + 'dev': _('Development') + }[app.status] || app.status; + + return E('div', { + 'class': 'app-card ' + statusClass, + 'data-app-id': app.id, + 'data-category': app.category + }, [ + E('div', { 'class': 'app-header' }, [ + E('div', { 'class': 'app-icon' }, app.icon || 'πŸ“¦'), + E('div', { 'class': 'app-title' }, [ + E('h3', {}, app.name), + E('span', { 'class': 'app-version' }, 'v' + app.version) + ]), + E('span', { + 'class': 'app-status ' + statusClass + }, statusLabel) + ]), + E('div', { 'class': 'app-description' }, app.description), + app.notes ? E('div', { 'class': 'app-notes' }, [ + E('strong', {}, _('Note: ')), + E('span', {}, app.notes) + ]) : null, + app.luci_app ? E('div', { 'class': 'app-luci' }, [ + E('span', { 'class': 'luci-icon' }, 'πŸŽ›οΈ'), + E('span', {}, _('Includes LuCI interface')) + ]) : null, + E('div', { 'class': 'app-actions' }, [ + app.installed ? + E('button', { + 'class': 'btn btn-secondary', + 'click': function(ev) { + self.removeApp(app.id, ev.target); + } + }, _('Remove')) : + E('button', { + 'class': 'btn btn-primary', + 'click': function(ev) { + self.installApp(app.id, ev.target); + } + }, _('Install')), + E('button', { + 'class': 'btn btn-link', + 'click': function(ev) { + self.showAppDetails(app.id); + } + }, _('Details')) + ]) + ]); + }, + + switchFilter: function(filterId) { + this.currentFilter = filterId; + + // Update active tab + var tabs = document.querySelectorAll('.filter-tab'); + tabs.forEach(function(tab) { + if (tab.getAttribute('data-filter') === filterId) { + tab.classList.add('active'); + } else { + tab.classList.remove('active'); + } + }); + + // Update grid + this.updateAppsGrid(); + }, + + updateAppsGrid: function() { + var grid = document.getElementById('apps-grid'); + if (!grid) return; + + dom.content(grid, this.renderAppCards(this.appsData, this.currentFilter)); + }, + + installApp: function(appId, button) { + var self = this; + button.disabled = true; + button.textContent = _('Installing...'); + + return API.installAppstoreApp(appId).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', _('App installed successfully')), 'info'); + return self.refreshData().then(function() { + self.updateAppsGrid(); + }); + } else { + ui.addNotification(null, E('p', _('Installation failed: ') + (result.error || result.details)), 'error'); + button.disabled = false; + button.textContent = _('Install'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', _('Installation error: ') + err.message), 'error'); + button.disabled = false; + button.textContent = _('Install'); + }); + }, + + removeApp: function(appId, button) { + var self = this; + + if (!confirm(_('Are you sure you want to remove this app?'))) { + return; + } + + button.disabled = true; + button.textContent = _('Removing...'); + + return API.removeAppstoreApp(appId).then(function(result) { + if (result.success) { + ui.addNotification(null, E('p', _('App removed successfully')), 'info'); + return self.refreshData().then(function() { + self.updateAppsGrid(); + }); + } else { + ui.addNotification(null, E('p', _('Removal failed: ') + (result.error || result.details)), 'error'); + button.disabled = false; + button.textContent = _('Remove'); + } + }).catch(function(err) { + ui.addNotification(null, E('p', _('Removal error: ') + err.message), 'error'); + button.disabled = false; + button.textContent = _('Remove'); + }); + }, + + showAppDetails: function(appId) { + var self = this; + + return API.getAppstoreApp(appId).then(function(app) { + if (!app || app.error) { + ui.addNotification(null, E('p', _('Failed to load app details')), 'error'); + return; + } + + var content = E('div', { 'class': 'app-details-modal' }, [ + E('div', { 'class': 'modal-header' }, [ + E('span', { 'class': 'app-icon-large' }, app.icon || 'πŸ“¦'), + E('div', {}, [ + E('h2', {}, app.name), + E('p', {}, app.version) + ]) + ]), + E('div', { 'class': 'modal-body' }, [ + E('p', { 'class': 'app-description-full' }, app.description), + app.notes ? E('div', { 'class': 'app-notes-box' }, [ + E('strong', {}, _('Important Notes:')), + E('p', {}, app.notes) + ]) : null, + E('div', { 'class': 'app-meta' }, [ + E('div', { 'class': 'meta-item' }, [ + E('strong', {}, _('Author:')), + E('span', {}, app.author) + ]), + E('div', { 'class': 'meta-item' }, [ + E('strong', {}, _('License:')), + E('span', {}, app.license) + ]), + E('div', { 'class': 'meta-item' }, [ + E('strong', {}, _('Category:')), + E('span', {}, app.category) + ]), + E('div', { 'class': 'meta-item' }, [ + E('strong', {}, _('Status:')), + E('span', {}, app.status) + ]) + ]), + app.dependencies && app.dependencies.length > 0 ? E('div', { 'class': 'app-dependencies' }, [ + E('strong', {}, _('Dependencies:')), + E('ul', {}, app.dependencies.map(function(dep) { + return E('li', {}, dep); + })) + ]) : null, + app.tags && app.tags.length > 0 ? E('div', { 'class': 'app-tags' }, [ + E('strong', {}, _('Tags:')), + E('div', { 'class': 'tags-list' }, app.tags.map(function(tag) { + return E('span', { 'class': 'tag' }, tag); + })) + ]) : null, + app.url ? E('div', { 'class': 'app-links' }, [ + E('a', { + 'href': app.url, + 'target': '_blank', + 'class': 'btn btn-link' + }, _('Visit Project Website β†’')) + ]) : null + ]) + ]); + + ui.showModal(_('App Details'), content, 'max-content'); + }).catch(function(err) { + ui.addNotification(null, E('p', _('Error loading app details: ') + err.message), 'error'); + }); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/appstore.js b/luci-app-secubox/htdocs/luci-static/resources/view/secubox/appstore.js deleted file mode 100644 index c14cc5d..0000000 --- a/luci-app-secubox/htdocs/luci-static/resources/view/secubox/appstore.js +++ /dev/null @@ -1,470 +0,0 @@ -'use strict'; -'require view'; -'require ui'; -'require dom'; -'require secubox/api as API'; -'require secubox-theme/theme as Theme'; -'require secubox/nav as SecuNav'; - -// Load theme resources -document.head.appendChild(E('link', { - 'rel': 'stylesheet', - 'type': 'text/css', - 'href': L.resource('secubox-theme/secubox-theme.css') -})); - -var secuLang = (typeof L !== 'undefined' && L.env && L.env.lang) || - (document.documentElement && document.documentElement.getAttribute('lang')) || - (navigator.language ? navigator.language.split('-')[0] : 'en'); -Theme.init({ language: secuLang }); - -var RUNTIME_FILTERS = [ - { id: 'all', label: _('All runtimes') }, - { id: 'docker', label: _('Docker') }, - { id: 'lxc', label: _('LXC') }, - { id: 'native', label: _('Native') } -]; - -var STATE_FILTERS = [ - { id: 'all', label: _('All states') }, - { id: 'installed', label: _('Installed') }, - { id: 'available', label: _('Available') } -]; - -var RUNTIME_ICONS = { - docker: '🐳', - lxc: 'πŸ“¦', - native: 'βš™οΈ', - hybrid: '🧬' -}; - -return view.extend({ - load: function() { - return Promise.all([ - API.listApps() - ]); - }, - - render: function(payload) { - this.apps = (payload[0] && payload[0].apps) || []; - this.searchQuery = ''; - this.runtimeFilter = 'all'; - this.stateFilter = 'all'; - this.filterButtons = { runtime: {}, state: {} }; - - this.root = E('div', { 'class': 'secubox-appstore-page' }, [ - E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/core/variables.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/common.css') }), - SecuNav.renderTabs('appstore'), - this.renderHeader(), - this.renderStats(), - this.renderFilterBar(), - this.renderAppGrid() - ]); - - this.updateStats(); - this.updateAppGrid(); - return this.root; - }, - - renderHeader: function() { - return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [ - E('div', {}, [ - E('h2', { 'class': 'sh-page-title' }, [ - E('span', { 'class': 'sh-page-title-icon' }, 'πŸ›’'), - _('SecuBox App Store') - ]), - E('p', { 'class': 'sh-page-subtitle' }, - _('Browse manifest-driven apps, launch guided wizards, and copy CLI commands from the SecuBox App Store.')) - ]) - ]); - }, - - renderStats: function() { - this.statsNodes = { - total: E('div', { 'class': 'sb-stat-value' }, '0'), - installed: E('div', { 'class': 'sb-stat-value' }, '0'), - docker: E('div', { 'class': 'sb-stat-value' }, '0'), - lxc: E('div', { 'class': 'sb-stat-value' }, '0') - }; - - return E('div', { 'class': 'sb-stats-row' }, [ - this.renderStatCard('πŸ“¦', _('Total apps'), this.statsNodes.total, _('Manifest entries detected')), - this.renderStatCard('βœ…', _('Installed'), this.statsNodes.installed, _('Apps currently deployed')), - this.renderStatCard('🐳', _('Docker'), this.statsNodes.docker, _('Containerized services')), - this.renderStatCard('πŸ“¦', _('LXC'), this.statsNodes.lxc, _('Lightweight containers')) - ]); - }, - - renderStatCard: function(icon, title, valueEl, subtitle) { - return E('div', { 'class': 'sb-stat-card' }, [ - E('div', { 'class': 'sb-stat-icon' }, icon), - E('div', { 'class': 'sb-stat-label' }, title), - valueEl, - E('div', { 'class': 'sb-stat-sub' }, subtitle) - ]); - }, - - renderFilterBar: function() { - var self = this; - this.searchInput = E('input', { - 'class': 'sb-wizard-input', - 'type': 'search', - 'placeholder': _('Search apps…') - }); - this.searchInput.addEventListener('input', function(ev) { - self.searchQuery = (ev.target.value || '').trim().toLowerCase(); - self.updateAppGrid(); - }); - - return E('div', { 'class': 'secubox-appstore-filters' }, [ - E('div', { 'class': 'sb-filter-group' }, [ - E('div', { 'class': 'sb-filter-label' }, _('Type')), - E('div', { 'class': 'sb-filter-pills' }, RUNTIME_FILTERS.map(function(filter) { - var pill = E('button', { - 'class': 'sb-filter-pill' + (filter.id === self.runtimeFilter ? ' active' : ''), - 'click': self.handleFilterClick.bind(self, 'runtime', filter.id) - }, filter.label); - self.filterButtons.runtime[filter.id] = pill; - return pill; - })) - ]), - E('div', { 'class': 'sb-filter-group' }, [ - E('div', { 'class': 'sb-filter-label' }, _('State')), - E('div', { 'class': 'sb-filter-pills' }, STATE_FILTERS.map(function(filter) { - var pill = E('button', { - 'class': 'sb-filter-pill' + (filter.id === self.stateFilter ? ' active' : ''), - 'click': self.handleFilterClick.bind(self, 'state', filter.id) - }, filter.label); - self.filterButtons.state[filter.id] = pill; - return pill; - })) - ]), - E('div', { 'class': 'sb-filter-search' }, [ - E('span', { 'class': 'sb-filter-search-icon' }, 'πŸ”'), - this.searchInput - ]) - ]); - }, - - handleFilterClick: function(group, value, ev) { - ev.preventDefault(); - if (group === 'runtime') - this.runtimeFilter = value; - else - this.stateFilter = value; - - this.updateFilterButtons(group); - this.updateAppGrid(); - }, - - updateFilterButtons: function(group) { - var buttons = this.filterButtons[group] || {}; - Object.keys(buttons).forEach(function(key) { - var el = buttons[key]; - if (!el) - return; - if ((group === 'runtime' && key === this.runtimeFilter) || - (group === 'state' && key === this.stateFilter)) - el.classList.add('active'); - else - el.classList.remove('active'); - }, this); - }, - - renderAppGrid: function() { - this.appGrid = E('div', { 'class': 'sb-app-grid secubox-appstore-grid' }); - return this.appGrid; - }, - - updateStats: function() { - var total = this.apps.length; - var installed = this.apps.filter(function(app) { return app.state === 'installed'; }).length; - var docker = this.apps.filter(function(app) { return (app.runtime || app.type || '') === 'docker'; }).length; - var lxc = this.apps.filter(function(app) { return (app.runtime || app.type || '') === 'lxc'; }).length; - - if (this.statsNodes) { - this.statsNodes.total.textContent = total.toString(); - this.statsNodes.installed.textContent = installed.toString(); - this.statsNodes.docker.textContent = docker.toString(); - this.statsNodes.lxc.textContent = lxc.toString(); - } - }, - - getFilteredApps: function() { - var q = this.searchQuery; - var runtimeFilter = this.runtimeFilter; - var state = this.stateFilter; - - return this.apps.filter(function(app) { - var runtime = (app.runtime || app.type || '').toLowerCase(); - var desc = ((app.description || '') + ' ' + (app.name || '') + ' ' + (app.id || '')).toLowerCase(); - var matchesRuntime = runtimeFilter === 'all' || runtime === runtimeFilter; - var matchesState = state === 'all' || - (state === 'installed' && app.state === 'installed') || - (state === 'available' && app.state !== 'installed'); - var matchesSearch = !q || desc.indexOf(q) !== -1; - return matchesRuntime && matchesState && matchesSearch; - }); - }, - - updateAppGrid: function() { - if (!this.appGrid) - return; - var apps = this.getFilteredApps(); - if (!apps.length) { - dom.content(this.appGrid, [ - E('div', { 'class': 'secubox-empty-state' }, [ - E('div', { 'class': 'secubox-empty-icon' }, 'πŸ•΅οΈ'), - E('div', { 'class': 'secubox-empty-title' }, _('No apps found')), - E('div', { 'class': 'secubox-empty-text' }, _('Adjust filters or add manifests under /usr/share/secubox/plugins/.')) - ]) - ]); - return; - } - dom.content(this.appGrid, apps.map(this.renderAppCard, this)); - }, - - renderAppCard: function(app) { - var runtime = (app.runtime || app.type || 'other').toLowerCase(); - var icon = RUNTIME_ICONS[runtime] || '🧩'; - var stateClass = app.state === 'installed' ? ' ok' : ''; - var badges = [ - E('span', { 'class': 'sb-app-tag' }, icon + ' ' + (app.runtime || app.type || _('Unknown'))) - ]; - if (app.category) - badges.push(E('span', { 'class': 'sb-app-tag' }, _('Category: %s').format(app.category))); - if (app.maturity) - badges.push(E('span', { 'class': 'sb-app-tag' }, _('Maturity: %s').format(app.maturity))); - if (app.version) - badges.push(E('span', { 'class': 'sb-app-tag sb-app-version' }, 'v' + app.version)); - - return E('div', { 'class': 'sb-app-card' }, [ - E('div', { 'class': 'sb-app-card-info' }, [ - E('div', { 'class': 'sb-app-name' }, [ - app.name || app.id, - E('span', { 'class': 'sb-app-state' + stateClass }, app.state || _('unknown')) - ]), - E('div', { 'class': 'sb-app-desc' }, app.description || _('No description provided')), - E('div', { 'class': 'sb-app-tags' }, badges) - ]), - E('div', { 'class': 'sb-app-actions' }, [ - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': this.showAppDetails.bind(this, app) - }, _('Details')), - (app.has_wizard ? E('button', { - 'class': 'cbi-button', - 'click': this.openAppWizard.bind(this, app) - }, _('Configure')) : null) - ]) - ]); - }, - - showAppDetails: function(app, ev) { - var self = this; - ui.showModal(_('Loading %s…').format(app.name || app.id), [E('div', { 'class': 'spinning' })]); - API.getAppManifest(app.id).then(function(manifest) { - ui.hideModal(); - manifest = manifest || {}; - var wizard = manifest.wizard || {}; - var packages = manifest.packages || []; - var ports = manifest.ports || []; - var volumes = manifest.volumes || []; - var requirements = manifest.requirements || {}; - var hardware = manifest.hardware || {}; - var network = manifest.network || {}; - var privileges = manifest.privileges || {}; - var profiles = (manifest.profiles && manifest.profiles.recommended) || manifest.profiles || []; - if (!Array.isArray(profiles)) - profiles = []; - - var makeRow = function(label, value) { - return E('div', { 'class': 'sb-app-detail-row' }, [ - E('strong', {}, label), - E('span', {}, value) - ]); - }; - - var detailRows = [ - makeRow(_('Runtime:'), manifest.runtime || app.runtime || manifest.type || app.type || _('Unknown')), - makeRow(_('Category:'), manifest.category || _('Unknown')), - makeRow(_('Maturity:'), manifest.maturity || _('Unspecified')), - makeRow(_('Version:'), manifest.version || app.version || 'β€”'), - makeRow(_('State:'), app.state || _('unknown')) - ]; - - var requirementRows = []; - if (requirements.arch && requirements.arch.length) - requirementRows.push(makeRow(_('Architectures:'), requirements.arch.join(', '))); - if (requirements.min_ram_mb) - requirementRows.push(makeRow(_('Min RAM:'), _('%s MB').format(requirements.min_ram_mb))); - if (requirements.min_storage_mb) - requirementRows.push(makeRow(_('Min storage:'), _('%s MB').format(requirements.min_storage_mb))); - - var hardwareRows = []; - if (typeof hardware.usb === 'boolean') - hardwareRows.push(makeRow(_('USB access:'), hardware.usb ? _('Required') : _('Not needed'))); - if (typeof hardware.serial === 'boolean') - hardwareRows.push(makeRow(_('Serial access:'), hardware.serial ? _('Required') : _('Not needed'))); - - var privilegeRows = []; - if (typeof privileges.needs_usb === 'boolean') - privilegeRows.push(makeRow(_('USB privileges:'), privileges.needs_usb ? _('Required') : _('Not needed'))); - if (typeof privileges.needs_serial === 'boolean') - privilegeRows.push(makeRow(_('Serial privileges:'), privileges.needs_serial ? _('Required') : _('Not needed'))); - if (typeof privileges.needs_net_admin === 'boolean') - privilegeRows.push(makeRow(_('Net admin:'), privileges.needs_net_admin ? _('Required') : _('Not needed'))); - - var networkRows = []; - if ((network.inbound_ports || []).length) - networkRows.push(makeRow(_('Inbound ports:'), network.inbound_ports.join(', '))); - if ((network.protocols || []).length) - networkRows.push(makeRow(_('Protocols:'), network.protocols.join(', '))); - if (typeof network.outbound_only === 'boolean') - networkRows.push(makeRow(_('Network mode:'), network.outbound_only ? _('Outbound only') : _('Inbound/Outbound'))); - - var cliCommands = E('pre', { 'class': 'sb-app-cli' }, [ - 'secubox-app install ' + app.id + '\n', - (wizard.fields && wizard.fields.length ? 'secubox-app wizard ' + app.id + '\n' : ''), - 'secubox-app status ' + app.id + '\n', - 'secubox-app remove ' + app.id - ]); - - var sections = [ - E('p', { 'class': 'sb-app-desc' }, manifest.description || app.description || ''), - E('div', { 'class': 'sb-app-detail-grid' }, detailRows), - requirementRows.length ? E('div', { 'class': 'sb-app-detail-list' }, [ - E('strong', {}, _('Requirements')), - E('div', { 'class': 'sb-app-detail-grid' }, requirementRows) - ]) : '', - hardwareRows.length ? E('div', { 'class': 'sb-app-detail-list' }, [ - E('strong', {}, _('Hardware')), - E('div', { 'class': 'sb-app-detail-grid' }, hardwareRows) - ]) : '', - privilegeRows.length ? E('div', { 'class': 'sb-app-detail-list' }, [ - E('strong', {}, _('Privileges')), - E('div', { 'class': 'sb-app-detail-grid' }, privilegeRows) - ]) : '', - networkRows.length ? E('div', { 'class': 'sb-app-detail-list' }, [ - E('strong', {}, _('Network')), - E('div', { 'class': 'sb-app-detail-grid' }, networkRows) - ]) : '', - packages.length ? E('div', { 'class': 'sb-app-detail-list' }, [ - E('strong', {}, _('Packages')), - E('ul', {}, packages.map(function(pkg) { return E('li', {}, pkg); })) - ]) : '', - ports.length ? E('div', { 'class': 'sb-app-detail-list' }, [ - E('strong', {}, _('Ports')), - E('ul', {}, ports.map(function(port) { - var label = [port.name || 'port', port.protocol || '', port.port || ''].filter(Boolean).join(' Β· '); - return E('li', {}, label); - })) - ]) : '', - volumes.length ? E('div', { 'class': 'sb-app-detail-list' }, [ - E('strong', {}, _('Volumes')), - E('ul', {}, volumes.map(function(volume) { return E('li', {}, volume); })) - ]) : '', - profiles.length ? E('div', { 'class': 'sb-app-detail-list' }, [ - E('strong', {}, _('Profiles')), - E('ul', {}, profiles.map(function(profile) { return E('li', {}, profile); })) - ]) : '', - E('div', { 'class': 'sb-app-detail-list' }, [ - E('strong', {}, _('CLI commands')), - cliCommands - ]) - ]; - - var actions = [ - E('button', { - 'class': 'cbi-button cbi-button-cancel', - 'click': ui.hideModal - }, _('Close')), - (app.has_wizard ? E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function() { - ui.hideModal(); - self.openAppWizard(app); - } - }, _('Launch wizard')) : null) - ].filter(Boolean); - - ui.showModal(app.name || app.id, [ - E('div', { 'class': 'sb-app-detail-body' }, sections), - E('div', { 'class': 'right', 'style': 'margin-top:16px;' }, actions) - ]); - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, err && err.message ? err.message : _('Unable to load manifest')), 'error'); - }); - }, - - openAppWizard: function(app) { - var self = this; - ui.showModal(_('Loading %s wizard…').format(app.name || app.id), [E('div', { 'class': 'spinning' })]); - API.getAppManifest(app.id).then(function(manifest) { - ui.hideModal(); - manifest = manifest || {}; - var wizard = manifest.wizard || {}; - var fields = wizard.fields || []; - if (!fields.length) { - ui.addNotification(null, E('p', {}, _('No wizard metadata for this app.')), 'warn'); - return; - } - var form = E('div', { 'class': 'sb-app-wizard-form' }, fields.map(function(field) { - return E('div', { 'class': 'sb-form-group' }, [ - E('label', {}, field.label || field.id), - E('input', { - 'class': 'sb-wizard-input', - 'name': field.id, - 'type': field.type || 'text', - 'placeholder': field.placeholder || '' - }) - ]); - })); - ui.showModal(_('Configure %s').format(app.name || app.id), [ - form, - E('div', { 'class': 'right', 'style': 'margin-top:16px;' }, [ - E('button', { - 'class': 'cbi-button cbi-button-cancel', - 'click': ui.hideModal - }, _('Cancel')), - E('button', { - 'class': 'cbi-button cbi-button-action', - 'click': function() { - self.submitAppWizard(app.id, form, fields); - } - }, _('Apply')) - ]) - ]); - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, err && err.message ? err.message : _('Failed to load wizard')), 'error'); - }); - }, - - submitAppWizard: function(appId, form, fields) { - var values = {}; - fields.forEach(function(field) { - var input = form.querySelector('[name="' + field.id + '"]'); - if (input && input.value !== '') - values[field.id] = input.value; - }); - ui.showModal(_('Saving…'), [E('div', { 'class': 'spinning' })]); - API.applyAppWizard(appId, values).then(function(result) { - ui.hideModal(); - if (result && result.success) { - ui.addNotification(null, E('p', {}, _('Wizard applied.')), 'info'); - } else { - ui.addNotification(null, E('p', {}, _('Failed to apply wizard.')), 'error'); - } - }).catch(function(err) { - ui.hideModal(); - ui.addNotification(null, E('p', {}, err && err.message ? err.message : _('Failed to apply wizard.')), 'error'); - }); - }, - - handleSaveApply: null, - handleSave: null, - handleReset: null -}); diff --git a/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox b/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox index a718aa4..ee5e630 100755 --- a/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox +++ b/luci-app-secubox/root/usr/libexec/rpcd/luci.secubox @@ -1540,6 +1540,301 @@ rollback_profile() { json_add_string "message" "Restored $latest_backup" json_dump } + +# ============================================================================ +# App Store Functions +# ============================================================================ + +APPSTORE_JSON="/usr/share/secubox/.appstore/apps.json" + +# Get all apps from app store catalog +get_appstore_apps() { + if [ ! -f "$APPSTORE_JSON" ]; then + json_init + json_add_array "apps" + json_close_array + json_add_object "categories" + json_close_object + json_dump + return + fi + + local appstore_data=$(cat "$APPSTORE_JSON") + local app_count=$(jsonfilter -s "$appstore_data" -e '@.apps[*]' | wc -l) + + json_init + json_add_array "apps" + + local i=0 + while [ $i -lt $app_count ]; do + local app_id=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].id") + local app_name=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].name") + local app_version=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].version") + local app_category=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].category") + local app_description=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].description") + local app_icon=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].icon") + local app_status=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].status") + local app_luci=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].luci_app") + local app_notes=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].notes") + + # Check if app is installed + local is_installed=0 + if command -v opkg >/dev/null 2>&1; then + opkg list-installed "$app_id" 2>/dev/null | grep -q "^$app_id " && is_installed=1 + elif command -v apk >/dev/null 2>&1; then + apk info -e "$app_id" >/dev/null 2>&1 && is_installed=1 + fi + + json_add_object + json_add_string "id" "$app_id" + json_add_string "name" "$app_name" + json_add_string "version" "$app_version" + json_add_string "category" "$app_category" + json_add_string "description" "$app_description" + json_add_string "icon" "$app_icon" + json_add_string "status" "$app_status" + json_add_boolean "installed" "$is_installed" + [ -n "$app_luci" ] && json_add_string "luci_app" "$app_luci" + [ -n "$app_notes" ] && json_add_string "notes" "$app_notes" + json_close_object + + i=$((i + 1)) + done + + json_close_array + + # Add categories + json_add_object "categories" + local cat_keys=$(jsonfilter -s "$appstore_data" -e '@.categories' | jsonfilter -e '@[@]') + for cat in security network iot media; do + local cat_name=$(jsonfilter -s "$appstore_data" -e "@.categories.$cat.name") + local cat_icon=$(jsonfilter -s "$appstore_data" -e "@.categories.$cat.icon") + local cat_desc=$(jsonfilter -s "$appstore_data" -e "@.categories.$cat.description") + if [ -n "$cat_name" ]; then + json_add_object "$cat" + json_add_string "name" "$cat_name" + json_add_string "icon" "$cat_icon" + json_add_string "description" "$cat_desc" + json_close_object + fi + done + json_close_object + + json_dump +} + +# Get single app from app store +get_appstore_app() { + local input app_id + read input + json_load "$input" + json_get_var app_id app_id + json_cleanup + + if [ ! -f "$APPSTORE_JSON" ]; then + json_init + json_add_boolean "success" 0 + json_add_string "error" "App store catalog not found" + json_dump + return + fi + + local appstore_data=$(cat "$APPSTORE_JSON") + local app_count=$(jsonfilter -s "$appstore_data" -e '@.apps[*]' | wc -l) + + local i=0 + while [ $i -lt $app_count ]; do + local current_id=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].id") + if [ "$current_id" = "$app_id" ]; then + # Found the app + json_init + json_add_string "id" "$current_id" + json_add_string "name" "$(jsonfilter -s "$appstore_data" -e "@.apps[$i].name")" + json_add_string "version" "$(jsonfilter -s "$appstore_data" -e "@.apps[$i].version")" + json_add_string "category" "$(jsonfilter -s "$appstore_data" -e "@.apps[$i].category")" + json_add_string "description" "$(jsonfilter -s "$appstore_data" -e "@.apps[$i].description")" + json_add_string "icon" "$(jsonfilter -s "$appstore_data" -e "@.apps[$i].icon")" + json_add_string "author" "$(jsonfilter -s "$appstore_data" -e "@.apps[$i].author")" + json_add_string "license" "$(jsonfilter -s "$appstore_data" -e "@.apps[$i].license")" + json_add_string "url" "$(jsonfilter -s "$appstore_data" -e "@.apps[$i].url")" + json_add_string "status" "$(jsonfilter -s "$appstore_data" -e "@.apps[$i].status")" + + local app_luci=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].luci_app") + [ -n "$app_luci" ] && [ "$app_luci" != "null" ] && json_add_string "luci_app" "$app_luci" + + local app_notes=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].notes") + [ -n "$app_notes" ] && [ "$app_notes" != "null" ] && json_add_string "notes" "$app_notes" + + # Add dependencies array + json_add_array "dependencies" + local dep_count=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].dependencies[*]" | wc -l) + local j=0 + while [ $j -lt $dep_count ]; do + local dep=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].dependencies[$j]") + json_add_string "" "$dep" + j=$((j + 1)) + done + json_close_array + + # Add tags array + json_add_array "tags" + local tag_count=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].tags[*]" | wc -l) + j=0 + while [ $j -lt $tag_count ]; do + local tag=$(jsonfilter -s "$appstore_data" -e "@.apps[$i].tags[$j]") + json_add_string "" "$tag" + j=$((j + 1)) + done + json_close_array + + # Check installation status + local is_installed=0 + if command -v opkg >/dev/null 2>&1; then + opkg list-installed "$app_id" 2>/dev/null | grep -q "^$app_id " && is_installed=1 + elif command -v apk >/dev/null 2>&1; then + apk info -e "$app_id" >/dev/null 2>&1 && is_installed=1 + fi + json_add_boolean "installed" "$is_installed" + + json_dump + return + fi + i=$((i + 1)) + done + + # App not found + json_init + json_add_boolean "success" 0 + json_add_string "error" "App not found" + json_dump +} + +# Install app from app store +install_appstore_app() { + local input app_id + read input + json_load "$input" + json_get_var app_id app_id + json_cleanup + + [ -z "$app_id" ] && { + json_init + json_add_boolean "success" 0 + json_add_string "error" "app_id required" + json_dump + return + } + + # Update package lists if not done in this session + if [ "$OPKG_UPDATED" -eq 0 ]; then + if command -v opkg >/dev/null 2>&1; then + opkg update >/dev/null 2>&1 + elif command -v apk >/dev/null 2>&1; then + apk update >/dev/null 2>&1 + fi + OPKG_UPDATED=1 + fi + + # Install the package + local install_output + if command -v opkg >/dev/null 2>&1; then + install_output=$(opkg install "$app_id" 2>&1) + local ret=$? + if [ $ret -eq 0 ]; then + json_init + json_add_boolean "success" 1 + json_add_string "message" "App installed successfully" + json_add_string "app_id" "$app_id" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "Installation failed" + json_add_string "details" "$install_output" + json_dump + fi + elif command -v apk >/dev/null 2>&1; then + install_output=$(apk add "$app_id" 2>&1) + local ret=$? + if [ $ret -eq 0 ]; then + json_init + json_add_boolean "success" 1 + json_add_string "message" "App installed successfully" + json_add_string "app_id" "$app_id" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "Installation failed" + json_add_string "details" "$install_output" + json_dump + fi + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "No package manager found" + json_dump + fi +} + +# Remove app from app store +remove_appstore_app() { + local input app_id + read input + json_load "$input" + json_get_var app_id app_id + json_cleanup + + [ -z "$app_id" ] && { + json_init + json_add_boolean "success" 0 + json_add_string "error" "app_id required" + json_dump + return + } + + # Remove the package + local remove_output + if command -v opkg >/dev/null 2>&1; then + remove_output=$(opkg remove "$app_id" 2>&1) + local ret=$? + if [ $ret -eq 0 ]; then + json_init + json_add_boolean "success" 1 + json_add_string "message" "App removed successfully" + json_add_string "app_id" "$app_id" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "Removal failed" + json_add_string "details" "$remove_output" + json_dump + fi + elif command -v apk >/dev/null 2>&1; then + remove_output=$(apk del "$app_id" 2>&1) + local ret=$? + if [ $ret -eq 0 ]; then + json_init + json_add_boolean "success" 1 + json_add_string "message" "App removed successfully" + json_add_string "app_id" "$app_id" + json_dump + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "Removal failed" + json_add_string "details" "$remove_output" + json_dump + fi + else + json_init + json_add_boolean "success" 0 + json_add_string "error" "No package manager found" + json_dump + fi +} + case "$1" in list) json_init @@ -1615,6 +1910,17 @@ case "$1" in json_close_object json_add_object "rollback_profile" json_close_object + json_add_object "get_appstore_apps" + json_close_object + json_add_object "get_appstore_app" + json_add_string "app_id" "string" + json_close_object + json_add_object "install_appstore_app" + json_add_string "app_id" "string" + json_close_object + json_add_object "remove_appstore_app" + json_add_string "app_id" "string" + json_close_object json_add_object "first_run_status" json_close_object json_add_object "apply_first_run" @@ -1751,6 +2057,18 @@ case "$1" in rollback_profile) rollback_profile ;; + get_appstore_apps) + get_appstore_apps + ;; + get_appstore_app) + get_appstore_app + ;; + install_appstore_app) + install_appstore_app + ;; + remove_appstore_app) + remove_appstore_app + ;; *) echo '{"error":"Unknown method"}' ;; diff --git a/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json b/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json index e20109e..bbc7f1e 100644 --- a/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json +++ b/luci-app-secubox/root/usr/share/luci/menu.d/luci-app-secubox.json @@ -22,12 +22,12 @@ "path": "secubox/wizard" } }, - "admin/secubox/appstore": { + "admin/secubox/apps": { "title": "App Store", "order": 18, "action": { "type": "view", - "path": "secubox/appstore" + "path": "secubox/apps" } }, "admin/secubox/modules": { diff --git a/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json b/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json index 1203586..d2b8e72 100644 --- a/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json +++ b/luci-app-secubox/root/usr/share/rpcd/acl.d/luci-app-secubox.json @@ -18,7 +18,9 @@ "first_run_status", "list_apps", "get_app_manifest", - "list_profiles" + "list_profiles", + "get_appstore_apps", + "get_appstore_app" ], "uci": [ "get", @@ -45,7 +47,9 @@ "apply_first_run", "apply_app_wizard", "apply_profile", - "rollback_profile" + "rollback_profile", + "install_appstore_app", + "remove_appstore_app" ], "uci": [ "set", 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 index 14f0381..7caf663 100644 --- 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 @@ -1,6 +1,7 @@ 'use strict'; +'require baseclass'; -return { +return baseclass.extend({ stylesheet: function(name) { var primary = L.resource('secubox-theme/system-hub/' + name); var fallback = L.resource('system-hub/' + name); @@ -15,4 +16,4 @@ return { }; return link; } -}; +}); diff --git a/package/secubox/.appstore/apps.json b/package/secubox/.appstore/apps.json index 5d307ff..79e4acb 100644 --- a/package/secubox/.appstore/apps.json +++ b/package/secubox/.appstore/apps.json @@ -12,14 +12,16 @@ "url": "https://github.com/crowdsecurity/crowdsec", "tags": ["security", "ids", "ips", "firewall", "threat-detection"], "requires": { - "go": "1.25+", + "go": "1.23+", "memory": "128MB", - "storage": "50MB" + "storage": "50MB", + "build": "full" }, - "status": "beta", + "status": "dev", "luci_app": "luci-app-crowdsec-dashboard", "dependencies": ["iptables-nft"], - "conflicts": [] + "conflicts": [], + "notes": "Requires full OpenWrt build environment (not SDK). Go 1.23.12 available in OpenWrt 24.10." }, { "id": "secubox-app-nodogsplash", diff --git a/package/secubox/secubox-app-crowdsec/Makefile b/package/secubox/secubox-app-crowdsec/Makefile index a4edb33..3269f20 100644 --- a/package/secubox/secubox-app-crowdsec/Makefile +++ b/package/secubox/secubox-app-crowdsec/Makefile @@ -24,7 +24,7 @@ PKG_BUILD_FLAGS:=no-mips16 CWD_SYSTEM:=openwrt CWD_BUILD_VERSION?=v$(PKG_VERSION) -CWD_BUILD_GOVERSION:=$(shell go version | cut -d " " -f3 | sed -E 's/[go]+//g') +CWD_BUILD_GOVERSION:=$(shell go version 2>/dev/null | cut -d " " -f3 | sed -E 's/[go]+//g' || echo "1.23") CWD_BUILD_CODENAME:=alphaga CWD_BUILD_TIMESTAMP:=$(shell date +%F"_"%T) CWD_BUILD_TAG:=openwrt-$(PKG_VERSION)-$(PKG_RELEASE) @@ -52,7 +52,7 @@ endef define Package/crowdsec $(call Package/crowdsec/Default) - DEPENDS:=$(GO_ARCH_DEPENDS) + DEPENDS:=$(GO_ARCH_DEPENDS) +libc endef define Package/golang-crowdsec-dev @@ -98,28 +98,28 @@ define Package/crowdsec/install $(INSTALL_DATA) \ $(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/config.yaml \ - $(1)/etc/crowdsec + $(1)/etc/crowdsec/ $(INSTALL_DATA) \ $(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/dev.yaml \ - $(1)/etc/crowdsec + $(1)/etc/crowdsec/ $(INSTALL_DATA) \ $(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/user.yaml \ - $(1)/etc/crowdsec + $(1)/etc/crowdsec/ $(INSTALL_DATA) \ $(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/acquis.yaml \ - $(1)/etc/crowdsec + $(1)/etc/crowdsec/ $(INSTALL_DATA) \ $(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/profiles.yaml \ - $(1)/etc/crowdsec + $(1)/etc/crowdsec/ $(INSTALL_DATA) \ $(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/simulation.yaml \ - $(1)/etc/crowdsec + $(1)/etc/crowdsec/ $(INSTALL_DATA) \ $(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/local_api_credentials.yaml \ - $(1)/etc/crowdsec + $(1)/etc/crowdsec/ $(INSTALL_DATA) \ $(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/online_api_credentials.yaml \ - $(1)/etc/crowdsec + $(1)/etc/crowdsec/ $(CP) \ $(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/patterns/* \