release: bump secubox hub to 0.6.1-0
This commit is contained in:
parent
6f115a3cf3
commit
280dd91798
@ -173,7 +173,10 @@
|
|||||||
"Bash(feeds/packages/net/crowdsec/Makefile)",
|
"Bash(feeds/packages/net/crowdsec/Makefile)",
|
||||||
"Bash(feeds/packages/net/crowdsec/patches/002-fix_go_version.patch)",
|
"Bash(feeds/packages/net/crowdsec/patches/002-fix_go_version.patch)",
|
||||||
"Bash(tail:*)",
|
"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:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -87,9 +87,10 @@ Metadata for all 5 SecuBox applications:
|
|||||||
|
|
||||||
1. **secubox-app-crowdsec** (v1.7.4)
|
1. **secubox-app-crowdsec** (v1.7.4)
|
||||||
- Category: Security 🛡️
|
- Category: Security 🛡️
|
||||||
- Status: Beta (requires Go 1.25+)
|
- Status: Development (requires full OpenWrt build environment)
|
||||||
- LuCI App: `luci-app-crowdsec-dashboard`
|
- LuCI App: `luci-app-crowdsec-dashboard`
|
||||||
- Dependencies: `iptables-nft`
|
- Dependencies: `iptables-nft`
|
||||||
|
- Build: Go 1.23+ (available in OpenWrt 24.10), full build environment required
|
||||||
|
|
||||||
2. **secubox-app-nodogsplash** (v5.0.2)
|
2. **secubox-app-nodogsplash** (v5.0.2)
|
||||||
- Category: Network 🌐
|
- Category: Network 🌐
|
||||||
|
|||||||
@ -2,17 +2,38 @@
|
|||||||
|
|
||||||
## Completed Today
|
## Completed Today
|
||||||
|
|
||||||
- Introduced SecuBox cascade layout helper (CSS + JS) and migrated SecuNav + MQTT tabs to the new layered system.
|
**SecuBox App Store Integration (luci-app-secubox v0.6.1):**
|
||||||
- MQTT Bridge now exposes Zigbee/SMSC USB2134B presets with dmesg hints, tty detection, and documentation updates.
|
- ✅ Added 4 new RPC backend methods (get_appstore_apps, get_appstore_app, install_appstore_app, remove_appstore_app)
|
||||||
- New `mqtt-bridge` daemon keeps adapter metadata (port/bus/health) synced, updates stats, and runs automation rules/templates.
|
- ✅ Created frontend App Store view (apps.js) with category filters and install/remove functionality
|
||||||
- Unified Monitoring + Modules filters and Help view with SecuNav styling.
|
- ✅ Added apps.css with SecuBox theme integration and responsive design
|
||||||
- Added Bonus tab to navbar, refreshed alerts action buttons, removed legacy hero blocks.
|
- ✅ 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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`.
|
- 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.
|
- System Hub views now import SecuBox theme CSS, hide default LuCI tabs, and respect `data-secubox-theme` for consistent styling.
|
||||||
|
|
||||||
## In Progress
|
## In Progress
|
||||||
|
|||||||
138
luci-app-secubox/.appstore/apps.json
Normal file
138
luci-app-secubox/.appstore/apps.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,8 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
PKG_NAME:=luci-app-secubox
|
PKG_NAME:=luci-app-secubox
|
||||||
PKG_VERSION:=0.6.0
|
PKG_VERSION:=0.6.1
|
||||||
PKG_RELEASE:=1
|
PKG_RELEASE:=0
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|
||||||
@ -28,6 +28,8 @@ define Package/$(PKG_NAME)/install
|
|||||||
for file in $(CURDIR)/profiles/*.json; do \
|
for file in $(CURDIR)/profiles/*.json; do \
|
||||||
$(INSTALL_DATA) $$file $(1)/usr/share/secubox/profiles/$$(basename $$file); \
|
$(INSTALL_DATA) $$file $(1)/usr/share/secubox/profiles/$$(basename $$file); \
|
||||||
done
|
done
|
||||||
|
$(INSTALL_DIR) $(1)/usr/share/secubox/.appstore
|
||||||
|
$(INSTALL_DATA) $(CURDIR)/.appstore/apps.json $(1)/usr/share/secubox/.appstore/apps.json
|
||||||
endef
|
endef
|
||||||
|
|
||||||
# call BuildPackage - OpenWrt buildroot
|
# call BuildPackage - OpenWrt buildroot
|
||||||
|
|||||||
@ -191,6 +191,34 @@ var callRollbackProfile = rpc.declare({
|
|||||||
method: 'rollback_profile'
|
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) {
|
function formatUptime(seconds) {
|
||||||
if (!seconds) return '0s';
|
if (!seconds) return '0s';
|
||||||
var d = Math.floor(seconds / 86400);
|
var d = Math.floor(seconds / 86400);
|
||||||
@ -242,6 +270,11 @@ return baseclass.extend({
|
|||||||
listProfiles: callListProfiles,
|
listProfiles: callListProfiles,
|
||||||
applyProfile: callApplyProfile,
|
applyProfile: callApplyProfile,
|
||||||
rollbackProfile: callRollbackProfile,
|
rollbackProfile: callRollbackProfile,
|
||||||
|
// App Store
|
||||||
|
getAppstoreApps: callGetAppstoreApps,
|
||||||
|
getAppstoreApp: callGetAppstoreApp,
|
||||||
|
installAppstoreApp: callInstallAppstoreApp,
|
||||||
|
removeAppstoreApp: callRemoveAppstoreApp,
|
||||||
// Utilities
|
// Utilities
|
||||||
formatUptime: formatUptime,
|
formatUptime: formatUptime,
|
||||||
formatBytes: formatBytes
|
formatBytes: formatBytes
|
||||||
|
|||||||
363
luci-app-secubox/htdocs/luci-static/resources/secubox/apps.css
Normal file
363
luci-app-secubox/htdocs/luci-static/resources/secubox/apps.css
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,7 @@ var tabs = [
|
|||||||
{ id: 'dashboard', icon: '🚀', label: _('Dashboard'), path: ['admin', 'secubox', 'dashboard'] },
|
{ id: 'dashboard', icon: '🚀', label: _('Dashboard'), path: ['admin', 'secubox', 'dashboard'] },
|
||||||
{ id: 'modules', icon: '🧩', label: _('Modules'), path: ['admin', 'secubox', 'modules'] },
|
{ id: 'modules', icon: '🧩', label: _('Modules'), path: ['admin', 'secubox', 'modules'] },
|
||||||
{ id: 'wizard', icon: '✨', label: _('Wizard'), path: ['admin', 'secubox', 'wizard'] },
|
{ 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: 'monitoring', icon: '📡', label: _('Monitoring'), path: ['admin', 'secubox', 'monitoring'] },
|
||||||
{ id: 'alerts', icon: '⚠️', label: _('Alerts'), path: ['admin', 'secubox', 'alerts'] },
|
{ id: 'alerts', icon: '⚠️', label: _('Alerts'), path: ['admin', 'secubox', 'alerts'] },
|
||||||
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'settings'] },
|
{ id: 'settings', icon: '⚙️', label: _('Settings'), path: ['admin', 'secubox', 'settings'] },
|
||||||
|
|||||||
@ -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
|
||||||
|
});
|
||||||
@ -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
|
|
||||||
});
|
|
||||||
@ -1540,6 +1540,301 @@ rollback_profile() {
|
|||||||
json_add_string "message" "Restored $latest_backup"
|
json_add_string "message" "Restored $latest_backup"
|
||||||
json_dump
|
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
|
case "$1" in
|
||||||
list)
|
list)
|
||||||
json_init
|
json_init
|
||||||
@ -1615,6 +1910,17 @@ case "$1" in
|
|||||||
json_close_object
|
json_close_object
|
||||||
json_add_object "rollback_profile"
|
json_add_object "rollback_profile"
|
||||||
json_close_object
|
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_add_object "first_run_status"
|
||||||
json_close_object
|
json_close_object
|
||||||
json_add_object "apply_first_run"
|
json_add_object "apply_first_run"
|
||||||
@ -1751,6 +2057,18 @@ case "$1" in
|
|||||||
rollback_profile)
|
rollback_profile)
|
||||||
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"}'
|
echo '{"error":"Unknown method"}'
|
||||||
;;
|
;;
|
||||||
|
|||||||
@ -22,12 +22,12 @@
|
|||||||
"path": "secubox/wizard"
|
"path": "secubox/wizard"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/secubox/appstore": {
|
"admin/secubox/apps": {
|
||||||
"title": "App Store",
|
"title": "App Store",
|
||||||
"order": 18,
|
"order": 18,
|
||||||
"action": {
|
"action": {
|
||||||
"type": "view",
|
"type": "view",
|
||||||
"path": "secubox/appstore"
|
"path": "secubox/apps"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/secubox/modules": {
|
"admin/secubox/modules": {
|
||||||
|
|||||||
@ -18,7 +18,9 @@
|
|||||||
"first_run_status",
|
"first_run_status",
|
||||||
"list_apps",
|
"list_apps",
|
||||||
"get_app_manifest",
|
"get_app_manifest",
|
||||||
"list_profiles"
|
"list_profiles",
|
||||||
|
"get_appstore_apps",
|
||||||
|
"get_appstore_app"
|
||||||
],
|
],
|
||||||
"uci": [
|
"uci": [
|
||||||
"get",
|
"get",
|
||||||
@ -45,7 +47,9 @@
|
|||||||
"apply_first_run",
|
"apply_first_run",
|
||||||
"apply_app_wizard",
|
"apply_app_wizard",
|
||||||
"apply_profile",
|
"apply_profile",
|
||||||
"rollback_profile"
|
"rollback_profile",
|
||||||
|
"install_appstore_app",
|
||||||
|
"remove_appstore_app"
|
||||||
],
|
],
|
||||||
"uci": [
|
"uci": [
|
||||||
"set",
|
"set",
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
'require baseclass';
|
||||||
|
|
||||||
return {
|
return baseclass.extend({
|
||||||
stylesheet: function(name) {
|
stylesheet: function(name) {
|
||||||
var primary = L.resource('secubox-theme/system-hub/' + name);
|
var primary = L.resource('secubox-theme/system-hub/' + name);
|
||||||
var fallback = L.resource('system-hub/' + name);
|
var fallback = L.resource('system-hub/' + name);
|
||||||
@ -15,4 +16,4 @@ return {
|
|||||||
};
|
};
|
||||||
return link;
|
return link;
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|||||||
@ -12,14 +12,16 @@
|
|||||||
"url": "https://github.com/crowdsecurity/crowdsec",
|
"url": "https://github.com/crowdsecurity/crowdsec",
|
||||||
"tags": ["security", "ids", "ips", "firewall", "threat-detection"],
|
"tags": ["security", "ids", "ips", "firewall", "threat-detection"],
|
||||||
"requires": {
|
"requires": {
|
||||||
"go": "1.25+",
|
"go": "1.23+",
|
||||||
"memory": "128MB",
|
"memory": "128MB",
|
||||||
"storage": "50MB"
|
"storage": "50MB",
|
||||||
|
"build": "full"
|
||||||
},
|
},
|
||||||
"status": "beta",
|
"status": "dev",
|
||||||
"luci_app": "luci-app-crowdsec-dashboard",
|
"luci_app": "luci-app-crowdsec-dashboard",
|
||||||
"dependencies": ["iptables-nft"],
|
"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",
|
"id": "secubox-app-nodogsplash",
|
||||||
|
|||||||
@ -24,7 +24,7 @@ PKG_BUILD_FLAGS:=no-mips16
|
|||||||
CWD_SYSTEM:=openwrt
|
CWD_SYSTEM:=openwrt
|
||||||
|
|
||||||
CWD_BUILD_VERSION?=v$(PKG_VERSION)
|
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_CODENAME:=alphaga
|
||||||
CWD_BUILD_TIMESTAMP:=$(shell date +%F"_"%T)
|
CWD_BUILD_TIMESTAMP:=$(shell date +%F"_"%T)
|
||||||
CWD_BUILD_TAG:=openwrt-$(PKG_VERSION)-$(PKG_RELEASE)
|
CWD_BUILD_TAG:=openwrt-$(PKG_VERSION)-$(PKG_RELEASE)
|
||||||
@ -52,7 +52,7 @@ endef
|
|||||||
|
|
||||||
define Package/crowdsec
|
define Package/crowdsec
|
||||||
$(call Package/crowdsec/Default)
|
$(call Package/crowdsec/Default)
|
||||||
DEPENDS:=$(GO_ARCH_DEPENDS)
|
DEPENDS:=$(GO_ARCH_DEPENDS) +libc
|
||||||
endef
|
endef
|
||||||
|
|
||||||
define Package/golang-crowdsec-dev
|
define Package/golang-crowdsec-dev
|
||||||
@ -98,28 +98,28 @@ define Package/crowdsec/install
|
|||||||
|
|
||||||
$(INSTALL_DATA) \
|
$(INSTALL_DATA) \
|
||||||
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/config.yaml \
|
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/config.yaml \
|
||||||
$(1)/etc/crowdsec
|
$(1)/etc/crowdsec/
|
||||||
$(INSTALL_DATA) \
|
$(INSTALL_DATA) \
|
||||||
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/dev.yaml \
|
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/dev.yaml \
|
||||||
$(1)/etc/crowdsec
|
$(1)/etc/crowdsec/
|
||||||
$(INSTALL_DATA) \
|
$(INSTALL_DATA) \
|
||||||
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/user.yaml \
|
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/user.yaml \
|
||||||
$(1)/etc/crowdsec
|
$(1)/etc/crowdsec/
|
||||||
$(INSTALL_DATA) \
|
$(INSTALL_DATA) \
|
||||||
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/acquis.yaml \
|
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/acquis.yaml \
|
||||||
$(1)/etc/crowdsec
|
$(1)/etc/crowdsec/
|
||||||
$(INSTALL_DATA) \
|
$(INSTALL_DATA) \
|
||||||
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/profiles.yaml \
|
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/profiles.yaml \
|
||||||
$(1)/etc/crowdsec
|
$(1)/etc/crowdsec/
|
||||||
$(INSTALL_DATA) \
|
$(INSTALL_DATA) \
|
||||||
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/simulation.yaml \
|
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/simulation.yaml \
|
||||||
$(1)/etc/crowdsec
|
$(1)/etc/crowdsec/
|
||||||
$(INSTALL_DATA) \
|
$(INSTALL_DATA) \
|
||||||
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/local_api_credentials.yaml \
|
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/local_api_credentials.yaml \
|
||||||
$(1)/etc/crowdsec
|
$(1)/etc/crowdsec/
|
||||||
$(INSTALL_DATA) \
|
$(INSTALL_DATA) \
|
||||||
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/online_api_credentials.yaml \
|
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/online_api_credentials.yaml \
|
||||||
$(1)/etc/crowdsec
|
$(1)/etc/crowdsec/
|
||||||
|
|
||||||
$(CP) \
|
$(CP) \
|
||||||
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/patterns/* \
|
$(GO_PKG_BUILD_DIR)/src/$(GO_PKG)/config/patterns/* \
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user