Compare commits
3 Commits
3cff626582
...
066bedafef
| Author | SHA1 | Date | |
|---|---|---|---|
| 066bedafef | |||
| 06faaabc19 | |||
| 26b1f476f8 |
@ -168,7 +168,13 @@
|
|||||||
"Bash(python3 -m json.tool:*)",
|
"Bash(python3 -m json.tool:*)",
|
||||||
"Bash(git restore:*)",
|
"Bash(git restore:*)",
|
||||||
"Bash(__NEW_LINE_80f7f5dbdf93db8a__ echo \"\")",
|
"Bash(__NEW_LINE_80f7f5dbdf93db8a__ echo \"\")",
|
||||||
"Bash(# Check for other service-like apps in other secubox menus echo \"\"=== Mitmproxy location ===\"\" grep -h ''\"\"admin/'' package/secubox/luci-app-mitmproxy/root/usr/share/luci/menu.d/*.json)"
|
"Bash(# Check for other service-like apps in other secubox menus echo \"\"=== Mitmproxy location ===\"\" grep -h ''\"\"admin/'' package/secubox/luci-app-mitmproxy/root/usr/share/luci/menu.d/*.json)",
|
||||||
|
"Bash(ar -p:*)",
|
||||||
|
"WebFetch(domain:openwrt.org)",
|
||||||
|
"Bash(ar -t:*)",
|
||||||
|
"Bash(zstd:*)",
|
||||||
|
"Bash(source /home/reepost/CyberMindStudio/secubox-openwrt/secubox-tools/local-build.sh)",
|
||||||
|
"Bash(strip_libc_from_ipk:*)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
277
.gitea/workflows/secubox-feed.yml
Normal file
277
.gitea/workflows/secubox-feed.yml
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
name: SecuBox Feed Health & Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master, release/*]
|
||||||
|
paths:
|
||||||
|
- 'package/secubox/**'
|
||||||
|
- 'secubox-tools/**'
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
publish_feed:
|
||||||
|
description: 'Publish feed to remote'
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
FEED_URL: ${{ vars.SECUBOX_FEED_URL || 'https://feed.maegia.tv/secubox-feed' }}
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# ============================================
|
||||||
|
# Validate feed structure and packages
|
||||||
|
# ============================================
|
||||||
|
validate-feed:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Validate Feed Structure
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y jq shellcheck
|
||||||
|
|
||||||
|
- name: Validate secubox-feed scripts
|
||||||
|
run: |
|
||||||
|
echo "Validating feed management scripts..."
|
||||||
|
|
||||||
|
SCRIPTS=(
|
||||||
|
"package/secubox/secubox-app-bonus/root/usr/sbin/secubox-feed"
|
||||||
|
"package/secubox/secubox-app-bonus/root/usr/sbin/secubox-feed-health"
|
||||||
|
"package/secubox/secubox-app-bonus/root/etc/init.d/secubox-feed"
|
||||||
|
)
|
||||||
|
|
||||||
|
ERRORS=0
|
||||||
|
for script in "${SCRIPTS[@]}"; do
|
||||||
|
if [[ -f "$script" ]]; then
|
||||||
|
echo " Checking $script..."
|
||||||
|
if ! shellcheck -s sh "$script"; then
|
||||||
|
ERRORS=$((ERRORS + 1))
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " Missing: $script"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $ERRORS -gt 0 ]]; then
|
||||||
|
echo "Found $ERRORS shellcheck issues (warnings only)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Validate HAProxy config
|
||||||
|
run: |
|
||||||
|
echo "Validating HAProxy configuration..."
|
||||||
|
|
||||||
|
HAPROXY_CFG="package/secubox/secubox-app-bonus/root/etc/haproxy/conf.d/secubox-feed.cfg"
|
||||||
|
|
||||||
|
if [[ -f "$HAPROXY_CFG" ]]; then
|
||||||
|
# Basic syntax check
|
||||||
|
if grep -qE "^(backend|frontend|acl|use_backend|server)" "$HAPROXY_CFG"; then
|
||||||
|
echo " HAProxy config structure valid"
|
||||||
|
else
|
||||||
|
echo " Warning: HAProxy config may be incomplete"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for required backends
|
||||||
|
if grep -q "backend secubox_feed_backend" "$HAPROXY_CFG"; then
|
||||||
|
echo " Backend defined: secubox_feed_backend"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for health checks
|
||||||
|
if grep -q "http-check\|option httpchk" "$HAPROXY_CFG"; then
|
||||||
|
echo " Health checks configured"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " No HAProxy config found (optional)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Validate RPCD interface
|
||||||
|
run: |
|
||||||
|
echo "Validating RPCD interface..."
|
||||||
|
|
||||||
|
RPCD_SCRIPT="package/secubox/secubox-app-bonus/root/usr/libexec/rpcd/luci.secubox-feed"
|
||||||
|
|
||||||
|
if [[ -f "$RPCD_SCRIPT" ]]; then
|
||||||
|
# Check list method returns valid JSON
|
||||||
|
if $RPCD_SCRIPT list | jq empty 2>/dev/null; then
|
||||||
|
echo " RPCD list method: valid JSON"
|
||||||
|
echo " Methods: $($RPCD_SCRIPT list | jq -r 'keys[]' | tr '\n' ' ')"
|
||||||
|
else
|
||||||
|
echo " RPCD list method: invalid JSON"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo " No RPCD script found (optional)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Test feed health endpoint
|
||||||
|
# ============================================
|
||||||
|
test-health-endpoint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Test Health Endpoint
|
||||||
|
needs: validate-feed
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Test health check script
|
||||||
|
run: |
|
||||||
|
echo "Testing health check script..."
|
||||||
|
|
||||||
|
HEALTH_SCRIPT="package/secubox/secubox-app-bonus/root/usr/sbin/secubox-feed-health"
|
||||||
|
|
||||||
|
if [[ -f "$HEALTH_SCRIPT" ]]; then
|
||||||
|
chmod +x "$HEALTH_SCRIPT"
|
||||||
|
|
||||||
|
# Create mock environment
|
||||||
|
mkdir -p /tmp/www/secubox-feed /tmp/var/opkg-lists
|
||||||
|
echo "Package: test-package" > /tmp/www/secubox-feed/Packages
|
||||||
|
echo "Version: 1.0.0" >> /tmp/www/secubox-feed/Packages
|
||||||
|
echo "" >> /tmp/www/secubox-feed/Packages
|
||||||
|
|
||||||
|
# Modify script to use test paths
|
||||||
|
sed -e 's|/www/secubox-feed|/tmp/www/secubox-feed|g' \
|
||||||
|
-e 's|/var/opkg-lists|/tmp/var/opkg-lists|g' \
|
||||||
|
"$HEALTH_SCRIPT" > /tmp/test-health.sh
|
||||||
|
chmod +x /tmp/test-health.sh
|
||||||
|
|
||||||
|
# Test JSON output
|
||||||
|
echo " Testing JSON output..."
|
||||||
|
OUTPUT=$(/tmp/test-health.sh json)
|
||||||
|
echo "$OUTPUT" | jq .
|
||||||
|
|
||||||
|
# Test simple output
|
||||||
|
echo " Testing simple output..."
|
||||||
|
/tmp/test-health.sh simple || true
|
||||||
|
|
||||||
|
# Test nagios output
|
||||||
|
echo " Testing nagios output..."
|
||||||
|
/tmp/test-health.sh nagios || true
|
||||||
|
|
||||||
|
echo " Health check script tests passed"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Check remote feed health
|
||||||
|
# ============================================
|
||||||
|
check-remote-feed:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Check Remote Feed Health
|
||||||
|
if: github.event_name == 'workflow_dispatch' || github.event_name == 'release'
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check feed.maegia.tv health
|
||||||
|
run: |
|
||||||
|
echo "Checking remote feed health..."
|
||||||
|
|
||||||
|
FEED_URL="${{ env.FEED_URL }}"
|
||||||
|
|
||||||
|
# Test Packages file accessibility
|
||||||
|
echo " Testing: $FEED_URL/Packages"
|
||||||
|
if curl -sf -o /tmp/Packages "$FEED_URL/Packages"; then
|
||||||
|
PKG_COUNT=$(grep -c '^Package:' /tmp/Packages || echo 0)
|
||||||
|
echo " Remote feed accessible: $PKG_COUNT packages"
|
||||||
|
|
||||||
|
# Show first few packages
|
||||||
|
echo " Sample packages:"
|
||||||
|
grep '^Package:' /tmp/Packages | head -5 | sed 's/^/ /'
|
||||||
|
else
|
||||||
|
echo " Remote feed not accessible or empty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test health endpoint if available
|
||||||
|
echo ""
|
||||||
|
echo " Testing health endpoint..."
|
||||||
|
if curl -sf "$FEED_URL/health" | jq . 2>/dev/null; then
|
||||||
|
echo " Health endpoint available"
|
||||||
|
else
|
||||||
|
echo " Health endpoint not available (optional)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Publish feed on release
|
||||||
|
# ============================================
|
||||||
|
publish-feed:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Publish Feed
|
||||||
|
needs: [validate-feed, test-health-endpoint]
|
||||||
|
if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && github.event.inputs.publish_feed == 'true')
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download release artifacts
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
run: |
|
||||||
|
echo "Downloading release IPK files..."
|
||||||
|
|
||||||
|
mkdir -p /tmp/feed
|
||||||
|
|
||||||
|
# Download IPKs from release assets
|
||||||
|
RELEASE_TAG="${{ github.event.release.tag_name }}"
|
||||||
|
echo "Release: $RELEASE_TAG"
|
||||||
|
|
||||||
|
# Use gh CLI to download assets
|
||||||
|
gh release download "$RELEASE_TAG" \
|
||||||
|
--pattern "*.ipk" \
|
||||||
|
--dir /tmp/feed \
|
||||||
|
|| echo "No IPK assets found in release"
|
||||||
|
|
||||||
|
ls -la /tmp/feed/
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
|
- name: Generate Packages index
|
||||||
|
run: |
|
||||||
|
echo "Generating Packages index..."
|
||||||
|
|
||||||
|
FEED_DIR="/tmp/feed"
|
||||||
|
rm -f "$FEED_DIR/Packages" "$FEED_DIR/Packages.gz"
|
||||||
|
|
||||||
|
for ipk in "$FEED_DIR"/*.ipk; do
|
||||||
|
[[ -f "$ipk" ]] || continue
|
||||||
|
|
||||||
|
TMP=$(mktemp -d)
|
||||||
|
(
|
||||||
|
cd "$TMP"
|
||||||
|
tar -xzf "$ipk" 2>/dev/null || ar -x "$ipk" 2>/dev/null
|
||||||
|
tar -xzf control.tar.gz 2>/dev/null || tar -xzf control.tar.zst 2>/dev/null
|
||||||
|
|
||||||
|
if [[ -f control ]]; then
|
||||||
|
# Strip libc dependency
|
||||||
|
sed -e 's/^Depends: libc$//' \
|
||||||
|
-e 's/^Depends: libc, /Depends: /' \
|
||||||
|
-e '/^$/d' control
|
||||||
|
echo "Filename: $(basename "$ipk")"
|
||||||
|
echo "Size: $(stat -c%s "$ipk")"
|
||||||
|
echo "SHA256sum: $(sha256sum "$ipk" | cut -d' ' -f1)"
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
) >> "$FEED_DIR/Packages"
|
||||||
|
rm -rf "$TMP"
|
||||||
|
done
|
||||||
|
|
||||||
|
gzip -kf "$FEED_DIR/Packages"
|
||||||
|
|
||||||
|
echo "Generated index:"
|
||||||
|
grep -c '^Package:' "$FEED_DIR/Packages" || echo "0"
|
||||||
|
echo "packages"
|
||||||
|
|
||||||
|
- name: Upload feed artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: secubox-feed
|
||||||
|
path: /tmp/feed/
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
- name: Notify feed update
|
||||||
|
run: |
|
||||||
|
echo "Feed published successfully"
|
||||||
|
echo "Packages: $(grep -c '^Package:' /tmp/feed/Packages || echo 0)"
|
||||||
|
echo "Size: $(du -sh /tmp/feed | cut -f1)"
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@ -19,6 +19,8 @@ docs/.DS_Store
|
|||||||
|
|
||||||
|
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
package/secubox/luci-app-secubox-bonus/root/www/
|
##package/secubox/luci-app-secubox-bonus/root/www/
|
||||||
|
|
||||||
openwrt
|
openwrt# Large package files - should not be in git
|
||||||
|
package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-bonus_*.ipk
|
||||||
|
package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-app-bonus_*.ipk
|
||||||
|
|||||||
0
package/secubox/luci-app-lyrion/root/usr/libexec/rpcd/luci.lyrion
Normal file → Executable file
0
package/secubox/luci-app-lyrion/root/usr/libexec/rpcd/luci.lyrion
Normal file → Executable file
0
package/secubox/luci-app-mailinabox/root/usr/libexec/rpcd/luci.mailinabox
Normal file → Executable file
0
package/secubox/luci-app-mailinabox/root/usr/libexec/rpcd/luci.mailinabox
Normal file → Executable file
0
package/secubox/luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer
Normal file → Executable file
0
package/secubox/luci-app-metablogizer/root/usr/libexec/rpcd/luci.metablogizer
Normal file → Executable file
0
package/secubox/luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud
Normal file → Executable file
0
package/secubox/luci-app-nextcloud/root/usr/libexec/rpcd/luci.nextcloud
Normal file → Executable file
@ -207,6 +207,124 @@ var callValidateComponentState = rpc.declare({
|
|||||||
expect: { valid: true }
|
expect: { valid: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ===== Feed Management API =====
|
||||||
|
|
||||||
|
var callListFeeds = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'list_feeds',
|
||||||
|
expect: { feeds: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callAddFeed = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'add_feed',
|
||||||
|
params: ['name', 'url', 'feed_type', 'visibility'],
|
||||||
|
expect: { success: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callRemoveFeed = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'remove_feed',
|
||||||
|
params: ['feed_id'],
|
||||||
|
expect: { success: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callShareFeed = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'share_feed',
|
||||||
|
params: ['feed_id'],
|
||||||
|
expect: { share_url: '' }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callImportFeed = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'import_feed',
|
||||||
|
params: ['share_url'],
|
||||||
|
expect: { success: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Profile Management API =====
|
||||||
|
|
||||||
|
var callExportProfile = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'export_profile',
|
||||||
|
params: ['name', 'include_feeds'],
|
||||||
|
expect: { profile: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callImportProfile = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'import_profile',
|
||||||
|
params: ['profile_data', 'mode'],
|
||||||
|
expect: { success: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callShareProfile = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'share_profile',
|
||||||
|
params: ['profile_id'],
|
||||||
|
expect: { share_url: '' }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Skill Management API =====
|
||||||
|
|
||||||
|
var callListSkills = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'list_skills',
|
||||||
|
expect: { skills: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGetSkillProviders = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'get_skill_providers',
|
||||||
|
params: ['skill_id'],
|
||||||
|
expect: { providers: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callInstallSkill = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'install_skill',
|
||||||
|
params: ['skill_id'],
|
||||||
|
expect: { success: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callCheckSkills = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'check_skills',
|
||||||
|
params: ['profile_id'],
|
||||||
|
expect: { }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ===== Feedback Management API =====
|
||||||
|
|
||||||
|
var callReportIssue = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'report_issue',
|
||||||
|
params: ['app_id', 'issue_type', 'summary', 'description'],
|
||||||
|
expect: { success: false, issue_id: '' }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callResolveIssue = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'resolve_issue',
|
||||||
|
params: ['issue_id', 'resolution'],
|
||||||
|
expect: { success: false }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSearchResolutions = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'search_resolutions',
|
||||||
|
params: ['keyword'],
|
||||||
|
expect: { results: [] }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callListIssues = rpc.declare({
|
||||||
|
object: 'luci.secubox',
|
||||||
|
method: 'list_issues',
|
||||||
|
params: ['status'],
|
||||||
|
expect: { issues: [] }
|
||||||
|
});
|
||||||
|
|
||||||
// Utility functions
|
// Utility functions
|
||||||
function formatBytes(bytes) {
|
function formatBytes(bytes) {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return '0 B';
|
||||||
@ -448,6 +566,30 @@ return baseclass.extend({
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ===== Feed Management =====
|
||||||
|
listFeeds: debugRPC('listFeeds', callListFeeds, { retries: 2 }),
|
||||||
|
addFeed: debugRPC('addFeed', callAddFeed, { retries: 1 }),
|
||||||
|
removeFeed: debugRPC('removeFeed', callRemoveFeed, { retries: 1 }),
|
||||||
|
shareFeed: debugRPC('shareFeed', callShareFeed, { retries: 1 }),
|
||||||
|
importFeed: debugRPC('importFeed', callImportFeed, { retries: 1 }),
|
||||||
|
|
||||||
|
// ===== Profile Management =====
|
||||||
|
exportProfile: debugRPC('exportProfile', callExportProfile, { retries: 1 }),
|
||||||
|
importProfile: debugRPC('importProfile', callImportProfile, { retries: 1 }),
|
||||||
|
shareProfile: debugRPC('shareProfile', callShareProfile, { retries: 1 }),
|
||||||
|
|
||||||
|
// ===== Skill Management =====
|
||||||
|
listSkills: debugRPC('listSkills', callListSkills, { retries: 2 }),
|
||||||
|
getSkillProviders: debugRPC('getSkillProviders', callGetSkillProviders, { retries: 1 }),
|
||||||
|
installSkill: debugRPC('installSkill', callInstallSkill, { retries: 1 }),
|
||||||
|
checkSkills: debugRPC('checkSkills', callCheckSkills, { retries: 1 }),
|
||||||
|
|
||||||
|
// ===== Feedback Management =====
|
||||||
|
reportIssue: debugRPC('reportIssue', callReportIssue, { retries: 1 }),
|
||||||
|
resolveIssue: debugRPC('resolveIssue', callResolveIssue, { retries: 1 }),
|
||||||
|
searchResolutions: debugRPC('searchResolutions', callSearchResolutions, { retries: 2 }),
|
||||||
|
listIssues: debugRPC('listIssues', callListIssues, { retries: 2 }),
|
||||||
|
|
||||||
// Utilities
|
// Utilities
|
||||||
formatBytes: formatBytes,
|
formatBytes: formatBytes,
|
||||||
formatUptime: formatUptime,
|
formatUptime: formatUptime,
|
||||||
|
|||||||
@ -269,6 +269,25 @@ return view.extend({
|
|||||||
if (source.active) itemClass += ' active';
|
if (source.active) itemClass += ' active';
|
||||||
if (!source.enabled) itemClass += ' offline';
|
if (!source.enabled) itemClass += ' offline';
|
||||||
|
|
||||||
|
// Feed type badge colors
|
||||||
|
var feedTypeBadge = null;
|
||||||
|
var feedType = source.feed_type || 'published';
|
||||||
|
switch (feedType) {
|
||||||
|
case 'published':
|
||||||
|
feedTypeBadge = E('span', { 'class': 'cyber-badge', 'style': 'background: rgba(16, 185, 129, 0.2); color: #10b981; border-color: rgba(16, 185, 129, 0.3);' }, 'PUB');
|
||||||
|
break;
|
||||||
|
case 'unpublished':
|
||||||
|
feedTypeBadge = E('span', { 'class': 'cyber-badge', 'style': 'background: rgba(245, 158, 11, 0.2); color: #f59e0b; border-color: rgba(245, 158, 11, 0.3);' }, 'PRV');
|
||||||
|
break;
|
||||||
|
case 'development':
|
||||||
|
feedTypeBadge = E('span', { 'class': 'cyber-badge', 'style': 'background: rgba(59, 130, 246, 0.2); color: #3b82f6; border-color: rgba(59, 130, 246, 0.3);' }, 'DEV');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visibility indicator
|
||||||
|
var visibility = source.visibility || 'public';
|
||||||
|
var visibilityIcon = visibility === 'public' ? '🌍' : visibility === 'private' ? '🔒' : '💻';
|
||||||
|
|
||||||
return E('div', {
|
return E('div', {
|
||||||
'class': itemClass,
|
'class': itemClass,
|
||||||
'data-source': source.name
|
'data-source': source.name
|
||||||
@ -284,6 +303,9 @@ return view.extend({
|
|||||||
E('div', { 'class': 'cyber-list-content' }, [
|
E('div', { 'class': 'cyber-list-content' }, [
|
||||||
E('div', { 'class': 'cyber-list-title' }, [
|
E('div', { 'class': 'cyber-list-title' }, [
|
||||||
source.name.toUpperCase(),
|
source.name.toUpperCase(),
|
||||||
|
' ',
|
||||||
|
feedTypeBadge,
|
||||||
|
' ',
|
||||||
source.active ? E('span', { 'class': 'cyber-badge success' }, [
|
source.active ? E('span', { 'class': 'cyber-badge success' }, [
|
||||||
E('span', { 'class': 'cyber-status-dot online' }),
|
E('span', { 'class': 'cyber-status-dot online' }),
|
||||||
' ACTIVE'
|
' ACTIVE'
|
||||||
@ -296,6 +318,10 @@ return view.extend({
|
|||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
E('div', { 'class': 'cyber-list-meta' }, [
|
E('div', { 'class': 'cyber-list-meta' }, [
|
||||||
|
E('span', { 'class': 'cyber-list-meta-item' }, [
|
||||||
|
E('span', {}, visibilityIcon + ' '),
|
||||||
|
visibility
|
||||||
|
]),
|
||||||
E('span', { 'class': 'cyber-list-meta-item' }, [
|
E('span', { 'class': 'cyber-list-meta-item' }, [
|
||||||
E('span', {}, '🔢 '),
|
E('span', {}, '🔢 '),
|
||||||
'Priority: ' + source.priority
|
'Priority: ' + source.priority
|
||||||
@ -313,6 +339,10 @@ return view.extend({
|
|||||||
E('span', {}, '📁 '),
|
E('span', {}, '📁 '),
|
||||||
source.path
|
source.path
|
||||||
]) : null,
|
]) : null,
|
||||||
|
source.description ? E('span', { 'class': 'cyber-list-meta-item' }, [
|
||||||
|
E('span', {}, '📝 '),
|
||||||
|
source.description
|
||||||
|
]) : null,
|
||||||
source.last_success ? E('span', { 'class': 'cyber-list-meta-item' }, [
|
source.last_success ? E('span', { 'class': 'cyber-list-meta-item' }, [
|
||||||
E('span', {}, '⏱️ '),
|
E('span', {}, '⏱️ '),
|
||||||
this.formatTimestamp(source.last_success)
|
this.formatTimestamp(source.last_success)
|
||||||
@ -336,6 +366,14 @@ return view.extend({
|
|||||||
self.testSource(source.name);
|
self.testSource(source.name);
|
||||||
}
|
}
|
||||||
}, '🧪 TEST') : null,
|
}, '🧪 TEST') : null,
|
||||||
|
// Share button for non-development feeds
|
||||||
|
(feedType !== 'development' && source.enabled) ? E('button', {
|
||||||
|
'class': 'cyber-btn',
|
||||||
|
'click': function() {
|
||||||
|
console.log('[CATALOG-SOURCES] Share source:', source.name);
|
||||||
|
self.shareSource(source);
|
||||||
|
}
|
||||||
|
}, '🔗 SHARE') : null,
|
||||||
!source.active && source.enabled ? E('button', {
|
!source.active && source.enabled ? E('button', {
|
||||||
'class': 'cyber-btn warning',
|
'class': 'cyber-btn warning',
|
||||||
'click': function() {
|
'click': function() {
|
||||||
@ -354,6 +392,51 @@ return view.extend({
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
shareSource: function(source) {
|
||||||
|
var feedType = source.feed_type || 'published';
|
||||||
|
var shareUrl = 'secubox://feed/' + source.name + '?url=' + encodeURIComponent(source.url || source.path || '');
|
||||||
|
|
||||||
|
if (feedType === 'unpublished' && source.share_token) {
|
||||||
|
shareUrl += '&token=' + source.share_token;
|
||||||
|
}
|
||||||
|
|
||||||
|
var modalContent = [
|
||||||
|
E('h3', { 'style': 'margin-bottom: 15px; color: var(--cyber-text-bright);' }, 'Share Feed: ' + source.name),
|
||||||
|
E('p', { 'style': 'color: var(--cyber-text-dim); margin-bottom: 15px;' },
|
||||||
|
feedType === 'unpublished'
|
||||||
|
? 'This is a private feed. The share URL includes an auth token - only share with trusted users.'
|
||||||
|
: 'This is a public feed. Anyone can import it.'
|
||||||
|
),
|
||||||
|
E('div', { 'style': 'background: rgba(99, 102, 241, 0.1); padding: 15px; border-radius: 8px; margin-bottom: 15px;' }, [
|
||||||
|
E('code', { 'style': 'word-break: break-all; font-size: 12px; color: var(--cyber-accent-cyan);' }, shareUrl)
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() {
|
||||||
|
navigator.clipboard.writeText(shareUrl).then(function() {
|
||||||
|
ui.addNotification(null, E('p', 'Share URL copied to clipboard!'), 'success');
|
||||||
|
}).catch(function() {
|
||||||
|
ui.addNotification(null, E('p', 'Failed to copy - please copy manually'), 'warning');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 'Copy to Clipboard'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Close')
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
if (feedType === 'unpublished') {
|
||||||
|
modalContent.splice(3, 0, E('p', { 'style': 'color: #f59e0b; font-size: 12px; margin-bottom: 10px;' },
|
||||||
|
'⚠️ Warning: This URL contains your authentication token. Only share with trusted parties.'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.showModal('Share Feed Source', modalContent);
|
||||||
|
},
|
||||||
|
|
||||||
getStatusClass: function(status) {
|
getStatusClass: function(status) {
|
||||||
switch(status) {
|
switch(status) {
|
||||||
case 'online':
|
case 'online':
|
||||||
|
|||||||
@ -0,0 +1,604 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require secubox-admin/api as API';
|
||||||
|
'require secubox-admin/components as Components';
|
||||||
|
'require ui';
|
||||||
|
'require poll';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
|
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||||
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||||
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||||
|
Theme.init({ language: lang });
|
||||||
|
|
||||||
|
var ADMIN_NAV = [
|
||||||
|
{ id: 'dashboard', icon: '🎛️', label: 'Control Panel' },
|
||||||
|
{ id: 'cyber-dashboard', icon: '🔮', label: 'Cyber Console' },
|
||||||
|
{ id: 'apps', icon: '📦', label: 'Apps Manager' },
|
||||||
|
{ id: 'profiles', icon: '📋', label: 'Profiles' },
|
||||||
|
{ id: 'skills', icon: '🎯', label: 'Skills' },
|
||||||
|
{ id: 'catalog-sources', icon: '📚', label: 'Catalog' },
|
||||||
|
{ id: 'feedback', icon: '💬', label: 'Feedback' },
|
||||||
|
{ id: 'health', icon: '💚', label: 'Health' },
|
||||||
|
{ id: 'settings', icon: '⚙️', label: 'Settings' }
|
||||||
|
];
|
||||||
|
|
||||||
|
var ISSUE_TYPES = [
|
||||||
|
{ value: 'bug', label: 'Bug', icon: '🐛', color: '#ef4444' },
|
||||||
|
{ value: 'feature', label: 'Feature Request', icon: '✨', color: '#8b5cf6' },
|
||||||
|
{ value: 'docs', label: 'Documentation', icon: '📚', color: '#3b82f6' },
|
||||||
|
{ value: 'question', label: 'Question', icon: '❓', color: '#f59e0b' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderAdminNav(activeId) {
|
||||||
|
return E('div', {
|
||||||
|
'class': 'sb-app-nav',
|
||||||
|
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;flex-wrap:wrap;'
|
||||||
|
}, ADMIN_NAV.map(function(item) {
|
||||||
|
var isActive = activeId === item.id;
|
||||||
|
return E('a', {
|
||||||
|
'href': L.url('admin', 'secubox', 'admin', item.id),
|
||||||
|
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||||
|
(isActive ? 'background:linear-gradient(135deg,#667eea,#764ba2);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||||
|
}, [
|
||||||
|
E('span', {}, item.icon),
|
||||||
|
E('span', {}, _(item.label))
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIssueType(typeValue) {
|
||||||
|
return ISSUE_TYPES.find(function(t) { return t.value === typeValue; }) || ISSUE_TYPES[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return Promise.all([
|
||||||
|
API.listIssues(null).catch(function() { return { issues: [] }; }),
|
||||||
|
API.getApps().catch(function() { return []; })
|
||||||
|
]).then(function(results) {
|
||||||
|
return {
|
||||||
|
issues: results[0].issues || [],
|
||||||
|
apps: results[1] || []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var issues = data.issues || [];
|
||||||
|
var apps = data.apps || [];
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Categorize issues
|
||||||
|
var openIssues = issues.filter(function(i) { return i.status === 'open'; });
|
||||||
|
var resolvedIssues = issues.filter(function(i) { return i.status === 'resolved'; });
|
||||||
|
|
||||||
|
var container = E('div', { 'class': 'cyberpunk-mode secubox-feedback' }, [
|
||||||
|
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||||
|
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||||
|
E('link', { 'rel': 'stylesheet',
|
||||||
|
'href': L.resource('secubox-admin/common.css') }),
|
||||||
|
E('link', { 'rel': 'stylesheet',
|
||||||
|
'href': L.resource('secubox-admin/admin.css') }),
|
||||||
|
|
||||||
|
// Header
|
||||||
|
E('div', { 'class': 'cyber-header' }, [
|
||||||
|
E('div', { 'class': 'cyber-header-title' }, '💬 FEEDBACK CENTER'),
|
||||||
|
E('div', { 'class': 'cyber-header-subtitle' },
|
||||||
|
'Report issues, share resolutions, and help improve SecuBox')
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Stats panel
|
||||||
|
E('div', { 'class': 'cyber-panel' }, [
|
||||||
|
E('div', { 'class': 'cyber-panel-header' }, [
|
||||||
|
E('span', { 'class': 'cyber-panel-title' }, 'OVERVIEW')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-panel-body' }, [
|
||||||
|
E('div', { 'class': 'cyber-stats-grid' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-card' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '📝'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, issues.length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'Total Issues')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-stat-card', 'style': 'border-left-color: #f59e0b;' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '🔓'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, openIssues.length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'Open')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-stat-card', 'style': 'border-left-color: #10b981;' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '✅'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, resolvedIssues.length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'Resolved')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-stat-card', 'style': 'border-left-color: #8b5cf6;' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '💡'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, resolvedIssues.length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'Resolutions')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Actions bar
|
||||||
|
E('div', { 'class': 'cyber-panel' }, [
|
||||||
|
E('div', { 'class': 'cyber-panel-body' }, [
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px; flex-wrap: wrap;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn primary',
|
||||||
|
'click': function() { self.showReportDialog(apps); }
|
||||||
|
}, '📝 REPORT ISSUE'),
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'resolution-search',
|
||||||
|
'placeholder': 'Search resolutions...',
|
||||||
|
'style': 'flex: 1; min-width: 200px; padding: 12px 16px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 8px; color: var(--cyber-text-bright); font-size: 14px;'
|
||||||
|
}),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn',
|
||||||
|
'click': function() {
|
||||||
|
var query = document.getElementById('resolution-search').value;
|
||||||
|
if (query) self.searchResolutions(query);
|
||||||
|
}
|
||||||
|
}, '🔍 SEARCH')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Open Issues
|
||||||
|
E('div', { 'class': 'cyber-panel' }, [
|
||||||
|
E('div', { 'class': 'cyber-panel-header' }, [
|
||||||
|
E('span', { 'class': 'cyber-panel-title' }, 'OPEN ISSUES'),
|
||||||
|
E('span', { 'class': 'cyber-badge', 'style': 'background: rgba(245, 158, 11, 0.2); color: #f59e0b;' }, openIssues.length)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-panel-body' }, [
|
||||||
|
openIssues.length > 0 ?
|
||||||
|
E('div', { 'id': 'open-issues-list', 'style': 'display: flex; flex-direction: column; gap: 10px;' },
|
||||||
|
openIssues.map(function(issue) {
|
||||||
|
return self.renderIssueCard(issue, apps);
|
||||||
|
})
|
||||||
|
) :
|
||||||
|
E('div', { 'style': 'text-align: center; padding: 30px; color: var(--cyber-text-dim);' }, [
|
||||||
|
E('div', { 'style': 'font-size: 36px; margin-bottom: 15px;' }, '✅'),
|
||||||
|
E('div', {}, 'No open issues'),
|
||||||
|
E('div', { 'style': 'font-size: 12px; margin-top: 10px;' }, 'All issues have been resolved!')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Resolved Issues / Knowledge Base
|
||||||
|
E('div', { 'class': 'cyber-panel' }, [
|
||||||
|
E('div', { 'class': 'cyber-panel-header' }, [
|
||||||
|
E('span', { 'class': 'cyber-panel-title' }, 'KNOWLEDGE BASE'),
|
||||||
|
E('span', { 'class': 'cyber-badge', 'style': 'background: rgba(16, 185, 129, 0.2); color: #10b981;' }, resolvedIssues.length + ' resolutions')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-panel-body' }, [
|
||||||
|
resolvedIssues.length > 0 ?
|
||||||
|
E('div', { 'id': 'resolved-issues-list', 'style': 'display: flex; flex-direction: column; gap: 10px;' },
|
||||||
|
resolvedIssues.map(function(issue) {
|
||||||
|
return self.renderResolutionCard(issue, apps);
|
||||||
|
})
|
||||||
|
) :
|
||||||
|
E('div', { 'style': 'text-align: center; padding: 30px; color: var(--cyber-text-dim);' }, [
|
||||||
|
E('div', { 'style': 'font-size: 36px; margin-bottom: 15px;' }, '💡'),
|
||||||
|
E('div', {}, 'No resolutions yet'),
|
||||||
|
E('div', { 'style': 'font-size: 12px; margin-top: 10px;' },
|
||||||
|
'Resolved issues with solutions will appear here')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||||
|
wrapper.appendChild(SbHeader.render());
|
||||||
|
wrapper.appendChild(renderAdminNav('feedback'));
|
||||||
|
wrapper.appendChild(container);
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderIssueCard: function(issue, apps) {
|
||||||
|
var self = this;
|
||||||
|
var issueType = getIssueType(issue.type);
|
||||||
|
var appName = issue.app_id || 'Unknown';
|
||||||
|
|
||||||
|
// Try to find app name
|
||||||
|
var app = apps.find(function(a) { return a.id === issue.app_id; });
|
||||||
|
if (app) appName = app.name || app.id;
|
||||||
|
|
||||||
|
return E('div', {
|
||||||
|
'class': 'issue-card',
|
||||||
|
'style': 'background: rgba(99, 102, 241, 0.05); border: 1px solid rgba(99, 102, 241, 0.15); border-radius: 10px; padding: 15px;'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;' }, [
|
||||||
|
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
||||||
|
E('span', { 'style': 'font-size: 20px;' }, issueType.icon),
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'style': 'font-weight: 600; color: var(--cyber-text-bright);' }, issue.summary || 'No summary'),
|
||||||
|
E('div', { 'style': 'font-size: 12px; color: var(--cyber-text-dim);' }, [
|
||||||
|
E('span', { 'style': 'color: ' + issueType.color + ';' }, issueType.label),
|
||||||
|
' • ',
|
||||||
|
E('span', {}, appName),
|
||||||
|
' • ',
|
||||||
|
E('span', {}, issue.created || 'Unknown date')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('span', {
|
||||||
|
'class': 'cyber-badge',
|
||||||
|
'style': 'background: rgba(245, 158, 11, 0.2); color: #f59e0b;'
|
||||||
|
}, 'OPEN')
|
||||||
|
]),
|
||||||
|
issue.description ? E('div', {
|
||||||
|
'style': 'color: var(--cyber-text-dim); font-size: 13px; margin-bottom: 15px; padding: 10px; background: rgba(0,0,0,0.2); border-radius: 6px;'
|
||||||
|
}, issue.description) : null,
|
||||||
|
E('div', { 'style': 'display: flex; gap: 8px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn primary',
|
||||||
|
'style': 'font-size: 12px; padding: 8px 12px;',
|
||||||
|
'click': function() { self.showResolveDialog(issue); }
|
||||||
|
}, '✅ Resolve'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn',
|
||||||
|
'style': 'font-size: 12px; padding: 8px 12px;',
|
||||||
|
'click': function() { self.showIssueDetails(issue); }
|
||||||
|
}, '👁️ Details')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
renderResolutionCard: function(issue, apps) {
|
||||||
|
var self = this;
|
||||||
|
var issueType = getIssueType(issue.type);
|
||||||
|
var appName = issue.app_id || 'Unknown';
|
||||||
|
|
||||||
|
var app = apps.find(function(a) { return a.id === issue.app_id; });
|
||||||
|
if (app) appName = app.name || app.id;
|
||||||
|
|
||||||
|
return E('div', {
|
||||||
|
'class': 'resolution-card',
|
||||||
|
'style': 'background: rgba(16, 185, 129, 0.05); border: 1px solid rgba(16, 185, 129, 0.15); border-radius: 10px; padding: 15px;'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;' }, [
|
||||||
|
E('div', { 'style': 'display: flex; align-items: center; gap: 10px;' }, [
|
||||||
|
E('span', { 'style': 'font-size: 20px;' }, '💡'),
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'style': 'font-weight: 600; color: var(--cyber-text-bright);' }, issue.summary || 'No summary'),
|
||||||
|
E('div', { 'style': 'font-size: 12px; color: var(--cyber-text-dim);' }, [
|
||||||
|
E('span', { 'style': 'color: ' + issueType.color + ';' }, issueType.label),
|
||||||
|
' • ',
|
||||||
|
E('span', {}, appName),
|
||||||
|
' • ',
|
||||||
|
E('span', {}, 'Resolved: ' + (issue.resolved_at || 'Unknown'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('span', {
|
||||||
|
'class': 'cyber-badge',
|
||||||
|
'style': 'background: rgba(16, 185, 129, 0.2); color: #10b981;'
|
||||||
|
}, 'RESOLVED')
|
||||||
|
]),
|
||||||
|
issue.resolution ? E('div', {
|
||||||
|
'style': 'color: #10b981; font-size: 13px; margin-bottom: 10px; padding: 10px; background: rgba(16, 185, 129, 0.1); border-radius: 6px; border-left: 3px solid #10b981;'
|
||||||
|
}, [
|
||||||
|
E('strong', {}, 'Resolution: '),
|
||||||
|
issue.resolution
|
||||||
|
]) : null,
|
||||||
|
E('div', { 'style': 'display: flex; gap: 8px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn',
|
||||||
|
'style': 'font-size: 12px; padding: 8px 12px;',
|
||||||
|
'click': function() { self.showIssueDetails(issue); }
|
||||||
|
}, '👁️ View Details'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn',
|
||||||
|
'style': 'font-size: 12px; padding: 8px 12px;',
|
||||||
|
'click': function() { self.copyResolution(issue); }
|
||||||
|
}, '📋 Copy')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
showReportDialog: function(apps) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Build app options
|
||||||
|
var appOptions = apps.map(function(app) {
|
||||||
|
return E('option', { 'value': app.id }, app.name || app.id);
|
||||||
|
});
|
||||||
|
appOptions.unshift(E('option', { 'value': '' }, '-- Select App --'));
|
||||||
|
|
||||||
|
// Build type options
|
||||||
|
var typeOptions = ISSUE_TYPES.map(function(type) {
|
||||||
|
return E('option', { 'value': type.value }, type.icon + ' ' + type.label);
|
||||||
|
});
|
||||||
|
|
||||||
|
var content = [
|
||||||
|
E('h3', { 'style': 'margin-bottom: 20px; color: var(--cyber-text-bright);' }, '📝 Report New Issue'),
|
||||||
|
|
||||||
|
E('div', { 'style': 'margin-bottom: 15px;' }, [
|
||||||
|
E('label', { 'style': 'display: block; margin-bottom: 5px; color: var(--cyber-text-dim); font-size: 12px;' }, 'APP'),
|
||||||
|
E('select', {
|
||||||
|
'id': 'report-app-id',
|
||||||
|
'style': 'width: 100%; padding: 10px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 6px; color: var(--cyber-text-bright);'
|
||||||
|
}, appOptions)
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'style': 'margin-bottom: 15px;' }, [
|
||||||
|
E('label', { 'style': 'display: block; margin-bottom: 5px; color: var(--cyber-text-dim); font-size: 12px;' }, 'TYPE'),
|
||||||
|
E('select', {
|
||||||
|
'id': 'report-type',
|
||||||
|
'style': 'width: 100%; padding: 10px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 6px; color: var(--cyber-text-bright);'
|
||||||
|
}, typeOptions)
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'style': 'margin-bottom: 15px;' }, [
|
||||||
|
E('label', { 'style': 'display: block; margin-bottom: 5px; color: var(--cyber-text-dim); font-size: 12px;' }, 'SUMMARY'),
|
||||||
|
E('input', {
|
||||||
|
'id': 'report-summary',
|
||||||
|
'type': 'text',
|
||||||
|
'placeholder': 'Brief description of the issue...',
|
||||||
|
'style': 'width: 100%; padding: 10px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 6px; color: var(--cyber-text-bright);'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'style': 'margin-bottom: 20px;' }, [
|
||||||
|
E('label', { 'style': 'display: block; margin-bottom: 5px; color: var(--cyber-text-dim); font-size: 12px;' }, 'DESCRIPTION (optional)'),
|
||||||
|
E('textarea', {
|
||||||
|
'id': 'report-description',
|
||||||
|
'rows': 4,
|
||||||
|
'placeholder': 'Detailed description, steps to reproduce, etc...',
|
||||||
|
'style': 'width: 100%; padding: 10px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 6px; color: var(--cyber-text-bright); resize: vertical;'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() { self.submitReport(); }
|
||||||
|
}, 'Submit Report'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Cancel')
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
ui.showModal('Report Issue', content);
|
||||||
|
},
|
||||||
|
|
||||||
|
submitReport: function() {
|
||||||
|
var appId = document.getElementById('report-app-id').value;
|
||||||
|
var type = document.getElementById('report-type').value;
|
||||||
|
var summary = document.getElementById('report-summary').value;
|
||||||
|
var description = document.getElementById('report-description').value;
|
||||||
|
|
||||||
|
if (!appId || !summary) {
|
||||||
|
ui.addNotification(null, E('p', 'Please select an app and provide a summary'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.showModal('Submitting Report', [
|
||||||
|
Components.renderLoader('Submitting issue report...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.reportIssue(appId, type, summary, description).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', 'Issue reported successfully! ID: ' + (result.issue_id || 'N/A')), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Failed to submit report: ' + (result.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Error submitting report: ' + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showResolveDialog: function(issue) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var content = [
|
||||||
|
E('h3', { 'style': 'margin-bottom: 15px; color: var(--cyber-text-bright);' }, '✅ Resolve Issue'),
|
||||||
|
E('div', {
|
||||||
|
'style': 'background: rgba(99, 102, 241, 0.1); padding: 12px; border-radius: 8px; margin-bottom: 20px;'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'font-weight: 600; color: var(--cyber-text-bright);' }, issue.summary),
|
||||||
|
E('div', { 'style': 'font-size: 12px; color: var(--cyber-text-dim); margin-top: 5px;' },
|
||||||
|
'App: ' + issue.app_id + ' • ID: ' + issue.id)
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'style': 'margin-bottom: 20px;' }, [
|
||||||
|
E('label', { 'style': 'display: block; margin-bottom: 5px; color: var(--cyber-text-dim); font-size: 12px;' }, 'RESOLUTION'),
|
||||||
|
E('textarea', {
|
||||||
|
'id': 'resolve-description',
|
||||||
|
'rows': 4,
|
||||||
|
'placeholder': 'Describe how you fixed this issue...',
|
||||||
|
'style': 'width: 100%; padding: 10px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 6px; color: var(--cyber-text-bright); resize: vertical;'
|
||||||
|
})
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() { self.submitResolution(issue.id); }
|
||||||
|
}, 'Submit Resolution'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Cancel')
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
ui.showModal('Resolve Issue', content);
|
||||||
|
},
|
||||||
|
|
||||||
|
submitResolution: function(issueId) {
|
||||||
|
var resolution = document.getElementById('resolve-description').value;
|
||||||
|
|
||||||
|
if (!resolution) {
|
||||||
|
ui.addNotification(null, E('p', 'Please describe the resolution'), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.showModal('Submitting Resolution', [
|
||||||
|
Components.renderLoader('Saving resolution...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.resolveIssue(issueId, resolution).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', 'Issue resolved successfully!'), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Failed to resolve issue: ' + (result.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Error resolving issue: ' + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showIssueDetails: function(issue) {
|
||||||
|
var issueType = getIssueType(issue.type);
|
||||||
|
|
||||||
|
var content = [
|
||||||
|
E('div', { 'style': 'display: flex; align-items: center; gap: 10px; margin-bottom: 20px;' }, [
|
||||||
|
E('span', { 'style': 'font-size: 24px;' }, issueType.icon),
|
||||||
|
E('div', {}, [
|
||||||
|
E('h3', { 'style': 'margin: 0; color: var(--cyber-text-bright);' }, issue.summary || 'No summary'),
|
||||||
|
E('div', { 'style': 'font-size: 12px; color: var(--cyber-text-dim);' }, 'ID: ' + issue.id)
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'style': 'display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; margin-bottom: 20px;' }, [
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'style': 'font-size: 11px; color: var(--cyber-text-dim); margin-bottom: 3px;' }, 'APP'),
|
||||||
|
E('div', { 'style': 'color: var(--cyber-text-bright);' }, issue.app_id || 'Unknown')
|
||||||
|
]),
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'style': 'font-size: 11px; color: var(--cyber-text-dim); margin-bottom: 3px;' }, 'TYPE'),
|
||||||
|
E('div', { 'style': 'color: ' + issueType.color + ';' }, issueType.label)
|
||||||
|
]),
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'style': 'font-size: 11px; color: var(--cyber-text-dim); margin-bottom: 3px;' }, 'STATUS'),
|
||||||
|
E('span', {
|
||||||
|
'class': 'cyber-badge',
|
||||||
|
'style': issue.status === 'resolved' ?
|
||||||
|
'background: rgba(16, 185, 129, 0.2); color: #10b981;' :
|
||||||
|
'background: rgba(245, 158, 11, 0.2); color: #f59e0b;'
|
||||||
|
}, (issue.status || 'open').toUpperCase())
|
||||||
|
]),
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'style': 'font-size: 11px; color: var(--cyber-text-dim); margin-bottom: 3px;' }, 'CREATED'),
|
||||||
|
E('div', { 'style': 'color: var(--cyber-text-bright);' }, issue.created || 'Unknown')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
issue.description ? E('div', { 'style': 'margin-bottom: 20px;' }, [
|
||||||
|
E('div', { 'style': 'font-size: 11px; color: var(--cyber-text-dim); margin-bottom: 5px;' }, 'DESCRIPTION'),
|
||||||
|
E('div', {
|
||||||
|
'style': 'padding: 12px; background: rgba(0,0,0,0.2); border-radius: 8px; color: var(--cyber-text-bright);'
|
||||||
|
}, issue.description)
|
||||||
|
]) : null,
|
||||||
|
|
||||||
|
issue.resolution ? E('div', { 'style': 'margin-bottom: 20px;' }, [
|
||||||
|
E('div', { 'style': 'font-size: 11px; color: var(--cyber-text-dim); margin-bottom: 5px;' }, 'RESOLUTION'),
|
||||||
|
E('div', {
|
||||||
|
'style': 'padding: 12px; background: rgba(16, 185, 129, 0.1); border-left: 3px solid #10b981; border-radius: 8px; color: #10b981;'
|
||||||
|
}, issue.resolution)
|
||||||
|
]) : null,
|
||||||
|
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Close')
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
ui.showModal('Issue Details', content);
|
||||||
|
},
|
||||||
|
|
||||||
|
searchResolutions: function(query) {
|
||||||
|
var self = this;
|
||||||
|
ui.showModal('Searching', [
|
||||||
|
Components.renderLoader('Searching resolutions for: "' + query + '"...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.searchResolutions(query).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
var results = result.results || [];
|
||||||
|
|
||||||
|
var content = [
|
||||||
|
E('h3', { 'style': 'margin-bottom: 15px; color: var(--cyber-text-bright);' },
|
||||||
|
'🔍 Search Results for "' + query + '"'),
|
||||||
|
E('div', { 'style': 'color: var(--cyber-text-dim); margin-bottom: 20px; font-size: 12px;' },
|
||||||
|
results.length + ' result(s) found')
|
||||||
|
];
|
||||||
|
|
||||||
|
if (results.length > 0) {
|
||||||
|
content.push(E('div', { 'style': 'max-height: 400px; overflow-y: auto;' },
|
||||||
|
results.map(function(item) {
|
||||||
|
return E('div', {
|
||||||
|
'style': 'padding: 12px; background: rgba(99, 102, 241, 0.05); border-radius: 8px; margin-bottom: 10px;'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'font-weight: 600; color: var(--cyber-text-bright); margin-bottom: 5px;' },
|
||||||
|
item.summary || 'No summary'),
|
||||||
|
E('div', { 'style': 'font-size: 12px; color: var(--cyber-text-dim); margin-bottom: 8px;' },
|
||||||
|
'App: ' + (item.app_id || 'Unknown') + ' • ' + (item.type || 'bug')),
|
||||||
|
item.resolution ? E('div', {
|
||||||
|
'style': 'padding: 8px; background: rgba(16, 185, 129, 0.1); border-left: 2px solid #10b981; border-radius: 4px; color: #10b981; font-size: 13px;'
|
||||||
|
}, item.resolution) : null
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
content.push(E('div', { 'style': 'text-align: center; padding: 30px; color: var(--cyber-text-dim);' }, [
|
||||||
|
E('div', { 'style': 'font-size: 36px; margin-bottom: 15px;' }, '🔍'),
|
||||||
|
E('div', {}, 'No resolutions found'),
|
||||||
|
E('div', { 'style': 'font-size: 12px; margin-top: 10px;' }, 'Try different keywords')
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
content.push(E('div', { 'style': 'display: flex; gap: 10px; margin-top: 20px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Close')
|
||||||
|
]));
|
||||||
|
|
||||||
|
ui.showModal('Search Results', content);
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Search error: ' + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
copyResolution: function(issue) {
|
||||||
|
var text = 'Issue: ' + (issue.summary || 'N/A') + '\n' +
|
||||||
|
'App: ' + (issue.app_id || 'Unknown') + '\n' +
|
||||||
|
'Resolution: ' + (issue.resolution || 'N/A');
|
||||||
|
|
||||||
|
if (navigator.clipboard) {
|
||||||
|
navigator.clipboard.writeText(text).then(function() {
|
||||||
|
ui.addNotification(null, E('p', 'Resolution copied to clipboard!'), 'success');
|
||||||
|
}).catch(function() {
|
||||||
|
ui.addNotification(null, E('p', 'Failed to copy to clipboard'), 'error');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Fallback for older browsers
|
||||||
|
var textarea = document.createElement('textarea');
|
||||||
|
textarea.value = text;
|
||||||
|
document.body.appendChild(textarea);
|
||||||
|
textarea.select();
|
||||||
|
document.execCommand('copy');
|
||||||
|
document.body.removeChild(textarea);
|
||||||
|
ui.addNotification(null, E('p', 'Resolution copied to clipboard!'), 'success');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,451 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require secubox-admin/api as API';
|
||||||
|
'require secubox-admin/components as Components';
|
||||||
|
'require secubox-admin/data-utils as DataUtils';
|
||||||
|
'require ui';
|
||||||
|
'require poll';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
|
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||||
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||||
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||||
|
Theme.init({ language: lang });
|
||||||
|
|
||||||
|
var ADMIN_NAV = [
|
||||||
|
{ id: 'dashboard', icon: '🎛️', label: 'Control Panel' },
|
||||||
|
{ id: 'cyber-dashboard', icon: '🔮', label: 'Cyber Console' },
|
||||||
|
{ id: 'apps', icon: '📦', label: 'Apps Manager' },
|
||||||
|
{ id: 'profiles', icon: '📋', label: 'Profiles' },
|
||||||
|
{ id: 'skills', icon: '🎯', label: 'Skills' },
|
||||||
|
{ id: 'catalog-sources', icon: '📚', label: 'Catalog' },
|
||||||
|
{ id: 'feedback', icon: '💬', label: 'Feedback' },
|
||||||
|
{ id: 'health', icon: '💚', label: 'Health' },
|
||||||
|
{ id: 'settings', icon: '⚙️', label: 'Settings' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderAdminNav(activeId) {
|
||||||
|
return E('div', {
|
||||||
|
'class': 'sb-app-nav',
|
||||||
|
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;flex-wrap:wrap;'
|
||||||
|
}, ADMIN_NAV.map(function(item) {
|
||||||
|
var isActive = activeId === item.id;
|
||||||
|
return E('a', {
|
||||||
|
'href': L.url('admin', 'secubox', 'admin', item.id),
|
||||||
|
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||||
|
(isActive ? 'background:linear-gradient(135deg,#667eea,#764ba2);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||||
|
}, [
|
||||||
|
E('span', {}, item.icon),
|
||||||
|
E('span', {}, _(item.label))
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return API.listProfiles().then(function(result) {
|
||||||
|
return { profiles: result.profiles || [] };
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('[PROFILES] Load error:', err);
|
||||||
|
return { profiles: [] };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var profiles = data.profiles || [];
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
var container = E('div', { 'class': 'cyberpunk-mode secubox-profiles' }, [
|
||||||
|
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||||
|
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||||
|
E('link', { 'rel': 'stylesheet',
|
||||||
|
'href': L.resource('secubox-admin/common.css') }),
|
||||||
|
E('link', { 'rel': 'stylesheet',
|
||||||
|
'href': L.resource('secubox-admin/admin.css') }),
|
||||||
|
|
||||||
|
// Header
|
||||||
|
E('div', { 'class': 'cyber-header' }, [
|
||||||
|
E('div', { 'class': 'cyber-header-title' }, '📋 CONFIGURATION PROFILES'),
|
||||||
|
E('div', { 'class': 'cyber-header-subtitle' },
|
||||||
|
'Export, import, and share configuration profiles · ' + profiles.length + ' profiles available')
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Stats panel
|
||||||
|
E('div', { 'class': 'cyber-panel' }, [
|
||||||
|
E('div', { 'class': 'cyber-panel-header' }, [
|
||||||
|
E('span', { 'class': 'cyber-panel-title' }, 'PROFILE SYSTEM')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-panel-body' }, [
|
||||||
|
E('div', { 'class': 'cyber-stats-grid' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-card' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '📋'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, profiles.length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'Total Profiles')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-stat-card accent' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '📡'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, profiles.filter(function(p) {
|
||||||
|
return p.feed_sources && p.feed_sources.length > 0;
|
||||||
|
}).length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'With Feeds')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-stat-card warning' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '🎯'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, profiles.filter(function(p) {
|
||||||
|
return p.skills_required && p.skills_required.length > 0;
|
||||||
|
}).length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'With Skills')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Quick actions
|
||||||
|
E('div', { 'class': 'cyber-panel' }, [
|
||||||
|
E('div', { 'class': 'cyber-panel-header' }, [
|
||||||
|
E('span', { 'class': 'cyber-panel-title' }, 'QUICK ACTIONS')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-panel-body' }, [
|
||||||
|
E('div', { 'class': 'cyber-quick-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-action-btn',
|
||||||
|
'click': function() { self.showExportDialog(); }
|
||||||
|
}, [
|
||||||
|
E('span', { 'class': 'cyber-action-icon' }, '📤'),
|
||||||
|
E('span', { 'class': 'cyber-action-label' }, 'EXPORT CURRENT CONFIG'),
|
||||||
|
E('span', { 'class': 'cyber-action-arrow' }, '→')
|
||||||
|
]),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-action-btn',
|
||||||
|
'click': function() { self.showImportDialog(); }
|
||||||
|
}, [
|
||||||
|
E('span', { 'class': 'cyber-action-icon' }, '📥'),
|
||||||
|
E('span', { 'class': 'cyber-action-label' }, 'IMPORT PROFILE'),
|
||||||
|
E('span', { 'class': 'cyber-action-arrow' }, '→')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Profiles list
|
||||||
|
E('div', { 'class': 'cyber-panel' }, [
|
||||||
|
E('div', { 'class': 'cyber-panel-header' }, [
|
||||||
|
E('span', { 'class': 'cyber-panel-title' }, 'AVAILABLE PROFILES')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-panel-body' }, [
|
||||||
|
E('div', { 'class': 'cyber-list', 'id': 'profiles-container' },
|
||||||
|
profiles.length > 0 ?
|
||||||
|
profiles.map(function(profile) {
|
||||||
|
return self.renderProfileCard(profile);
|
||||||
|
}) :
|
||||||
|
[E('div', { 'style': 'text-align: center; padding: 40px; color: var(--cyber-text-dim);' }, [
|
||||||
|
E('div', { 'style': 'font-size: 48px; margin-bottom: 20px;' }, '📋'),
|
||||||
|
E('div', {}, 'No profiles found'),
|
||||||
|
E('div', { 'style': 'font-size: 12px; margin-top: 10px;' },
|
||||||
|
'Export your current configuration to create a profile')
|
||||||
|
])]
|
||||||
|
)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||||
|
wrapper.appendChild(SbHeader.render());
|
||||||
|
wrapper.appendChild(renderAdminNav('profiles'));
|
||||||
|
wrapper.appendChild(container);
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderProfileCard: function(profile) {
|
||||||
|
var self = this;
|
||||||
|
var profileData = profile.profile || profile;
|
||||||
|
var id = profileData.id || profileData.name || 'unknown';
|
||||||
|
var name = profileData.name || id;
|
||||||
|
var description = profileData.description || '';
|
||||||
|
|
||||||
|
var modules = profile.modules || {};
|
||||||
|
var requiredCount = (modules.required || []).length;
|
||||||
|
var recommendedCount = (modules.recommended || []).length;
|
||||||
|
|
||||||
|
var hasFeeds = profile.feed_sources && profile.feed_sources.length > 0;
|
||||||
|
var hasSkills = profile.skills_required && profile.skills_required.length > 0;
|
||||||
|
|
||||||
|
return E('div', { 'class': 'cyber-list-item', 'data-profile': id }, [
|
||||||
|
E('div', { 'class': 'cyber-list-icon' }, '📋'),
|
||||||
|
E('div', { 'class': 'cyber-list-content' }, [
|
||||||
|
E('div', { 'class': 'cyber-list-title' }, [
|
||||||
|
name.toUpperCase(),
|
||||||
|
' ',
|
||||||
|
hasFeeds ? E('span', { 'class': 'cyber-badge', 'style': 'background: rgba(6, 182, 212, 0.2); color: #06b6d4;' }, 'FEEDS') : null,
|
||||||
|
' ',
|
||||||
|
hasSkills ? E('span', { 'class': 'cyber-badge', 'style': 'background: rgba(245, 158, 11, 0.2); color: #f59e0b;' }, 'SKILLS') : null
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-list-meta' }, [
|
||||||
|
E('span', { 'class': 'cyber-list-meta-item' }, [
|
||||||
|
'📦 ' + requiredCount + ' required'
|
||||||
|
]),
|
||||||
|
recommendedCount > 0 ? E('span', { 'class': 'cyber-list-meta-item' }, [
|
||||||
|
'✨ ' + recommendedCount + ' recommended'
|
||||||
|
]) : null,
|
||||||
|
hasFeeds ? E('span', { 'class': 'cyber-list-meta-item' }, [
|
||||||
|
'📡 ' + profile.feed_sources.length + ' feeds'
|
||||||
|
]) : null,
|
||||||
|
hasSkills ? E('span', { 'class': 'cyber-list-meta-item' }, [
|
||||||
|
'🎯 ' + profile.skills_required.length + ' skills'
|
||||||
|
]) : null
|
||||||
|
]),
|
||||||
|
description ? E('div', { 'style': 'color: var(--cyber-text-dim); font-size: 12px; margin-top: 5px;' }, description) : null
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-list-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn primary',
|
||||||
|
'click': function() { self.applyProfile(id); }
|
||||||
|
}, '▶ APPLY'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn',
|
||||||
|
'click': function() { self.viewProfile(profile); }
|
||||||
|
}, '👁️ VIEW'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn',
|
||||||
|
'click': function() { self.shareProfile(profile); }
|
||||||
|
}, '🔗 SHARE')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
showExportDialog: function() {
|
||||||
|
var self = this;
|
||||||
|
var nameInput = E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'placeholder': 'My Configuration',
|
||||||
|
'style': 'width: 100%; padding: 10px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 8px; color: var(--cyber-text-bright);'
|
||||||
|
});
|
||||||
|
|
||||||
|
var includeFeedsCheckbox = E('input', {
|
||||||
|
'type': 'checkbox',
|
||||||
|
'id': 'include-feeds',
|
||||||
|
'checked': true
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.showModal('Export Configuration Profile', [
|
||||||
|
E('div', { 'style': 'margin-bottom: 15px;' }, [
|
||||||
|
E('label', { 'style': 'display: block; margin-bottom: 5px; color: var(--cyber-text-dim);' }, 'Profile Name'),
|
||||||
|
nameInput
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'margin-bottom: 15px;' }, [
|
||||||
|
E('label', { 'style': 'display: flex; align-items: center; gap: 10px; cursor: pointer;' }, [
|
||||||
|
includeFeedsCheckbox,
|
||||||
|
E('span', {}, 'Include feed sources in export')
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() {
|
||||||
|
var name = nameInput.value || 'Exported Configuration';
|
||||||
|
var includeFeeds = includeFeedsCheckbox.checked;
|
||||||
|
self.exportProfile(name, includeFeeds);
|
||||||
|
}
|
||||||
|
}, 'Export'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Cancel')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
showImportDialog: function() {
|
||||||
|
var self = this;
|
||||||
|
var urlInput = E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'placeholder': 'https://example.com/profile.json or local file path',
|
||||||
|
'style': 'width: 100%; padding: 10px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 8px; color: var(--cyber-text-bright);'
|
||||||
|
});
|
||||||
|
|
||||||
|
var modeSelect = E('select', {
|
||||||
|
'style': 'width: 100%; padding: 10px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 8px; color: var(--cyber-text-bright);'
|
||||||
|
}, [
|
||||||
|
E('option', { 'value': '--merge' }, 'Merge (add to existing)'),
|
||||||
|
E('option', { 'value': '--replace' }, 'Replace (overwrite existing)')
|
||||||
|
]);
|
||||||
|
|
||||||
|
ui.showModal('Import Configuration Profile', [
|
||||||
|
E('div', { 'style': 'margin-bottom: 15px;' }, [
|
||||||
|
E('label', { 'style': 'display: block; margin-bottom: 5px; color: var(--cyber-text-dim);' }, 'Profile URL or File Path'),
|
||||||
|
urlInput
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'margin-bottom: 15px;' }, [
|
||||||
|
E('label', { 'style': 'display: block; margin-bottom: 5px; color: var(--cyber-text-dim);' }, 'Import Mode'),
|
||||||
|
modeSelect
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() {
|
||||||
|
var url = urlInput.value;
|
||||||
|
var mode = modeSelect.value;
|
||||||
|
if (url) {
|
||||||
|
self.importProfile(url, mode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 'Import'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Cancel')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
exportProfile: function(name, includeFeeds) {
|
||||||
|
var self = this;
|
||||||
|
ui.showModal('Exporting Profile', [
|
||||||
|
Components.renderLoader('Exporting configuration...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.exportProfile(name, includeFeeds).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.profile) {
|
||||||
|
// Show the exported JSON
|
||||||
|
var jsonStr = JSON.stringify(result, null, 2);
|
||||||
|
ui.showModal('Profile Exported', [
|
||||||
|
E('p', { 'style': 'color: var(--cyber-text-dim); margin-bottom: 15px;' },
|
||||||
|
'Your configuration has been exported. Copy the JSON below or download it.'),
|
||||||
|
E('textarea', {
|
||||||
|
'style': 'width: 100%; height: 300px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 8px; color: var(--cyber-text-bright); font-family: monospace; font-size: 12px;',
|
||||||
|
'readonly': true
|
||||||
|
}, jsonStr),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px; margin-top: 15px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() {
|
||||||
|
navigator.clipboard.writeText(jsonStr).then(function() {
|
||||||
|
ui.addNotification(null, E('p', 'Copied to clipboard!'), 'success');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 'Copy to Clipboard'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() {
|
||||||
|
var blob = new Blob([jsonStr], { type: 'application/json' });
|
||||||
|
var url = URL.createObjectURL(blob);
|
||||||
|
var a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = name.toLowerCase().replace(/\s+/g, '-') + '.json';
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
}, 'Download'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Close')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Export failed'), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Export error: ' + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
importProfile: function(url, mode) {
|
||||||
|
var self = this;
|
||||||
|
ui.showModal('Importing Profile', [
|
||||||
|
Components.renderLoader('Importing profile...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.importProfile(url, mode).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', 'Profile imported successfully!'), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Import failed: ' + (result.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Import error: ' + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
applyProfile: function(profileId) {
|
||||||
|
var self = this;
|
||||||
|
ui.showModal('Apply Profile', [
|
||||||
|
E('p', {}, 'Are you sure you want to apply profile: ' + profileId + '?'),
|
||||||
|
E('p', { 'style': 'color: var(--cyber-text-dim); font-size: 12px;' },
|
||||||
|
'This will install required modules and apply UCI configuration changes.'),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px; margin-top: 20px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal('Applying Profile', [
|
||||||
|
Components.renderLoader('Applying profile: ' + profileId + '...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.applyProfile(profileId, false).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', 'Profile applied successfully!'), 'success');
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Apply failed: ' + (result.error || result.message || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Apply error: ' + err.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 'Apply'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Cancel')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
viewProfile: function(profile) {
|
||||||
|
var jsonStr = JSON.stringify(profile, null, 2);
|
||||||
|
ui.showModal('Profile Details', [
|
||||||
|
E('textarea', {
|
||||||
|
'style': 'width: 100%; height: 400px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 8px; color: var(--cyber-text-bright); font-family: monospace; font-size: 12px;',
|
||||||
|
'readonly': true
|
||||||
|
}, jsonStr),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px; margin-top: 15px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Close')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
shareProfile: function(profile) {
|
||||||
|
var profileData = profile.profile || profile;
|
||||||
|
var id = profileData.id || 'unknown';
|
||||||
|
|
||||||
|
ui.showModal('Share Profile', [
|
||||||
|
E('p', { 'style': 'margin-bottom: 15px; color: var(--cyber-text-dim);' },
|
||||||
|
'To share this profile, host the JSON file on a web server and share the URL.'),
|
||||||
|
E('div', { 'style': 'background: rgba(99, 102, 241, 0.1); padding: 15px; border-radius: 8px; margin-bottom: 15px;' }, [
|
||||||
|
E('p', { 'style': 'color: var(--cyber-text-bright); margin-bottom: 10px;' }, 'Import command:'),
|
||||||
|
E('code', { 'style': 'word-break: break-all; font-size: 12px; color: var(--cyber-accent-cyan);' },
|
||||||
|
'secubox profile import <your-url>/' + id + '.json')
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Close')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -0,0 +1,379 @@
|
|||||||
|
'use strict';
|
||||||
|
'require view';
|
||||||
|
'require secubox-admin/api as API';
|
||||||
|
'require secubox-admin/components as Components';
|
||||||
|
'require ui';
|
||||||
|
'require poll';
|
||||||
|
'require secubox-theme/theme as Theme';
|
||||||
|
'require secubox-portal/header as SbHeader';
|
||||||
|
|
||||||
|
var lang = (typeof L !== 'undefined' && L.env && L.env.lang) ||
|
||||||
|
(document.documentElement && document.documentElement.getAttribute('lang')) ||
|
||||||
|
(navigator.language ? navigator.language.split('-')[0] : 'en');
|
||||||
|
Theme.init({ language: lang });
|
||||||
|
|
||||||
|
var ADMIN_NAV = [
|
||||||
|
{ id: 'dashboard', icon: '🎛️', label: 'Control Panel' },
|
||||||
|
{ id: 'cyber-dashboard', icon: '🔮', label: 'Cyber Console' },
|
||||||
|
{ id: 'apps', icon: '📦', label: 'Apps Manager' },
|
||||||
|
{ id: 'profiles', icon: '📋', label: 'Profiles' },
|
||||||
|
{ id: 'skills', icon: '🎯', label: 'Skills' },
|
||||||
|
{ id: 'catalog-sources', icon: '📚', label: 'Catalog' },
|
||||||
|
{ id: 'feedback', icon: '💬', label: 'Feedback' },
|
||||||
|
{ id: 'health', icon: '💚', label: 'Health' },
|
||||||
|
{ id: 'settings', icon: '⚙️', label: 'Settings' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function renderAdminNav(activeId) {
|
||||||
|
return E('div', {
|
||||||
|
'class': 'sb-app-nav',
|
||||||
|
'style': 'display:flex;gap:8px;margin-bottom:20px;padding:12px 16px;background:#141419;border:1px solid rgba(255,255,255,0.08);border-radius:12px;flex-wrap:wrap;'
|
||||||
|
}, ADMIN_NAV.map(function(item) {
|
||||||
|
var isActive = activeId === item.id;
|
||||||
|
return E('a', {
|
||||||
|
'href': L.url('admin', 'secubox', 'admin', item.id),
|
||||||
|
'style': 'display:flex;align-items:center;gap:8px;padding:10px 16px;border-radius:8px;text-decoration:none;font-size:14px;font-weight:500;transition:all 0.2s;' +
|
||||||
|
(isActive ? 'background:linear-gradient(135deg,#667eea,#764ba2);color:white;' : 'color:#a0a0b0;background:transparent;')
|
||||||
|
}, [
|
||||||
|
E('span', {}, item.icon),
|
||||||
|
E('span', {}, _(item.label))
|
||||||
|
]);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view.extend({
|
||||||
|
load: function() {
|
||||||
|
return API.listSkills().then(function(result) {
|
||||||
|
return { skills: result.skills || [] };
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('[SKILLS] Load error:', err);
|
||||||
|
return { skills: [] };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function(data) {
|
||||||
|
var skills = data.skills || [];
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Group skills by provider count
|
||||||
|
var wellSupported = skills.filter(function(s) { return s.providers >= 3; });
|
||||||
|
var goodSupport = skills.filter(function(s) { return s.providers === 2; });
|
||||||
|
var limitedSupport = skills.filter(function(s) { return s.providers === 1; });
|
||||||
|
|
||||||
|
var container = E('div', { 'class': 'cyberpunk-mode secubox-skills' }, [
|
||||||
|
E('link', { 'rel': 'stylesheet', 'type': 'text/css',
|
||||||
|
'href': L.resource('secubox-admin/cyberpunk.css') + '?v=' + Date.now() }),
|
||||||
|
E('link', { 'rel': 'stylesheet',
|
||||||
|
'href': L.resource('secubox-admin/common.css') }),
|
||||||
|
E('link', { 'rel': 'stylesheet',
|
||||||
|
'href': L.resource('secubox-admin/admin.css') }),
|
||||||
|
|
||||||
|
// Header
|
||||||
|
E('div', { 'class': 'cyber-header' }, [
|
||||||
|
E('div', { 'class': 'cyber-header-title' }, '🎯 SKILL BROWSER'),
|
||||||
|
E('div', { 'class': 'cyber-header-subtitle' },
|
||||||
|
'Discover capabilities and find apps that provide them · ' + skills.length + ' skills available')
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Stats panel
|
||||||
|
E('div', { 'class': 'cyber-panel' }, [
|
||||||
|
E('div', { 'class': 'cyber-panel-header' }, [
|
||||||
|
E('span', { 'class': 'cyber-panel-title' }, 'SKILL OVERVIEW')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-panel-body' }, [
|
||||||
|
E('div', { 'class': 'cyber-stats-grid' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-card' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '🎯'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, skills.length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'Total Skills')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-stat-card', 'style': 'border-left-color: #10b981;' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '★★★'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, wellSupported.length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'Well Supported')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-stat-card', 'style': 'border-left-color: #f59e0b;' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '★★☆'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, goodSupport.length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'Good Support')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-stat-card', 'style': 'border-left-color: #3b82f6;' }, [
|
||||||
|
E('div', { 'class': 'cyber-stat-icon' }, '★☆☆'),
|
||||||
|
E('div', { 'class': 'cyber-stat-value' }, limitedSupport.length),
|
||||||
|
E('div', { 'class': 'cyber-stat-label' }, 'Limited Support')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Search bar
|
||||||
|
E('div', { 'class': 'cyber-panel' }, [
|
||||||
|
E('div', { 'class': 'cyber-panel-body' }, [
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px;' }, [
|
||||||
|
E('input', {
|
||||||
|
'type': 'text',
|
||||||
|
'id': 'skill-search',
|
||||||
|
'placeholder': 'Search skills...',
|
||||||
|
'style': 'flex: 1; padding: 12px 16px; background: rgba(99, 102, 241, 0.1); border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 8px; color: var(--cyber-text-bright); font-size: 14px;',
|
||||||
|
'input': function(ev) {
|
||||||
|
self.filterSkills(ev.target.value);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn primary',
|
||||||
|
'click': function() { self.checkSystemSkills(); }
|
||||||
|
}, '🔍 CHECK SYSTEM')
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Skills grid
|
||||||
|
E('div', { 'class': 'cyber-panel' }, [
|
||||||
|
E('div', { 'class': 'cyber-panel-header' }, [
|
||||||
|
E('span', { 'class': 'cyber-panel-title' }, 'AVAILABLE SKILLS')
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cyber-panel-body' }, [
|
||||||
|
E('div', {
|
||||||
|
'id': 'skills-grid',
|
||||||
|
'style': 'display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px;'
|
||||||
|
}, skills.length > 0 ?
|
||||||
|
skills.map(function(skill) {
|
||||||
|
return self.renderSkillCard(skill);
|
||||||
|
}) :
|
||||||
|
[E('div', { 'style': 'text-align: center; padding: 40px; color: var(--cyber-text-dim); grid-column: 1 / -1;' }, [
|
||||||
|
E('div', { 'style': 'font-size: 48px; margin-bottom: 20px;' }, '🎯'),
|
||||||
|
E('div', {}, 'No skills found'),
|
||||||
|
E('div', { 'style': 'font-size: 12px; margin-top: 10px;' },
|
||||||
|
'Skills are discovered from installed app capabilities')
|
||||||
|
])]
|
||||||
|
)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
var wrapper = E('div', { 'class': 'secubox-page-wrapper' });
|
||||||
|
wrapper.appendChild(SbHeader.render());
|
||||||
|
wrapper.appendChild(renderAdminNav('skills'));
|
||||||
|
wrapper.appendChild(container);
|
||||||
|
return wrapper;
|
||||||
|
},
|
||||||
|
|
||||||
|
renderSkillCard: function(skill) {
|
||||||
|
var self = this;
|
||||||
|
var id = skill.id || 'unknown';
|
||||||
|
var displayName = id.replace(/-/g, ' ').replace(/\b\w/g, function(l) { return l.toUpperCase(); });
|
||||||
|
var providers = skill.providers || 0;
|
||||||
|
|
||||||
|
// Quality badge
|
||||||
|
var qualityBadge;
|
||||||
|
var qualityColor;
|
||||||
|
if (providers >= 3) {
|
||||||
|
qualityBadge = '★★★';
|
||||||
|
qualityColor = '#10b981';
|
||||||
|
} else if (providers >= 2) {
|
||||||
|
qualityBadge = '★★☆';
|
||||||
|
qualityColor = '#f59e0b';
|
||||||
|
} else {
|
||||||
|
qualityBadge = '★☆☆';
|
||||||
|
qualityColor = '#3b82f6';
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', {
|
||||||
|
'class': 'skill-card',
|
||||||
|
'data-skill': id,
|
||||||
|
'style': 'background: rgba(99, 102, 241, 0.05); border: 1px solid rgba(99, 102, 241, 0.15); border-radius: 10px; padding: 15px; transition: all 0.3s ease;'
|
||||||
|
}, [
|
||||||
|
E('div', { 'style': 'display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 10px;' }, [
|
||||||
|
E('div', { 'style': 'font-weight: 600; color: var(--cyber-text-bright);' }, displayName),
|
||||||
|
E('span', { 'style': 'color: ' + qualityColor + '; font-size: 12px;' }, qualityBadge)
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'color: var(--cyber-text-dim); font-size: 12px; margin-bottom: 15px;' }, [
|
||||||
|
E('span', {}, providers + ' provider' + (providers !== 1 ? 's' : ''))
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 8px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn',
|
||||||
|
'style': 'flex: 1; font-size: 12px; padding: 8px;',
|
||||||
|
'click': function() { self.showProviders(id); }
|
||||||
|
}, '👁️ Providers'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cyber-btn primary',
|
||||||
|
'style': 'flex: 1; font-size: 12px; padding: 8px;',
|
||||||
|
'click': function() { self.installSkill(id); }
|
||||||
|
}, '📥 Install')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
filterSkills: function(query) {
|
||||||
|
var grid = document.getElementById('skills-grid');
|
||||||
|
if (!grid) return;
|
||||||
|
|
||||||
|
var cards = grid.querySelectorAll('.skill-card');
|
||||||
|
query = query.toLowerCase();
|
||||||
|
|
||||||
|
cards.forEach(function(card) {
|
||||||
|
var skillId = card.getAttribute('data-skill') || '';
|
||||||
|
if (!query || skillId.toLowerCase().indexOf(query) !== -1) {
|
||||||
|
card.style.display = '';
|
||||||
|
} else {
|
||||||
|
card.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
showProviders: function(skillId) {
|
||||||
|
var self = this;
|
||||||
|
ui.showModal('Loading Providers', [
|
||||||
|
Components.renderLoader('Finding providers for: ' + skillId + '...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.getSkillProviders(skillId).then(function(result) {
|
||||||
|
var providers = result.providers || [];
|
||||||
|
ui.hideModal();
|
||||||
|
|
||||||
|
var content = [
|
||||||
|
E('h3', { 'style': 'margin-bottom: 15px; color: var(--cyber-text-bright);' },
|
||||||
|
'Providers for: ' + skillId.replace(/-/g, ' '))
|
||||||
|
];
|
||||||
|
|
||||||
|
if (providers.length === 0) {
|
||||||
|
content.push(E('p', { 'style': 'color: var(--cyber-text-dim);' }, 'No providers found for this skill.'));
|
||||||
|
} else {
|
||||||
|
content.push(E('div', { 'style': 'max-height: 400px; overflow-y: auto;' },
|
||||||
|
providers.map(function(provider) {
|
||||||
|
return E('div', {
|
||||||
|
'style': 'display: flex; justify-content: space-between; align-items: center; padding: 12px; background: rgba(99, 102, 241, 0.05); border-radius: 8px; margin-bottom: 10px;'
|
||||||
|
}, [
|
||||||
|
E('div', {}, [
|
||||||
|
E('div', { 'style': 'font-weight: 600; color: var(--cyber-text-bright);' }, [
|
||||||
|
provider.featured ? E('span', { 'style': 'color: #8b5cf6;' }, '★ ') : null,
|
||||||
|
provider.name
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'font-size: 12px; color: var(--cyber-text-dim);' }, provider.id),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px; margin-top: 5px;' }, [
|
||||||
|
E('span', {
|
||||||
|
'class': 'cyber-badge',
|
||||||
|
'style': provider.status === 'stable' ?
|
||||||
|
'background: rgba(16, 185, 129, 0.2); color: #10b981;' :
|
||||||
|
'background: rgba(245, 158, 11, 0.2); color: #f59e0b;'
|
||||||
|
}, (provider.status || 'unknown').toUpperCase()),
|
||||||
|
provider.installed ? E('span', {
|
||||||
|
'class': 'cyber-badge',
|
||||||
|
'style': 'background: rgba(16, 185, 129, 0.2); color: #10b981;'
|
||||||
|
}, 'INSTALLED') : null
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
!provider.installed ? E('button', {
|
||||||
|
'class': 'cyber-btn primary',
|
||||||
|
'style': 'font-size: 12px;',
|
||||||
|
'click': function() {
|
||||||
|
ui.hideModal();
|
||||||
|
self.installProvider(provider.id);
|
||||||
|
}
|
||||||
|
}, 'Install') : E('span', { 'style': 'color: #10b981;' }, '✓')
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
content.push(E('div', { 'style': 'display: flex; gap: 10px; margin-top: 15px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Close')
|
||||||
|
]));
|
||||||
|
|
||||||
|
ui.showModal('Skill Providers', content);
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Error loading providers: ' + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
installSkill: function(skillId) {
|
||||||
|
var self = this;
|
||||||
|
ui.showModal('Install Skill', [
|
||||||
|
E('p', {}, 'Install the best provider for skill: ' + skillId.replace(/-/g, ' ') + '?'),
|
||||||
|
E('p', { 'style': 'color: var(--cyber-text-dim); font-size: 12px;' },
|
||||||
|
'This will automatically select and install the recommended provider.'),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px; margin-top: 20px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() {
|
||||||
|
ui.showModal('Installing Skill', [
|
||||||
|
Components.renderLoader('Installing provider for: ' + skillId + '...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.installSkill(skillId).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result && result.success) {
|
||||||
|
ui.addNotification(null, E('p', 'Skill provider installed successfully!'), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', 'Installation failed: ' + (result.error || 'Unknown error')), 'error');
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Installation error: ' + err.message), 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 'Install'),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Cancel')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
|
installProvider: function(providerId) {
|
||||||
|
ui.showModal('Installing Provider', [
|
||||||
|
Components.renderLoader('Installing: ' + providerId + '...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.installModule(providerId, false).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (result) {
|
||||||
|
ui.addNotification(null, E('p', 'Provider installed successfully!'), 'success');
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Installation error: ' + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
checkSystemSkills: function() {
|
||||||
|
var self = this;
|
||||||
|
ui.showModal('Checking System Skills', [
|
||||||
|
Components.renderLoader('Analyzing installed capabilities...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
API.checkSkills(null).then(function(result) {
|
||||||
|
ui.hideModal();
|
||||||
|
// Display results in a modal
|
||||||
|
ui.showModal('System Skills', [
|
||||||
|
E('p', { 'style': 'color: var(--cyber-text-dim); margin-bottom: 15px;' },
|
||||||
|
'Skills available on this system:'),
|
||||||
|
E('div', { 'style': 'background: rgba(99, 102, 241, 0.1); padding: 15px; border-radius: 8px; max-height: 300px; overflow-y: auto;' }, [
|
||||||
|
E('pre', { 'style': 'color: var(--cyber-text-bright); font-size: 12px; white-space: pre-wrap;' },
|
||||||
|
typeof result === 'string' ? result : JSON.stringify(result, null, 2))
|
||||||
|
]),
|
||||||
|
E('div', { 'style': 'display: flex; gap: 10px; margin-top: 15px;' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { ui.hideModal(); }
|
||||||
|
}, 'Close')
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.hideModal();
|
||||||
|
ui.addNotification(null, E('p', 'Check error: ' + err.message), 'error');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleSave: null,
|
||||||
|
handleReset: null
|
||||||
|
});
|
||||||
@ -38,14 +38,38 @@
|
|||||||
"path": "secubox-admin/updates"
|
"path": "secubox-admin/updates"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin/secubox/admin/catalog-sources": {
|
"admin/secubox/admin/profiles": {
|
||||||
"title": "Catalog Sources",
|
"title": "Profiles",
|
||||||
|
"order": 26,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "secubox-admin/profiles"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/secubox/admin/skills": {
|
||||||
|
"title": "Skills",
|
||||||
"order": 27,
|
"order": 27,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "secubox-admin/skills"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"admin/secubox/admin/catalog-sources": {
|
||||||
|
"title": "Catalog Sources",
|
||||||
|
"order": 28,
|
||||||
"action": {
|
"action": {
|
||||||
"type": "view",
|
"type": "view",
|
||||||
"path": "secubox-admin/catalog-sources"
|
"path": "secubox-admin/catalog-sources"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"admin/secubox/admin/feedback": {
|
||||||
|
"title": "Feedback",
|
||||||
|
"order": 29,
|
||||||
|
"action": {
|
||||||
|
"type": "view",
|
||||||
|
"path": "secubox-admin/feedback"
|
||||||
|
}
|
||||||
|
},
|
||||||
"admin/secubox/admin/settings": {
|
"admin/secubox/admin/settings": {
|
||||||
"title": "App Settings",
|
"title": "App Settings",
|
||||||
"order": 30,
|
"order": 30,
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
include $(TOPDIR)/rules.mk
|
|
||||||
|
|
||||||
PKG_NAME:=luci-app-secubox-bonus
|
|
||||||
PKG_VERSION:=0.2.0
|
|
||||||
PKG_RELEASE:=2
|
|
||||||
PKG_ARCH:=all
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI - SecuBox Bonus Content & Local Package Store
|
|
||||||
LUCI_DESCRIPTION:=SecuBox documentation, local package repository, and app store. Includes all SecuBox packages as a local opkg feed for offline installation. Accessible at /luci-static/secubox/
|
|
||||||
LUCI_DEPENDS:=+luci-base +rpcd +luci-lib-jsonc
|
|
||||||
LUCI_PKGARCH:=all
|
|
||||||
|
|
||||||
include $(TOPDIR)/feeds/luci/luci.mk
|
|
||||||
|
|
||||||
define Package/luci-app-secubox-bonus/conffiles
|
|
||||||
/etc/opkg/customfeeds.conf
|
|
||||||
endef
|
|
||||||
|
|
||||||
define Package/luci-app-secubox-bonus/install
|
|
||||||
# Documentation and static content
|
|
||||||
$(INSTALL_DIR) $(1)/www/luci-static/secubox
|
|
||||||
$(CP) ./htdocs/luci-static/secubox/* $(1)/www/luci-static/secubox/
|
|
||||||
|
|
||||||
# Local package feed (populated by build)
|
|
||||||
$(INSTALL_DIR) $(1)/www/secubox-feed
|
|
||||||
if [ -d ./root/www/secubox-feed ] && [ -n "$$$$(ls -A ./root/www/secubox-feed 2>/dev/null)" ]; then \
|
|
||||||
$(CP) ./root/www/secubox-feed/* $(1)/www/secubox-feed/; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# opkg custom feeds configuration
|
|
||||||
$(INSTALL_DIR) $(1)/etc/opkg
|
|
||||||
$(INSTALL_CONF) ./root/etc/opkg/customfeeds.conf $(1)/etc/opkg/customfeeds.conf
|
|
||||||
|
|
||||||
# RPCD backend for package management
|
|
||||||
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
|
||||||
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.secubox-store $(1)/usr/libexec/rpcd/
|
|
||||||
|
|
||||||
# ACL permissions
|
|
||||||
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
|
|
||||||
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/luci-app-secubox-bonus.json $(1)/usr/share/rpcd/acl.d/
|
|
||||||
|
|
||||||
# LuCI menu entry
|
|
||||||
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
|
|
||||||
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/luci-app-secubox-bonus.json $(1)/usr/share/luci/menu.d/
|
|
||||||
|
|
||||||
# JavaScript view
|
|
||||||
$(INSTALL_DIR) $(1)/www/luci-static/resources/view/secubox-bonus
|
|
||||||
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/secubox-bonus/*.js $(1)/www/luci-static/resources/view/secubox-bonus/
|
|
||||||
endef
|
|
||||||
|
|
||||||
define Package/luci-app-secubox-bonus/postinst
|
|
||||||
#!/bin/sh
|
|
||||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
|
||||||
# Restart rpcd to load new backend
|
|
||||||
/etc/init.d/rpcd restart
|
|
||||||
rm -rf /tmp/luci-modulecache /tmp/luci-indexcache 2>/dev/null
|
|
||||||
echo "SecuBox Bonus & Package Store installed."
|
|
||||||
}
|
|
||||||
exit 0
|
|
||||||
endef
|
|
||||||
|
|
||||||
# call BuildPackage - OpenWrt buildroot
|
|
||||||
@ -1,287 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
'require view';
|
|
||||||
'require rpc';
|
|
||||||
'require ui';
|
|
||||||
'require poll';
|
|
||||||
|
|
||||||
var callListPackages = rpc.declare({
|
|
||||||
object: 'luci.secubox-store',
|
|
||||||
method: 'list_packages',
|
|
||||||
expect: { packages: [] }
|
|
||||||
});
|
|
||||||
|
|
||||||
var callInstallPackage = rpc.declare({
|
|
||||||
object: 'luci.secubox-store',
|
|
||||||
method: 'install_package',
|
|
||||||
params: ['package'],
|
|
||||||
expect: { success: false }
|
|
||||||
});
|
|
||||||
|
|
||||||
var callRemovePackage = rpc.declare({
|
|
||||||
object: 'luci.secubox-store',
|
|
||||||
method: 'remove_package',
|
|
||||||
params: ['package'],
|
|
||||||
expect: { success: false }
|
|
||||||
});
|
|
||||||
|
|
||||||
var callGetFeedStatus = rpc.declare({
|
|
||||||
object: 'luci.secubox-store',
|
|
||||||
method: 'get_feed_status',
|
|
||||||
expect: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Icon mapping
|
|
||||||
var iconMap = {
|
|
||||||
'shield': '\u{1F6E1}',
|
|
||||||
'lock': '\u{1F512}',
|
|
||||||
'activity': '\u{1F4CA}',
|
|
||||||
'filter': '\u{1F50D}',
|
|
||||||
'users': '\u{1F465}',
|
|
||||||
'wifi': '\u{1F4F6}',
|
|
||||||
'server': '\u{1F5A5}',
|
|
||||||
'box': '\u{1F4E6}',
|
|
||||||
'radio': '\u{1F4FB}',
|
|
||||||
'message-square': '\u{1F4AC}',
|
|
||||||
'eye': '\u{1F441}',
|
|
||||||
'bar-chart-2': '\u{1F4CA}',
|
|
||||||
'settings': '\u{2699}',
|
|
||||||
'globe': '\u{1F310}',
|
|
||||||
'cpu': '\u{1F4BB}',
|
|
||||||
'film': '\u{1F3AC}',
|
|
||||||
'monitor': '\u{1F5B5}',
|
|
||||||
'key': '\u{1F511}',
|
|
||||||
'palette': '\u{1F3A8}',
|
|
||||||
'package': '\u{1F4E6}'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Category colors
|
|
||||||
var categoryColors = {
|
|
||||||
'security': '#e74c3c',
|
|
||||||
'network': '#3498db',
|
|
||||||
'vpn': '#9b59b6',
|
|
||||||
'iot': '#27ae60',
|
|
||||||
'monitoring': '#f39c12',
|
|
||||||
'system': '#34495e',
|
|
||||||
'media': '#e91e63',
|
|
||||||
'theme': '#00bcd4',
|
|
||||||
'secubox': '#2ecc71',
|
|
||||||
'utility': '#95a5a6'
|
|
||||||
};
|
|
||||||
|
|
||||||
function formatSize(bytes) {
|
|
||||||
if (bytes < 1024) return bytes + ' B';
|
|
||||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
||||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getIcon(iconName) {
|
|
||||||
return iconMap[iconName] || '\u{1F4E6}';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCategoryColor(category) {
|
|
||||||
return categoryColors[category] || '#95a5a6';
|
|
||||||
}
|
|
||||||
|
|
||||||
return view.extend({
|
|
||||||
load: function() {
|
|
||||||
return Promise.all([
|
|
||||||
callListPackages(),
|
|
||||||
callGetFeedStatus()
|
|
||||||
]);
|
|
||||||
},
|
|
||||||
|
|
||||||
renderPackageCard: function(pkg) {
|
|
||||||
var self = this;
|
|
||||||
var icon = getIcon(pkg.icon);
|
|
||||||
var color = getCategoryColor(pkg.category);
|
|
||||||
|
|
||||||
var card = E('div', { 'class': 'package-card', 'data-category': pkg.category }, [
|
|
||||||
E('div', { 'class': 'package-header' }, [
|
|
||||||
E('span', { 'class': 'package-icon', 'style': 'background-color: ' + color }, icon),
|
|
||||||
E('div', { 'class': 'package-title' }, [
|
|
||||||
E('h3', {}, pkg.name),
|
|
||||||
E('span', { 'class': 'package-version' }, 'v' + pkg.version)
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
E('p', { 'class': 'package-description' }, pkg.description || 'SecuBox package'),
|
|
||||||
E('div', { 'class': 'package-meta' }, [
|
|
||||||
E('span', { 'class': 'package-category' }, pkg.category),
|
|
||||||
E('span', { 'class': 'package-size' }, formatSize(pkg.size || 0))
|
|
||||||
]),
|
|
||||||
E('div', { 'class': 'package-actions' }, [
|
|
||||||
pkg.installed
|
|
||||||
? E('button', {
|
|
||||||
'class': 'btn cbi-button cbi-button-remove',
|
|
||||||
'click': ui.createHandlerFn(self, 'handleRemove', pkg.name)
|
|
||||||
}, 'Remove')
|
|
||||||
: E('button', {
|
|
||||||
'class': 'btn cbi-button cbi-button-action',
|
|
||||||
'click': ui.createHandlerFn(self, 'handleInstall', pkg.name)
|
|
||||||
}, 'Install'),
|
|
||||||
pkg.installed
|
|
||||||
? E('span', { 'class': 'status-installed' }, '\u2713 Installed')
|
|
||||||
: E('span', { 'class': 'status-available' }, 'Available')
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
|
|
||||||
return card;
|
|
||||||
},
|
|
||||||
|
|
||||||
handleInstall: function(pkgName, ev) {
|
|
||||||
var btn = ev.currentTarget;
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Installing...';
|
|
||||||
|
|
||||||
return callInstallPackage(pkgName).then(function(result) {
|
|
||||||
if (result.success) {
|
|
||||||
ui.addNotification(null, E('p', {}, 'Package ' + pkgName + ' installed successfully. Refreshing...'));
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
ui.addNotification(null, E('p', {}, 'Failed to install ' + pkgName + ': ' + (result.error || 'Unknown error')), 'error');
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Install';
|
|
||||||
}
|
|
||||||
}).catch(function(err) {
|
|
||||||
ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'error');
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Install';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleRemove: function(pkgName, ev) {
|
|
||||||
var btn = ev.currentTarget;
|
|
||||||
|
|
||||||
if (!confirm('Remove package ' + pkgName + '?'))
|
|
||||||
return;
|
|
||||||
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Removing...';
|
|
||||||
|
|
||||||
return callRemovePackage(pkgName).then(function(result) {
|
|
||||||
if (result.success) {
|
|
||||||
ui.addNotification(null, E('p', {}, 'Package ' + pkgName + ' removed successfully. Refreshing...'));
|
|
||||||
window.location.reload();
|
|
||||||
} else {
|
|
||||||
ui.addNotification(null, E('p', {}, 'Failed to remove ' + pkgName + ': ' + (result.error || 'Unknown error')), 'error');
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Remove';
|
|
||||||
}
|
|
||||||
}).catch(function(err) {
|
|
||||||
ui.addNotification(null, E('p', {}, 'Error: ' + err.message), 'error');
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Remove';
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
filterPackages: function(category) {
|
|
||||||
var cards = document.querySelectorAll('.package-card');
|
|
||||||
cards.forEach(function(card) {
|
|
||||||
if (category === 'all' || card.dataset.category === category) {
|
|
||||||
card.style.display = '';
|
|
||||||
} else {
|
|
||||||
card.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var btns = document.querySelectorAll('.filter-btn');
|
|
||||||
btns.forEach(function(btn) {
|
|
||||||
btn.classList.remove('active');
|
|
||||||
if (btn.dataset.category === category) {
|
|
||||||
btn.classList.add('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function(data) {
|
|
||||||
var packages = data[0].packages || data[0] || [];
|
|
||||||
var feedStatus = data[1] || {};
|
|
||||||
var self = this;
|
|
||||||
|
|
||||||
// Get unique categories
|
|
||||||
var categories = ['all'];
|
|
||||||
packages.forEach(function(pkg) {
|
|
||||||
if (pkg.category && categories.indexOf(pkg.category) === -1) {
|
|
||||||
categories.push(pkg.category);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sort packages: installed first, then by name
|
|
||||||
packages.sort(function(a, b) {
|
|
||||||
if (a.installed !== b.installed) return b.installed ? 1 : -1;
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
});
|
|
||||||
|
|
||||||
var installedCount = packages.filter(function(p) { return p.installed; }).length;
|
|
||||||
|
|
||||||
var view = E('div', { 'class': 'secubox-store' }, [
|
|
||||||
E('style', {}, [
|
|
||||||
'.secubox-store { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }',
|
|
||||||
'.store-header { margin-bottom: 20px; padding: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 10px; color: white; }',
|
|
||||||
'.store-header h2 { margin: 0 0 10px 0; }',
|
|
||||||
'.store-stats { display: flex; gap: 20px; }',
|
|
||||||
'.store-stats span { background: rgba(255,255,255,0.2); padding: 5px 15px; border-radius: 20px; }',
|
|
||||||
'.filter-bar { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 20px; }',
|
|
||||||
'.filter-btn { padding: 8px 16px; border: 1px solid #ddd; border-radius: 20px; background: white; cursor: pointer; transition: all 0.2s; }',
|
|
||||||
'.filter-btn:hover { background: #f0f0f0; }',
|
|
||||||
'.filter-btn.active { background: #667eea; color: white; border-color: #667eea; }',
|
|
||||||
'.package-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }',
|
|
||||||
'.package-card { background: white; border: 1px solid #e0e0e0; border-radius: 10px; padding: 20px; transition: box-shadow 0.2s; }',
|
|
||||||
'.package-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); }',
|
|
||||||
'.package-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }',
|
|
||||||
'.package-icon { width: 48px; height: 48px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 24px; color: white; }',
|
|
||||||
'.package-title h3 { margin: 0; font-size: 16px; }',
|
|
||||||
'.package-version { color: #888; font-size: 12px; }',
|
|
||||||
'.package-description { color: #666; font-size: 14px; margin-bottom: 12px; line-height: 1.4; }',
|
|
||||||
'.package-meta { display: flex; gap: 10px; margin-bottom: 15px; }',
|
|
||||||
'.package-category { background: #f0f0f0; padding: 3px 10px; border-radius: 12px; font-size: 12px; text-transform: capitalize; }',
|
|
||||||
'.package-size { color: #888; font-size: 12px; }',
|
|
||||||
'.package-actions { display: flex; align-items: center; gap: 10px; }',
|
|
||||||
'.package-actions .btn { padding: 8px 20px; }',
|
|
||||||
'.status-installed { color: #27ae60; font-weight: 500; }',
|
|
||||||
'.status-available { color: #888; }',
|
|
||||||
'.cbi-button-remove { background: #e74c3c !important; border-color: #e74c3c !important; color: white !important; }',
|
|
||||||
'.cbi-button-remove:hover { background: #c0392b !important; }',
|
|
||||||
'.no-packages { text-align: center; padding: 40px; color: #888; }'
|
|
||||||
].join('\n')),
|
|
||||||
|
|
||||||
E('div', { 'class': 'store-header' }, [
|
|
||||||
E('h2', {}, 'SecuBox Package Store'),
|
|
||||||
E('p', {}, 'Install and manage SecuBox packages from the local repository'),
|
|
||||||
E('div', { 'class': 'store-stats' }, [
|
|
||||||
E('span', {}, packages.length + ' packages available'),
|
|
||||||
E('span', {}, installedCount + ' installed'),
|
|
||||||
feedStatus.feed_configured
|
|
||||||
? E('span', {}, '\u2713 Feed configured')
|
|
||||||
: E('span', {}, '\u26A0 Feed not configured')
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
|
|
||||||
E('div', { 'class': 'filter-bar' },
|
|
||||||
categories.map(function(cat) {
|
|
||||||
return E('button', {
|
|
||||||
'class': 'filter-btn' + (cat === 'all' ? ' active' : ''),
|
|
||||||
'data-category': cat,
|
|
||||||
'click': function() { self.filterPackages(cat); }
|
|
||||||
}, cat === 'all' ? 'All' : cat.charAt(0).toUpperCase() + cat.slice(1));
|
|
||||||
})
|
|
||||||
),
|
|
||||||
|
|
||||||
packages.length > 0
|
|
||||||
? E('div', { 'class': 'package-grid' },
|
|
||||||
packages.map(function(pkg) {
|
|
||||||
return self.renderPackageCard(pkg);
|
|
||||||
})
|
|
||||||
)
|
|
||||||
: E('div', { 'class': 'no-packages' }, [
|
|
||||||
E('p', {}, 'No packages found in local feed.'),
|
|
||||||
E('p', {}, 'The local package feed may not be populated yet.')
|
|
||||||
])
|
|
||||||
]);
|
|
||||||
|
|
||||||
return view;
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSaveApply: null,
|
|
||||||
handleSave: null,
|
|
||||||
handleReset: null
|
|
||||||
});
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
# SecuBox Local Package Feed
|
|
||||||
# Pre-built packages available for offline installation
|
|
||||||
# Managed by luci-app-secubox-bonus
|
|
||||||
|
|
||||||
src/gz secubox file:///www/secubox-feed
|
|
||||||
@ -1,274 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# SPDX-License-Identifier: Apache-2.0
|
|
||||||
# SecuBox Local Package Store - RPCD Backend
|
|
||||||
# Manages installation/removal of packages from local feed
|
|
||||||
|
|
||||||
. /usr/share/libubox/jshn.sh
|
|
||||||
|
|
||||||
FEED_DIR="/www/secubox-feed"
|
|
||||||
APPS_JSON="$FEED_DIR/apps-local.json"
|
|
||||||
|
|
||||||
# List available packages from local feed
|
|
||||||
list_packages() {
|
|
||||||
if [ -f "$APPS_JSON" ]; then
|
|
||||||
# Read apps-local.json and add installation status
|
|
||||||
local packages=$(cat "$APPS_JSON")
|
|
||||||
|
|
||||||
json_init
|
|
||||||
json_add_boolean "success" 1
|
|
||||||
json_add_string "feed_url" "/secubox-feed"
|
|
||||||
json_add_array "packages"
|
|
||||||
|
|
||||||
# Parse apps-local.json and check each package
|
|
||||||
local pkg_list=$(jsonfilter -s "$packages" -e '@.packages[*].name' 2>/dev/null)
|
|
||||||
|
|
||||||
for name in $pkg_list; do
|
|
||||||
local pkg_data=$(jsonfilter -s "$packages" -e "@.packages[@.name='$name']" 2>/dev/null)
|
|
||||||
local version=$(echo "$pkg_data" | jsonfilter -e '@.version' 2>/dev/null)
|
|
||||||
local filename=$(echo "$pkg_data" | jsonfilter -e '@.filename' 2>/dev/null)
|
|
||||||
local size=$(echo "$pkg_data" | jsonfilter -e '@.size' 2>/dev/null)
|
|
||||||
local category=$(echo "$pkg_data" | jsonfilter -e '@.category' 2>/dev/null)
|
|
||||||
local icon=$(echo "$pkg_data" | jsonfilter -e '@.icon' 2>/dev/null)
|
|
||||||
local description=$(echo "$pkg_data" | jsonfilter -e '@.description' 2>/dev/null)
|
|
||||||
|
|
||||||
# Check if installed
|
|
||||||
local installed=0
|
|
||||||
if opkg list-installed 2>/dev/null | grep -q "^${name} "; then
|
|
||||||
installed=1
|
|
||||||
fi
|
|
||||||
|
|
||||||
json_add_object ""
|
|
||||||
json_add_string "name" "$name"
|
|
||||||
json_add_string "version" "$version"
|
|
||||||
json_add_string "filename" "$filename"
|
|
||||||
json_add_int "size" "${size:-0}"
|
|
||||||
json_add_string "category" "$category"
|
|
||||||
json_add_string "icon" "$icon"
|
|
||||||
json_add_string "description" "$description"
|
|
||||||
json_add_boolean "installed" "$installed"
|
|
||||||
json_close_object
|
|
||||||
done
|
|
||||||
|
|
||||||
json_close_array
|
|
||||||
json_dump
|
|
||||||
else
|
|
||||||
json_init
|
|
||||||
json_add_boolean "success" 0
|
|
||||||
json_add_string "error" "Local feed not found"
|
|
||||||
json_add_array "packages"
|
|
||||||
json_close_array
|
|
||||||
json_dump
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Install a package from local feed
|
|
||||||
install_package() {
|
|
||||||
local pkg_name="$1"
|
|
||||||
|
|
||||||
if [ -z "$pkg_name" ]; then
|
|
||||||
json_init
|
|
||||||
json_add_boolean "success" 0
|
|
||||||
json_add_string "error" "Package name required"
|
|
||||||
json_dump
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Find the package file
|
|
||||||
local pkg_file=$(ls "$FEED_DIR/${pkg_name}_"*.ipk 2>/dev/null | head -1)
|
|
||||||
|
|
||||||
if [ -z "$pkg_file" ] || [ ! -f "$pkg_file" ]; then
|
|
||||||
json_init
|
|
||||||
json_add_boolean "success" 0
|
|
||||||
json_add_string "error" "Package file not found: $pkg_name"
|
|
||||||
json_dump
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Update opkg lists first
|
|
||||||
opkg update 2>/dev/null || true
|
|
||||||
|
|
||||||
# Install the package
|
|
||||||
local output
|
|
||||||
output=$(opkg install "$pkg_file" 2>&1)
|
|
||||||
local result=$?
|
|
||||||
|
|
||||||
json_init
|
|
||||||
if [ $result -eq 0 ]; then
|
|
||||||
json_add_boolean "success" 1
|
|
||||||
json_add_string "message" "Package installed successfully"
|
|
||||||
json_add_string "package" "$pkg_name"
|
|
||||||
|
|
||||||
# Clear LuCI cache
|
|
||||||
rm -rf /tmp/luci-modulecache /tmp/luci-indexcache 2>/dev/null
|
|
||||||
else
|
|
||||||
json_add_boolean "success" 0
|
|
||||||
json_add_string "error" "$output"
|
|
||||||
fi
|
|
||||||
json_add_string "output" "$output"
|
|
||||||
json_dump
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove a package
|
|
||||||
remove_package() {
|
|
||||||
local pkg_name="$1"
|
|
||||||
|
|
||||||
if [ -z "$pkg_name" ]; then
|
|
||||||
json_init
|
|
||||||
json_add_boolean "success" 0
|
|
||||||
json_add_string "error" "Package name required"
|
|
||||||
json_dump
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if installed
|
|
||||||
if ! opkg list-installed 2>/dev/null | grep -q "^${pkg_name} "; then
|
|
||||||
json_init
|
|
||||||
json_add_boolean "success" 0
|
|
||||||
json_add_string "error" "Package not installed: $pkg_name"
|
|
||||||
json_dump
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove the package
|
|
||||||
local output
|
|
||||||
output=$(opkg remove "$pkg_name" 2>&1)
|
|
||||||
local result=$?
|
|
||||||
|
|
||||||
json_init
|
|
||||||
if [ $result -eq 0 ]; then
|
|
||||||
json_add_boolean "success" 1
|
|
||||||
json_add_string "message" "Package removed successfully"
|
|
||||||
json_add_string "package" "$pkg_name"
|
|
||||||
|
|
||||||
# Clear LuCI cache
|
|
||||||
rm -rf /tmp/luci-modulecache /tmp/luci-indexcache 2>/dev/null
|
|
||||||
else
|
|
||||||
json_add_boolean "success" 0
|
|
||||||
json_add_string "error" "$output"
|
|
||||||
fi
|
|
||||||
json_add_string "output" "$output"
|
|
||||||
json_dump
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get package info
|
|
||||||
get_package_info() {
|
|
||||||
local pkg_name="$1"
|
|
||||||
|
|
||||||
if [ -z "$pkg_name" ]; then
|
|
||||||
json_init
|
|
||||||
json_add_boolean "success" 0
|
|
||||||
json_add_string "error" "Package name required"
|
|
||||||
json_dump
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
json_init
|
|
||||||
json_add_boolean "success" 1
|
|
||||||
json_add_string "name" "$pkg_name"
|
|
||||||
|
|
||||||
# Check if installed
|
|
||||||
local installed_info=$(opkg list-installed "$pkg_name" 2>/dev/null)
|
|
||||||
if [ -n "$installed_info" ]; then
|
|
||||||
json_add_boolean "installed" 1
|
|
||||||
json_add_string "installed_version" "$(echo "$installed_info" | cut -d' ' -f3)"
|
|
||||||
else
|
|
||||||
json_add_boolean "installed" 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get info from feed
|
|
||||||
if [ -f "$APPS_JSON" ]; then
|
|
||||||
local pkg_data=$(cat "$APPS_JSON" | jsonfilter -e "@.packages[@.name='$pkg_name']" 2>/dev/null)
|
|
||||||
if [ -n "$pkg_data" ]; then
|
|
||||||
json_add_string "feed_version" "$(echo "$pkg_data" | jsonfilter -e '@.version' 2>/dev/null)"
|
|
||||||
json_add_string "description" "$(echo "$pkg_data" | jsonfilter -e '@.description' 2>/dev/null)"
|
|
||||||
json_add_string "category" "$(echo "$pkg_data" | jsonfilter -e '@.category' 2>/dev/null)"
|
|
||||||
json_add_int "size" "$(echo "$pkg_data" | jsonfilter -e '@.size' 2>/dev/null)"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
json_dump
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get feed status
|
|
||||||
get_feed_status() {
|
|
||||||
json_init
|
|
||||||
|
|
||||||
if [ -d "$FEED_DIR" ]; then
|
|
||||||
json_add_boolean "feed_exists" 1
|
|
||||||
json_add_string "feed_path" "$FEED_DIR"
|
|
||||||
|
|
||||||
local pkg_count=$(ls -1 "$FEED_DIR"/*.ipk 2>/dev/null | wc -l)
|
|
||||||
json_add_int "package_count" "$pkg_count"
|
|
||||||
|
|
||||||
if [ -f "$APPS_JSON" ]; then
|
|
||||||
json_add_boolean "index_exists" 1
|
|
||||||
local generated=$(jsonfilter -i "$APPS_JSON" -e '@.generated' 2>/dev/null)
|
|
||||||
json_add_string "generated" "$generated"
|
|
||||||
else
|
|
||||||
json_add_boolean "index_exists" 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if feed is in opkg config
|
|
||||||
if grep -q "secubox-feed" /etc/opkg/customfeeds.conf 2>/dev/null; then
|
|
||||||
json_add_boolean "feed_configured" 1
|
|
||||||
else
|
|
||||||
json_add_boolean "feed_configured" 0
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
json_add_boolean "feed_exists" 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
json_dump
|
|
||||||
}
|
|
||||||
|
|
||||||
# Main dispatcher
|
|
||||||
case "$1" in
|
|
||||||
list)
|
|
||||||
json_init
|
|
||||||
json_add_object "list_packages"
|
|
||||||
json_close_object
|
|
||||||
json_add_object "install_package"
|
|
||||||
json_add_string "package" "str"
|
|
||||||
json_close_object
|
|
||||||
json_add_object "remove_package"
|
|
||||||
json_add_string "package" "str"
|
|
||||||
json_close_object
|
|
||||||
json_add_object "get_package_info"
|
|
||||||
json_add_string "package" "str"
|
|
||||||
json_close_object
|
|
||||||
json_add_object "get_feed_status"
|
|
||||||
json_close_object
|
|
||||||
json_dump
|
|
||||||
;;
|
|
||||||
call)
|
|
||||||
case "$2" in
|
|
||||||
list_packages)
|
|
||||||
list_packages
|
|
||||||
;;
|
|
||||||
install_package)
|
|
||||||
read -r input
|
|
||||||
pkg=$(echo "$input" | jsonfilter -e '@.package')
|
|
||||||
install_package "$pkg"
|
|
||||||
;;
|
|
||||||
remove_package)
|
|
||||||
read -r input
|
|
||||||
pkg=$(echo "$input" | jsonfilter -e '@.package')
|
|
||||||
remove_package "$pkg"
|
|
||||||
;;
|
|
||||||
get_package_info)
|
|
||||||
read -r input
|
|
||||||
pkg=$(echo "$input" | jsonfilter -e '@.package')
|
|
||||||
get_package_info "$pkg"
|
|
||||||
;;
|
|
||||||
get_feed_status)
|
|
||||||
get_feed_status
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo '{"error":"Unknown method"}'
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo '{"error":"Invalid action"}'
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
{
|
|
||||||
"admin/secubox/local-packages": {
|
|
||||||
"title": "Local Packages",
|
|
||||||
"order": 19,
|
|
||||||
"action": {
|
|
||||||
"type": "view",
|
|
||||||
"path": "secubox-bonus/store"
|
|
||||||
},
|
|
||||||
"depends": {
|
|
||||||
"acl": ["luci-app-secubox-bonus"],
|
|
||||||
"uci": {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"luci-app-secubox-bonus": {
|
|
||||||
"description": "Grant access to SecuBox local package store",
|
|
||||||
"read": {
|
|
||||||
"ubus": {
|
|
||||||
"luci.secubox-store": [
|
|
||||||
"list_packages",
|
|
||||||
"get_package_info",
|
|
||||||
"get_feed_status"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"write": {
|
|
||||||
"ubus": {
|
|
||||||
"luci.secubox-store": [
|
|
||||||
"install_package",
|
|
||||||
"remove_package"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
0
package/secubox/luci-app-service-registry/root/usr/libexec/rpcd/luci.service-registry
Normal file → Executable file
0
package/secubox/luci-app-service-registry/root/usr/libexec/rpcd/luci.service-registry
Normal file → Executable file
79
package/secubox/secubox-app-bonus/Makefile
Normal file
79
package/secubox/secubox-app-bonus/Makefile
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
include $(TOPDIR)/rules.mk
|
||||||
|
|
||||||
|
PKG_NAME:=secubox-app-bonus
|
||||||
|
PKG_VERSION:=0.3.0
|
||||||
|
PKG_RELEASE:=1
|
||||||
|
PKG_LICENSE:=Apache-2.0
|
||||||
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
PKG_ARCH:=all
|
||||||
|
|
||||||
|
include $(INCLUDE_DIR)/package.mk
|
||||||
|
|
||||||
|
define Package/secubox-app-bonus
|
||||||
|
SECTION:=secubox
|
||||||
|
CATEGORY:=SecuBox
|
||||||
|
TITLE:=SecuBox Local Package Feed & Documentation
|
||||||
|
PKGARCH:=all
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-bonus/description
|
||||||
|
SecuBox local package repository for offline installation.
|
||||||
|
Provides pre-built SecuBox packages via opkg local feed at /www/secubox-feed/.
|
||||||
|
Also includes SecuBox documentation at /www/luci-static/secubox/.
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-bonus/conffiles
|
||||||
|
/etc/opkg/customfeeds.conf
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Build/Compile
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-bonus/install
|
||||||
|
# Documentation and static content
|
||||||
|
$(INSTALL_DIR) $(1)/www/luci-static/secubox
|
||||||
|
$(CP) ./htdocs/luci-static/secubox/* $(1)/www/luci-static/secubox/
|
||||||
|
|
||||||
|
# Local package feed (populated by build)
|
||||||
|
$(INSTALL_DIR) $(1)/www/secubox-feed
|
||||||
|
if [ -d ./root/www/secubox-feed ] && [ -n "$$$$(ls -A ./root/www/secubox-feed 2>/dev/null)" ]; then \
|
||||||
|
$(CP) ./root/www/secubox-feed/* $(1)/www/secubox-feed/; \
|
||||||
|
fi
|
||||||
|
# Remove any Packages.sig to avoid opkg signature verification issues
|
||||||
|
$(RM) $(1)/www/secubox-feed/Packages.sig 2>/dev/null || true
|
||||||
|
|
||||||
|
# opkg custom feeds configuration
|
||||||
|
$(INSTALL_DIR) $(1)/etc/opkg
|
||||||
|
$(INSTALL_CONF) ./root/etc/opkg/customfeeds.conf $(1)/etc/opkg/customfeeds.conf
|
||||||
|
|
||||||
|
# Init script to sync package list on boot
|
||||||
|
$(INSTALL_DIR) $(1)/etc/init.d
|
||||||
|
$(INSTALL_BIN) ./root/etc/init.d/secubox-feed $(1)/etc/init.d/secubox-feed
|
||||||
|
|
||||||
|
# Feed management CLI tools
|
||||||
|
$(INSTALL_DIR) $(1)/usr/sbin
|
||||||
|
$(INSTALL_BIN) ./root/usr/sbin/secubox-feed $(1)/usr/sbin/secubox-feed
|
||||||
|
$(INSTALL_BIN) ./root/usr/sbin/secubox-feed-health $(1)/usr/sbin/secubox-feed-health
|
||||||
|
|
||||||
|
# RPCD interface for ubus/LuCI integration
|
||||||
|
$(INSTALL_DIR) $(1)/usr/libexec/rpcd
|
||||||
|
$(INSTALL_BIN) ./root/usr/libexec/rpcd/luci.secubox-feed $(1)/usr/libexec/rpcd/luci.secubox-feed
|
||||||
|
|
||||||
|
# HAProxy configuration snippet (optional)
|
||||||
|
$(INSTALL_DIR) $(1)/etc/haproxy/conf.d
|
||||||
|
$(INSTALL_DATA) ./root/etc/haproxy/conf.d/secubox-feed.cfg $(1)/etc/haproxy/conf.d/secubox-feed.cfg
|
||||||
|
endef
|
||||||
|
|
||||||
|
define Package/secubox-app-bonus/postinst
|
||||||
|
#!/bin/sh
|
||||||
|
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||||
|
# Enable and start the feed sync service
|
||||||
|
/etc/init.d/secubox-feed enable 2>/dev/null || true
|
||||||
|
/etc/init.d/secubox-feed start 2>/dev/null || true
|
||||||
|
echo "SecuBox local package feed installed at /www/secubox-feed/"
|
||||||
|
[ -f /var/opkg-lists/secubox ] && echo "Package list: $$(grep -c '^Package:' /var/opkg-lists/secubox) packages available"
|
||||||
|
}
|
||||||
|
exit 0
|
||||||
|
endef
|
||||||
|
|
||||||
|
$(eval $(call BuildPackage,secubox-app-bonus))
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user