release: bump secubox hub to 0.6.1-0

This commit is contained in:
CyberMind-FR 2025-12-30 14:42:45 +01:00
parent 6f115a3cf3
commit 280dd91798
16 changed files with 1298 additions and 503 deletions

View File

@ -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:*)"
]
}
}

View File

@ -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 🌐

View File

@ -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

View File

@ -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"
}
}

View File

@ -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 <contact@cybermind.fr>
@ -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

View File

@ -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

View File

@ -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;
}
}

View File

@ -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'] },

View File

@ -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
});

View File

@ -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
});

View File

@ -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"}'
;;

View File

@ -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": {

View File

@ -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",

View File

@ -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;
}
};
});

View File

@ -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",

View File

@ -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/* \