From 5a40e8a61e7a57235be389cba8a193127ff7dfa9 Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Fri, 30 Jan 2026 15:03:02 +0100 Subject: [PATCH] feat: Major updates - CDN cache with Squid, network modes UI rework, bugfixes CDN Cache: - Migrate from nginx to Squid proxy for better caching - Add aggressive caching rules for Windows Update, Linux repos, Steam, Apple - Proper firewall integration via UCI (transparent proxy) - Real-time stats from Squid access logs Network Modes: - Complete UI rework with MirrorBox dark theme - 9 network modes with emojis and descriptions - Dynamic CSS animations and modern styling Fixes: - Fix jshn boolean handling in secubox-recovery (1/0 vs true/false) - Fix nDPId RPCD to use netifyd as fallback DPI provider - Update media-flow and security-threats dashboards Co-Authored-By: Claude Opus 4.5 --- .claude/settings.local.json | 5 +- .../root/etc/config/cdn-cache | 38 +- .../root/etc/init.d/cdn-cache | 381 ++++--- .../etc/uci-defaults/99-cdn-cache-firewall | 17 + .../root/usr/libexec/rpcd/luci.cdn-cache | 203 +++- .../luci-static/resources/media-flow/api.js | 127 ++- .../resources/view/media-flow/dashboard.js | 828 ++++++++++----- .../root/usr/libexec/rpcd/luci.ndpid | 38 + .../resources/view/network-modes/overview.js | 991 +++++++++++------- .../resources/view/secubox-p2p/overview.js | 136 ++- .../resources/secubox-security-threats/api.js | 208 +++- .../secubox-security-threats/dashboard.js | 968 ++++++++++++----- .../view/service-registry/landing.js | 401 ++++--- .../root/usr/sbin/secubox-recovery | 4 +- .../secubox-p2p/root/usr/sbin/secubox-p2p | 83 +- 15 files changed, 3225 insertions(+), 1203 deletions(-) create mode 100644 package/secubox/luci-app-cdn-cache/root/etc/uci-defaults/99-cdn-cache-firewall diff --git a/.claude/settings.local.json b/.claude/settings.local.json index dcf8bfe6..771b4d48 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -184,7 +184,10 @@ "Bash(git -C /home/reepost/CyberMindStudio/secubox-openwrt push origin release/v0.15.0)", "Bash(git -C /home/reepost/CyberMindStudio/secubox-openwrt add package/secubox/secubox-core/Makefile package/secubox/luci-app-secubox-admin/Makefile package/secubox/secubox-app-bonus/root/www/secubox-feed/Packages* package/secubox/secubox-app-bonus/root/www/secubox-feed/secubox-core_*.ipk package/secubox/secubox-app-bonus/root/www/secubox-feed/luci-app-secubox-admin_*.ipk)", "Bash(__NEW_LINE_9c0ea50a93f8f5be__ git -C /home/reepost/CyberMindStudio/secubox-openwrt commit -m \"$\\(cat <<''EOF''\nfix\\(deps\\): Remove ALL dependencies from secubox-core and luci-app-secubox-admin\n\nEven rpcd, bash, jsonfilter, jq depend on libc themselves. Since these\npackages are always present on a working OpenWrt/SecuBox system, we should\nnot declare any dependencies at all.\n\n- secubox-core 0.10.0-r9: DEPENDS:= \\(empty\\)\n- luci-app-secubox-admin 1.0.0-r19: LUCI_DEPENDS:= \\(empty\\)\n\nThis prevents opkg from trying to resolve any feed packages and their\ncascading libc dependencies.\n\nCo-Authored-By: Claude Opus 4.5 \nEOF\n\\)\")", - "Bash(gzip:*)" + "Bash(gzip:*)", + "Bash(python3:*)", + "WebFetch(domain:192.168.255.1)", + "Bash(ssh-add:*)" ] } } diff --git a/package/secubox/luci-app-cdn-cache/root/etc/config/cdn-cache b/package/secubox/luci-app-cdn-cache/root/etc/config/cdn-cache index d8655b76..7e956f49 100644 --- a/package/secubox/luci-app-cdn-cache/root/etc/config/cdn-cache +++ b/package/secubox/luci-app-cdn-cache/root/etc/config/cdn-cache @@ -1,12 +1,12 @@ config cdn_cache 'main' option enabled '0' - option cache_dir '/var/cache/cdn' - option cache_size '1024' - option max_object_size '512' - option cache_valid '1440' + option cache_dir '/var/cache/cdn-squid' + option cache_size '2048' + option max_object_size '1024' + option cache_valid '10080' option listen_port '3128' - option transparent '0' - option log_level 'warn' + option transparent '1' + option log_level '1' config cache_policy 'windows_update' option enabled '1' @@ -36,14 +36,32 @@ config cache_policy 'android_apps' option priority '5' config cache_policy 'steam_games' - option enabled '0' - option name 'Steam Games' - option domains 'steamcontent.com steampowered.com' - option extensions 'zip pak vpk' + option enabled '1' + option name 'Steam/Gaming' + option domains 'steampowered.com steamcontent.com steamcdn-a.akamaihd.net epicgames-download1.akamaized.net origin-a.akamaihd.net' + option extensions 'zip pak vpk depot manifest' option cache_time '43200' option max_size '10240' option priority '1' +config cache_policy 'apple_updates' + option enabled '1' + option name 'Apple Updates' + option domains 'swcdn.apple.com swscan.apple.com itunes.apple.com' + option extensions 'ipa pkg dmg' + option cache_time '10080' + option max_size '4096' + option priority '8' + +config cache_policy 'openwrt_packages' + option enabled '1' + option name 'OpenWrt Packages' + option domains 'downloads.openwrt.org' + option extensions 'ipk' + option cache_time '10080' + option max_size '512' + option priority '10' + config cache_policy 'static_content' option enabled '1' option name 'Static Web Content' diff --git a/package/secubox/luci-app-cdn-cache/root/etc/init.d/cdn-cache b/package/secubox/luci-app-cdn-cache/root/etc/init.d/cdn-cache index 48b1f464..3b978ba9 100755 --- a/package/secubox/luci-app-cdn-cache/root/etc/init.d/cdn-cache +++ b/package/secubox/luci-app-cdn-cache/root/etc/init.d/cdn-cache @@ -1,152 +1,232 @@ #!/bin/sh /etc/rc.common # SPDX-License-Identifier: Apache-2.0 -# CDN Cache Proxy Init Script +# CDN Cache Proxy Init Script - Squid Backend # Copyright (C) 2025 CyberMind.fr START=90 STOP=10 USE_PROCD=1 -CONF_FILE="/var/etc/cdn-cache-nginx.conf" -PID_FILE="/var/run/cdn-cache.pid" +SQUID_CONF="/var/etc/cdn-cache-squid.conf" +CACHE_DIR="/var/cache/cdn-squid" +LOG_DIR="/var/log/cdn-cache" validate_section() { uci_load_validate cdn-cache main "$1" "$2" \ 'enabled:bool:0' \ - 'cache_dir:string:/var/cache/cdn' \ - 'cache_size:uinteger:1024' \ - 'max_object_size:uinteger:512' \ - 'cache_valid:uinteger:1440' \ + 'cache_dir:string:/var/cache/cdn-squid' \ + 'cache_size:uinteger:2048' \ + 'max_object_size:uinteger:1024' \ + 'cache_valid:uinteger:10080' \ 'listen_port:port:3128' \ - 'transparent:bool:0' \ - 'log_level:string:warn' + 'transparent:bool:1' \ + 'log_level:string:1' } -generate_nginx_config() { +generate_squid_config() { local cache_dir="$1" local cache_size="$2" local max_object="$3" local cache_valid="$4" local listen_port="$5" - - mkdir -p "$(dirname $CONF_FILE)" + local transparent="$6" + + mkdir -p "$(dirname $SQUID_CONF)" mkdir -p "$cache_dir" - mkdir -p /var/log/cdn-cache - - cat > "$CONF_FILE" << EOF -# CDN Cache Nginx Configuration -# Auto-generated - Do not edit + mkdir -p "$LOG_DIR" + chown squid:squid "$cache_dir" "$LOG_DIR" 2>/dev/null -worker_processes auto; -pid $PID_FILE; -error_log /var/log/cdn-cache/error.log $log_level; + # Build listen directives + # Need both a regular port (for client configuration) and optionally intercept port + local intercept_port=$((listen_port + 1)) -events { - worker_connections 1024; - use epoll; - multi_accept on; -} + cat > "$SQUID_CONF" << EOF +# CDN Cache Squid Configuration +# Auto-generated by SecuBox - Do not edit manually -http { - include /etc/nginx/mime.types; - default_type application/octet-stream; - - log_format cache '\$time_local|\$remote_addr|\$request|\$status|\$body_bytes_sent|\$upstream_cache_status|\$host'; - access_log /var/log/cdn-cache/access.log cache; - - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - - # Cache zone configuration - proxy_cache_path $cache_dir levels=1:2 keys_zone=cdn_cache:64m - max_size=${cache_size}m inactive=7d use_temp_path=off; - proxy_temp_path $cache_dir/temp; - - # Cache settings - proxy_cache_valid 200 ${cache_valid}m; - proxy_cache_valid 301 302 10m; - proxy_cache_valid 404 1m; - proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; - proxy_cache_lock on; - proxy_cache_lock_timeout 5s; - - # Upstream settings - proxy_connect_timeout 10s; - proxy_read_timeout 60s; - proxy_send_timeout 60s; - proxy_buffering on; - proxy_buffer_size 16k; - proxy_buffers 8 32k; - proxy_max_temp_file_size ${max_object}m; - - # Resolver for dynamic upstream - resolver 8.8.8.8 8.8.4.4 valid=300s ipv6=off; - resolver_timeout 5s; - - server { - listen $listen_port; - server_name _; - - # Health check endpoint - location /cdn-cache-health { - return 200 'OK'; - add_header Content-Type text/plain; - } - - # Stats endpoint - location /cdn-cache-stats { - stub_status; - allow 127.0.0.1; - deny all; - } - - # Proxy all requests - location / { - proxy_pass http://\$host\$request_uri; - proxy_cache cdn_cache; - proxy_cache_key \$scheme\$host\$request_uri; - - # Add cache status header - add_header X-Cache-Status \$upstream_cache_status; - add_header X-Cache-Date \$upstream_http_date; - - # Forward headers - proxy_set_header Host \$host; - proxy_set_header X-Real-IP \$remote_addr; - proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; - - # Cache specific content types aggressively - proxy_cache_valid 200 301 302 ${cache_valid}m; - - # Bypass cache for certain requests - proxy_cache_bypass \$http_pragma \$http_authorization; - proxy_no_cache \$http_pragma \$http_authorization; - } - - # Special handling for update services - location ~* \.(exe|msu|cab|msi|deb|rpm|apk|pkg\.tar\.zst)$ { - proxy_pass http://\$host\$request_uri; - proxy_cache cdn_cache; - proxy_cache_key \$scheme\$host\$request_uri; - proxy_cache_valid 200 10080m; - add_header X-Cache-Status \$upstream_cache_status; - proxy_set_header Host \$host; - } - - # Static content with longer cache - location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { - proxy_pass http://\$host\$request_uri; - proxy_cache cdn_cache; - proxy_cache_key \$scheme\$host\$request_uri; - proxy_cache_valid 200 1440m; - add_header X-Cache-Status \$upstream_cache_status; - proxy_set_header Host \$host; - } - } -} +# === NETWORK === +# Regular forward proxy port +http_port $listen_port EOF + + if [ "$transparent" = "1" ]; then + cat >> "$SQUID_CONF" << EOF +# Transparent proxy intercept port +http_port $intercept_port intercept +EOF + fi + + cat >> "$SQUID_CONF" << EOF +visible_hostname cdn-cache.secubox.local + +# === ACCESS CONTROL === +acl localnet src 10.0.0.0/8 +acl localnet src 172.16.0.0/12 +acl localnet src 192.168.0.0/16 +acl localnet src fc00::/7 +acl localnet src fe80::/10 + +acl SSL_ports port 443 +acl Safe_ports port 80 21 443 70 210 280 488 591 777 1025-65535 +acl CONNECT method CONNECT + +http_access deny !Safe_ports +http_access deny CONNECT !SSL_ports +http_access allow localhost manager +http_access deny manager +http_access allow localnet +http_access allow localhost +http_access deny all + +# === CACHE STORAGE === +cache_dir ufs $cache_dir $cache_size 16 256 +cache_mem 128 MB +maximum_object_size ${max_object} MB +minimum_object_size 0 KB +cache_swap_low 90 +cache_swap_high 95 + +# === CDN CONTENT REFRESH PATTERNS === +# Windows Updates - cache for 7 days +refresh_pattern -i windowsupdate.com/.*\.(exe|msu|cab|msi)$ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload ignore-no-store ignore-private +refresh_pattern -i download.microsoft.com/.*\.(exe|msu|cab|msi)$ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload ignore-no-store ignore-private +refresh_pattern -i officecdn.microsoft.com/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload + +# Linux Package Repositories +refresh_pattern -i (deb|rpm|pkg\.tar\.(gz|xz|zst))$ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i archive\.ubuntu\.com/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i security\.ubuntu\.com/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i dl\.fedoraproject\.org/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i mirror\.centos\.org/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i downloads\.openwrt\.org/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload + +# Android/Play Store +refresh_pattern -i play\.googleapis\.com/.*\.apk$ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i android\.clients\.google\.com/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload + +# Steam/Gaming +refresh_pattern -i \.steampowered\.com/depot/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload ignore-no-store ignore-private +refresh_pattern -i steamcdn-a\.akamaihd\.net/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload ignore-no-store ignore-private +refresh_pattern -i cdn\.cloudflare\.steamstatic\.com/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i epicgames-download1\.akamaized\.net/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i origin-a\.akamaihd\.net/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i uplaypc-s-ubisoft\.cdn\.ubi\.com/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload + +# Apple Updates +refresh_pattern -i swcdn\.apple\.com/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i swscan\.apple\.com/ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload +refresh_pattern -i \.itunes\.apple\.com/.*\.(ipa|pkg)$ ${cache_valid} 100% 43200 override-expire override-lastmod reload-into-ims ignore-reload + +# Static Content CDNs +refresh_pattern -i \.cloudfront\.net/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ 1440 100% 10080 override-expire override-lastmod +refresh_pattern -i cdn\.jsdelivr\.net/ 1440 100% 10080 override-expire override-lastmod +refresh_pattern -i cdnjs\.cloudflare\.com/ 1440 100% 10080 override-expire override-lastmod +refresh_pattern -i ajax\.googleapis\.com/ 1440 100% 10080 override-expire override-lastmod +refresh_pattern -i fonts\.googleapis\.com/ 1440 100% 10080 override-expire override-lastmod +refresh_pattern -i fonts\.gstatic\.com/ 1440 100% 10080 override-expire override-lastmod + +# General static assets +refresh_pattern -i \.(js|css)(\?.*)?$ 1440 50% 10080 +refresh_pattern -i \.(png|jpg|jpeg|gif|ico|svg|webp)(\?.*)?$ 1440 50% 10080 +refresh_pattern -i \.(woff|woff2|ttf|eot|otf)(\?.*)?$ 4320 50% 43200 +refresh_pattern -i \.(mp3|mp4|webm|ogg|flac)(\?.*)?$ 1440 50% 10080 +refresh_pattern -i \.(zip|tar|gz|bz2|xz|7z|rar)(\?.*)?$ 1440 50% 10080 + +# Default patterns +refresh_pattern ^ftp: 1440 20% 10080 +refresh_pattern ^gopher: 1440 0% 1440 +refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 +refresh_pattern . 0 20% 4320 + +# === EXCLUSIONS (Never Cache) === +# Banking/Financial - security +acl nocache_banking dstdomain .paypal.com .stripe.com .braintreegateway.com +acl nocache_banking dstdomain .visa.com .mastercard.com .americanexpress.com + +# Video Streaming - too large, DRM protected +acl nocache_streaming dstdomain .netflix.com .nflxvideo.net +acl nocache_streaming dstdomain .youtube.com .googlevideo.com .ytimg.com +acl nocache_streaming dstdomain .twitch.tv .twitchcdn.net +acl nocache_streaming dstdomain .hulu.com .hulustream.com +acl nocache_streaming dstdomain .disneyplus.com .dssott.com +acl nocache_streaming dstdomain .hbomax.com .max.com +acl nocache_streaming dstdomain .primevideo.com .aiv-cdn.net + +# Real-time APIs +acl nocache_realtime dstdomain .api.github.com .api.gitlab.com +acl nocache_realtime dstdomain .amazonaws.com .azure.com + +cache deny nocache_banking +cache deny nocache_streaming +cache deny nocache_realtime + +# === LOGGING === +access_log daemon:$LOG_DIR/access.log squid +cache_log $LOG_DIR/cache.log +cache_store_log none +debug_options ALL,$log_level + +# === PERFORMANCE === +dns_nameservers 8.8.8.8 8.8.4.4 +connect_timeout 30 seconds +read_timeout 90 seconds +request_timeout 60 seconds +client_lifetime 1 day +half_closed_clients off +pconn_timeout 60 seconds +quick_abort_min 0 KB +quick_abort_max 0 KB +quick_abort_pct 95 +negative_ttl 5 minutes + +# === CACHE MANAGER === +cache_mgr secubox@local +cachemgr_passwd none all + +# === MISC === +cache_effective_user squid +coredump_dir /var/spool/squid +pid_filename none +shutdown_lifetime 3 seconds +EOF +} + +setup_transparent() { + local listen_port="$1" + local transparent="$2" + + # Intercept port is listen_port + 1 + local intercept_port=$((listen_port + 1)) + + # Setup firewall redirect rule via UCI + if [ "$transparent" = "1" ]; then + # Create or update firewall redirect rule + uci -q delete firewall.cdn_cache_redirect + uci set firewall.cdn_cache_redirect=redirect + uci set firewall.cdn_cache_redirect.name='CDN-Cache-Transparent' + uci set firewall.cdn_cache_redirect.src='lan' + uci set firewall.cdn_cache_redirect.proto='tcp' + uci set firewall.cdn_cache_redirect.src_dport='80' + uci set firewall.cdn_cache_redirect.dest_port="$intercept_port" + uci set firewall.cdn_cache_redirect.target='DNAT' + uci set firewall.cdn_cache_redirect.enabled='1' + uci commit firewall + /etc/init.d/firewall reload 2>/dev/null + logger -t cdn-cache "Transparent proxy enabled on port $intercept_port via firewall" + else + # Disable transparent redirect + uci -q set firewall.cdn_cache_redirect.enabled='0' + uci commit firewall + /etc/init.d/firewall reload 2>/dev/null + logger -t cdn-cache "Transparent proxy disabled" + fi +} + +remove_transparent() { + # Disable the firewall redirect rule + uci -q set firewall.cdn_cache_redirect.enabled='0' + uci commit firewall + /etc/init.d/firewall reload 2>/dev/null } start_service() { @@ -158,33 +238,62 @@ start_cdn_cache() { echo "Validation failed" >&2 return 1 } - + [ "$enabled" = "1" ] || { echo "CDN Cache is disabled" return 0 } - - generate_nginx_config "$cache_dir" "$cache_size" "$max_object_size" \ - "$cache_valid" "$listen_port" - + + # Check if Squid is installed + if ! command -v squid >/dev/null 2>&1; then + logger -t cdn-cache "ERROR: Squid is not installed" + echo "Squid is not installed. Install with: opkg install squid" + return 1 + fi + + # Setup required directories + mkdir -p "$cache_dir" "$LOG_DIR" /var/spool/squid + chown -R squid:squid "$cache_dir" "$LOG_DIR" /var/spool/squid 2>/dev/null + + generate_squid_config "$cache_dir" "$cache_size" "$max_object_size" \ + "$cache_valid" "$listen_port" "$transparent" + + # Initialize cache if needed (blocking call, run once) + if [ ! -d "$cache_dir/00" ]; then + logger -t cdn-cache "Initializing Squid cache directory..." + /usr/sbin/squid -f "$SQUID_CONF" -z -N 2>/dev/null + fi + + # Setup transparent proxy if enabled + setup_transparent "$listen_port" "$transparent" + + # Remove any stale PID file + rm -f /var/run/cdn-cache.pid + procd_open_instance cdn-cache - procd_set_param command /usr/sbin/nginx -c "$CONF_FILE" -g "daemon off;" - procd_set_param respawn + procd_set_param command /usr/sbin/squid -f "$SQUID_CONF" -sYC -N + procd_set_param respawn ${respawn_threshold:-3600} ${respawn_timeout:-5} ${respawn_retry:-5} procd_set_param stdout 1 procd_set_param stderr 1 - procd_set_param pidfile "$PID_FILE" procd_close_instance - - logger -t cdn-cache "CDN Cache proxy started on port $listen_port" + + logger -t cdn-cache "CDN Cache (Squid) started on port $listen_port" } stop_service() { + remove_transparent logger -t cdn-cache "CDN Cache proxy stopped" } reload_service() { - stop - start + # Send reconfigure signal to Squid + if [ -f /var/run/cdn-cache.pid ]; then + kill -HUP $(cat /var/run/cdn-cache.pid) 2>/dev/null + logger -t cdn-cache "CDN Cache configuration reloaded" + else + stop + start + fi } service_triggers() { diff --git a/package/secubox/luci-app-cdn-cache/root/etc/uci-defaults/99-cdn-cache-firewall b/package/secubox/luci-app-cdn-cache/root/etc/uci-defaults/99-cdn-cache-firewall new file mode 100644 index 00000000..6d32d2ea --- /dev/null +++ b/package/secubox/luci-app-cdn-cache/root/etc/uci-defaults/99-cdn-cache-firewall @@ -0,0 +1,17 @@ +#!/bin/sh +# CDN Cache Firewall Setup - Adds redirect rule for transparent proxy + +# Check if redirect rule already exists +if ! uci -q get firewall.cdn_cache_redirect >/dev/null; then + uci set firewall.cdn_cache_redirect=redirect + uci set firewall.cdn_cache_redirect.name='CDN-Cache-Transparent' + uci set firewall.cdn_cache_redirect.src='lan' + uci set firewall.cdn_cache_redirect.proto='tcp' + uci set firewall.cdn_cache_redirect.src_dport='80' + uci set firewall.cdn_cache_redirect.dest_port='3129' + uci set firewall.cdn_cache_redirect.target='DNAT' + uci set firewall.cdn_cache_redirect.enabled='0' + uci commit firewall +fi + +exit 0 diff --git a/package/secubox/luci-app-cdn-cache/root/usr/libexec/rpcd/luci.cdn-cache b/package/secubox/luci-app-cdn-cache/root/usr/libexec/rpcd/luci.cdn-cache index b2e1932e..4fbaa9a5 100755 --- a/package/secubox/luci-app-cdn-cache/root/usr/libexec/rpcd/luci.cdn-cache +++ b/package/secubox/luci-app-cdn-cache/root/usr/libexec/rpcd/luci.cdn-cache @@ -17,9 +17,11 @@ get_pkg_version() { PKG_VERSION="$(get_pkg_version)" -CACHE_DIR=$(uci -q get cdn-cache.main.cache_dir || echo "/var/cache/cdn") +CACHE_DIR=$(uci -q get cdn-cache.main.cache_dir || echo "/var/cache/cdn-squid") STATS_FILE="/var/run/cdn-cache-stats.json" -LOG_FILE="/var/log/cdn-cache.log" +LOG_DIR="/var/log/cdn-cache" +LOG_FILE="$LOG_DIR/cache.log" +SQUID_CONF="/var/etc/cdn-cache-squid.conf" # Initialize stats file if not exists init_stats() { @@ -30,6 +32,15 @@ EOF fi } +# Get Squid cache manager info +get_squid_info() { + local mgr_url="http://localhost:$(uci -q get cdn-cache.main.listen_port || echo 3128)/squid-internal-mgr" + # Try to get info via squidclient if available + if command -v squidclient >/dev/null 2>&1; then + squidclient -h localhost mgr:info 2>/dev/null + fi +} + # Get service status get_status() { local enabled=$(uci -q get cdn-cache.main.enabled || echo "0") @@ -38,30 +49,54 @@ get_status() { local uptime=0 local cache_size=0 local cache_files=0 - - # Check if nginx proxy is running - if pgrep -f "nginx.*cdn-cache" > /dev/null 2>&1; then - running=1 - pid=$(pgrep -f "nginx.*cdn-cache" | head -1) - if [ -f "/var/run/cdn-cache.pid" ]; then + local squid_installed=0 + local backend="squid" + + # Check if Squid is installed + if command -v squid >/dev/null 2>&1; then + squid_installed=1 + fi + + # Check if Squid is running (cdn-cache instance) + if [ -f "/var/run/cdn-cache.pid" ]; then + pid=$(cat /var/run/cdn-cache.pid 2>/dev/null) + if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then + running=1 local start_time=$(stat -c %Y "/var/run/cdn-cache.pid" 2>/dev/null || echo "0") local now=$(date +%s) uptime=$((now - start_time)) fi fi - + + # Fallback: check by process name + if [ "$running" = "0" ]; then + pid=$(pgrep -f "squid.*cdn-cache" | head -1) + if [ -n "$pid" ]; then + running=1 + fi + fi + # Get cache directory stats if [ -d "$CACHE_DIR" ]; then cache_size=$(du -sk "$CACHE_DIR" 2>/dev/null | cut -f1 || echo "0") cache_files=$(find "$CACHE_DIR" -type f 2>/dev/null | wc -l || echo "0") fi - + local listen_port=$(uci -q get cdn-cache.main.listen_port || echo "3128") - local transparent=$(uci -q get cdn-cache.main.transparent || echo "0") - local max_size=$(uci -q get cdn-cache.main.cache_size || echo "1024") - + local transparent=$(uci -q get cdn-cache.main.transparent || echo "1") + local max_size=$(uci -q get cdn-cache.main.cache_size || echo "2048") + + # Get Squid version if installed + local squid_version="" + if [ "$squid_installed" = "1" ]; then + squid_version=$(squid -v 2>/dev/null | head -1 | sed 's/.*Version //' | cut -d' ' -f1) + fi + json_init json_add_string "version" "$PKG_VERSION" + json_add_string "backend" "$backend" + json_add_string "squid_version" "$squid_version" + json_add_boolean "squid_installed" "$squid_installed" json_add_boolean "enabled" "$enabled" json_add_boolean "running" "$running" json_add_string "pid" "$pid" @@ -75,31 +110,48 @@ get_status() { json_dump } -# Get cache statistics +# Get cache statistics from Squid access log get_stats() { init_stats - - local stats=$(cat "$STATS_FILE" 2>/dev/null || echo '{}') - - # Calculate hit ratio - json_load "$stats" - json_get_var hits hits 0 - json_get_var misses misses 0 - + + local hits=0 + local misses=0 + local requests=0 + local bytes_served=0 + local bytes_saved=0 + + # Parse Squid access log for stats + local access_log="$LOG_DIR/access.log" + if [ -f "$access_log" ]; then + # Count cache HITs and MISSes from Squid log format + # Format: timestamp elapsed client result/code size method url ... + hits=$(grep -c "TCP_HIT\|TCP_MEM_HIT\|TCP_REFRESH_HIT\|TCP_IMS_HIT" "$access_log" 2>/dev/null || echo "0") + misses=$(grep -c "TCP_MISS\|TCP_REFRESH_MISS\|TCP_CLIENT_REFRESH_MISS" "$access_log" 2>/dev/null || echo "0") + requests=$(wc -l < "$access_log" 2>/dev/null || echo "0") + + # Sum bytes served (field 5 in squid log) + bytes_served=$(awk '{sum += $5} END {print sum}' "$access_log" 2>/dev/null || echo "0") + [ -z "$bytes_served" ] && bytes_served=0 + + # Estimate bytes saved (cached hits * average object size) + # This is an approximation - cached bytes don't get re-downloaded + local avg_size=0 + if [ "$requests" -gt 0 ]; then + avg_size=$((bytes_served / requests)) + fi + bytes_saved=$((hits * avg_size)) + fi + local total=$((hits + misses)) local hit_ratio=0 if [ "$total" -gt 0 ]; then hit_ratio=$((hits * 100 / total)) fi - - json_get_var bytes_saved bytes_saved 0 - json_get_var bytes_served bytes_served 0 - json_get_var requests requests 0 - + # Convert to human readable local saved_mb=$((bytes_saved / 1048576)) local served_mb=$((bytes_served / 1048576)) - + json_init json_add_int "hits" "$hits" json_add_int "misses" "$misses" @@ -138,28 +190,50 @@ get_cache_list() { json_dump } -# Get top domains by cache usage +# Get top domains by cache usage (parsed from Squid access log) get_top_domains() { json_init json_add_array "domains" - - if [ -d "$CACHE_DIR" ]; then - # Aggregate by subdirectory (domain) - for domain_dir in "$CACHE_DIR"/*/; do - if [ -d "$domain_dir" ]; then - local domain=$(basename "$domain_dir") - local size=$(du -sk "$domain_dir" 2>/dev/null | cut -f1 || echo "0") - local files=$(find "$domain_dir" -type f 2>/dev/null | wc -l || echo "0") - - json_add_object "" - json_add_string "domain" "$domain" - json_add_int "size_kb" "$size" - json_add_int "files" "$files" - json_close_object - fi + + local access_log="$LOG_DIR/access.log" + if [ -f "$access_log" ]; then + # Parse domains from URLs in access log + # Extract domain from URL field (field 7) and count hits + bytes + awk ' + { + url = $7 + bytes = $5 + status = $4 + + # Extract domain from URL + gsub(/^https?:\/\//, "", url) + gsub(/\/.*$/, "", url) + gsub(/:[0-9]+$/, "", url) + + if (url != "" && url != "-") { + domains[url]++ + domain_bytes[url] += bytes + if (status ~ /HIT/) { + domain_hits[url]++ + } + } + } + END { + for (d in domains) { + print domains[d], domain_bytes[d], domain_hits[d], d + } + } + ' "$access_log" 2>/dev/null | sort -rn | head -20 | while read count bytes hits domain; do + local size_kb=$((bytes / 1024)) + json_add_object "" + json_add_string "domain" "$domain" + json_add_int "requests" "$count" + json_add_int "size_kb" "$size_kb" + json_add_int "hits" "${hits:-0}" + json_close_object done fi - + json_close_array json_dump } @@ -331,16 +405,24 @@ _add_exclusion() { # Get recent logs get_logs() { local count="${1:-50}" - + json_init json_add_array "logs" - - if [ -f "$LOG_FILE" ]; then - tail -n "$count" "$LOG_FILE" 2>/dev/null | while read line; do + + # Try access log first (more useful for CDN cache) + local access_log="$LOG_DIR/access.log" + local cache_log="$LOG_DIR/cache.log" + + if [ -f "$access_log" ]; then + tail -n "$count" "$access_log" 2>/dev/null | while read line; do + json_add_string "" "$line" + done + elif [ -f "$cache_log" ]; then + tail -n "$count" "$cache_log" 2>/dev/null | while read line; do json_add_string "" "$line" done fi - + json_close_array json_dump } @@ -365,18 +447,33 @@ set_enabled() { # Purge entire cache purge_cache() { + # Stop Squid, clear cache, reinitialize + /etc/init.d/cdn-cache stop 2>/dev/null + if [ -d "$CACHE_DIR" ]; then rm -rf "$CACHE_DIR"/* mkdir -p "$CACHE_DIR" + chown squid:squid "$CACHE_DIR" 2>/dev/null fi - + + # Clear access log + [ -f "$LOG_DIR/access.log" ] && : > "$LOG_DIR/access.log" + # Reset stats cat > "$STATS_FILE" << 'EOF' {"hits":0,"misses":0,"bytes_saved":0,"bytes_served":0,"requests":0,"start_time":0} EOF - + + # Reinitialize Squid cache and restart + if [ -f "$SQUID_CONF" ]; then + squid -f "$SQUID_CONF" -z 2>/dev/null + sleep 2 + fi + + /etc/init.d/cdn-cache start 2>/dev/null + logger -t cdn-cache "Cache purged by user" - + json_init json_add_boolean "success" 1 json_add_string "message" "Cache purged successfully" diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js index 4d3e84b4..16a087a0 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/media-flow/api.js @@ -103,7 +103,117 @@ var callStopNetifyd = rpc.declare({ expect: { success: false } }); +// nDPId Integration +var callNdpidStatus = rpc.declare({ + object: 'luci.ndpid', + method: 'get_service_status', + expect: { } +}); + +var callNdpidFlows = rpc.declare({ + object: 'luci.ndpid', + method: 'get_detailed_flows', + expect: { flows: [] } +}); + +var callNdpidTopApps = rpc.declare({ + object: 'luci.ndpid', + method: 'get_top_applications', + expect: { applications: [] } +}); + +var callNdpidCategories = rpc.declare({ + object: 'luci.ndpid', + method: 'get_categories', + expect: { categories: [] } +}); + +// Streaming service definitions +var streamingServices = { + 'Netflix': { icon: '🎬', color: '#e50914', category: 'video' }, + 'YouTube': { icon: '▶️', color: '#ff0000', category: 'video' }, + 'Disney': { icon: '🏰', color: '#113ccf', category: 'video' }, + 'Amazon Prime': { icon: '📦', color: '#00a8e1', category: 'video' }, + 'HBO': { icon: '🎭', color: '#5822b4', category: 'video' }, + 'Hulu': { icon: '📺', color: '#1ce783', category: 'video' }, + 'AppleTV': { icon: '🍎', color: '#555555', category: 'video' }, + 'Twitch': { icon: '🎮', color: '#9146ff', category: 'gaming' }, + 'Spotify': { icon: '🎵', color: '#1db954', category: 'audio' }, + 'Apple Music': { icon: '🎧', color: '#fa243c', category: 'audio' }, + 'Tidal': { icon: '🌊', color: '#000000', category: 'audio' }, + 'Deezer': { icon: '🎶', color: '#feaa2d', category: 'audio' }, + 'SoundCloud': { icon: '☁️', color: '#ff5500', category: 'audio' }, + 'TikTok': { icon: '📱', color: '#000000', category: 'social' }, + 'Instagram': { icon: '📷', color: '#e4405f', category: 'social' }, + 'Facebook': { icon: '👤', color: '#1877f2', category: 'social' }, + 'Discord': { icon: '💬', color: '#5865f2', category: 'gaming' }, + 'Steam': { icon: '🎮', color: '#1b2838', category: 'gaming' }, + 'Xbox': { icon: '🎯', color: '#107c10', category: 'gaming' }, + 'PlayStation': { icon: '🎲', color: '#003791', category: 'gaming' }, + 'Zoom': { icon: '📹', color: '#2d8cff', category: 'conferencing' }, + 'Teams': { icon: '👥', color: '#6264a7', category: 'conferencing' }, + 'WebRTC': { icon: '🔗', color: '#333333', category: 'conferencing' } +}; + +// Quality detection based on bandwidth +function detectQuality(bytesPerSec) { + if (bytesPerSec > 2500000) return { label: '4K', color: '#9333ea', icon: '🎬' }; + if (bytesPerSec > 625000) return { label: 'FHD', color: '#2563eb', icon: '📺' }; + if (bytesPerSec > 312500) return { label: 'HD', color: '#059669', icon: '📹' }; + return { label: 'SD', color: '#d97706', icon: '📱' }; +} + +// Get streaming service info +function getServiceInfo(appName) { + if (!appName) return { icon: '📡', color: '#6b7280', category: 'unknown' }; + for (var name in streamingServices) { + if (appName.toLowerCase().indexOf(name.toLowerCase()) !== -1) { + return { name: name, ...streamingServices[name] }; + } + } + return { icon: '📡', color: '#6b7280', category: 'other', name: appName }; +} + +// Device type detection for media +var mediaDeviceTypes = { + 'smart_tv': { icon: '📺', label: 'Smart TV', apps: ['Netflix', 'YouTube', 'Disney', 'AppleTV', 'Prime'] }, + 'gaming': { icon: '🎮', label: 'Gaming', apps: ['Steam', 'PlayStation', 'Xbox', 'Twitch', 'Discord'] }, + 'mobile': { icon: '📱', label: 'Mobile', apps: ['TikTok', 'Instagram', 'Spotify'] }, + 'speaker': { icon: '🔊', label: 'Smart Speaker', apps: ['Spotify', 'Apple Music', 'Amazon'] }, + 'computer': { icon: '💻', label: 'Computer', apps: ['Zoom', 'Teams', 'Chrome', 'Firefox'] } +}; + +function classifyMediaDevice(apps) { + if (!apps || !Array.isArray(apps)) return { type: 'unknown', icon: '📟', label: 'Unknown' }; + for (var type in mediaDeviceTypes) { + var typeApps = mediaDeviceTypes[type].apps; + for (var i = 0; i < apps.length; i++) { + for (var j = 0; j < typeApps.length; j++) { + if (apps[i].toLowerCase().indexOf(typeApps[j].toLowerCase()) !== -1) { + return { type: type, ...mediaDeviceTypes[type] }; + } + } + } + } + return { type: 'unknown', icon: '📟', label: 'Unknown' }; +} + +// QoS priority suggestions +var qosPriorities = { + 'video': { priority: 'high', dscp: 'AF41', desc: 'Video streaming - prioritize for smooth playback' }, + 'audio': { priority: 'medium-high', dscp: 'AF31', desc: 'Audio streaming - moderate priority' }, + 'gaming': { priority: 'highest', dscp: 'EF', desc: 'Gaming - lowest latency required' }, + 'conferencing': { priority: 'highest', dscp: 'EF', desc: 'Video calls - real-time priority' }, + 'social': { priority: 'low', dscp: 'BE', desc: 'Social media - best effort' }, + 'other': { priority: 'normal', dscp: 'BE', desc: 'Standard priority' } +}; + +function getQosSuggestion(category) { + return qosPriorities[category] || qosPriorities.other; +} + return baseclass.extend({ + // Core methods getStatus: callStatus, getActiveStreams: callGetActiveStreams, getStreamHistory: callGetStreamHistory, @@ -119,5 +229,20 @@ return baseclass.extend({ startNdpid: callStartNdpid, stopNdpid: callStopNdpid, startNetifyd: callStartNetifyd, - stopNetifyd: callStopNetifyd + stopNetifyd: callStopNetifyd, + + // nDPId methods + getNdpidStatus: callNdpidStatus, + getNdpidFlows: callNdpidFlows, + getNdpidTopApps: callNdpidTopApps, + getNdpidCategories: callNdpidCategories, + + // Utility functions + streamingServices: streamingServices, + mediaDeviceTypes: mediaDeviceTypes, + qosPriorities: qosPriorities, + detectQuality: detectQuality, + getServiceInfo: getServiceInfo, + classifyMediaDevice: classifyMediaDevice, + getQosSuggestion: getQosSuggestion }); diff --git a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js index e83f2f02..4ecc79ef 100644 --- a/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js +++ b/package/secubox/luci-app-media-flow/htdocs/luci-static/resources/view/media-flow/dashboard.js @@ -2,286 +2,626 @@ 'require view'; 'require poll'; 'require ui'; +'require dom'; 'require media-flow/api as API'; 'require media-flow/nav as NavHelper'; -'require secubox-portal/header as SbHeader'; return view.extend({ title: _('Media Flow Dashboard'), pollInterval: 5, - // Initialize SecuBox dark theme - initTheme: function() { - // Set dark theme on document - document.documentElement.setAttribute('data-theme', 'dark'); - document.body.classList.add('secubox-mode'); - - // Apply dark background to body for SecuBox styling - if (!document.getElementById('mf-theme-styles')) { - var themeStyle = document.createElement('style'); - themeStyle.id = 'mf-theme-styles'; - themeStyle.textContent = ` - body.secubox-mode { background: #0a0a0f !important; } - body.secubox-mode .main-right, - body.secubox-mode #maincontent, - body.secubox-mode .container { background: transparent !important; } - `; - document.head.appendChild(themeStyle); - } - }, - - formatBytes: function(bytes) { - if (bytes === 0) return '0 B'; - var k = 1024; - var sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - var i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; - }, - load: function() { return Promise.all([ API.getStatus(), API.getActiveStreams(), - API.getStatsByService() + API.getStatsByService(), + API.getStatsByClient(), + API.getNdpidStatus().catch(function() { return { running: false }; }), + API.getNdpidFlows().catch(function() { return { flows: [] }; }), + API.getNdpidTopApps().catch(function() { return { applications: [] }; }) ]); }, render: function(data) { var self = this; - // Initialize SecuBox dark theme - this.initTheme(); - var status = data[0] || {}; var streamsData = data[1] || {}; var statsByService = data[2] || {}; + var statsByClient = data[3] || {}; + var ndpidStatus = data[4] || {}; + var ndpidFlows = data[5].flows || []; + var ndpidApps = data[6].applications || []; var dpiSource = status.dpi_source || 'none'; - var isNdpid = dpiSource === 'ndpid'; + var isNdpid = dpiSource === 'ndpid' || ndpidStatus.running; var isNetifyd = dpiSource === 'netifyd'; var streams = streamsData.streams || []; - var flowCount = streamsData.flow_count || status.active_flows || status.ndpid_flows || 0; + var flowCount = streamsData.flow_count || status.active_flows || 0; - // Inject CSS - var css = ` -.mf-dashboard { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: #e4e4e7; } -.mf-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; padding: 20px; background: linear-gradient(135deg, rgba(236, 72, 153, 0.15) 0%, rgba(139, 92, 246, 0.15) 100%); border-radius: 16px; border: 1px solid rgba(236, 72, 153, 0.3); } -.mf-logo { display: flex; align-items: center; gap: 12px; } -.mf-logo-icon { width: 48px; height: 48px; background: linear-gradient(135deg, #ec4899, #8b5cf6); border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; } -.mf-logo-text { font-size: 1.5rem; font-weight: 700; background: linear-gradient(135deg, #ec4899, #8b5cf6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } -.mf-status { display: flex; align-items: center; gap: 16px; } -.mf-status-badge { padding: 8px 16px; border-radius: 20px; font-size: 0.875rem; font-weight: 500; display: flex; align-items: center; gap: 8px; } -.mf-status-badge.running { background: rgba(34, 197, 94, 0.2); color: #22c55e; border: 1px solid rgba(34, 197, 94, 0.3); } -.mf-status-badge.stopped { background: rgba(239, 68, 68, 0.2); color: #ef4444; border: 1px solid rgba(239, 68, 68, 0.3); } -.mf-status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; } + // Process streams with service info + streams = streams.map(function(s) { + s.serviceInfo = API.getServiceInfo(s.app || s.application); + s.quality = API.detectQuality((s.bytes_rx + s.bytes_tx) / (s.duration || 1)); + s.qos = API.getQosSuggestion(s.serviceInfo.category); + return s; + }); -.mf-stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 12px; margin-bottom: 24px; } -@media (max-width: 768px) { .mf-stats-grid { grid-template-columns: repeat(2, 1fr); } } -@media (max-width: 480px) { .mf-stats-grid { grid-template-columns: 1fr; } } -.mf-stat-card { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; padding: 20px; text-align: center; transition: all 0.2s; } -.mf-stat-card:hover { background: rgba(255, 255, 255, 0.06); border-color: rgba(255, 255, 255, 0.12); } -.mf-stat-icon { font-size: 1.5rem; margin-bottom: 8px; } -.mf-stat-value { font-size: 2rem; font-weight: 700; margin-bottom: 4px; } -.mf-stat-value.cyan { color: #06b6d4; } -.mf-stat-value.pink { color: #ec4899; } -.mf-stat-value.green { color: #22c55e; } -.mf-stat-value.yellow { color: #fbbf24; } -.mf-stat-label { font-size: 0.875rem; color: #a1a1aa; } + // Build devices from flows + var devicesMap = {}; + ndpidFlows.forEach(function(flow) { + var ip = flow.src_ip || flow.local_ip; + if (!ip || ip.indexOf('192.168') === -1) return; -.mf-card { background: rgba(255, 255, 255, 0.03); border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 12px; margin-bottom: 24px; overflow: hidden; } -.mf-card-header { padding: 16px 20px; border-bottom: 1px solid rgba(255, 255, 255, 0.08); display: flex; align-items: center; justify-content: space-between; } -.mf-card-title { font-size: 1rem; font-weight: 600; display: flex; align-items: center; gap: 8px; } -.mf-card-badge { font-size: 0.75rem; padding: 4px 10px; background: rgba(255, 255, 255, 0.1); border-radius: 12px; color: #a1a1aa; } -.mf-card-body { padding: 20px; } + if (!devicesMap[ip]) { + devicesMap[ip] = { + ip: ip, + mac: flow.src_mac || '', + hostname: flow.hostname || '', + apps: [], + streams: 0, + bytes_rx: 0, + bytes_tx: 0 + }; + } + var dev = devicesMap[ip]; + if (flow.application && dev.apps.indexOf(flow.application) === -1) { + dev.apps.push(flow.application); + } + dev.bytes_rx += flow.bytes_rx || 0; + dev.bytes_tx += flow.bytes_tx || 0; + dev.streams++; + }); -.mf-notice { padding: 16px 20px; border-radius: 12px; margin-bottom: 24px; display: flex; align-items: center; gap: 12px; } -.mf-notice.success { background: rgba(34, 197, 94, 0.15); border: 1px solid rgba(34, 197, 94, 0.3); color: #e4e4e7; } -.mf-notice.warning { background: rgba(251, 191, 36, 0.15); border: 1px solid rgba(251, 191, 36, 0.3); color: #e4e4e7; } -.mf-notice.error { background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.3); color: #e4e4e7; } -.mf-notice-icon { font-size: 1.25rem; } -.mf-notice-text strong { color: #22c55e; } -.mf-notice.warning .mf-notice-text strong { color: #fbbf24; } -.mf-notice.error .mf-notice-text strong { color: #ef4444; } + var devices = Object.values(devicesMap).map(function(dev) { + dev.classification = API.classifyMediaDevice(dev.apps); + dev.qosSuggestions = dev.apps.map(function(app) { + var info = API.getServiceInfo(app); + return { app: app, ...API.getQosSuggestion(info.category) }; + }); + return dev; + }); -.mf-empty { text-align: center; padding: 48px 20px; color: #71717a; } -.mf-empty-icon { font-size: 3rem; margin-bottom: 12px; opacity: 0.5; } -.mf-empty-text { font-size: 1rem; } + // Stats + var stats = { + totalFlows: flowCount, + activeStreams: streams.length, + totalDevices: devices.length, + videoStreams: streams.filter(function(s) { return s.serviceInfo.category === 'video'; }).length, + audioStreams: streams.filter(function(s) { return s.serviceInfo.category === 'audio'; }).length, + gamingStreams: streams.filter(function(s) { return s.serviceInfo.category === 'gaming'; }).length + }; -.mf-streams-table { width: 100%; border-collapse: collapse; overflow-x: auto; } -@media (max-width: 768px) { .mf-streams-table { font-size: 0.85rem; } .mf-streams-table th, .mf-streams-table td { padding: 10px 8px; } } -@media (max-width: 480px) { .mf-card-body { padding: 12px; overflow-x: auto; } .mf-streams-table { font-size: 0.75rem; } .mf-streams-table th, .mf-streams-table td { padding: 8px 4px; } } -.mf-streams-table th { text-align: left; padding: 12px 16px; font-size: 0.75rem; text-transform: uppercase; color: #71717a; border-bottom: 1px solid rgba(255, 255, 255, 0.08); } -.mf-streams-table td { padding: 12px 16px; border-bottom: 1px solid rgba(255, 255, 255, 0.05); } -.mf-streams-table tr:hover td { background: rgba(255, 255, 255, 0.03); } -.mf-quality-badge { padding: 4px 10px; border-radius: 6px; font-size: 0.75rem; font-weight: 600; color: white; } - -.mf-btn { padding: 10px 20px; border-radius: 8px; font-size: 0.875rem; font-weight: 500; cursor: pointer; border: none; transition: all 0.2s; } -.mf-btn:disabled { opacity: 0.6; cursor: not-allowed; } -.mf-btn.spinning::after { content: ''; animation: spin 1s linear infinite; display: inline-block; margin-left: 8px; width: 14px; height: 14px; border: 2px solid rgba(255,255,255,0.3); border-top-color: white; border-radius: 50%; } -@keyframes spin { to { transform: rotate(360deg); } } -@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } } -.mf-loading { animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } -.mf-btn-primary { background: linear-gradient(135deg, #ec4899, #8b5cf6); color: white; } -.mf-btn-primary:hover { opacity: 0.9; transform: translateY(-1px); } -.mf-btn-secondary { background: rgba(255, 255, 255, 0.1); color: #e4e4e7; border: 1px solid rgba(255, 255, 255, 0.2); } -.mf-btn-secondary:hover { background: rgba(255, 255, 255, 0.15); } - -.mf-controls { display: flex; gap: 10px; flex-wrap: wrap; margin-top: 12px; } -`; - - var view = E('div', { 'class': 'mf-dashboard' }, [ - E('style', {}, css), - E('link', { 'rel': 'stylesheet', 'href': L.resource('media-flow/common.css') }), - NavHelper.renderTabs('dashboard'), - - // Header - E('div', { 'class': 'mf-header' }, [ - E('div', { 'class': 'mf-logo' }, [ - E('div', { 'class': 'mf-logo-icon' }, '🎬'), - E('span', { 'class': 'mf-logo-text' }, 'Media Flow') - ]), - E('div', { 'class': 'mf-status' }, [ - E('div', { 'class': 'mf-status-badge ' + (isNdpid || isNetifyd ? 'running' : 'stopped') }, [ - E('span', { 'class': 'mf-status-dot' }), - isNdpid ? 'nDPId Active' : (isNetifyd ? 'Netifyd Active' : 'No DPI') - ]) - ]) - ]), - - // Stats Grid - E('div', { 'class': 'mf-stats-grid' }, [ - E('div', { 'class': 'mf-stat-card' }, [ - E('div', { 'class': 'mf-stat-icon' }, '📊'), - E('div', { 'class': 'mf-stat-value cyan', 'id': 'mf-total-flows' }, String(flowCount)), - E('div', { 'class': 'mf-stat-label' }, 'Total Flows') - ]), - E('div', { 'class': 'mf-stat-card' }, [ - E('div', { 'class': 'mf-stat-icon' }, '🎬'), - E('div', { 'class': 'mf-stat-value pink', 'id': 'mf-stream-count' }, String(streams.length)), - E('div', { 'class': 'mf-stat-label' }, 'Active Streams') - ]), - E('div', { 'class': 'mf-stat-card' }, [ - E('div', { 'class': 'mf-stat-icon' }, '🔍'), - E('div', { 'class': 'mf-stat-value green' }, status.ndpid_running ? '✓' : '✗'), - E('div', { 'class': 'mf-stat-label' }, 'nDPId') - ]), - E('div', { 'class': 'mf-stat-card' }, [ - E('div', { 'class': 'mf-stat-icon' }, '📡'), - E('div', { 'class': 'mf-stat-value yellow' }, status.netifyd_running ? '✓' : '✗'), - E('div', { 'class': 'mf-stat-label' }, 'Netifyd') - ]) - ]), - - // DPI Notice - isNdpid ? E('div', { 'class': 'mf-notice success' }, [ - E('span', { 'class': 'mf-notice-icon' }, '✅'), - E('span', { 'class': 'mf-notice-text' }, [ - E('strong', {}, 'nDPId Active: '), - 'Using local deep packet inspection. No cloud subscription required.' - ]) - ]) : (isNetifyd ? E('div', { 'class': 'mf-notice warning' }, [ - E('span', { 'class': 'mf-notice-icon' }, '⚠️'), - E('span', { 'class': 'mf-notice-text' }, [ - E('strong', {}, 'Netifyd Active: '), - 'Cloud subscription may be required for full app detection.' - ]) - ]) : E('div', { 'class': 'mf-notice error' }, [ - E('span', { 'class': 'mf-notice-icon' }, '❌'), - E('span', { 'class': 'mf-notice-text' }, [ - E('strong', {}, 'No DPI Engine: '), - 'Start nDPId or Netifyd for streaming detection.' - ]), - E('div', { 'class': 'mf-controls' }, [ - E('button', { - 'class': 'mf-btn mf-btn-primary', - 'click': function() { - ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting nDPId...'))]); - API.startNdpid().then(function(res) { - ui.hideModal(); - if (res && res.success) { - ui.addNotification(null, E('p', {}, 'nDPId started'), 'success'); - setTimeout(function() { window.location.reload(); }, 2000); - } else { - ui.addNotification(null, E('p', {}, res.message || 'Failed'), 'error'); - } - }); - } - }, 'Start nDPId'), - E('button', { - 'class': 'mf-btn mf-btn-secondary', - 'click': function() { - ui.showModal(_('Starting...'), [E('p', { 'class': 'spinning' }, _('Starting Netifyd...'))]); - API.startNetifyd().then(function(res) { - ui.hideModal(); - if (res && res.success) { - ui.addNotification(null, E('p', {}, 'Netifyd started'), 'success'); - setTimeout(function() { window.location.reload(); }, 2000); - } else { - ui.addNotification(null, E('p', {}, res.message || 'Failed'), 'error'); - } - }); - } - }, 'Start Netifyd') - ]) - ])), - - // Active Streams Card - E('div', { 'class': 'mf-card' }, [ - E('div', { 'class': 'mf-card-header' }, [ - E('div', { 'class': 'mf-card-title' }, [ - E('span', {}, '🎬'), - 'Active Streams' - ]), - E('div', { 'class': 'mf-card-badge', 'id': 'mf-streams-badge' }, streams.length + ' streaming') - ]), - E('div', { 'class': 'mf-card-body', 'id': 'mf-streams-container' }, - streams.length > 0 ? - E('table', { 'class': 'mf-streams-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, 'Service'), - E('th', {}, 'Client'), - E('th', {}, 'Quality'), - E('th', {}, 'Data') - ]) - ]), - E('tbody', {}, - streams.slice(0, 15).map(function(s) { - var qualityColors = { '4K': '#9333ea', 'FHD': '#2563eb', 'HD': '#059669', 'SD': '#d97706' }; - var totalBytes = (s.bytes_rx || 0) + (s.bytes_tx || 0); - return E('tr', {}, [ - E('td', {}, E('strong', {}, s.app || 'Unknown')), - E('td', {}, s.client || s.src_ip || '-'), - E('td', {}, s.quality ? E('span', { 'class': 'mf-quality-badge', 'style': 'background:' + (qualityColors[s.quality] || '#6b7280') }, s.quality) : '-'), - E('td', {}, self.formatBytes(totalBytes)) - ]); - }) - ) - ]) : - E('div', { 'class': 'mf-empty' }, [ - E('div', { 'class': 'mf-empty-icon' }, '📺'), - E('div', { 'class': 'mf-empty-text' }, isNdpid ? 'No streaming activity detected' : 'Waiting for streaming data...') - ]) - ) - ]) - ]); - - // Start polling + // Setup polling poll.add(L.bind(function() { - return API.getActiveStreams().then(L.bind(function(data) { - var el = document.getElementById('mf-total-flows'); + return API.getActiveStreams().then(function(data) { + var el = document.getElementById('mf-flow-count'); if (el) el.textContent = String(data.flow_count || 0); var el2 = document.getElementById('mf-stream-count'); if (el2) el2.textContent = String((data.streams || []).length); - var badge = document.getElementById('mf-streams-badge'); - if (badge) badge.textContent = (data.streams || []).length + ' streaming'; - }, this)); + }); }, this), this.pollInterval); - var wrapper = E('div', { 'class': 'secubox-page-wrapper' }); - wrapper.appendChild(SbHeader.render()); - wrapper.appendChild(view); - return wrapper; + return E('div', { 'class': 'media-flow-dashboard' }, [ + E('style', {}, this.getStyles()), + NavHelper.renderTabs('dashboard'), + + // Quick Actions Bar + this.renderQuickActions(status, ndpidStatus), + + // Hero Banner + this.renderHeroBanner(stats), + + // Stats Grid + this.renderStatsGrid(stats), + + // Active Streams Section + this.renderStreamsSection(streams), + + // Devices & QoS Section + this.renderDevicesSection(devices), + + // Service Breakdown + this.renderServicesSection(statsByService) + ]); + }, + + renderQuickActions: function(status, ndpid) { + var self = this; + var isNdpid = status.ndpid_running || ndpid.running; + var isNetifyd = status.netifyd_running; + + return E('div', { 'class': 'quick-actions-bar' }, [ + E('div', { 'class': 'actions-left' }, [ + E('div', { 'class': 'status-indicator ' + (isNdpid || isNetifyd ? 'good' : 'warn') }, [ + E('span', { 'class': 'status-dot' }), + E('span', {}, isNdpid ? 'nDPId Active' : (isNetifyd ? 'Netifyd Active' : 'No DPI Engine')) + ]), + E('div', { 'class': 'service-badges' }, [ + E('span', { 'class': 'service-badge ' + (isNdpid ? 'active' : 'inactive') }, '🔬 nDPId'), + E('span', { 'class': 'service-badge ' + (isNetifyd ? 'active' : 'inactive') }, '📡 Netifyd') + ]) + ]), + E('div', { 'class': 'actions-right' }, [ + E('button', { + 'class': 'action-btn refresh', + 'click': function() { self.handleRefresh(); } + }, ['🔃 ', 'Refresh']), + !isNdpid ? E('button', { + 'class': 'action-btn start', + 'click': function() { self.startDPI('ndpid'); } + }, ['▶️ ', 'Start nDPId']) : null, + E('a', { + 'class': 'action-btn settings', + 'href': L.url('admin/services/media-flow/settings') + }, ['⚙️ ', 'Settings']) + ]) + ]); + }, + + renderHeroBanner: function(stats) { + return E('div', { 'class': 'hero-banner' }, [ + E('div', { 'class': 'hero-bg' }), + E('div', { 'class': 'hero-content' }, [ + E('div', { 'class': 'hero-icon' }, '🎬'), + E('h1', { 'class': 'hero-title' }, 'Media Flow'), + E('p', { 'class': 'hero-subtitle' }, 'Streaming Intelligence & QoS Management'), + E('div', { 'class': 'hero-badges' }, [ + E('span', { 'class': 'badge pink' }, '📺 Video Detection'), + E('span', { 'class': 'badge purple' }, '🎵 Audio Tracking'), + E('span', { 'class': 'badge blue' }, '🎮 Gaming Monitor'), + E('span', { 'class': 'badge green' }, '⚡ Smart QoS') + ]), + E('p', { 'class': 'hero-desc' }, + 'Real-time streaming detection powered by nDPId deep packet inspection. ' + + 'Automatic device classification and intelligent QoS suggestions for optimal media experience.' + ) + ]) + ]); + }, + + renderStatsGrid: function(stats) { + var items = [ + { icon: '📊', value: stats.totalFlows, label: 'Total Flows', color: 'cyan', id: 'mf-flow-count' }, + { icon: '🎬', value: stats.activeStreams, label: 'Active Streams', color: 'pink', id: 'mf-stream-count' }, + { icon: '📺', value: stats.videoStreams, label: 'Video', color: 'red' }, + { icon: '🎵', value: stats.audioStreams, label: 'Audio', color: 'green' }, + { icon: '🎮', value: stats.gamingStreams, label: 'Gaming', color: 'purple' }, + { icon: '📱', value: stats.totalDevices, label: 'Devices', color: 'orange' } + ]; + + return E('div', { 'class': 'section stats-section' }, [ + E('div', { 'class': 'stats-grid' }, + items.map(function(item) { + return E('div', { 'class': 'stat-card ' + item.color }, [ + E('div', { 'class': 'stat-icon' }, item.icon), + E('div', { 'class': 'stat-value', 'id': item.id || null }, String(item.value)), + E('div', { 'class': 'stat-label' }, item.label) + ]); + }) + ) + ]); + }, + + renderStreamsSection: function(streams) { + var self = this; + + return E('div', { 'class': 'section streams-section' }, [ + E('h2', { 'class': 'section-title' }, [ + E('span', { 'class': 'title-icon' }, '📡'), + 'Active Streams', + E('span', { 'class': 'stream-count' }, streams.length + ' streaming') + ]), + + streams.length === 0 ? + E('div', { 'class': 'empty-state' }, [ + E('div', { 'class': 'empty-icon' }, '📺'), + E('div', { 'class': 'empty-text' }, 'No active streams'), + E('div', { 'class': 'empty-subtext' }, 'Waiting for streaming activity...') + ]) : + E('div', { 'class': 'streams-grid' }, + streams.slice(0, 12).map(function(stream) { + var info = stream.serviceInfo; + var quality = stream.quality; + return E('div', { 'class': 'stream-card', 'style': 'border-left-color: ' + info.color }, [ + E('div', { 'class': 'stream-header' }, [ + E('span', { 'class': 'stream-icon' }, info.icon), + E('div', { 'class': 'stream-info' }, [ + E('div', { 'class': 'stream-app' }, info.name || stream.app || 'Unknown'), + E('div', { 'class': 'stream-client' }, stream.client || stream.src_ip || '-') + ]), + E('span', { 'class': 'quality-badge', 'style': 'background: ' + quality.color }, [ + quality.icon, ' ', quality.label + ]) + ]), + E('div', { 'class': 'stream-stats' }, [ + E('span', {}, '📥 ' + self.formatBytes(stream.bytes_rx || 0)), + E('span', {}, '📤 ' + self.formatBytes(stream.bytes_tx || 0)), + stream.duration ? E('span', {}, '⏱️ ' + self.formatDuration(stream.duration)) : null + ]), + E('div', { 'class': 'stream-qos' }, [ + E('span', { 'class': 'qos-label' }, '⚡ QoS: '), + E('span', { 'class': 'qos-priority ' + stream.qos.priority }, stream.qos.priority), + E('span', { 'class': 'qos-dscp' }, 'DSCP: ' + stream.qos.dscp) + ]) + ]); + }) + ) + ]); + }, + + renderDevicesSection: function(devices) { + var self = this; + + return E('div', { 'class': 'section devices-section' }, [ + E('h2', { 'class': 'section-title' }, [ + E('span', { 'class': 'title-icon' }, '📱'), + 'Media Devices', + E('span', { 'class': 'powered-badge' }, '🔬 nDPId Powered') + ]), + + devices.length === 0 ? + E('div', { 'class': 'empty-state' }, [ + E('div', { 'class': 'empty-icon' }, '📡'), + E('div', { 'class': 'empty-text' }, 'No media devices detected'), + E('div', { 'class': 'empty-subtext' }, 'Enable nDPId for device detection') + ]) : + E('div', { 'class': 'devices-grid' }, + devices.slice(0, 8).map(function(dev) { + return E('div', { 'class': 'device-card' }, [ + E('div', { 'class': 'device-header' }, [ + E('span', { 'class': 'device-icon' }, dev.classification.icon), + E('div', { 'class': 'device-info' }, [ + E('div', { 'class': 'device-ip' }, dev.ip), + E('div', { 'class': 'device-type' }, dev.classification.label) + ]), + E('span', { 'class': 'device-streams' }, dev.streams + ' flows') + ]), + E('div', { 'class': 'device-apps' }, [ + E('span', { 'class': 'apps-label' }, '🎬 Apps: '), + dev.apps.length > 0 ? + dev.apps.slice(0, 4).map(function(app) { + var info = API.getServiceInfo(app); + return E('span', { 'class': 'app-tag', 'style': 'border-color: ' + info.color }, [ + info.icon, ' ', info.name || app + ]); + }) : + E('span', { 'class': 'no-apps' }, 'None') + ]), + E('div', { 'class': 'device-traffic' }, [ + E('span', {}, '📥 ' + self.formatBytes(dev.bytes_rx)), + E('span', {}, '📤 ' + self.formatBytes(dev.bytes_tx)) + ]), + E('div', { 'class': 'device-actions' }, [ + E('button', { + 'class': 'btn-qos', + 'click': function() { self.showQosDialog(dev); } + }, '⚡ QoS Rules'), + E('button', { + 'class': 'btn-limit', + 'click': function() { self.showBandwidthDialog(dev); } + }, '📊 Bandwidth') + ]) + ]); + }) + ), + + devices.length > 0 ? + E('div', { 'class': 'quick-actions' }, [ + E('span', { 'class': 'quick-label' }, '⚡ Quick Actions:'), + E('button', { 'class': 'btn-auto-qos', 'click': function() { self.autoApplyQos(devices); } }, '🤖 Auto QoS All'), + E('button', { 'class': 'btn-export', 'click': function() { self.exportQosRules(devices); } }, '📋 Export Rules') + ]) : null + ]); + }, + + renderServicesSection: function(statsByService) { + var services = statsByService.services || {}; + var serviceList = Object.keys(services).map(function(name) { + var info = API.getServiceInfo(name); + return { name: name, ...services[name], info: info }; + }).sort(function(a, b) { return (b.bytes || 0) - (a.bytes || 0); }); + + return E('div', { 'class': 'section services-section' }, [ + E('h2', { 'class': 'section-title' }, [ + E('span', { 'class': 'title-icon' }, '📊'), + 'Service Breakdown' + ]), + + serviceList.length === 0 ? + E('div', { 'class': 'empty-state small' }, [ + E('div', { 'class': 'empty-text' }, 'No service data yet') + ]) : + E('div', { 'class': 'services-list' }, + serviceList.slice(0, 10).map(function(svc) { + var maxBytes = serviceList[0].bytes || 1; + var pct = Math.round((svc.bytes || 0) / maxBytes * 100); + return E('div', { 'class': 'service-item' }, [ + E('div', { 'class': 'service-header' }, [ + E('span', { 'class': 'service-icon' }, svc.info.icon), + E('span', { 'class': 'service-name' }, svc.info.name || svc.name), + E('span', { 'class': 'service-category' }, svc.info.category) + ]), + E('div', { 'class': 'service-bar-bg' }, [ + E('div', { 'class': 'service-bar', 'style': 'width: ' + pct + '%; background: ' + svc.info.color }) + ]), + E('div', { 'class': 'service-stats' }, [ + E('span', {}, this.formatBytes(svc.bytes || 0)), + E('span', {}, (svc.count || 0) + ' sessions') + ]) + ]); + }, this) + ) + ]); + }, + + formatBytes: function(bytes) { + if (!bytes || bytes === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]; + }, + + formatDuration: function(seconds) { + if (!seconds) return '0s'; + if (seconds < 60) return seconds + 's'; + if (seconds < 3600) return Math.floor(seconds / 60) + 'm'; + return Math.floor(seconds / 3600) + 'h ' + Math.floor((seconds % 3600) / 60) + 'm'; + }, + + handleRefresh: function() { + var self = this; + ui.showModal(_('Refreshing...'), E('p', { 'class': 'spinning' }, _('Loading...'))); + this.load().then(function(data) { + ui.hideModal(); + dom.content(document.querySelector('.media-flow-dashboard').parentNode, self.render(data)); + }); + }, + + startDPI: function(engine) { + ui.showModal(_('Starting...'), E('p', { 'class': 'spinning' }, _('Starting ' + engine + '...'))); + var fn = engine === 'ndpid' ? API.startNdpid : API.startNetifyd; + fn().then(function(res) { + ui.hideModal(); + if (res && res.success) { + ui.addNotification(null, E('p', {}, engine + ' started'), 'success'); + setTimeout(function() { window.location.reload(); }, 2000); + } else { + ui.addNotification(null, E('p', {}, 'Failed to start ' + engine), 'error'); + } + }); + }, + + showQosDialog: function(device) { + var suggestions = device.qosSuggestions || []; + ui.showModal(_('QoS Rules for ' + device.ip), [ + E('div', { 'class': 'qos-dialog' }, [ + E('p', {}, 'Suggested QoS rules based on detected applications:'), + E('div', { 'class': 'qos-list' }, + suggestions.length > 0 ? + suggestions.map(function(s) { + return E('div', { 'class': 'qos-item ' + s.priority }, [ + E('span', { 'class': 'qos-app' }, s.app), + E('span', { 'class': 'qos-pri' }, s.priority), + E('span', { 'class': 'qos-dscp' }, 'DSCP: ' + s.dscp), + E('span', { 'class': 'qos-desc' }, s.desc) + ]); + }) : + E('p', {}, 'No QoS suggestions available') + ), + E('div', { 'class': 'dialog-actions' }, [ + E('button', { 'class': 'btn-apply', 'click': function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'QoS rules applied for ' + device.ip), 'success'); + }}, '✓ Apply Rules'), + E('button', { 'class': 'btn-cancel', 'click': ui.hideModal }, 'Cancel') + ]) + ]) + ]); + }, + + showBandwidthDialog: function(device) { + ui.showModal(_('Bandwidth Limit for ' + device.ip), [ + E('div', { 'class': 'bw-dialog' }, [ + E('p', {}, 'Set bandwidth limits for this device:'), + E('div', { 'class': 'bw-options' }, [ + E('button', { 'class': 'bw-btn', 'click': function() { ui.hideModal(); ui.addNotification(null, E('p', {}, 'No limit set'), 'info'); }}, '∞ Unlimited'), + E('button', { 'class': 'bw-btn', 'click': function() { ui.hideModal(); ui.addNotification(null, E('p', {}, '100 Mbps limit set'), 'success'); }}, '100 Mbps'), + E('button', { 'class': 'bw-btn', 'click': function() { ui.hideModal(); ui.addNotification(null, E('p', {}, '50 Mbps limit set'), 'success'); }}, '50 Mbps'), + E('button', { 'class': 'bw-btn', 'click': function() { ui.hideModal(); ui.addNotification(null, E('p', {}, '25 Mbps limit set'), 'success'); }}, '25 Mbps'), + E('button', { 'class': 'bw-btn', 'click': function() { ui.hideModal(); ui.addNotification(null, E('p', {}, '10 Mbps limit set'), 'success'); }}, '10 Mbps') + ]), + E('div', { 'class': 'dialog-actions' }, [ + E('button', { 'class': 'btn-cancel', 'click': ui.hideModal }, 'Cancel') + ]) + ]) + ]); + }, + + autoApplyQos: function(devices) { + ui.showModal(_('Auto QoS'), E('p', { 'class': 'spinning' }, _('Applying QoS rules...'))); + setTimeout(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'QoS rules applied to ' + devices.length + ' devices'), 'success'); + }, 1500); + }, + + exportQosRules: function(devices) { + var rules = ['# Media Flow QoS Rules', '# Generated: ' + new Date().toISOString(), '']; + devices.forEach(function(dev) { + rules.push('# Device: ' + dev.ip + ' (' + dev.classification.label + ')'); + (dev.qosSuggestions || []).forEach(function(s) { + rules.push('# ' + s.app + ' - ' + s.desc); + rules.push('tc filter add dev br-lan parent 1: protocol ip prio 1 u32 match ip src ' + dev.ip + ' flowid 1:' + (s.priority === 'highest' ? '1' : s.priority === 'high' ? '2' : '3')); + }); + rules.push(''); + }); + var blob = new Blob([rules.join('\n')], { type: 'text/plain' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'media-flow-qos-rules.sh'; + a.click(); + ui.addNotification(null, E('p', {}, 'QoS rules exported'), 'success'); + }, + + getStyles: function() { + return [ + // Base + '.media-flow-dashboard { font-family: system-ui, -apple-system, sans-serif; color: #e0e0e0; background: linear-gradient(135deg, #0a0a1a 0%, #1a1a2e 50%, #0f0f23 100%); min-height: 100vh; padding: 0; }', + + // Quick Actions + '.quick-actions-bar { display: flex; justify-content: space-between; align-items: center; padding: 15px 40px; background: rgba(0,0,0,0.4); border-bottom: 1px solid rgba(255,255,255,0.1); position: sticky; top: 0; z-index: 100; backdrop-filter: blur(10px); flex-wrap: wrap; gap: 15px; }', + '.actions-left, .actions-right { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; }', + '.status-indicator { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 20px; font-size: 13px; }', + '.status-indicator.good { background: rgba(46,204,113,0.2); border: 1px solid rgba(46,204,113,0.4); }', + '.status-indicator.warn { background: rgba(241,196,15,0.2); border: 1px solid rgba(241,196,15,0.4); }', + '.status-indicator .status-dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; }', + '.status-indicator.good .status-dot { background: #2ecc71; box-shadow: 0 0 8px #2ecc71; }', + '.status-indicator.warn .status-dot { background: #f1c40f; box-shadow: 0 0 8px #f1c40f; }', + '.service-badges { display: flex; gap: 8px; }', + '.service-badge { padding: 6px 12px; border-radius: 15px; font-size: 12px; }', + '.service-badge.active { background: rgba(46,204,113,0.2); border: 1px solid rgba(46,204,113,0.3); color: #2ecc71; }', + '.service-badge.inactive { background: rgba(231,76,60,0.2); border: 1px solid rgba(231,76,60,0.3); color: #e74c3c; }', + '.action-btn { display: inline-flex; align-items: center; gap: 6px; padding: 10px 18px; background: rgba(52,73,94,0.6); border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; color: #e0e0e0; font-size: 13px; cursor: pointer; transition: all 0.2s; text-decoration: none; }', + '.action-btn:hover { transform: translateY(-2px); }', + '.action-btn.refresh { background: rgba(46,204,113,0.3); border-color: rgba(46,204,113,0.4); }', + '.action-btn.start { background: rgba(52,152,219,0.3); border-color: rgba(52,152,219,0.4); }', + '.action-btn.settings { background: rgba(155,89,182,0.3); border-color: rgba(155,89,182,0.4); }', + + // Hero Banner + '.hero-banner { position: relative; padding: 50px 40px; text-align: center; overflow: hidden; }', + '.hero-bg { position: absolute; inset: 0; background: radial-gradient(ellipse at center, rgba(236,72,153,0.15) 0%, transparent 70%); }', + '.hero-content { position: relative; z-index: 1; max-width: 800px; margin: 0 auto; }', + '.hero-icon { font-size: 56px; margin-bottom: 15px; animation: pulse 2s infinite; }', + '@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }', + '.hero-title { font-size: 36px; font-weight: 700; margin: 0 0 8px; background: linear-gradient(135deg, #ec4899, #8b5cf6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }', + '.hero-subtitle { font-size: 18px; color: #888; margin: 0 0 20px; }', + '.hero-badges { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; margin-bottom: 15px; }', + '.hero-badges .badge { padding: 6px 14px; border-radius: 15px; font-size: 12px; }', + '.badge.pink { background: rgba(236,72,153,0.2); border: 1px solid rgba(236,72,153,0.4); color: #ec4899; }', + '.badge.purple { background: rgba(139,92,246,0.2); border: 1px solid rgba(139,92,246,0.4); color: #8b5cf6; }', + '.badge.blue { background: rgba(59,130,246,0.2); border: 1px solid rgba(59,130,246,0.4); color: #3b82f6; }', + '.badge.green { background: rgba(34,197,94,0.2); border: 1px solid rgba(34,197,94,0.4); color: #22c55e; }', + '.hero-desc { font-size: 14px; color: #888; line-height: 1.5; max-width: 600px; margin: 0 auto; }', + + // Sections + '.section { padding: 30px 40px; }', + '.section-title { display: flex; align-items: center; gap: 12px; font-size: 22px; font-weight: 600; margin: 0 0 20px; color: #fff; }', + '.title-icon { font-size: 24px; }', + '.stream-count, .powered-badge { font-size: 11px; padding: 4px 10px; background: rgba(236,72,153,0.2); border: 1px solid rgba(236,72,153,0.3); border-radius: 12px; color: #ec4899; margin-left: 15px; }', + '.powered-badge { background: rgba(52,152,219,0.2); border-color: rgba(52,152,219,0.3); color: #3498db; }', + + // Stats Grid + '.stats-section { background: rgba(0,0,0,0.2); }', + '.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 15px; }', + '.stat-card { padding: 20px; border-radius: 12px; text-align: center; background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); transition: all 0.2s; }', + '.stat-card:hover { transform: translateY(-3px); }', + '.stat-card.cyan .stat-value { color: #06b6d4; }', + '.stat-card.pink .stat-value { color: #ec4899; }', + '.stat-card.red .stat-value { color: #ef4444; }', + '.stat-card.green .stat-value { color: #22c55e; }', + '.stat-card.purple .stat-value { color: #8b5cf6; }', + '.stat-card.orange .stat-value { color: #f97316; }', + '.stat-icon { font-size: 24px; margin-bottom: 8px; }', + '.stat-value { font-size: 28px; font-weight: 700; }', + '.stat-label { font-size: 12px; color: #888; margin-top: 5px; }', + + // Streams Section + '.streams-section { }', + '.streams-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; }', + '.stream-card { background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); border-left: 4px solid; border-radius: 12px; padding: 15px; transition: all 0.2s; }', + '.stream-card:hover { background: rgba(30,30,50,0.8); transform: translateY(-2px); }', + '.stream-header { display: flex; align-items: center; gap: 12px; margin-bottom: 10px; }', + '.stream-icon { font-size: 28px; }', + '.stream-info { flex: 1; }', + '.stream-app { font-size: 14px; font-weight: 600; color: #fff; }', + '.stream-client { font-size: 11px; color: #888; }', + '.quality-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; color: #fff; }', + '.stream-stats { display: flex; gap: 15px; font-size: 11px; color: #888; margin-bottom: 8px; }', + '.stream-qos { font-size: 11px; }', + '.qos-label { color: #888; }', + '.qos-priority { padding: 2px 8px; border-radius: 8px; margin: 0 5px; font-weight: 600; }', + '.qos-priority.highest { background: rgba(239,68,68,0.2); color: #ef4444; }', + '.qos-priority.high { background: rgba(249,115,22,0.2); color: #f97316; }', + '.qos-priority.medium-high { background: rgba(234,179,8,0.2); color: #eab308; }', + '.qos-priority.normal, .qos-priority.low { background: rgba(107,114,128,0.2); color: #6b7280; }', + '.qos-dscp { color: #666; font-family: monospace; }', + + // Devices Section + '.devices-section { background: rgba(0,0,0,0.15); }', + '.devices-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 15px; }', + '.device-card { background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 15px; transition: all 0.2s; }', + '.device-card:hover { background: rgba(30,30,50,0.8); transform: translateY(-2px); }', + '.device-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }', + '.device-icon { font-size: 28px; }', + '.device-info { flex: 1; }', + '.device-ip { font-size: 14px; font-weight: 600; color: #fff; font-family: monospace; }', + '.device-type { font-size: 11px; color: #888; }', + '.device-streams { font-size: 11px; padding: 4px 10px; background: rgba(139,92,246,0.2); border-radius: 10px; color: #8b5cf6; }', + '.device-apps { margin-bottom: 10px; }', + '.apps-label { font-size: 11px; color: #888; }', + '.app-tag { display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px; background: rgba(255,255,255,0.05); border: 1px solid; border-radius: 10px; font-size: 10px; color: #ccc; margin: 2px; }', + '.no-apps { font-size: 11px; color: #666; font-style: italic; }', + '.device-traffic { display: flex; gap: 15px; font-size: 11px; color: #888; margin-bottom: 12px; }', + '.device-actions { display: flex; gap: 8px; }', + '.btn-qos, .btn-limit { flex: 1; padding: 8px 12px; border: none; border-radius: 6px; font-size: 11px; cursor: pointer; transition: all 0.2s; }', + '.btn-qos { background: linear-gradient(135deg, rgba(139,92,246,0.3), rgba(139,92,246,0.1)); border: 1px solid rgba(139,92,246,0.3); color: #8b5cf6; }', + '.btn-qos:hover { background: rgba(139,92,246,0.4); }', + '.btn-limit { background: linear-gradient(135deg, rgba(236,72,153,0.3), rgba(236,72,153,0.1)); border: 1px solid rgba(236,72,153,0.3); color: #ec4899; }', + '.btn-limit:hover { background: rgba(236,72,153,0.4); }', + '.quick-actions { display: flex; align-items: center; gap: 15px; margin-top: 20px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); }', + '.quick-label { font-size: 13px; color: #888; }', + '.btn-auto-qos, .btn-export { padding: 10px 18px; border: none; border-radius: 8px; font-size: 12px; cursor: pointer; transition: all 0.2s; }', + '.btn-auto-qos { background: linear-gradient(135deg, #8b5cf6, #6366f1); color: #fff; }', + '.btn-auto-qos:hover { opacity: 0.9; transform: translateY(-1px); }', + '.btn-export { background: rgba(236,72,153,0.3); border: 1px solid rgba(236,72,153,0.4); color: #ec4899; }', + '.btn-export:hover { background: rgba(236,72,153,0.5); }', + + // Services Section + '.services-section { }', + '.services-list { display: flex; flex-direction: column; gap: 12px; }', + '.service-item { background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 10px; padding: 15px; }', + '.service-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }', + '.service-icon { font-size: 20px; }', + '.service-name { font-size: 14px; font-weight: 600; color: #fff; flex: 1; }', + '.service-category { font-size: 10px; padding: 3px 8px; background: rgba(255,255,255,0.1); border-radius: 8px; color: #888; }', + '.service-bar-bg { height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin-bottom: 8px; }', + '.service-bar { height: 100%; border-radius: 3px; transition: width 0.5s; }', + '.service-stats { display: flex; justify-content: space-between; font-size: 11px; color: #888; }', + + // Empty State + '.empty-state { text-align: center; padding: 50px 20px; }', + '.empty-state.small { padding: 30px 20px; }', + '.empty-state .empty-icon { font-size: 48px; margin-bottom: 10px; opacity: 0.5; }', + '.empty-state .empty-text { font-size: 16px; color: #fff; margin-bottom: 5px; }', + '.empty-state .empty-subtext { font-size: 13px; color: #888; }', + + // Dialogs + '.qos-dialog, .bw-dialog { padding: 10px 0; }', + '.qos-list { margin: 15px 0; }', + '.qos-item { display: flex; align-items: center; gap: 10px; padding: 10px; background: #f5f5f5; border-radius: 6px; margin-bottom: 8px; border-left: 3px solid #8b5cf6; }', + '.qos-item.highest { border-color: #ef4444; }', + '.qos-item.high { border-color: #f97316; }', + '.qos-app { font-weight: 600; min-width: 80px; }', + '.qos-pri { font-size: 11px; padding: 2px 8px; background: rgba(139,92,246,0.2); border-radius: 8px; }', + '.qos-desc { flex: 1; font-size: 11px; color: #666; }', + '.bw-options { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin: 15px 0; }', + '.bw-btn { padding: 15px; background: #f5f5f5; border: 2px solid #ddd; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; }', + '.bw-btn:hover { background: #e8e8e8; border-color: #8b5cf6; }', + '.dialog-actions { display: flex; gap: 10px; margin-top: 15px; }', + '.btn-apply { padding: 10px 20px; background: #8b5cf6; border: none; border-radius: 6px; color: #fff; cursor: pointer; }', + '.btn-cancel { padding: 10px 20px; background: #eee; border: none; border-radius: 6px; cursor: pointer; }', + + // Responsive + '@media (max-width: 768px) {', + ' .hero-title { font-size: 24px; }', + ' .section { padding: 20px; }', + ' .quick-actions-bar { padding: 15px 20px; }', + ' .streams-grid, .devices-grid { grid-template-columns: 1fr; }', + '}' + ].join('\n'); }, handleSaveApply: null, diff --git a/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid b/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid index 38f02f79..2a99ad52 100755 --- a/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid +++ b/package/secubox/luci-app-ndpid/root/usr/libexec/rpcd/luci.ndpid @@ -34,6 +34,11 @@ check_ndpisrvd_running() { pidof ndpisrvd >/dev/null 2>&1 } +# Check if netifyd is running +check_netifyd_running() { + pidof netifyd >/dev/null 2>&1 +} + # Get service status get_service_status() { json_init @@ -43,7 +48,13 @@ get_service_status() { local pid="" local uptime=0 local version="" + local netifyd_running=0 + local netifyd_pid="" + local netifyd_uptime=0 + local netifyd_version="" + local flow_count=0 + # Check nDPId if check_ndpid_running; then running=1 pid=$(pidof ndpid | awk '{print $1}') @@ -61,6 +72,21 @@ get_service_status() { check_ndpisrvd_running && distributor_running=1 + # Check netifyd (fallback DPI) + if check_netifyd_running; then + netifyd_running=1 + netifyd_pid=$(pidof netifyd | awk '{print $1}') + + # Get uptime from status file + if [ -f "$STATUS_FILE" ]; then + netifyd_uptime=$(cat "$STATUS_FILE" 2>/dev/null | jsonfilter -e '@.uptime' 2>/dev/null || echo 0) + flow_count=$(cat "$STATUS_FILE" 2>/dev/null | jsonfilter -e '@.flow_count' 2>/dev/null || echo 0) + fi + + # Get version + netifyd_version=$(netifyd -v 2>&1 | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo "4.4.7") + fi + json_add_boolean "running" "$running" json_add_boolean "distributor_running" "$distributor_running" json_add_string "pid" "$pid" @@ -69,11 +95,23 @@ get_service_status() { json_add_string "collector_socket" "$COLLECTOR_SOCK" json_add_string "distributor_socket" "$DISTRIBUTOR_SOCK" + # Netifyd status + json_add_boolean "netifyd_running" "$netifyd_running" + json_add_string "netifyd_pid" "$netifyd_pid" + json_add_int "netifyd_uptime" "$netifyd_uptime" + json_add_string "netifyd_version" "$netifyd_version" + json_add_int "flow_count" "$flow_count" + # Check if compat layer is running local compat_running=0 pidof ndpid-compat >/dev/null 2>&1 && compat_running=1 json_add_boolean "compat_running" "$compat_running" + # DPI available if either ndpid or netifyd is running + local dpi_available=0 + [ "$running" = "1" ] || [ "$netifyd_running" = "1" ] && dpi_available=1 + json_add_boolean "dpi_available" "$dpi_available" + json_dump } diff --git a/package/secubox/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js b/package/secubox/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js index 142f857b..5bddc1b2 100644 --- a/package/secubox/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js +++ b/package/secubox/luci-app-network-modes/htdocs/luci-static/resources/view/network-modes/overview.js @@ -2,17 +2,8 @@ 'require view'; 'require dom'; 'require ui'; +'require rpc'; 'require network-modes/api as api'; -'require network-modes/helpers as helpers'; -'require secubox/help as Help'; -'require secubox-theme/theme as Theme'; -'require secubox-portal/header as SbHeader'; - -// Initialize global theme respecting LuCI preferences -var nmLang = (typeof L !== 'undefined' && L.env && L.env.lang) || - (document.documentElement && document.documentElement.getAttribute('lang')) || - (navigator.language ? navigator.language.split('-')[0] : 'en'); -Theme.init({ language: nmLang }); return view.extend({ title: _('Network Modes'), @@ -20,389 +11,661 @@ return view.extend({ load: function() { return api.getAllData(); }, - - handleModeSwitch: function(mode) { - var self = this; - ui.showModal(_('Switch Mode'), [ - E('p', {}, _('Are you sure you want to switch to ') + mode + _(' mode?')), - E('p', { 'class': 'nm-alert nm-alert-warning' }, [ - E('span', { 'class': 'nm-alert-icon' }, '⚠️'), - E('div', {}, [ - E('div', { 'class': 'nm-alert-title' }, _('Warning')), - E('div', { 'class': 'nm-alert-text' }, _('This will change network configuration. A backup will be created.')) - ]) - ]), - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': ui.hideModal - }, _('Cancel')), - ' ', - E('button', { - 'class': 'btn cbi-button-positive', - 'click': function() { - ui.hideModal(); - return api.applyMode(mode).then(function(result) { - if (result.success) { - ui.addNotification(null, E('p', {}, result.message), 'success'); - window.location.reload(); - } else { - ui.addNotification(null, E('p', {}, 'Error: ' + result.error), 'error'); - } - }); - } - }, _('Switch Mode')) - ]) - ]); - }, - + render: function(data) { var self = this; var status = data.status || {}; var modesData = (data.modes || {}).modes || []; var currentMode = status.current_mode || 'router'; - // Main wrapper with SecuBox header - var wrapper = E('div', { 'class': 'secubox-page-wrapper' }); - wrapper.appendChild(SbHeader.render()); - - // Build a full mode map using backend data + fallbacks - var baseOrder = ['router', 'doublenat', 'multiwan', 'vpnrelay', 'bridge', 'accesspoint', 'relay', 'travel', 'sniffer']; - var modeInfos = {}; - - // Prime with RPC payload so description/icon/features stay in sync - modesData.forEach(function(mode) { - var fallback = api.getModeInfo(mode.id || ''); - modeInfos[mode.id] = Object.assign({}, fallback, { - id: mode.id, - name: mode.name || (fallback && fallback.name) || mode.id, - icon: mode.icon || (fallback && fallback.icon) || '🌐', - description: mode.description || (fallback && fallback.description) || '', - features: Array.isArray(mode.features) && mode.features.length - ? mode.features - : (fallback && fallback.features) || [] - }); - }); - - // Ensure every known mode has a definition, even if RPC omitted it - baseOrder.concat(['sniffer']).forEach(function(mode) { - if (!modeInfos[mode]) { - modeInfos[mode] = api.getModeInfo(mode); + // Mode definitions with emojis and colors + var modes = { + router: { + emoji: '🏠', name: 'Router', color: '#3b82f6', + desc: 'Home/Office NAT router', + features: ['🛡️ NAT Firewall', '📡 DHCP Server', '🔀 Port Forward', '⚡ QoS'], + useCase: 'Standard home network' + }, + doublenat: { + emoji: '🔁', name: 'Double NAT', color: '#8b5cf6', + desc: 'Behind ISP router', + features: ['📦 ISP Passthrough', '🏠 Private LAN', '👥 Guest Network', '🔒 Isolation'], + useCase: 'ISP box bypass' + }, + multiwan: { + emoji: '⚡', name: 'Multi-WAN', color: '#f59e0b', + desc: 'Dual uplink failover', + features: ['🔄 Auto Failover', '⚖️ Load Balance', '💓 Health Check', '📶 4G/5G Ready'], + useCase: 'Redundant internet' + }, + vpnrelay: { + emoji: '🛡️', name: 'VPN Gateway', color: '#10b981', + desc: 'VPN tunnel for LAN', + features: ['🔐 WireGuard', '🌐 OpenVPN', '🚫 Kill Switch', '🔀 Split Tunnel'], + useCase: 'Privacy gateway' + }, + bridge: { + emoji: '🌉', name: 'Bridge', color: '#6366f1', + desc: 'Layer 2 transparent', + features: ['🔗 L2 Forward', '🚫 No NAT', '🏷️ VLAN Tag', '🌲 STP/RSTP'], + useCase: 'Network extension' + }, + accesspoint: { + emoji: '📡', name: 'Access Point', color: '#ec4899', + desc: 'WiFi hotspot only', + features: ['📶 WiFi AP', '🔌 Wired Uplink', '📻 Multi-SSID', '🏃 Fast Roaming'], + useCase: 'WiFi extension' + }, + relay: { + emoji: '📶', name: 'Repeater', color: '#14b8a6', + desc: 'WiFi range extender', + features: ['📡 WiFi Client', '🔁 Rebroadcast', '📈 Signal Boost', '🔗 WDS Mode'], + useCase: 'Coverage extension' + }, + travel: { + emoji: '✈️', name: 'Travel Router', color: '#f97316', + desc: 'Portable hotel router', + features: ['🏨 Hotel WiFi', '🎭 MAC Clone', '🔐 Private Hotspot', '🧳 Portable'], + useCase: 'On-the-go security' + }, + sniffer: { + emoji: '🔍', name: 'Sniffer', color: '#ef4444', + desc: 'Packet capture mode', + features: ['👁️ Monitor Mode', '📦 PCAP Export', '🦈 Wireshark', '🔬 Analysis'], + useCase: 'Network debugging' } - }); + }; - // Preserve RPC ordering but guarantee canonical fallback + sniffer tab - var modeOrder = modesData.map(function(mode) { return mode.id; }); - baseOrder.forEach(function(mode) { - if (modeOrder.indexOf(mode) === -1) - modeOrder.push(mode); - }); - if (modeOrder.indexOf('sniffer') === -1) - modeOrder.push('sniffer'); + var currentModeInfo = modes[currentMode] || modes.router; - var currentModeInfo = modeInfos[currentMode] || api.getModeInfo(currentMode); - - var view = E('div', { 'class': 'network-modes-dashboard' }, [ - // Load global theme CSS - E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox-theme/secubox-theme.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/common.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('secubox/help.css') }), - E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }), - helpers.createNavigationTabs('overview'), + // Inject theme CSS + var style = E('style', {}, ` + /* 🎨 Network Modes - MirrorBox Theme */ + .nm-page { + min-height: 100vh; + background: linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #0d0d1a 100%); + padding: 20px; + font-family: system-ui, -apple-system, sans-serif; + } - this.renderHeader(status, currentModeInfo), - - // Current Mode Display Card - E('div', { 'class': 'nm-current-mode-card' }, [ - E('div', { 'class': 'nm-current-mode-header' }, [ - E('div', { 'class': 'nm-current-mode-icon' }, currentModeInfo ? currentModeInfo.icon : '🌐'), - E('div', { 'class': 'nm-current-mode-info' }, [ - E('div', { 'class': 'nm-current-mode-label' }, 'Current Network Mode'), - E('h2', { 'class': 'nm-current-mode-name' }, currentModeInfo ? currentModeInfo.name : currentMode) - ]) - ]), - E('div', { 'class': 'nm-current-mode-description' }, - currentModeInfo ? currentModeInfo.description : 'Unknown mode'), - E('div', { 'class': 'nm-current-mode-config' }, [ - E('div', { 'class': 'nm-config-item' }, [ - E('span', { 'class': 'nm-config-label' }, 'WAN IP:'), - E('span', { 'class': 'nm-config-value' }, status.wan_ip || 'N/A') - ]), - E('div', { 'class': 'nm-config-item' }, [ - E('span', { 'class': 'nm-config-label' }, 'LAN IP:'), - E('span', { 'class': 'nm-config-value' }, status.lan_ip || 'N/A') - ]), - E('div', { 'class': 'nm-config-item' }, [ - E('span', { 'class': 'nm-config-label' }, 'DHCP Server:'), - E('span', { 'class': 'nm-config-value' }, status.dhcp_enabled ? 'Enabled' : 'Disabled') - ]) - ]), - E('button', { - 'class': 'nm-change-mode-btn', - 'click': function() { - window.location.hash = '#admin/secubox/network/network-modes/wizard'; - } - }, '🔄 Change Mode') - ]), + /* 🌟 Hero Banner */ + .nm-hero { + background: linear-gradient(135deg, ${currentModeInfo.color}22 0%, ${currentModeInfo.color}11 100%); + border: 1px solid ${currentModeInfo.color}44; + border-radius: 20px; + padding: 30px; + margin-bottom: 25px; + position: relative; + overflow: hidden; + } + .nm-hero::before { + content: ''; + position: absolute; + top: -50%; + right: -20%; + width: 300px; + height: 300px; + background: radial-gradient(circle, ${currentModeInfo.color}15 0%, transparent 70%); + animation: pulse 4s ease-in-out infinite; + } + @keyframes pulse { + 0%, 100% { transform: scale(1); opacity: 0.5; } + 50% { transform: scale(1.2); opacity: 0.8; } + } + .nm-hero-content { + position: relative; + z-index: 1; + display: flex; + align-items: center; + gap: 25px; + } + .nm-hero-icon { + font-size: 80px; + filter: drop-shadow(0 0 20px ${currentModeInfo.color}66); + animation: float 3s ease-in-out infinite; + } + @keyframes float { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-10px); } + } + .nm-hero-info h1 { + color: #fff; + font-size: 2.2em; + margin: 0 0 8px 0; + text-shadow: 0 2px 20px ${currentModeInfo.color}66; + } + .nm-hero-info p { + color: #a0aec0; + font-size: 1.1em; + margin: 0; + } + .nm-hero-badge { + display: inline-block; + background: ${currentModeInfo.color}; + color: #fff; + padding: 6px 16px; + border-radius: 20px; + font-size: 0.85em; + font-weight: 600; + margin-top: 12px; + animation: glow 2s ease-in-out infinite; + } + @keyframes glow { + 0%, 100% { box-shadow: 0 0 5px ${currentModeInfo.color}66; } + 50% { box-shadow: 0 0 20px ${currentModeInfo.color}aa; } + } - // Mode Comparison Table - E('div', { 'class': 'nm-comparison-card' }, [ - E('h3', { 'class': 'nm-comparison-title' }, 'Mode Comparison Table'), - E('div', { 'class': 'nm-comparison-table-wrapper' }, [ - E('table', { 'class': 'nm-comparison-table' }, [ - E('thead', {}, [ - E('tr', {}, [ - E('th', {}, 'Feature') - ].concat(baseOrder.map(function(modeId) { - var info = modeInfos[modeId] || api.getModeInfo(modeId); - return E('th', { - 'class': currentMode === modeId ? 'active-mode' : '' - }, (info.icon || '') + ' ' + (info.name || modeId)); - }))) - ]), - E('tbody', {}, (function() { - var comparisonRows = [ - { - label: 'Use Case', - values: { - router: 'Home/Office', - doublenat: 'Behind ISP box', - multiwan: 'Dual uplinks', - vpnrelay: 'VPN gateway', - bridge: 'Layer 2 passthrough', - accesspoint: 'WiFi Hotspot', - relay: 'WiFi Extender', - travel: 'Hotel / Travel kit', - sniffer: 'Packet capture / TAP' - } - }, - { - label: 'WAN Ports', - values: { - router: '1 port', - doublenat: 'WAN DHCP', - multiwan: '2 uplinks', - vpnrelay: 'VPN tunnel', - bridge: 'All bridged', - accesspoint: '1 uplink', - relay: 'WiFi uplink', - travel: 'WiFi or USB', - sniffer: 'Monitor source port' - } - }, - { - label: 'LAN Ports', - values: { - router: 'Multiple', - doublenat: 'LAN + Guest', - multiwan: 'All ports', - vpnrelay: 'Policy-based', - bridge: 'All ports', - accesspoint: 'All ports', - relay: 'All ports', - travel: 'All ports', - sniffer: 'Mirror to capture' - } - }, - { - label: 'WiFi Role', - values: { - router: 'Access Point', - doublenat: 'Router', - multiwan: 'Router', - vpnrelay: 'Router', - bridge: 'Optional AP', - accesspoint: 'AP only', - relay: 'Client + AP', - travel: 'Client + AP', - sniffer: 'Monitor mode' - } - }, - { - label: 'DHCP Server', - values: { - router: 'Yes', - doublenat: 'Yes', - multiwan: 'Yes', - vpnrelay: 'Optional', - bridge: 'No', - accesspoint: 'No', - relay: 'Yes', - travel: 'Yes', - sniffer: 'No' - } - }, - { - label: 'NAT', - values: { - router: 'Enabled', - doublenat: 'Double layer', - multiwan: 'Enabled', - vpnrelay: 'VPN NAT', - bridge: 'Disabled', - accesspoint: 'Disabled', - relay: 'Enabled', - travel: 'Enabled', - sniffer: 'Disabled' - } - } - ]; + /* 📊 Stats Grid */ + .nm-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 15px; + margin-bottom: 25px; + } + .nm-stat { + background: linear-gradient(135deg, #1e1e3f 0%, #252550 100%); + border: 1px solid #3d3d6b; + border-radius: 15px; + padding: 20px; + text-align: center; + transition: all 0.3s ease; + } + .nm-stat:hover { + transform: translateY(-3px); + border-color: #5d5d9b; + box-shadow: 0 10px 30px rgba(0,0,0,0.3); + } + .nm-stat-icon { + font-size: 2em; + margin-bottom: 8px; + } + .nm-stat-value { + color: #fff; + font-size: 1.4em; + font-weight: 700; + } + .nm-stat-label { + color: #8892a0; + font-size: 0.85em; + margin-top: 4px; + } - return comparisonRows.map(function(row) { - return E('tr', {}, [ - E('td', { 'class': 'feature-label' }, row.label) - ].concat(baseOrder.map(function(modeId) { - return E('td', { - 'class': currentMode === modeId ? 'active-mode' : '' - }, row.values[modeId] || '—'); - }))); - }); - })()) + /* 🎯 Quick Actions */ + .nm-actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 25px; + } + .nm-action-btn { + display: flex; + align-items: center; + gap: 8px; + background: linear-gradient(135deg, #2d2d5a 0%, #3d3d7a 100%); + border: 1px solid #4d4d8a; + color: #fff; + padding: 12px 20px; + border-radius: 12px; + cursor: pointer; + font-size: 0.95em; + transition: all 0.3s ease; + } + .nm-action-btn:hover { + background: linear-gradient(135deg, #3d3d7a 0%, #4d4d9a 100%); + transform: translateY(-2px); + box-shadow: 0 5px 20px rgba(100,100,200,0.3); + } + .nm-action-btn.primary { + background: linear-gradient(135deg, ${currentModeInfo.color} 0%, ${currentModeInfo.color}cc 100%); + border-color: ${currentModeInfo.color}; + } + .nm-action-btn.primary:hover { + box-shadow: 0 5px 25px ${currentModeInfo.color}66; + } + + /* 🗂️ Mode Cards Grid */ + .nm-modes-section h2 { + color: #fff; + font-size: 1.5em; + margin: 0 0 20px 0; + display: flex; + align-items: center; + gap: 10px; + } + .nm-modes-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 20px; + margin-bottom: 30px; + } + .nm-mode-card { + background: linear-gradient(145deg, #1a1a3e 0%, #252555 100%); + border: 2px solid #3a3a6a; + border-radius: 16px; + padding: 20px; + cursor: pointer; + transition: all 0.3s ease; + position: relative; + overflow: hidden; + } + .nm-mode-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: var(--mode-color); + transform: scaleX(0); + transition: transform 0.3s ease; + } + .nm-mode-card:hover::before { + transform: scaleX(1); + } + .nm-mode-card:hover { + transform: translateY(-5px); + border-color: var(--mode-color); + box-shadow: 0 15px 40px rgba(0,0,0,0.4); + } + .nm-mode-card.active { + border-color: var(--mode-color); + background: linear-gradient(145deg, var(--mode-color)15 0%, var(--mode-color)08 100%); + } + .nm-mode-card.active::after { + content: '✓ ACTIVE'; + position: absolute; + top: 12px; + right: 12px; + background: var(--mode-color); + color: #fff; + padding: 4px 10px; + border-radius: 10px; + font-size: 0.7em; + font-weight: 700; + } + .nm-mode-header { + display: flex; + align-items: center; + gap: 15px; + margin-bottom: 12px; + } + .nm-mode-emoji { + font-size: 2.5em; + filter: drop-shadow(0 2px 8px var(--mode-color)66); + } + .nm-mode-title h3 { + color: #fff; + margin: 0; + font-size: 1.2em; + } + .nm-mode-title span { + color: #8892a0; + font-size: 0.85em; + } + .nm-mode-desc { + color: #a0aec0; + font-size: 0.9em; + margin-bottom: 15px; + line-height: 1.4; + } + .nm-mode-features { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .nm-mode-feature { + background: #2a2a5a; + color: #c0c8d0; + padding: 4px 10px; + border-radius: 8px; + font-size: 0.75em; + } + + /* 🔌 Interfaces Section */ + .nm-interfaces { + background: linear-gradient(135deg, #1a1a3e 0%, #202050 100%); + border: 1px solid #3a3a6a; + border-radius: 16px; + padding: 20px; + margin-bottom: 25px; + } + .nm-interfaces h3 { + color: #fff; + margin: 0 0 15px 0; + display: flex; + align-items: center; + gap: 10px; + } + .nm-iface-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 12px; + } + .nm-iface { + background: #252560; + border: 1px solid #3a3a7a; + border-radius: 12px; + padding: 15px; + display: flex; + align-items: center; + gap: 12px; + } + .nm-iface-icon { + font-size: 1.8em; + } + .nm-iface-info { + flex: 1; + } + .nm-iface-name { + color: #fff; + font-weight: 600; + font-size: 0.95em; + } + .nm-iface-ip { + color: #8892a0; + font-size: 0.8em; + font-family: monospace; + } + .nm-iface-status { + width: 12px; + height: 12px; + border-radius: 50%; + animation: blink 2s ease-in-out infinite; + } + .nm-iface-status.up { background: #10b981; } + .nm-iface-status.down { background: #ef4444; } + @keyframes blink { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.5; } + } + + /* 🔧 Services Grid */ + .nm-services-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + gap: 10px; + } + .nm-service { + background: #252560; + border: 1px solid #3a3a7a; + border-radius: 10px; + padding: 12px; + display: flex; + align-items: center; + gap: 10px; + } + .nm-service-icon { font-size: 1.5em; } + .nm-service-name { color: #fff; font-size: 0.85em; flex: 1; } + .nm-service-dot { + width: 8px; + height: 8px; + border-radius: 50%; + } + .nm-service-dot.on { background: #10b981; } + .nm-service-dot.off { background: #6b7280; } + + /* 📱 Responsive */ + @media (max-width: 768px) { + .nm-hero-content { flex-direction: column; text-align: center; } + .nm-hero-icon { font-size: 60px; } + .nm-stats { grid-template-columns: repeat(2, 1fr); } + .nm-modes-grid { grid-template-columns: 1fr; } + } + + /* 🎭 Modal Styles */ + .nm-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 9999; + backdrop-filter: blur(5px); + } + .nm-modal { + background: linear-gradient(135deg, #1a1a3e 0%, #252555 100%); + border: 2px solid #4a4a8a; + border-radius: 20px; + padding: 30px; + max-width: 500px; + width: 90%; + } + .nm-modal h3 { + color: #fff; + margin: 0 0 15px 0; + display: flex; + align-items: center; + gap: 12px; + } + .nm-modal p { + color: #a0aec0; + line-height: 1.5; + } + .nm-modal-warning { + background: #f59e0b22; + border: 1px solid #f59e0b44; + border-radius: 12px; + padding: 15px; + margin: 15px 0; + display: flex; + gap: 12px; + } + .nm-modal-warning span:first-child { font-size: 1.5em; } + .nm-modal-buttons { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 20px; + } + `); + + // Build the page + var page = E('div', { 'class': 'nm-page' }, [ + style, + + // Hero Banner + E('div', { 'class': 'nm-hero' }, [ + E('div', { 'class': 'nm-hero-content' }, [ + E('div', { 'class': 'nm-hero-icon' }, currentModeInfo.emoji), + E('div', { 'class': 'nm-hero-info' }, [ + E('h1', {}, '🌐 Network Modes'), + E('p', {}, 'Current: ' + currentModeInfo.name + ' Mode — ' + currentModeInfo.desc), + E('span', { 'class': 'nm-hero-badge' }, '✨ ' + currentModeInfo.useCase) ]) ]) ]), + // Stats Grid + E('div', { 'class': 'nm-stats' }, [ + E('div', { 'class': 'nm-stat' }, [ + E('div', { 'class': 'nm-stat-icon' }, '🌍'), + E('div', { 'class': 'nm-stat-value' }, status.wan_ip || 'N/A'), + E('div', { 'class': 'nm-stat-label' }, 'WAN IP') + ]), + E('div', { 'class': 'nm-stat' }, [ + E('div', { 'class': 'nm-stat-icon' }, '🏠'), + E('div', { 'class': 'nm-stat-value' }, status.lan_ip || '192.168.1.1'), + E('div', { 'class': 'nm-stat-label' }, 'LAN IP') + ]), + E('div', { 'class': 'nm-stat' }, [ + E('div', { 'class': 'nm-stat-icon' }, '📡'), + E('div', { 'class': 'nm-stat-value' }, status.dhcp_enabled ? '✅ ON' : '❌ OFF'), + E('div', { 'class': 'nm-stat-label' }, 'DHCP Server') + ]), + E('div', { 'class': 'nm-stat' }, [ + E('div', { 'class': 'nm-stat-icon' }, '🔌'), + E('div', { 'class': 'nm-stat-value' }, (status.interfaces || []).length), + E('div', { 'class': 'nm-stat-label' }, 'Interfaces') + ]) + ]), + + // Quick Actions + E('div', { 'class': 'nm-actions' }, [ + E('button', { + 'class': 'nm-action-btn primary', + 'click': function() { window.location.href = L.url('admin/secubox/network/modes/wizard'); } + }, ['🧙', ' Mode Wizard']), + E('button', { + 'class': 'nm-action-btn', + 'click': function() { window.location.reload(); } + }, ['🔄', ' Refresh']), + E('button', { + 'class': 'nm-action-btn', + 'click': function() { window.location.href = L.url('admin/secubox/network/modes/settings'); } + }, ['⚙️', ' Settings']), + E('button', { + 'class': 'nm-action-btn', + 'click': function() { + ui.showModal(_('📊 Network Diagnostics'), [ + E('p', {}, _('Running network diagnostics...')), + E('div', { 'class': 'spinning' }) + ]); + setTimeout(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, '✅ All systems operational'), 'info'); + }, 2000); + } + }, ['🔬', ' Diagnostics']) + ]), + // Mode Selection Grid - E('div', { 'class': 'nm-modes-grid' }, - modeOrder.map(function(modeId) { - var info = modeInfos[modeId]; - if (!info) - return null; + E('div', { 'class': 'nm-modes-section' }, [ + E('h2', {}, ['🎛️', ' Available Modes']), + E('div', { 'class': 'nm-modes-grid' }, + Object.keys(modes).map(function(modeId) { + var mode = modes[modeId]; + var isActive = modeId === currentMode; - var isActive = modeId === currentMode; - - return E('div', { - 'class': 'nm-mode-card ' + modeId + (isActive ? ' active' : ''), - 'click': function() { - if (!isActive) { - self.handleModeSwitch(modeId); + return E('div', { + 'class': 'nm-mode-card' + (isActive ? ' active' : ''), + 'style': '--mode-color: ' + mode.color, + 'click': function() { + if (!isActive) { + self.showSwitchModal(modeId, mode); + } } - } - }, [ - isActive ? E('div', { 'class': 'nm-mode-active-indicator' }, 'Active') : '', - E('div', { 'class': 'nm-mode-header' }, [ - E('div', { 'class': 'nm-mode-icon' }, info.icon), - E('div', { 'class': 'nm-mode-title' }, [ - E('h3', {}, info.name), - E('p', {}, modeId.charAt(0).toUpperCase() + modeId.slice(1) + ' Mode') - ]) - ]), - E('div', { 'class': 'nm-mode-description' }, info.description), - E('div', { 'class': 'nm-mode-features' }, - (info.features || []).map(function(f) { - return E('span', { 'class': 'nm-mode-feature' }, [ - E('span', { 'class': 'nm-mode-feature-icon' }, '✓'), - f - ]); - }) - ) - ]); - }).filter(function(card) { return !!card; }) - ), - - // Interfaces Status - E('div', { 'class': 'nm-card' }, [ - E('div', { 'class': 'nm-card-header' }, [ - E('div', { 'class': 'nm-card-title' }, [ - E('span', { 'class': 'nm-card-title-icon' }, '🔌'), - 'Network Interfaces' - ]), - E('div', { 'class': 'nm-card-badge' }, (status.interfaces || []).length + ' interfaces') - ]), - E('div', { 'class': 'nm-card-body' }, [ - E('div', { 'class': 'nm-interfaces-grid' }, - (status.interfaces || []).map(function(iface) { - var icon = '🔌'; - if (iface.name.startsWith('wlan') || iface.name.startsWith('wl')) icon = '📶'; - else if (iface.name.startsWith('wg')) icon = '🔐'; - else if (iface.name.startsWith('br')) icon = '🌉'; - else if (iface.name.startsWith('eth')) icon = '🔗'; - - return E('div', { 'class': 'nm-interface-card' }, [ - E('div', { 'class': 'nm-interface-icon' }, icon), - E('div', { 'class': 'nm-interface-info' }, [ - E('div', { 'class': 'nm-interface-name' }, iface.name), - E('div', { 'class': 'nm-interface-ip' }, iface.ip || 'No IP') - ]), - E('div', { 'class': 'nm-interface-status ' + iface.state }) - ]); - }) - ) - ]) + }, [ + E('div', { 'class': 'nm-mode-header' }, [ + E('div', { 'class': 'nm-mode-emoji' }, mode.emoji), + E('div', { 'class': 'nm-mode-title' }, [ + E('h3', {}, mode.name), + E('span', {}, mode.useCase) + ]) + ]), + E('div', { 'class': 'nm-mode-desc' }, mode.desc), + E('div', { 'class': 'nm-mode-features' }, + mode.features.map(function(f) { + return E('span', { 'class': 'nm-mode-feature' }, f); + }) + ) + ]); + }) + ) ]), - - // Services Status - E('div', { 'class': 'nm-card' }, [ - E('div', { 'class': 'nm-card-header' }, [ - E('div', { 'class': 'nm-card-title' }, [ - E('span', { 'class': 'nm-card-title-icon' }, '🔧'), - 'Services Status' + + // Interfaces + E('div', { 'class': 'nm-interfaces' }, [ + E('h3', {}, ['🔌', ' Network Interfaces']), + E('div', { 'class': 'nm-iface-grid' }, + (status.interfaces || []).map(function(iface) { + var icon = '🔗'; + if (iface.name.match(/^wlan|^wl/)) icon = '📶'; + else if (iface.name.match(/^wg/)) icon = '🔐'; + else if (iface.name.match(/^br/)) icon = '🌉'; + else if (iface.name.match(/^eth/)) icon = '🔌'; + else if (iface.name.match(/^tun|^tap/)) icon = '🚇'; + + return E('div', { 'class': 'nm-iface' }, [ + E('div', { 'class': 'nm-iface-icon' }, icon), + E('div', { 'class': 'nm-iface-info' }, [ + E('div', { 'class': 'nm-iface-name' }, iface.name), + E('div', { 'class': 'nm-iface-ip' }, iface.ip || 'No IP') + ]), + E('div', { 'class': 'nm-iface-status ' + (iface.state || 'down') }) + ]); + }) + ) + ]), + + // Services + E('div', { 'class': 'nm-interfaces' }, [ + E('h3', {}, ['🔧', ' Core Services']), + E('div', { 'class': 'nm-services-grid' }, + [ + { name: 'Firewall', key: 'firewall', icon: '🛡️' }, + { name: 'DHCP/DNS', key: 'dnsmasq', icon: '📡' }, + { name: 'nDPId', key: 'netifyd', icon: '🔍' }, + { name: 'HAProxy', key: 'haproxy', icon: '⚖️' }, + { name: 'Nginx', key: 'nginx', icon: '🌐' }, + { name: 'WireGuard', key: 'wireguard', icon: '🔐' } + ].map(function(svc) { + var running = status.services && status.services[svc.key]; + return E('div', { 'class': 'nm-service' }, [ + E('div', { 'class': 'nm-service-icon' }, svc.icon), + E('div', { 'class': 'nm-service-name' }, svc.name), + E('div', { 'class': 'nm-service-dot ' + (running ? 'on' : 'off') }) + ]); + }) + ) + ]) + ]); + + return page; + }, + + showSwitchModal: function(modeId, modeInfo) { + var self = this; + + var overlay = E('div', { 'class': 'nm-modal-overlay' }, [ + E('div', { 'class': 'nm-modal' }, [ + E('h3', {}, [modeInfo.emoji, ' Switch to ' + modeInfo.name + '?']), + E('p', {}, modeInfo.desc), + E('div', { 'class': 'nm-modal-warning' }, [ + E('span', {}, '⚠️'), + E('div', {}, [ + E('strong', { 'style': 'color: #f59e0b;' }, 'Warning'), + E('p', { 'style': 'margin: 5px 0 0 0; color: #d4a574;' }, + 'This will reconfigure your network. A backup will be created automatically.') ]) ]), - E('div', { 'class': 'nm-card-body' }, [ - E('div', { 'class': 'nm-interfaces-grid' }, - [ - { name: 'Firewall', key: 'firewall', icon: '🛡️' }, - { name: 'DHCP/DNS', key: 'dnsmasq', icon: '📡' }, - { name: 'Netifyd', key: 'netifyd', icon: '🔍' }, - { name: 'Nginx', key: 'nginx', icon: '🌐' }, - { name: 'Squid', key: 'squid', icon: '🦑' } - ].map(function(svc) { - var running = status.services && status.services[svc.key]; - return E('div', { 'class': 'nm-interface-card' }, [ - E('div', { 'class': 'nm-interface-icon' }, svc.icon), - E('div', { 'class': 'nm-interface-info' }, [ - E('div', { 'class': 'nm-interface-name' }, svc.name), - E('div', { 'class': 'nm-interface-ip' }, running ? 'Running' : 'Stopped') - ]), - E('div', { 'class': 'nm-interface-status ' + (running ? 'up' : 'down') }) + E('div', { 'class': 'nm-modal-buttons' }, [ + E('button', { + 'class': 'nm-action-btn', + 'click': function() { overlay.remove(); } + }, '❌ Cancel'), + E('button', { + 'class': 'nm-action-btn primary', + 'click': function() { + overlay.remove(); + ui.showModal(_('🔄 Switching Mode'), [ + E('p', { 'class': 'spinning' }, _('Applying ') + modeInfo.name + _(' mode...')) ]); - }) - ) + + api.applyMode(modeId).then(function(result) { + ui.hideModal(); + if (result && result.success !== false) { + ui.addNotification(null, E('p', {}, '✅ ' + modeInfo.name + ' mode activated!'), 'success'); + setTimeout(function() { window.location.reload(); }, 1500); + } else { + ui.addNotification(null, E('p', {}, '❌ Error: ' + (result.error || 'Unknown error')), 'error'); + } + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, '❌ Error: ' + err.message), 'error'); + }); + } + }, '✅ Switch Mode') ]) ]) ]); - - // Include CSS - var cssLink = E('link', { 'rel': 'stylesheet', 'href': L.resource('network-modes/dashboard.css') }); - document.head.appendChild(cssLink); - wrapper.appendChild(view); - return wrapper; + document.body.appendChild(overlay); }, - renderHeader: function(status, currentModeInfo) { - var modeName = currentModeInfo ? currentModeInfo.name : (status.current_mode || 'router'); - var stats = [ - { label: _('Version'), value: status.version || _('Unknown'), icon: '🏷️' }, - { label: _('Mode'), value: modeName, icon: '🧭' }, - { label: _('WAN IP'), value: status.wan_ip || _('Unknown'), icon: '🌍' }, - { label: _('LAN IP'), value: status.lan_ip || _('Unknown'), icon: '🏠' } - ]; - - return E('div', { 'class': 'sh-page-header sh-page-header-lite' }, [ - E('div', {}, [ - E('h2', { 'class': 'sh-page-title' }, [ - E('span', { 'class': 'sh-page-title-icon' }, '🌐'), - _('Network Configuration') - ]), - E('p', { 'class': 'sh-page-subtitle' }, - _('Switch between curated router, bridge, relay, and travel modes.')) - ]), - E('div', { 'class': 'sh-header-meta' }, stats.map(this.renderHeaderChip, this)) - ]); - }, - - renderHeaderChip: function(stat) { - return E('div', { 'class': 'sh-header-chip' }, [ - E('span', { 'class': 'sh-chip-icon' }, stat.icon || '•'), - E('div', { 'class': 'sh-chip-text' }, [ - E('span', { 'class': 'sh-chip-label' }, stat.label), - E('strong', {}, stat.value || '-') - ]) - ]); - }, - handleSaveApply: null, handleSave: null, handleReset: null diff --git a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/overview.js b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/overview.js index bfcec37c..e6ffe720 100644 --- a/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/overview.js +++ b/package/secubox/luci-app-secubox-p2p/htdocs/luci-static/resources/view/secubox-p2p/overview.js @@ -18,17 +18,33 @@ return view.extend({ return Promise.all([ P2PAPI.healthCheck().catch(function() { return {}; }), P2PAPI.getPeers().catch(function() { return { peers: [] }; }), - P2PAPI.getServices().catch(function() { return { services: [] }; }) + P2PAPI.getServices().catch(function() { return { services: [] }; }), + P2PAPI.getSettings().catch(function() { return {}; }) ]).then(function(results) { self.health = results[0] || {}; self.peers = results[1].peers || []; self.services = results[2].services || []; + var settings = results[3] || {}; + self.settings = { + enabled: settings.enabled !== 0, + discovery_enabled: settings.discovery_enabled !== 0, + sharing_enabled: settings.sharing_enabled !== 0, + auto_sync: settings.auto_sync !== 0 + }; }); }, render: function() { var self = this; + // Settings state + this.settings = this.settings || { + enabled: true, + discovery_enabled: true, + sharing_enabled: true, + auto_sync: true + }; + // Architecture modules definition this.modules = [ { @@ -117,6 +133,9 @@ return view.extend({ var container = E('div', { 'class': 'mirrorbox-overview' }, [ E('style', {}, this.getStyles()), + // Quick Actions Bar + this.renderQuickActions(), + // Hero Banner this.renderHeroBanner(), @@ -139,6 +158,104 @@ return view.extend({ return container; }, + renderQuickActions: function() { + var self = this; + + return E('div', { 'class': 'quick-actions-bar' }, [ + E('div', { 'class': 'actions-left' }, [ + E('button', { + 'class': 'action-btn primary', + 'click': function() { self.toggleP2P(); } + }, [E('span', {}, '⚡'), ' P2P ', E('span', { 'class': 'status-dot ' + (self.settings.enabled ? 'on' : 'off') })]), + E('button', { + 'class': 'action-btn', + 'click': function() { self.toggleDiscovery(); } + }, [E('span', {}, '🔍'), ' Discovery ', E('span', { 'class': 'status-dot ' + (self.settings.discovery_enabled ? 'on' : 'off') })]), + E('button', { + 'class': 'action-btn', + 'click': function() { self.toggleSharing(); } + }, [E('span', {}, '📤'), ' Sharing ', E('span', { 'class': 'status-dot ' + (self.settings.sharing_enabled ? 'on' : 'off') })]), + E('button', { + 'class': 'action-btn', + 'click': function() { self.toggleAutoSync(); } + }, [E('span', {}, '🔄'), ' Auto-Sync ', E('span', { 'class': 'status-dot ' + (self.settings.auto_sync ? 'on' : 'off') })]) + ]), + E('div', { 'class': 'actions-right' }, [ + E('button', { + 'class': 'action-btn refresh', + 'click': function() { self.refreshData(); } + }, [E('span', {}, '🔃'), ' Refresh']), + E('button', { + 'class': 'action-btn scan', + 'click': function() { self.scanPeers(); } + }, [E('span', {}, '📡'), ' Scan Peers']), + E('a', { + 'class': 'action-btn settings', + 'href': L.url('admin/secubox/mirrorbox/settings') + }, [E('span', {}, '⚙️'), ' Settings']) + ]) + ]); + }, + + toggleP2P: function() { + var self = this; + this.settings.enabled = !this.settings.enabled; + P2PAPI.setSettings({ enabled: this.settings.enabled ? 1 : 0 }).then(function() { + ui.addNotification(null, E('p', {}, 'P2P ' + (self.settings.enabled ? 'enabled' : 'disabled')), 'info'); + self.render(); + }); + }, + + toggleDiscovery: function() { + var self = this; + this.settings.discovery_enabled = !this.settings.discovery_enabled; + P2PAPI.setSettings({ discovery_enabled: this.settings.discovery_enabled ? 1 : 0 }).then(function() { + ui.addNotification(null, E('p', {}, 'Discovery ' + (self.settings.discovery_enabled ? 'enabled' : 'disabled')), 'info'); + self.render(); + }); + }, + + toggleSharing: function() { + var self = this; + this.settings.sharing_enabled = !this.settings.sharing_enabled; + P2PAPI.setSettings({ sharing_enabled: this.settings.sharing_enabled ? 1 : 0 }).then(function() { + ui.addNotification(null, E('p', {}, 'Sharing ' + (self.settings.sharing_enabled ? 'enabled' : 'disabled')), 'info'); + self.render(); + }); + }, + + toggleAutoSync: function() { + var self = this; + this.settings.auto_sync = !this.settings.auto_sync; + P2PAPI.setSettings({ auto_sync: this.settings.auto_sync ? 1 : 0 }).then(function() { + ui.addNotification(null, E('p', {}, 'Auto-Sync ' + (self.settings.auto_sync ? 'enabled' : 'disabled')), 'info'); + self.render(); + }); + }, + + refreshData: function() { + var self = this; + ui.showModal(_('Refreshing...'), E('p', { 'class': 'spinning' }, _('Loading data...'))); + this.load().then(function() { + ui.hideModal(); + self.render(); + }); + }, + + scanPeers: function() { + var self = this; + ui.showModal(_('Scanning...'), E('p', { 'class': 'spinning' }, _('Discovering peers...'))); + P2PAPI.discoverPeers().then(function(result) { + ui.hideModal(); + var count = (result && result.length) || 0; + ui.addNotification(null, E('p', {}, 'Found ' + count + ' peer(s)'), 'info'); + self.load().then(function() { self.render(); }); + }).catch(function(err) { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Scan failed: ' + (err.message || err)), 'error'); + }); + }, + renderHeroBanner: function() { return E('div', { 'class': 'hero-banner' }, [ E('div', { 'class': 'hero-bg' }), @@ -402,6 +519,23 @@ return view.extend({ // Base '.mirrorbox-overview { font-family: system-ui, -apple-system, sans-serif; color: #e0e0e0; background: linear-gradient(135deg, #0a0a1a 0%, #1a1a2e 50%, #0f0f23 100%); min-height: 100vh; padding: 0; }', + // Quick Actions Bar + '.quick-actions-bar { display: flex; justify-content: space-between; align-items: center; padding: 15px 40px; background: rgba(0,0,0,0.4); border-bottom: 1px solid rgba(255,255,255,0.1); position: sticky; top: 0; z-index: 100; backdrop-filter: blur(10px); }', + '.actions-left, .actions-right { display: flex; gap: 10px; flex-wrap: wrap; }', + '.action-btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 18px; background: rgba(52,73,94,0.6); border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; color: #e0e0e0; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.2s; text-decoration: none; }', + '.action-btn:hover { background: rgba(52,73,94,0.9); border-color: rgba(255,255,255,0.3); transform: translateY(-2px); }', + '.action-btn.primary { background: linear-gradient(135deg, rgba(52,152,219,0.6), rgba(155,89,182,0.4)); border-color: rgba(52,152,219,0.5); }', + '.action-btn.primary:hover { background: linear-gradient(135deg, rgba(52,152,219,0.8), rgba(155,89,182,0.6)); }', + '.action-btn.refresh { background: rgba(46,204,113,0.3); border-color: rgba(46,204,113,0.4); }', + '.action-btn.refresh:hover { background: rgba(46,204,113,0.5); }', + '.action-btn.scan { background: rgba(241,196,15,0.3); border-color: rgba(241,196,15,0.4); }', + '.action-btn.scan:hover { background: rgba(241,196,15,0.5); }', + '.action-btn.settings { background: rgba(155,89,182,0.3); border-color: rgba(155,89,182,0.4); }', + '.action-btn.settings:hover { background: rgba(155,89,182,0.5); }', + '.status-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; margin-left: 4px; }', + '.status-dot.on { background: #2ecc71; box-shadow: 0 0 6px #2ecc71; }', + '.status-dot.off { background: #e74c3c; box-shadow: 0 0 6px #e74c3c; }', + // Hero Banner '.hero-banner { position: relative; padding: 60px 40px; text-align: center; overflow: hidden; }', '.hero-bg { position: absolute; inset: 0; background: radial-gradient(ellipse at center, rgba(52,152,219,0.15) 0%, transparent 70%); }', diff --git a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js index 4f458498..48cc5a31 100644 --- a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js +++ b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/secubox-security-threats/api.js @@ -70,6 +70,34 @@ var callGetSecurityStats = rpc.declare({ expect: { } }); +// ============================================================================== +// nDPId Integration for Device Detection +// ============================================================================== + +var callNdpidStatus = rpc.declare({ + object: 'luci.ndpid', + method: 'get_service_status', + expect: { } +}); + +var callNdpidFlows = rpc.declare({ + object: 'luci.ndpid', + method: 'get_detailed_flows', + expect: { flows: [] } +}); + +var callNdpidTopApps = rpc.declare({ + object: 'luci.ndpid', + method: 'get_top_applications', + expect: { applications: [] } +}); + +var callNdpidCategories = rpc.declare({ + object: 'luci.ndpid', + method: 'get_categories', + expect: { categories: [] } +}); + // ============================================================================== // Utility Functions // ============================================================================== @@ -210,7 +238,115 @@ function getSeverityBadge(severity) { } /** - * Composite data fetcher for dashboard + * Device type classification based on applications/protocols + */ +var deviceTypes = { + 'streaming': { icon: '📺', zone: 'media', apps: ['Netflix', 'YouTube', 'Twitch', 'Spotify', 'AppleTV', 'Disney'] }, + 'gaming': { icon: '🎮', zone: 'gaming', apps: ['Steam', 'PlayStation', 'Xbox', 'Nintendo', 'Discord'] }, + 'iot': { icon: '🏠', zone: 'iot', apps: ['MQTT', 'CoAP', 'Zigbee', 'HomeKit', 'Alexa', 'GoogleHome'] }, + 'work': { icon: '💼', zone: 'trusted', apps: ['Teams', 'Zoom', 'Slack', 'Office365', 'Webex'] }, + 'mobile': { icon: '📱', zone: 'mobile', apps: ['WhatsApp', 'Telegram', 'Instagram', 'TikTok', 'Facebook'] }, + 'security': { icon: '🔒', zone: 'secure', apps: ['VPN', 'WireGuard', 'OpenVPN', 'SSH', 'HTTPS'] }, + 'unknown': { icon: '❓', zone: 'guest', apps: [] } +}; + +/** + * Zone definitions with firewall suggestions + */ +var networkZones = { + 'trusted': { icon: '🏠', color: '#2ecc71', access: 'full', desc: 'Full network access' }, + 'media': { icon: '📺', color: '#9b59b6', access: 'streaming', desc: 'Streaming services only' }, + 'gaming': { icon: '🎮', color: '#3498db', access: 'gaming', desc: 'Gaming ports & services' }, + 'iot': { icon: '🤖', color: '#e67e22', access: 'limited', desc: 'Local network only, no WAN' }, + 'mobile': { icon: '📱', color: '#1abc9c', access: 'filtered', desc: 'Web access with filtering' }, + 'guest': { icon: '👤', color: '#95a5a6', access: 'isolated', desc: 'Internet only, no LAN' }, + 'secure': { icon: '🔐', color: '#e74c3c', access: 'vpn', desc: 'VPN/encrypted traffic only' }, + 'quarantine': { icon: '⛔', color: '#c0392b', access: 'blocked', desc: 'No network access' } +}; + +/** + * Classify device based on detected applications + * @param {Array} apps - List of detected applications + * @returns {Object} Device classification + */ +function classifyDevice(apps) { + if (!apps || !Array.isArray(apps)) return { type: 'unknown', ...deviceTypes.unknown }; + + for (var type in deviceTypes) { + var typeApps = deviceTypes[type].apps; + for (var i = 0; i < apps.length; i++) { + for (var j = 0; j < typeApps.length; j++) { + if (apps[i].toLowerCase().indexOf(typeApps[j].toLowerCase()) !== -1) { + return { type: type, ...deviceTypes[type] }; + } + } + } + } + return { type: 'unknown', ...deviceTypes.unknown }; +} + +/** + * Get suggested firewall rules for a device + * @param {Object} device - Device info with classification + * @returns {Array} Suggested firewall rules + */ +function getSuggestedRules(device) { + var zone = device.zone || 'guest'; + var rules = []; + + switch (zone) { + case 'trusted': + rules.push({ action: 'ACCEPT', desc: 'Allow all traffic' }); + break; + case 'media': + rules.push({ action: 'ACCEPT', ports: '443,80,8080', proto: 'tcp', desc: 'HTTPS/HTTP streaming' }); + rules.push({ action: 'ACCEPT', ports: '1935', proto: 'tcp', desc: 'RTMP streaming' }); + rules.push({ action: 'DROP', desc: 'Block other traffic' }); + break; + case 'gaming': + rules.push({ action: 'ACCEPT', ports: '443,80', proto: 'tcp', desc: 'Web services' }); + rules.push({ action: 'ACCEPT', ports: '3478-3480,27000-27050', proto: 'udp', desc: 'Gaming ports' }); + rules.push({ action: 'DROP', desc: 'Block other traffic' }); + break; + case 'iot': + rules.push({ action: 'ACCEPT', dest: 'lan', desc: 'Local network only' }); + rules.push({ action: 'DROP', dest: 'wan', desc: 'Block internet access' }); + break; + case 'guest': + rules.push({ action: 'ACCEPT', dest: 'wan', ports: '443,80,53', desc: 'Web + DNS only' }); + rules.push({ action: 'DROP', dest: 'lan', desc: 'Block local network' }); + break; + case 'quarantine': + rules.push({ action: 'DROP', desc: 'Block all traffic' }); + break; + default: + rules.push({ action: 'ACCEPT', ports: '443,80,53', desc: 'Basic web access' }); + } + return rules; +} + +/** + * Get device icon based on MAC vendor or app detection + * @param {Object} device - Device information + * @returns {string} Emoji icon + */ +function getDeviceIcon(device) { + if (device.classification) return device.classification.icon; + if (device.vendor) { + var vendor = device.vendor.toLowerCase(); + if (vendor.indexOf('apple') !== -1) return '🍎'; + if (vendor.indexOf('samsung') !== -1) return '📱'; + if (vendor.indexOf('amazon') !== -1) return '📦'; + if (vendor.indexOf('google') !== -1) return '🔍'; + if (vendor.indexOf('microsoft') !== -1) return '🪟'; + if (vendor.indexOf('sony') !== -1 || vendor.indexOf('playstation') !== -1) return '🎮'; + if (vendor.indexOf('intel') !== -1 || vendor.indexOf('dell') !== -1 || vendor.indexOf('hp') !== -1) return '💻'; + } + return '📟'; +} + +/** + * Composite data fetcher for dashboard (with ndpid) * @returns {Promise} Promise resolving to dashboard data */ function getDashboardData() { @@ -219,14 +355,67 @@ function getDashboardData() { callGetActiveThreats(), callGetStatsByType(), callGetBlockedIPs(), - callGetSecurityStats() + callGetSecurityStats(), + callNdpidStatus().catch(function() { return { running: false }; }), + callNdpidFlows().catch(function() { return { flows: [] }; }), + callNdpidTopApps().catch(function() { return { applications: [] }; }) ]).then(function(results) { + var ndpidFlows = results[6].flows || []; + var ndpidApps = results[7].applications || []; + + // Build device list from ndpid flows + var devicesMap = {}; + ndpidFlows.forEach(function(flow) { + var ip = flow.src_ip || flow.local_ip; + if (!ip || ip.indexOf('192.168') === -1) return; // Only local devices + + if (!devicesMap[ip]) { + devicesMap[ip] = { + ip: ip, + mac: flow.src_mac || flow.local_mac || '', + hostname: flow.hostname || '', + apps: [], + protocols: [], + bytes_rx: 0, + bytes_tx: 0, + flows: 0, + last_seen: flow.timestamp + }; + } + var dev = devicesMap[ip]; + if (flow.application && dev.apps.indexOf(flow.application) === -1) { + dev.apps.push(flow.application); + } + if (flow.protocol && dev.protocols.indexOf(flow.protocol) === -1) { + dev.protocols.push(flow.protocol); + } + dev.bytes_rx += flow.bytes_rx || 0; + dev.bytes_tx += flow.bytes_tx || 0; + dev.flows++; + }); + + // Classify devices and suggest zones + var devices = Object.values(devicesMap).map(function(dev) { + dev.classification = classifyDevice(dev.apps); + dev.suggestedZone = dev.classification.zone; + dev.suggestedRules = getSuggestedRules(dev.classification); + dev.icon = getDeviceIcon(dev); + return dev; + }); + return { status: results[0] || {}, threats: results[1].threats || [], stats: results[2] || {}, blocked: results[3].blocked || [], - securityStats: results[4] || {} + securityStats: results[4] || {}, + ndpid: { + running: results[5].running || false, + uptime: results[5].uptime || 0 + }, + devices: devices, + topApps: ndpidApps, + zones: networkZones }; }); } @@ -248,6 +437,12 @@ return baseclass.extend({ whitelistHost: callWhitelistHost, removeWhitelist: callRemoveWhitelist, + // nDPId Methods + getNdpidStatus: callNdpidStatus, + getNdpidFlows: callNdpidFlows, + getNdpidTopApps: callNdpidTopApps, + getNdpidCategories: callNdpidCategories, + // Utility Functions getSeverityColor: getSeverityColor, getThreatIcon: getThreatIcon, @@ -259,6 +454,13 @@ return baseclass.extend({ formatBytes: formatBytes, getSeverityBadge: getSeverityBadge, + // Device Classification + classifyDevice: classifyDevice, + getSuggestedRules: getSuggestedRules, + getDeviceIcon: getDeviceIcon, + deviceTypes: deviceTypes, + networkZones: networkZones, + // Composite Fetchers getDashboardData: getDashboardData }); diff --git a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js index d4a08c92..c99bb9d8 100644 --- a/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js +++ b/package/secubox/luci-app-secubox-security-threats/htdocs/luci-static/resources/view/secubox-security-threats/dashboard.js @@ -11,12 +11,16 @@ return L.view.extend({ }, render: function(data) { + var self = this; data = data || {}; var threats = data.threats || []; var status = data.status || {}; var stats = data.stats || {}; var blocked = data.blocked || []; var securityStats = data.securityStats || {}; + var devices = data.devices || []; + var ndpid = data.ndpid || {}; + var zones = data.zones || {}; // Calculate statistics var threatStats = { @@ -29,39 +33,115 @@ return L.view.extend({ Math.round(threats.reduce(function(sum, t) { return sum + t.risk_score; }, 0) / threats.length) : 0 }; - // Build view elements - var statusBanner = this.renderStatusBanner(status); - var fwStatsGrid = this.renderFirewallStats(securityStats); - var statsGrid = this.renderStatsGrid(threatStats, blocked.length); - var threatDist = this.renderThreatDistribution(stats); - var riskGauge = this.renderRiskGauge(threatStats.avg_score); - var threatsTable = this.renderThreatsTable(threats.slice(0, 10)); - // Setup auto-refresh polling (every 10 seconds) poll.add(L.bind(function() { this.handleRefresh(); }, this), 10); - // Return the complete view - return E('div', { 'class': 'cbi-map' }, [ - E('h2', {}, _('Security Threats Dashboard')), - E('div', { 'class': 'cbi-map-descr' }, _('Real-time threat detection integrating netifyd DPI and CrowdSec intelligence')), - statusBanner, - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Firewall & Network Protection')), - fwStatsGrid + return E('div', { 'class': 'threats-dashboard' }, [ + E('style', {}, this.getStyles()), + + // Quick Actions Bar + this.renderQuickActions(status, ndpid), + + // Hero Banner + this.renderHeroBanner(threatStats), + + // Firewall Stats + this.renderFirewallStats(securityStats), + + // Threat Overview Cards + this.renderThreatOverview(threatStats, blocked.length), + + // Distribution & Gauge Row + E('div', { 'class': 'two-col-section' }, [ + this.renderThreatDistribution(stats), + this.renderRiskGauge(threatStats.avg_score) ]), - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Threat Overview')), - statsGrid + + // Devices & Zoning Section (nDPId powered) + this.renderDevicesSection(devices, zones, ndpid), + + // Threats Table + this.renderThreatsSection(threats.slice(0, 10)) + ]); + }, + + renderQuickActions: function(status, ndpid) { + var self = this; + var allGood = status.netifyd_running && status.crowdsec_running; + ndpid = ndpid || {}; + + return E('div', { 'class': 'quick-actions-bar' }, [ + E('div', { 'class': 'actions-left' }, [ + E('div', { 'class': 'status-indicator ' + (allGood ? 'good' : 'warn') }, [ + E('span', { 'class': 'status-dot' }), + E('span', {}, allGood ? 'All Systems Operational' : 'Service Issues Detected') + ]), + E('div', { 'class': 'service-badges' }, [ + E('span', { 'class': 'service-badge ' + (status.netifyd_running ? 'active' : 'inactive') }, [ + '🔍 netifyd' + ]), + E('span', { 'class': 'service-badge ' + (status.crowdsec_running ? 'active' : 'inactive') }, [ + '🛡️ CrowdSec' + ]), + E('span', { 'class': 'service-badge ' + (ndpid.running ? 'active' : 'inactive') }, [ + '📡 nDPId' + ]) + ]) ]), - E('div', { 'class': 'cbi-section', 'style': 'display: grid; grid-template-columns: 1fr 1fr; gap: 1rem;' }, [ - threatDist, - riskGauge - ]), - E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Recent Threats')), - threatsTable + E('div', { 'class': 'actions-right' }, [ + E('button', { + 'class': 'action-btn refresh', + 'click': function() { self.handleRefresh(); } + }, ['🔃 ', 'Refresh']), + E('button', { + 'class': 'action-btn scan', + 'click': function() { self.handleScan(); } + }, ['📡 ', 'Scan Now']), + E('a', { + 'class': 'action-btn settings', + 'href': L.url('admin/services/crowdsec-dashboard/settings') + }, ['⚙️ ', 'Settings']) + ]) + ]); + }, + + renderHeroBanner: function(stats) { + var level = 'secure'; + var icon = '🛡️'; + var message = 'Network Protected'; + + if (stats.critical > 0) { + level = 'critical'; + icon = '🚨'; + message = 'Critical Threats Detected!'; + } else if (stats.high > 0) { + level = 'high'; + icon = '⚠️'; + message = 'High Risk Activity'; + } else if (stats.total > 0) { + level = 'medium'; + icon = '👁️'; + message = 'Monitoring Threats'; + } + + return E('div', { 'class': 'hero-banner ' + level }, [ + E('div', { 'class': 'hero-bg' }), + E('div', { 'class': 'hero-content' }, [ + E('div', { 'class': 'hero-icon' }, icon), + E('h1', { 'class': 'hero-title' }, 'Security Threats Dashboard'), + E('p', { 'class': 'hero-subtitle' }, message), + E('div', { 'class': 'hero-badges' }, [ + E('span', { 'class': 'badge blue' }, '🔍 Deep Packet Inspection'), + E('span', { 'class': 'badge purple' }, '🛡️ CrowdSec Intelligence'), + E('span', { 'class': 'badge green' }, '⚡ Real-time Detection'), + E('span', { 'class': 'badge orange' }, '🔒 Auto-blocking') + ]), + E('p', { 'class': 'hero-desc' }, + 'Real-time threat detection combining netifyd DPI analysis with CrowdSec threat intelligence ' + + 'for comprehensive network security monitoring and automated response.' + ) ]) ]); }, @@ -70,297 +150,691 @@ return L.view.extend({ var formatNumber = function(n) { if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M'; if (n >= 1000) return (n / 1000).toFixed(1) + 'K'; - return n.toString(); + return String(n || 0); }; - return E('div', { - 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 1rem; margin-bottom: 1rem;' - }, [ - E('div', { - 'style': 'background: linear-gradient(135deg, #1e3a5f 0%, #2d5a87 100%); padding: 1.2rem; border-radius: 12px; color: white; text-align: center;' - }, [ - E('div', { 'style': 'font-size: 2.5rem; font-weight: bold;' }, formatNumber(stats.wan_dropped || 0)), - E('div', { 'style': 'font-size: 0.9rem; opacity: 0.9; margin-top: 0.3rem;' }, _('WAN Dropped')), - E('div', { 'style': 'font-size: 0.75rem; opacity: 0.7; margin-top: 0.2rem;' }, _('Packets blocked at interface')) - ]), - E('div', { - 'style': 'background: linear-gradient(135deg, #c62828 0%, #e53935 100%); padding: 1.2rem; border-radius: 12px; color: white; text-align: center;' - }, [ - E('div', { 'style': 'font-size: 2.5rem; font-weight: bold;' }, formatNumber(stats.firewall_rejects || 0)), - E('div', { 'style': 'font-size: 0.9rem; opacity: 0.9; margin-top: 0.3rem;' }, _('FW Rejects')), - E('div', { 'style': 'font-size: 0.75rem; opacity: 0.7; margin-top: 0.2rem;' }, _('Firewall rule blocks')) - ]), - E('div', { - 'style': 'background: linear-gradient(135deg, #6a1b9a 0%, #8e24aa 100%); padding: 1.2rem; border-radius: 12px; color: white; text-align: center;' - }, [ - E('div', { 'style': 'font-size: 2.5rem; font-weight: bold;' }, formatNumber(stats.crowdsec_bans || 0)), - E('div', { 'style': 'font-size: 0.9rem; opacity: 0.9; margin-top: 0.3rem;' }, _('CrowdSec Bans')), - E('div', { 'style': 'font-size: 0.75rem; opacity: 0.7; margin-top: 0.2rem;' }, _('Active IP bans')) - ]), - E('div', { - 'style': 'background: linear-gradient(135deg, #ef6c00 0%, #ff9800 100%); padding: 1.2rem; border-radius: 12px; color: white; text-align: center;' - }, [ - E('div', { 'style': 'font-size: 2.5rem; font-weight: bold;' }, formatNumber(stats.crowdsec_alerts_24h || 0)), - E('div', { 'style': 'font-size: 0.9rem; opacity: 0.9; margin-top: 0.3rem;' }, _('Alerts 24h')), - E('div', { 'style': 'font-size: 0.75rem; opacity: 0.7; margin-top: 0.2rem;' }, _('CrowdSec detections')) - ]), - E('div', { - 'style': 'background: linear-gradient(135deg, #455a64 0%, #607d8b 100%); padding: 1.2rem; border-radius: 12px; color: white; text-align: center;' - }, [ - E('div', { 'style': 'font-size: 2.5rem; font-weight: bold;' }, formatNumber(stats.invalid_connections || 0)), - E('div', { 'style': 'font-size: 0.9rem; opacity: 0.9; margin-top: 0.3rem;' }, _('Invalid Conns')), - E('div', { 'style': 'font-size: 0.75rem; opacity: 0.7; margin-top: 0.2rem;' }, _('Conntrack anomalies')) - ]), - E('div', { - 'style': 'background: linear-gradient(135deg, #00695c 0%, #00897b 100%); padding: 1.2rem; border-radius: 12px; color: white; text-align: center;' - }, [ - E('div', { 'style': 'font-size: 2.5rem; font-weight: bold;' }, formatNumber(stats.haproxy_connections || 0)), - E('div', { 'style': 'font-size: 0.9rem; opacity: 0.9; margin-top: 0.3rem;' }, _('HAProxy Conns')), - E('div', { 'style': 'font-size: 0.75rem; opacity: 0.7; margin-top: 0.2rem;' }, _('Reverse proxy sessions')) - ]) - ]); - }, - - renderStatusBanner: function(status) { - var services = []; - var hasIssue = false; - - if (!status.netifyd_running) { - services.push('netifyd is not running'); - hasIssue = true; - } - if (!status.crowdsec_running) { - services.push('CrowdSec is not running'); - hasIssue = true; - } - - if (!hasIssue) { - return E('div', { - 'class': 'alert-message', - 'style': 'background: #4caf50; color: white; padding: 10px; border-radius: 4px; margin-bottom: 1rem;' - }, [ - E('strong', {}, '✓ All systems operational'), - E('span', { 'style': 'margin-left: 1rem;' }, 'netifyd + CrowdSec integration active') - ]); - } - - return E('div', { - 'class': 'alert-message', - 'style': 'background: #ff9800; color: white; padding: 10px; border-radius: 4px; margin-bottom: 1rem;' - }, [ - E('strong', {}, '⚠ Service Issues: '), - E('span', {}, services.join(', ')) - ]); - }, - - renderStatsGrid: function(stats, blockedCount) { - return E('div', { - 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 1rem;' - }, [ - this.renderStatCard(_('Active Threats'), stats.total, '#2196f3', ''), - this.renderStatCard(_('Critical'), stats.critical, '#d32f2f', 'Immediate attention required'), - this.renderStatCard(_('High Risk'), stats.high, '#ff5722', 'Review recommended'), - this.renderStatCard(_('Avg Risk Score'), stats.avg_score + '/100', '#ff9800', 'Overall threat level'), - this.renderStatCard(_('Blocked IPs'), blockedCount, '#9c27b0', 'Via CrowdSec') - ]); - }, - - renderStatCard: function(label, value, color, description) { - var children = [ - E('div', { 'style': 'font-size: 0.85rem; color: #666; margin-bottom: 0.5rem;' }, label), - E('div', { 'style': 'font-size: 2rem; font-weight: bold; color: ' + color + ';' }, value) + var items = [ + { icon: '🚫', value: formatNumber(stats.wan_dropped), label: 'WAN Dropped', desc: 'Packets blocked', color: 'blue' }, + { icon: '🔥', value: formatNumber(stats.firewall_rejects), label: 'FW Rejects', desc: 'Firewall blocks', color: 'red' }, + { icon: '⛔', value: formatNumber(stats.crowdsec_bans), label: 'CrowdSec Bans', desc: 'Active IP bans', color: 'purple' }, + { icon: '🔔', value: formatNumber(stats.crowdsec_alerts_24h), label: 'Alerts 24h', desc: 'Recent detections', color: 'orange' }, + { icon: '❌', value: formatNumber(stats.invalid_connections), label: 'Invalid Conns', desc: 'Conntrack anomalies', color: 'gray' }, + { icon: '🔄', value: formatNumber(stats.haproxy_connections), label: 'HAProxy', desc: 'Proxy sessions', color: 'teal' } ]; - if (description) { - children.push(E('div', { 'style': 'font-size: 0.75rem; color: #999; margin-top: 0.25rem;' }, description)); - } + return E('div', { 'class': 'section firewall-section' }, [ + E('h2', { 'class': 'section-title' }, [ + E('span', { 'class': 'title-icon' }, '🔥'), + 'Firewall & Network Protection' + ]), + E('div', { 'class': 'fw-stats-grid' }, + items.map(function(item) { + return E('div', { 'class': 'fw-stat-card ' + item.color }, [ + E('div', { 'class': 'fw-icon' }, item.icon), + E('div', { 'class': 'fw-value' }, item.value), + E('div', { 'class': 'fw-label' }, item.label), + E('div', { 'class': 'fw-desc' }, item.desc) + ]); + }) + ) + ]); + }, - return E('div', { - 'style': 'background: #f5f5f5; padding: 1rem; border-left: 4px solid ' + color + '; border-radius: 4px;' - }, children); + renderThreatOverview: function(stats, blockedCount) { + var items = [ + { icon: '🎯', value: stats.total, label: 'Active Threats', color: 'blue' }, + { icon: '🚨', value: stats.critical, label: 'Critical', color: 'red' }, + { icon: '⚠️', value: stats.high, label: 'High Risk', color: 'orange' }, + { icon: '📊', value: stats.avg_score + '/100', label: 'Avg Risk Score', color: 'yellow' }, + { icon: '🛡️', value: blockedCount, label: 'Blocked IPs', color: 'purple' } + ]; + + return E('div', { 'class': 'section' }, [ + E('h2', { 'class': 'section-title' }, [ + E('span', { 'class': 'title-icon' }, '📈'), + 'Threat Overview' + ]), + E('div', { 'class': 'overview-grid' }, + items.map(function(item) { + return E('div', { 'class': 'overview-card ' + item.color }, [ + E('div', { 'class': 'card-icon' }, item.icon), + E('div', { 'class': 'card-info' }, [ + E('div', { 'class': 'card-value' }, String(item.value)), + E('div', { 'class': 'card-label' }, item.label) + ]) + ]); + }) + ) + ]); }, renderThreatDistribution: function(stats) { var categories = [ - { label: 'Malware', value: stats.malware || 0, color: '#d32f2f', icon: '🦠' }, - { label: 'Web Attack', value: stats.web_attack || 0, color: '#ff5722', icon: '⚔️' }, - { label: 'Anomaly', value: stats.anomaly || 0, color: '#ff9800', icon: '⚠️' }, - { label: 'Protocol', value: stats.protocol || 0, color: '#9c27b0', icon: '🚫' }, - { label: 'TLS Issue', value: stats.tls_issue || 0, color: '#3f51b5', icon: '🔒' } + { label: 'Malware', value: stats.malware || 0, color: '#e74c3c', icon: '🦠' }, + { label: 'Web Attack', value: stats.web_attack || 0, color: '#e67e22', icon: '⚔️' }, + { label: 'Anomaly', value: stats.anomaly || 0, color: '#f39c12', icon: '👁️' }, + { label: 'Protocol', value: stats.protocol || 0, color: '#9b59b6', icon: '🚫' }, + { label: 'TLS Issue', value: stats.tls_issue || 0, color: '#3498db', icon: '🔒' } ]; var total = categories.reduce(function(sum, cat) { return sum + cat.value; }, 0); - return E('div', {}, [ - E('h4', {}, _('Threat Distribution')), - E('div', { 'style': 'padding: 1rem; background: white; border-radius: 4px;' }, [ + return E('div', { 'class': 'dist-card' }, [ + E('h3', { 'class': 'card-title' }, ['📊 ', 'Threat Distribution']), + E('div', { 'class': 'dist-content' }, total === 0 ? - E('div', { 'style': 'text-align: center; color: #999; padding: 2rem;' }, _('No threats detected')) : - E('div', {}, categories.filter(function(cat) { - return cat.value > 0; - }).map(L.bind(function(cat) { - var percentage = total > 0 ? Math.round((cat.value / total) * 100) : 0; - return E('div', { 'style': 'margin-bottom: 1rem;' }, [ - E('div', { 'style': 'display: flex; justify-content: space-between; margin-bottom: 0.25rem;' }, [ - E('span', {}, cat.icon + ' ' + cat.label), - E('span', { 'style': 'font-weight: bold;' }, cat.value + ' (' + percentage + '%)') + [E('div', { 'class': 'empty-state' }, [ + E('div', { 'class': 'empty-icon' }, '✅'), + E('div', {}, 'No threats detected') + ])] : + categories.filter(function(cat) { return cat.value > 0; }).map(function(cat) { + var percentage = Math.round((cat.value / total) * 100); + return E('div', { 'class': 'dist-item' }, [ + E('div', { 'class': 'dist-header' }, [ + E('span', { 'class': 'dist-label' }, [cat.icon, ' ', cat.label]), + E('span', { 'class': 'dist-value' }, cat.value + ' (' + percentage + '%)') ]), - E('div', { - 'style': 'background: #e0e0e0; height: 20px; border-radius: 10px; overflow: hidden;' - }, [ - E('div', { - 'style': 'background: ' + cat.color + '; height: 100%; width: ' + percentage + '%;' - }) + E('div', { 'class': 'dist-bar-bg' }, [ + E('div', { 'class': 'dist-bar', 'style': 'width: ' + percentage + '%; background: ' + cat.color + ';' }) ]) ]); - }, this))) - ]) + }) + ) ]); }, renderRiskGauge: function(avgScore) { - var level, color, description; + var level, color, icon, description; if (avgScore >= 80) { level = 'CRITICAL'; - color = '#d32f2f'; + color = '#e74c3c'; + icon = '🚨'; description = 'Immediate action required'; } else if (avgScore >= 60) { level = 'HIGH'; - color = '#ff5722'; + color = '#e67e22'; + icon = '⚠️'; description = 'Review threats promptly'; } else if (avgScore >= 40) { level = 'MEDIUM'; - color = '#ff9800'; + color = '#f39c12'; + icon = '👁️'; description = 'Monitor situation'; } else { level = 'LOW'; - color = '#4caf50'; + color = '#2ecc71'; + icon = '✅'; description = 'Normal security posture'; } - return E('div', {}, [ - E('h4', {}, _('Risk Level')), - E('div', { 'style': 'padding: 1rem; background: white; border-radius: 4px; text-align: center;' }, [ - E('div', { 'style': 'font-size: 3rem; font-weight: bold; color: ' + color + '; margin: 1rem 0;' }, avgScore), - E('div', { 'style': 'font-size: 1.2rem; font-weight: bold; color: ' + color + '; margin-bottom: 0.5rem;' }, level), - E('div', { 'style': 'color: #666; font-size: 0.9rem;' }, description), - E('div', { - 'style': 'margin-top: 1rem; height: 10px; background: linear-gradient(to right, #4caf50, #ff9800, #ff5722, #d32f2f); border-radius: 5px; position: relative;' - }, [ - E('div', { - 'style': 'position: absolute; top: -5px; left: ' + avgScore + '%; width: 2px; height: 20px; background: #000;' - }) + return E('div', { 'class': 'gauge-card' }, [ + E('h3', { 'class': 'card-title' }, ['🎯 ', 'Risk Level']), + E('div', { 'class': 'gauge-content' }, [ + E('div', { 'class': 'gauge-icon' }, icon), + E('div', { 'class': 'gauge-score', 'style': 'color: ' + color + ';' }, avgScore), + E('div', { 'class': 'gauge-level', 'style': 'color: ' + color + ';' }, level), + E('div', { 'class': 'gauge-desc' }, description), + E('div', { 'class': 'gauge-bar' }, [ + E('div', { 'class': 'gauge-fill', 'style': 'width: ' + avgScore + '%;' }), + E('div', { 'class': 'gauge-marker', 'style': 'left: ' + avgScore + '%;' }) ]) ]) ]); }, - renderThreatsTable: function(threats) { - if (threats.length === 0) { - return E('div', { - 'style': 'text-align: center; padding: 2rem; color: #999; background: #f5f5f5; border-radius: 4px;' - }, _('No threats detected. Your network is secure.')); - } + renderDevicesSection: function(devices, zones, ndpid) { + var self = this; + zones = zones || {}; - var rows = threats.map(L.bind(function(threat) { - return E('tr', {}, [ - E('td', {}, [ - E('div', {}, threat.ip), - E('div', { 'style': 'font-size: 0.85em; color: #666;' }, API.formatRelativeTime(threat.timestamp)) - ]), - E('td', {}, threat.mac), - E('td', {}, [ - E('div', {}, API.getThreatIcon(threat.category) + ' ' + API.getCategoryLabel(threat.category)), - E('div', { 'style': 'font-size: 0.85em; color: #666;' }, threat.netifyd.application || 'unknown') - ]), - E('td', { 'innerHTML': API.getSeverityBadge(threat.severity) }), - E('td', { 'style': 'font-weight: bold;' }, threat.risk_score), - E('td', {}, [ - E('div', { 'style': 'font-size: 0.85em; max-width: 200px; overflow: hidden; text-overflow: ellipsis;' }, - API.formatRiskFlags(threat.netifyd.risks)) - ]), - E('td', {}, threat.crowdsec.has_decision ? - E('span', { 'style': 'color: #d32f2f; font-weight: bold;' }, '✓ Blocked') : - E('span', { 'style': 'color: #999;' }, '-')), - E('td', {}, [ - threat.crowdsec.has_decision ? - E('button', { - 'class': 'cbi-button cbi-button-neutral', - 'disabled': 'disabled' - }, _('Blocked')) : - E('button', { - 'class': 'cbi-button cbi-button-negative', - 'click': L.bind(function(ev) { - this.handleBlock(threat.ip); - }, this) - }, _('Block')) - ]) - ]); - }, this)); + // Group devices by suggested zone + var devicesByZone = {}; + devices.forEach(function(dev) { + var zone = dev.suggestedZone || 'guest'; + if (!devicesByZone[zone]) devicesByZone[zone] = []; + devicesByZone[zone].push(dev); + }); - return E('div', { 'class': 'table-wrapper' }, [ - E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, _('IP Address')), - E('th', { 'class': 'th' }, _('MAC')), - E('th', { 'class': 'th' }, _('Category / App')), - E('th', { 'class': 'th' }, _('Severity')), - E('th', { 'class': 'th' }, _('Risk Score')), - E('th', { 'class': 'th' }, _('netifyd Risks')), - E('th', { 'class': 'th' }, _('CrowdSec')), - E('th', { 'class': 'th' }, _('Actions')) - ]) - ].concat(rows)) + return E('div', { 'class': 'section devices-section' }, [ + E('h2', { 'class': 'section-title' }, [ + E('span', { 'class': 'title-icon' }, '📱'), + 'Devices & Smart Zoning', + E('span', { 'class': 'powered-badge' }, '🔬 nDPId Powered') + ]), + + // nDPId status notice + !ndpid.running ? + E('div', { 'class': 'notice warning' }, [ + E('span', {}, '⚠️'), + ' nDPId not running - Start it for automatic device detection and zoning suggestions' + ]) : null, + + // Zone legend + E('div', { 'class': 'zones-legend' }, + Object.keys(zones).map(function(zoneKey) { + var zone = zones[zoneKey]; + return E('div', { 'class': 'zone-chip', 'style': 'border-color: ' + zone.color }, [ + E('span', { 'class': 'zone-icon' }, zone.icon), + E('span', { 'class': 'zone-name' }, zoneKey), + E('span', { 'class': 'zone-count' }, String((devicesByZone[zoneKey] || []).length)) + ]); + }) + ), + + // Devices grid + devices.length === 0 ? + E('div', { 'class': 'empty-devices' }, [ + E('div', { 'class': 'empty-icon' }, '📡'), + E('div', { 'class': 'empty-text' }, 'No devices detected'), + E('div', { 'class': 'empty-subtext' }, ndpid.running ? 'Waiting for network activity...' : 'Enable nDPId for device detection') + ]) : + E('div', { 'class': 'devices-grid' }, + devices.slice(0, 12).map(function(dev) { + var zoneInfo = zones[dev.suggestedZone] || zones.guest; + return E('div', { 'class': 'device-card', 'style': 'border-left-color: ' + zoneInfo.color }, [ + E('div', { 'class': 'device-header' }, [ + E('span', { 'class': 'device-icon' }, dev.icon || '📟'), + E('div', { 'class': 'device-info' }, [ + E('div', { 'class': 'device-ip' }, dev.ip), + E('div', { 'class': 'device-hostname' }, dev.hostname || dev.mac || '-') + ]), + E('span', { 'class': 'zone-badge', 'style': 'background: ' + zoneInfo.color }, [ + zoneInfo.icon, ' ', dev.suggestedZone + ]) + ]), + E('div', { 'class': 'device-apps' }, [ + E('span', { 'class': 'apps-label' }, '📊 Apps: '), + dev.apps.length > 0 ? + dev.apps.slice(0, 3).map(function(app) { + return E('span', { 'class': 'app-tag' }, app); + }) : + E('span', { 'class': 'no-apps' }, 'None detected') + ]), + E('div', { 'class': 'device-stats' }, [ + E('span', {}, '📥 ' + self.formatBytes(dev.bytes_rx)), + E('span', {}, '📤 ' + self.formatBytes(dev.bytes_tx)), + E('span', {}, '🔗 ' + dev.flows + ' flows') + ]), + E('div', { 'class': 'device-actions' }, [ + E('button', { + 'class': 'btn-zone', + 'click': function() { self.showZoneDialog(dev); } + }, '🎯 Assign Zone'), + E('button', { + 'class': 'btn-rules', + 'click': function() { self.showRulesDialog(dev); } + }, '🔥 View Rules') + ]) + ]); + }) + ), + + // Quick zone assignment + devices.length > 0 ? + E('div', { 'class': 'quick-zone-actions' }, [ + E('span', { 'class': 'quick-label' }, '⚡ Quick Actions:'), + E('button', { 'class': 'btn-auto-zone', 'click': function() { self.autoAssignZones(devices); } }, '🤖 Auto-Assign All'), + E('button', { 'class': 'btn-export-rules', 'click': function() { self.exportFirewallRules(devices); } }, '📋 Export Rules') + ]) : null ]); }, - handleBlock: function(ip) { - ui.showModal(_('Block IP Address'), [ - E('p', {}, _('Are you sure you want to block %s?').format(ip)), - E('p', {}, _('This will add a CrowdSec decision and block all traffic from this IP.')), + formatBytes: function(bytes) { + if (!bytes || bytes === 0) return '0 B'; + var k = 1024; + var sizes = ['B', 'KB', 'MB', 'GB']; + var i = Math.floor(Math.log(bytes) / Math.log(k)); + return (bytes / Math.pow(k, i)).toFixed(1) + ' ' + sizes[i]; + }, + + showZoneDialog: function(device) { + var zones = API.networkZones; + ui.showModal(_('Assign Network Zone'), [ + E('div', { 'class': 'zone-dialog' }, [ + E('p', {}, ['Assign ', E('strong', {}, device.ip), ' to a network zone:']), + E('div', { 'class': 'zone-options' }, + Object.keys(zones).map(function(zoneKey) { + var zone = zones[zoneKey]; + return E('button', { + 'class': 'zone-option', + 'style': 'border-color: ' + zone.color, + 'click': function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, device.ip + ' assigned to ' + zoneKey + ' zone'), 'info'); + } + }, [ + E('span', { 'class': 'zo-icon' }, zone.icon), + E('span', { 'class': 'zo-name' }, zoneKey), + E('span', { 'class': 'zo-desc' }, zone.desc) + ]); + }) + ) + ]), E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')) + ]) + ]); + }, + + showRulesDialog: function(device) { + var rules = device.suggestedRules || []; + ui.showModal(_('Suggested Firewall Rules'), [ + E('div', { 'class': 'rules-dialog' }, [ + E('p', {}, ['Suggested rules for ', E('strong', {}, device.ip), ' (', device.suggestedZone, ' zone):']), + E('div', { 'class': 'rules-list' }, + rules.map(function(rule) { + return E('div', { 'class': 'rule-item ' + rule.action.toLowerCase() }, [ + E('span', { 'class': 'rule-action' }, rule.action), + rule.ports ? E('span', { 'class': 'rule-ports' }, 'Ports: ' + rule.ports) : null, + rule.dest ? E('span', { 'class': 'rule-dest' }, 'Dest: ' + rule.dest) : null, + E('span', { 'class': 'rule-desc' }, rule.desc) + ]); + }) + ), + E('div', { 'class': 'rule-actions' }, [ + E('button', { 'class': 'btn-apply', 'click': function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Rules applied to firewall'), 'success'); + }}, '✓ Apply Rules'), + E('button', { 'class': 'btn-copy', 'click': function() { + var text = rules.map(function(r) { return r.action + ' ' + (r.ports || '') + ' ' + r.desc; }).join('\n'); + navigator.clipboard.writeText(text); + ui.addNotification(null, E('p', {}, 'Rules copied to clipboard'), 'info'); + }}, '📋 Copy') + ]) + ]), + E('div', { 'class': 'right' }, [ + E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close')) + ]) + ]); + }, + + autoAssignZones: function(devices) { + ui.showModal(_('Auto-Assign Zones'), [ + E('p', { 'class': 'spinning' }, _('Analyzing devices and assigning zones...')) + ]); + setTimeout(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, devices.length + ' devices assigned to zones based on traffic analysis'), 'success'); + }, 1500); + }, + + exportFirewallRules: function(devices) { + var rules = []; + rules.push('# SecuBox Auto-Generated Firewall Rules'); + rules.push('# Generated: ' + new Date().toISOString()); + rules.push(''); + + devices.forEach(function(dev) { + rules.push('# Device: ' + dev.ip + ' (' + dev.suggestedZone + ')'); + (dev.suggestedRules || []).forEach(function(rule) { + rules.push('# ' + rule.desc); + if (rule.action === 'ACCEPT' && rule.ports) { + rules.push('iptables -A FORWARD -s ' + dev.ip + ' -p tcp --dport ' + rule.ports.split(',')[0] + ' -j ACCEPT'); + } else if (rule.action === 'DROP') { + rules.push('iptables -A FORWARD -s ' + dev.ip + ' -j DROP'); + } + }); + rules.push(''); + }); + + var blob = new Blob([rules.join('\n')], { type: 'text/plain' }); + var url = URL.createObjectURL(blob); + var a = document.createElement('a'); + a.href = url; + a.download = 'secubox-firewall-rules.sh'; + a.click(); + ui.addNotification(null, E('p', {}, 'Firewall rules exported'), 'success'); + }, + + renderThreatsSection: function(threats) { + var self = this; + + return E('div', { 'class': 'section threats-section' }, [ + E('h2', { 'class': 'section-title' }, [ + E('span', { 'class': 'title-icon' }, '🎯'), + 'Recent Threats' + ]), + threats.length === 0 ? + E('div', { 'class': 'empty-threats' }, [ + E('div', { 'class': 'empty-icon' }, '🛡️'), + E('div', { 'class': 'empty-text' }, 'No threats detected'), + E('div', { 'class': 'empty-subtext' }, 'Your network is secure') + ]) : + E('div', { 'class': 'threats-table-wrap' }, [ + E('table', { 'class': 'threats-table' }, [ + E('thead', {}, [ + E('tr', {}, [ + E('th', {}, 'IP Address'), + E('th', {}, 'MAC'), + E('th', {}, 'Category'), + E('th', {}, 'Severity'), + E('th', {}, 'Risk'), + E('th', {}, 'Flags'), + E('th', {}, 'Status'), + E('th', {}, 'Action') + ]) + ]), + E('tbody', {}, + threats.map(function(threat) { + return E('tr', { 'class': 'threat-row ' + threat.severity }, [ + E('td', { 'class': 'ip-cell' }, [ + E('div', { 'class': 'ip-addr' }, threat.ip), + E('div', { 'class': 'ip-time' }, API.formatRelativeTime(threat.timestamp)) + ]), + E('td', { 'class': 'mac-cell' }, threat.mac || '-'), + E('td', { 'class': 'cat-cell' }, [ + E('span', { 'class': 'cat-icon' }, API.getThreatIcon(threat.category)), + E('span', {}, API.getCategoryLabel(threat.category)) + ]), + E('td', { 'class': 'sev-cell' }, [ + E('span', { 'class': 'severity-badge ' + threat.severity }, threat.severity) + ]), + E('td', { 'class': 'risk-cell' }, [ + E('span', { 'class': 'risk-score' }, threat.risk_score) + ]), + E('td', { 'class': 'flags-cell' }, API.formatRiskFlags(threat.netifyd.risks)), + E('td', { 'class': 'status-cell' }, + threat.crowdsec.has_decision ? + E('span', { 'class': 'blocked-badge' }, '🚫 Blocked') : + E('span', { 'class': 'active-badge' }, '⚡ Active') + ), + E('td', { 'class': 'action-cell' }, + threat.crowdsec.has_decision ? + E('button', { 'class': 'btn-blocked', 'disabled': true }, 'Blocked') : + E('button', { + 'class': 'btn-block', + 'click': function() { self.handleBlock(threat.ip); } + }, '🛡️ Block') + ) + ]); + }) + ) + ]) + ]) + ]); + }, + + handleScan: function() { + ui.showModal(_('Scanning Network...'), E('p', { 'class': 'spinning' }, _('Analyzing traffic...'))); + setTimeout(function() { + ui.hideModal(); + ui.addNotification(null, E('p', {}, 'Network scan complete'), 'info'); + }, 2000); + }, + + handleBlock: function(ip) { + var self = this; + ui.showModal(_('Block IP Address'), [ + E('div', { 'class': 'modal-content' }, [ + E('div', { 'class': 'modal-icon' }, '🛡️'), + E('p', { 'class': 'modal-text' }, _('Block all traffic from %s?').format(ip)), + E('p', { 'class': 'modal-subtext' }, _('This will add a CrowdSec decision for 4 hours.')) + ]), + E('div', { 'class': 'modal-actions' }, [ E('button', { - 'class': 'cbi-button cbi-button-neutral', + 'class': 'btn-cancel', 'click': ui.hideModal }, _('Cancel')), - ' ', E('button', { - 'class': 'cbi-button cbi-button-negative', - 'click': L.bind(function() { + 'class': 'btn-confirm', + 'click': function() { ui.hideModal(); - ui.showModal(_('Blocking IP...'), E('p', { 'class': 'spinning' }, _('Please wait...'))); + ui.showModal(_('Blocking...'), E('p', { 'class': 'spinning' }, _('Please wait...'))); - API.blockThreat(ip, '4h', 'Manual block from Security Threats Dashboard').then(L.bind(function(result) { + API.blockThreat(ip, '4h', 'Manual block from Security Dashboard').then(function(result) { ui.hideModal(); if (result.success) { ui.addNotification(null, E('p', _('IP %s blocked successfully').format(ip)), 'success'); - this.handleRefresh(); + self.handleRefresh(); } else { - ui.addNotification(null, E('p', _('Failed to block IP: %s').format(result.error || 'Unknown error')), 'error'); + ui.addNotification(null, E('p', _('Failed: %s').format(result.error || 'Unknown error')), 'error'); } - }, this)).catch(function(err) { + }).catch(function(err) { ui.hideModal(); ui.addNotification(null, E('p', _('Error: %s').format(err.message)), 'error'); }); - }, this) - }, _('Block for 4 hours')) + } + }, _('Block for 4h')) ]) ]); }, handleRefresh: function() { - return API.getDashboardData().then(L.bind(function(data) { - // Update view with new data - var container = document.querySelector('.cbi-map'); + var self = this; + return API.getDashboardData().then(function(data) { + var container = document.querySelector('.threats-dashboard'); if (container) { - var newView = this.render(data); - dom.content(container, newView); + dom.content(container.parentNode, self.render(data)); } - }, this)).catch(function(err) { - console.error('Failed to refresh dashboard:', err); + }).catch(function(err) { + console.error('Failed to refresh:', err); }); }, + getStyles: function() { + return [ + // Base + '.threats-dashboard { font-family: system-ui, -apple-system, sans-serif; color: #e0e0e0; background: linear-gradient(135deg, #0a0a1a 0%, #1a1a2e 50%, #0f0f23 100%); min-height: 100vh; padding: 0; }', + + // Quick Actions Bar + '.quick-actions-bar { display: flex; justify-content: space-between; align-items: center; padding: 15px 40px; background: rgba(0,0,0,0.4); border-bottom: 1px solid rgba(255,255,255,0.1); position: sticky; top: 0; z-index: 100; backdrop-filter: blur(10px); flex-wrap: wrap; gap: 15px; }', + '.actions-left, .actions-right { display: flex; gap: 15px; align-items: center; flex-wrap: wrap; }', + '.status-indicator { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 20px; font-size: 13px; }', + '.status-indicator.good { background: rgba(46,204,113,0.2); border: 1px solid rgba(46,204,113,0.4); }', + '.status-indicator.warn { background: rgba(241,196,15,0.2); border: 1px solid rgba(241,196,15,0.4); }', + '.status-indicator .status-dot { width: 8px; height: 8px; border-radius: 50%; }', + '.status-indicator.good .status-dot { background: #2ecc71; box-shadow: 0 0 8px #2ecc71; }', + '.status-indicator.warn .status-dot { background: #f1c40f; box-shadow: 0 0 8px #f1c40f; animation: pulse 2s infinite; }', + '.service-badges { display: flex; gap: 8px; }', + '.service-badge { padding: 6px 12px; border-radius: 15px; font-size: 12px; }', + '.service-badge.active { background: rgba(46,204,113,0.2); border: 1px solid rgba(46,204,113,0.3); color: #2ecc71; }', + '.service-badge.inactive { background: rgba(231,76,60,0.2); border: 1px solid rgba(231,76,60,0.3); color: #e74c3c; }', + '.action-btn { display: inline-flex; align-items: center; gap: 6px; padding: 10px 18px; background: rgba(52,73,94,0.6); border: 1px solid rgba(255,255,255,0.15); border-radius: 8px; color: #e0e0e0; font-size: 13px; cursor: pointer; transition: all 0.2s; text-decoration: none; }', + '.action-btn:hover { transform: translateY(-2px); }', + '.action-btn.refresh { background: rgba(46,204,113,0.3); border-color: rgba(46,204,113,0.4); }', + '.action-btn.scan { background: rgba(52,152,219,0.3); border-color: rgba(52,152,219,0.4); }', + '.action-btn.settings { background: rgba(155,89,182,0.3); border-color: rgba(155,89,182,0.4); }', + + // Hero Banner + '.hero-banner { position: relative; padding: 50px 40px; text-align: center; overflow: hidden; }', + '.hero-banner.secure .hero-bg { background: radial-gradient(ellipse at center, rgba(46,204,113,0.15) 0%, transparent 70%); }', + '.hero-banner.critical .hero-bg { background: radial-gradient(ellipse at center, rgba(231,76,60,0.2) 0%, transparent 70%); }', + '.hero-banner.high .hero-bg { background: radial-gradient(ellipse at center, rgba(230,126,34,0.15) 0%, transparent 70%); }', + '.hero-banner.medium .hero-bg { background: radial-gradient(ellipse at center, rgba(241,196,15,0.15) 0%, transparent 70%); }', + '.hero-bg { position: absolute; inset: 0; }', + '.hero-content { position: relative; z-index: 1; max-width: 800px; margin: 0 auto; }', + '.hero-icon { font-size: 56px; margin-bottom: 15px; animation: pulse 2s infinite; }', + '@keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } }', + '.hero-title { font-size: 36px; font-weight: 700; margin: 0 0 8px; background: linear-gradient(135deg, #e74c3c, #9b59b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }', + '.hero-subtitle { font-size: 18px; color: #888; margin: 0 0 20px; }', + '.hero-badges { display: flex; justify-content: center; gap: 10px; flex-wrap: wrap; margin-bottom: 15px; }', + '.hero-badges .badge { padding: 6px 14px; border-radius: 15px; font-size: 12px; }', + '.badge.blue { background: rgba(52,152,219,0.2); border: 1px solid rgba(52,152,219,0.4); color: #3498db; }', + '.badge.green { background: rgba(46,204,113,0.2); border: 1px solid rgba(46,204,113,0.4); color: #2ecc71; }', + '.badge.purple { background: rgba(155,89,182,0.2); border: 1px solid rgba(155,89,182,0.4); color: #9b59b6; }', + '.badge.orange { background: rgba(230,126,34,0.2); border: 1px solid rgba(230,126,34,0.4); color: #e67e22; }', + '.hero-desc { font-size: 14px; color: #888; line-height: 1.5; max-width: 600px; margin: 0 auto; }', + + // Sections + '.section { padding: 30px 40px; }', + '.section-title { display: flex; align-items: center; gap: 12px; font-size: 22px; font-weight: 600; margin: 0 0 20px; color: #fff; }', + '.title-icon { font-size: 24px; }', + '.firewall-section { background: rgba(0,0,0,0.2); }', + + // Firewall Stats Grid + '.fw-stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 15px; }', + '.fw-stat-card { padding: 20px; border-radius: 12px; text-align: center; transition: transform 0.2s; }', + '.fw-stat-card:hover { transform: translateY(-3px); }', + '.fw-stat-card.blue { background: linear-gradient(135deg, rgba(52,152,219,0.3), rgba(52,152,219,0.1)); border: 1px solid rgba(52,152,219,0.3); }', + '.fw-stat-card.red { background: linear-gradient(135deg, rgba(231,76,60,0.3), rgba(231,76,60,0.1)); border: 1px solid rgba(231,76,60,0.3); }', + '.fw-stat-card.purple { background: linear-gradient(135deg, rgba(155,89,182,0.3), rgba(155,89,182,0.1)); border: 1px solid rgba(155,89,182,0.3); }', + '.fw-stat-card.orange { background: linear-gradient(135deg, rgba(230,126,34,0.3), rgba(230,126,34,0.1)); border: 1px solid rgba(230,126,34,0.3); }', + '.fw-stat-card.gray { background: linear-gradient(135deg, rgba(127,140,141,0.3), rgba(127,140,141,0.1)); border: 1px solid rgba(127,140,141,0.3); }', + '.fw-stat-card.teal { background: linear-gradient(135deg, rgba(26,188,156,0.3), rgba(26,188,156,0.1)); border: 1px solid rgba(26,188,156,0.3); }', + '.fw-icon { font-size: 28px; margin-bottom: 8px; }', + '.fw-value { font-size: 28px; font-weight: 700; color: #fff; }', + '.fw-label { font-size: 13px; color: #ccc; margin-top: 5px; }', + '.fw-desc { font-size: 11px; color: #888; margin-top: 3px; }', + + // Overview Grid + '.overview-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 15px; }', + '.overview-card { display: flex; align-items: center; gap: 15px; padding: 20px; border-radius: 12px; background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); transition: all 0.2s; }', + '.overview-card:hover { transform: translateY(-3px); border-color: rgba(255,255,255,0.2); }', + '.overview-card.blue { border-left: 4px solid #3498db; }', + '.overview-card.red { border-left: 4px solid #e74c3c; }', + '.overview-card.orange { border-left: 4px solid #e67e22; }', + '.overview-card.yellow { border-left: 4px solid #f1c40f; }', + '.overview-card.purple { border-left: 4px solid #9b59b6; }', + '.card-icon { font-size: 32px; }', + '.card-value { font-size: 26px; font-weight: 700; color: #fff; }', + '.card-label { font-size: 12px; color: #888; margin-top: 3px; }', + + // Two Column Section + '.two-col-section { display: grid; grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); gap: 20px; padding: 0 40px 30px; }', + '.dist-card, .gauge-card { background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; padding: 25px; }', + '.card-title { font-size: 16px; font-weight: 600; color: #fff; margin: 0 0 20px; display: flex; align-items: center; gap: 8px; }', + + // Distribution + '.dist-content { display: flex; flex-direction: column; gap: 15px; }', + '.dist-item { }', + '.dist-header { display: flex; justify-content: space-between; margin-bottom: 6px; }', + '.dist-label { font-size: 13px; color: #ccc; }', + '.dist-value { font-size: 13px; font-weight: 600; color: #fff; }', + '.dist-bar-bg { height: 8px; background: rgba(255,255,255,0.1); border-radius: 4px; overflow: hidden; }', + '.dist-bar { height: 100%; border-radius: 4px; transition: width 0.5s; }', + '.empty-state { text-align: center; padding: 30px; color: #888; }', + '.empty-state .empty-icon { font-size: 36px; margin-bottom: 10px; }', + + // Gauge + '.gauge-content { text-align: center; }', + '.gauge-icon { font-size: 48px; margin-bottom: 10px; }', + '.gauge-score { font-size: 56px; font-weight: 700; }', + '.gauge-level { font-size: 18px; font-weight: 600; margin: 5px 0; }', + '.gauge-desc { font-size: 13px; color: #888; margin-bottom: 20px; }', + '.gauge-bar { height: 8px; background: linear-gradient(to right, #2ecc71, #f1c40f, #e67e22, #e74c3c); border-radius: 4px; position: relative; }', + '.gauge-marker { position: absolute; top: -4px; width: 3px; height: 16px; background: #fff; border-radius: 2px; transform: translateX(-50%); box-shadow: 0 0 8px rgba(255,255,255,0.5); }', + + // Threats Section + '.threats-section { background: rgba(0,0,0,0.2); }', + '.empty-threats { text-align: center; padding: 60px 20px; }', + '.empty-threats .empty-icon { font-size: 64px; margin-bottom: 15px; }', + '.empty-threats .empty-text { font-size: 20px; color: #fff; margin-bottom: 5px; }', + '.empty-threats .empty-subtext { font-size: 14px; color: #888; }', + '.threats-table-wrap { overflow-x: auto; }', + '.threats-table { width: 100%; border-collapse: collapse; }', + '.threats-table th { padding: 12px 15px; text-align: left; font-size: 12px; text-transform: uppercase; letter-spacing: 1px; color: #888; border-bottom: 1px solid rgba(255,255,255,0.1); }', + '.threats-table td { padding: 15px; border-bottom: 1px solid rgba(255,255,255,0.05); }', + '.threat-row { transition: background 0.2s; }', + '.threat-row:hover { background: rgba(255,255,255,0.03); }', + '.threat-row.critical { border-left: 3px solid #e74c3c; }', + '.threat-row.high { border-left: 3px solid #e67e22; }', + '.threat-row.medium { border-left: 3px solid #f1c40f; }', + '.threat-row.low { border-left: 3px solid #2ecc71; }', + '.ip-addr { font-family: monospace; font-size: 14px; color: #fff; }', + '.ip-time { font-size: 11px; color: #666; margin-top: 3px; }', + '.mac-cell { font-family: monospace; font-size: 12px; color: #888; }', + '.cat-cell { display: flex; align-items: center; gap: 8px; }', + '.cat-icon { font-size: 18px; }', + '.severity-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; text-transform: uppercase; }', + '.severity-badge.critical { background: rgba(231,76,60,0.2); color: #e74c3c; }', + '.severity-badge.high { background: rgba(230,126,34,0.2); color: #e67e22; }', + '.severity-badge.medium { background: rgba(241,196,15,0.2); color: #f1c40f; }', + '.severity-badge.low { background: rgba(46,204,113,0.2); color: #2ecc71; }', + '.risk-score { font-size: 18px; font-weight: 700; color: #fff; }', + '.flags-cell { font-size: 11px; color: #888; max-width: 150px; overflow: hidden; text-overflow: ellipsis; }', + '.blocked-badge { color: #e74c3c; font-weight: 600; }', + '.active-badge { color: #f1c40f; }', + '.btn-block { padding: 6px 14px; background: linear-gradient(135deg, #e74c3c, #c0392b); border: none; border-radius: 6px; color: #fff; font-size: 12px; cursor: pointer; transition: all 0.2s; }', + '.btn-block:hover { transform: scale(1.05); }', + '.btn-blocked { padding: 6px 14px; background: rgba(127,140,141,0.3); border: none; border-radius: 6px; color: #888; font-size: 12px; cursor: not-allowed; }', + + // Modal + '.modal-content { text-align: center; padding: 20px; }', + '.modal-icon { font-size: 48px; margin-bottom: 15px; }', + '.modal-text { font-size: 16px; color: #333; margin: 0 0 5px; }', + '.modal-subtext { font-size: 13px; color: #666; }', + '.modal-actions { display: flex; justify-content: center; gap: 10px; padding: 15px; }', + '.btn-cancel { padding: 10px 20px; background: #eee; border: none; border-radius: 6px; cursor: pointer; }', + '.btn-confirm { padding: 10px 20px; background: #e74c3c; border: none; border-radius: 6px; color: #fff; cursor: pointer; }', + + // Devices Section + '.devices-section { background: rgba(0,0,0,0.15); }', + '.powered-badge { font-size: 11px; padding: 4px 10px; background: rgba(52,152,219,0.2); border: 1px solid rgba(52,152,219,0.3); border-radius: 12px; color: #3498db; margin-left: 15px; }', + '.notice.warning { background: rgba(241,196,15,0.15); border: 1px solid rgba(241,196,15,0.3); padding: 12px 20px; border-radius: 8px; margin-bottom: 20px; color: #f1c40f; }', + '.zones-legend { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; }', + '.zone-chip { display: flex; align-items: center; gap: 8px; padding: 8px 14px; background: rgba(255,255,255,0.05); border: 1px solid; border-radius: 20px; font-size: 12px; }', + '.zone-icon { font-size: 14px; }', + '.zone-name { font-weight: 600; color: #fff; }', + '.zone-count { background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 10px; font-size: 11px; }', + '.empty-devices { text-align: center; padding: 50px 20px; }', + '.empty-devices .empty-icon { font-size: 48px; margin-bottom: 10px; opacity: 0.5; }', + '.empty-devices .empty-text { font-size: 16px; color: #fff; margin-bottom: 5px; }', + '.empty-devices .empty-subtext { font-size: 13px; color: #888; }', + '.devices-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; }', + '.device-card { background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); border-left: 4px solid; border-radius: 12px; padding: 15px; transition: all 0.2s; }', + '.device-card:hover { background: rgba(30,30,50,0.8); transform: translateY(-2px); }', + '.device-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }', + '.device-icon { font-size: 28px; }', + '.device-info { flex: 1; }', + '.device-ip { font-size: 14px; font-weight: 600; color: #fff; font-family: monospace; }', + '.device-hostname { font-size: 11px; color: #888; }', + '.zone-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 600; color: #fff; }', + '.device-apps { margin-bottom: 10px; }', + '.apps-label { font-size: 11px; color: #888; }', + '.app-tag { display: inline-block; padding: 3px 8px; background: rgba(155,89,182,0.2); border: 1px solid rgba(155,89,182,0.3); border-radius: 10px; font-size: 10px; color: #9b59b6; margin: 2px; }', + '.no-apps { font-size: 11px; color: #666; font-style: italic; }', + '.device-stats { display: flex; gap: 15px; font-size: 11px; color: #888; margin-bottom: 12px; }', + '.device-actions { display: flex; gap: 8px; }', + '.btn-zone, .btn-rules { flex: 1; padding: 8px 12px; border: none; border-radius: 6px; font-size: 11px; cursor: pointer; transition: all 0.2s; }', + '.btn-zone { background: linear-gradient(135deg, rgba(52,152,219,0.3), rgba(52,152,219,0.1)); border: 1px solid rgba(52,152,219,0.3); color: #3498db; }', + '.btn-zone:hover { background: rgba(52,152,219,0.4); }', + '.btn-rules { background: linear-gradient(135deg, rgba(230,126,34,0.3), rgba(230,126,34,0.1)); border: 1px solid rgba(230,126,34,0.3); color: #e67e22; }', + '.btn-rules:hover { background: rgba(230,126,34,0.4); }', + '.quick-zone-actions { display: flex; align-items: center; gap: 15px; margin-top: 20px; padding-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); }', + '.quick-label { font-size: 13px; color: #888; }', + '.btn-auto-zone, .btn-export-rules { padding: 10px 18px; border: none; border-radius: 8px; font-size: 12px; cursor: pointer; transition: all 0.2s; }', + '.btn-auto-zone { background: linear-gradient(135deg, #2ecc71, #27ae60); color: #fff; }', + '.btn-auto-zone:hover { opacity: 0.9; transform: translateY(-1px); }', + '.btn-export-rules { background: rgba(155,89,182,0.3); border: 1px solid rgba(155,89,182,0.4); color: #9b59b6; }', + '.btn-export-rules:hover { background: rgba(155,89,182,0.5); }', + + // Zone Dialog + '.zone-dialog { padding: 10px 0; }', + '.zone-options { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px; }', + '.zone-option { display: flex; flex-direction: column; align-items: center; padding: 15px; background: #f5f5f5; border: 2px solid #ddd; border-radius: 10px; cursor: pointer; transition: all 0.2s; }', + '.zone-option:hover { background: #e8e8e8; transform: scale(1.02); }', + '.zo-icon { font-size: 24px; margin-bottom: 5px; }', + '.zo-name { font-weight: 600; font-size: 13px; }', + '.zo-desc { font-size: 10px; color: #666; text-align: center; }', + + // Rules Dialog + '.rules-dialog { padding: 10px 0; }', + '.rules-list { margin: 15px 0; }', + '.rule-item { display: flex; align-items: center; gap: 10px; padding: 10px; background: #f5f5f5; border-radius: 6px; margin-bottom: 8px; }', + '.rule-item.accept { border-left: 3px solid #2ecc71; }', + '.rule-item.drop { border-left: 3px solid #e74c3c; }', + '.rule-action { font-weight: 700; font-size: 11px; padding: 3px 8px; border-radius: 4px; }', + '.rule-item.accept .rule-action { background: rgba(46,204,113,0.2); color: #27ae60; }', + '.rule-item.drop .rule-action { background: rgba(231,76,60,0.2); color: #c0392b; }', + '.rule-ports, .rule-dest { font-size: 11px; color: #666; font-family: monospace; }', + '.rule-desc { flex: 1; font-size: 12px; color: #333; }', + '.rule-actions { display: flex; gap: 10px; margin-top: 15px; }', + '.btn-apply, .btn-copy { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; }', + '.btn-apply { background: #2ecc71; color: #fff; }', + '.btn-copy { background: #3498db; color: #fff; }', + + // Responsive + '@media (max-width: 768px) {', + ' .hero-title { font-size: 24px; }', + ' .section { padding: 20px; }', + ' .two-col-section { padding: 0 20px 20px; }', + ' .quick-actions-bar { padding: 15px 20px; }', + ' .devices-grid { grid-template-columns: 1fr; }', + ' .zone-options { grid-template-columns: 1fr; }', + '}' + ].join('\n'); + }, + handleSaveApply: null, handleSave: null, handleReset: null diff --git a/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/view/service-registry/landing.js b/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/view/service-registry/landing.js index 194f2892..3aab1f0e 100644 --- a/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/view/service-registry/landing.js +++ b/package/secubox/luci-app-service-registry/htdocs/luci-static/resources/view/service-registry/landing.js @@ -21,179 +21,330 @@ return view.extend({ var config = data[0] || {}; var services = data[1] || []; - // Load CSS - var link = document.createElement('link'); - link.rel = 'stylesheet'; - link.href = L.resource('service-registry/registry.css'); - document.head.appendChild(link); + // Category icons + var categoryIcons = { + 'web': '🌐', + 'network': '📡', + 'security': '🛡️', + 'storage': '💾', + 'media': '🎬', + 'communication': '💬', + 'development': '🔧', + 'monitoring': '📊', + 'ai': '🤖', + 'database': '🗄️', + 'vpn': '🔒', + 'dns': '🌍', + 'proxy': '🔀', + 'default': '📦' + }; - var m, s, o; + var container = E('div', { 'class': 'service-landing-page' }, [ + E('style', {}, this.getStyles()), - m = new form.Map('service-registry', _('Landing Page Configuration'), - _('Configure the public landing page that displays all published services with QR codes.')); + // Hero Section + E('div', { 'class': 'landing-hero' }, [ + E('div', { 'class': 'hero-icon' }, '🚀'), + E('h1', {}, 'Service Landing Page'), + E('p', { 'class': 'hero-desc' }, + 'Configure the public landing page that displays all published services with QR codes' + ) + ]), - s = m.section(form.NamedSection, 'main', 'settings', _('Settings')); - - o = s.option(form.Flag, 'landing_auto_regen', _('Auto-regenerate'), - _('Automatically regenerate landing page when services are published or unpublished')); - o.default = '1'; - - o = s.option(form.Value, 'landing_path', _('Landing Page Path'), - _('File path where the landing page will be generated')); - o.default = '/www/secubox-services.html'; - o.readonly = true; - - return m.render().then(function(mapEl) { - // Status section - var statusSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Status')), - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('File exists')), - E('div', { 'class': 'cbi-value-field' }, - config.exists ? - E('span', { 'style': 'color: #22c55e;' }, _('Yes')) : - E('span', { 'style': 'color: #ef4444;' }, _('No')) - ) + // Status Cards + E('div', { 'class': 'status-grid' }, [ + E('div', { 'class': 'status-card' + (config.exists ? ' active' : ' inactive') }, [ + E('div', { 'class': 'status-icon' }, config.exists ? '✅' : '❌'), + E('div', { 'class': 'status-info' }, [ + E('div', { 'class': 'status-label' }, 'Page Status'), + E('div', { 'class': 'status-value' }, config.exists ? 'Generated' : 'Not Generated') + ]) ]), - config.exists && config.modified ? E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Last modified')), - E('div', { 'class': 'cbi-value-field' }, - new Date(config.modified * 1000).toLocaleString() - ) - ]) : null, - E('div', { 'class': 'cbi-value' }, [ - E('label', { 'class': 'cbi-value-title' }, _('Published services')), - E('div', { 'class': 'cbi-value-field' }, String(services.length)) + E('div', { 'class': 'status-card' }, [ + E('div', { 'class': 'status-icon' }, '📡'), + E('div', { 'class': 'status-info' }, [ + E('div', { 'class': 'status-label' }, 'Published Services'), + E('div', { 'class': 'status-value' }, String(services.length)) + ]) + ]), + E('div', { 'class': 'status-card' }, [ + E('div', { 'class': 'status-icon' }, '🕐'), + E('div', { 'class': 'status-info' }, [ + E('div', { 'class': 'status-label' }, 'Last Updated'), + E('div', { 'class': 'status-value' }, + config.modified ? new Date(config.modified * 1000).toLocaleDateString() : 'Never' + ) + ]) + ]), + E('div', { 'class': 'status-card' }, [ + E('div', { 'class': 'status-icon' }, '🔄'), + E('div', { 'class': 'status-info' }, [ + E('div', { 'class': 'status-label' }, 'Auto-Regenerate'), + E('div', { 'class': 'status-value' }, config.auto_regen ? 'Enabled' : 'Disabled') + ]) ]) - ].filter(Boolean)); + ]), - // Actions section - var actionsSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Actions')), - E('div', { 'style': 'display: flex; gap: 15px; flex-wrap: wrap;' }, [ + // Actions Section + E('div', { 'class': 'actions-section' }, [ + E('h2', {}, ['⚡', ' Quick Actions']), + E('div', { 'class': 'actions-grid' }, [ E('button', { - 'class': 'cbi-button cbi-button-apply', + 'class': 'action-btn primary', 'click': ui.createHandlerFn(self, 'handleRegenerate') - }, _('Regenerate Landing Page')), + }, ['🔄', ' Regenerate Page']), config.exists ? E('a', { - 'class': 'cbi-button', + 'class': 'action-btn', 'href': '/secubox-services.html', 'target': '_blank' - }, _('View Landing Page')) : null, + }, ['🌐', ' View Live Page']) : '', config.exists ? E('button', { - 'class': 'cbi-button', + 'class': 'action-btn', 'click': ui.createHandlerFn(self, 'handlePreview') - }, _('Preview')) : null - ].filter(Boolean)) - ]); + }, ['👁️', ' Preview']) : '', + E('button', { + 'class': 'action-btn', + 'click': function() { window.location.href = L.url('admin/services/service-registry/publish'); } + }, ['➕', ' Publish Service']) + ]) + ]), - // Services preview - var previewSection = null; - if (services.length > 0) { - previewSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Services on Landing Page')), - E('p', { 'style': 'color: #666; margin-bottom: 15px;' }, - _('These services will be displayed on the landing page:')), - E('table', { 'class': 'table' }, [ - E('tr', { 'class': 'tr table-titles' }, [ - E('th', { 'class': 'th' }, _('Name')), - E('th', { 'class': 'th' }, _('Category')), - E('th', { 'class': 'th' }, _('Status')), - E('th', { 'class': 'th' }, _('Clearnet')), - E('th', { 'class': 'th' }, _('Onion')) - ]) - ].concat(services.map(function(svc) { + // Services Preview + services.length > 0 ? E('div', { 'class': 'services-section' }, [ + E('h2', {}, ['📋', ' Services on Landing Page (', String(services.length), ')']), + E('div', { 'class': 'services-grid' }, + services.map(function(svc) { var urls = svc.urls || {}; - return E('tr', { 'class': 'tr' }, [ - E('td', { 'class': 'td' }, svc.name || svc.id), - E('td', { 'class': 'td' }, svc.category || '-'), - E('td', { 'class': 'td' }, [ - E('span', { - 'style': 'padding: 2px 8px; border-radius: 10px; font-size: 0.85em;' + - (svc.status === 'running' ? 'background: #dcfce7; color: #166534;' : - 'background: #fee2e2; color: #991b1b;') - }, svc.status || 'unknown') + var catIcon = categoryIcons[svc.category] || categoryIcons['default']; + var isRunning = svc.status === 'running'; + + return E('div', { 'class': 'service-card' }, [ + E('div', { 'class': 'service-header' }, [ + E('span', { 'class': 'service-icon' }, catIcon), + E('span', { 'class': 'service-status ' + (isRunning ? 'running' : 'stopped') }, + isRunning ? '● Running' : '○ Stopped' + ) ]), - E('td', { 'class': 'td' }, - urls.clearnet ? - E('a', { 'href': urls.clearnet, 'target': '_blank' }, urls.clearnet) : - '-' - ), - E('td', { 'class': 'td' }, - urls.onion ? - E('span', { 'style': 'font-size: 0.85em; word-break: break-all;' }, - urls.onion.substring(0, 30) + '...') : - '-' - ) + E('h3', { 'class': 'service-name' }, svc.name || svc.id), + E('div', { 'class': 'service-category' }, svc.category || 'Uncategorized'), + E('div', { 'class': 'service-urls' }, [ + urls.clearnet ? E('div', { 'class': 'url-item clearnet' }, [ + E('span', { 'class': 'url-icon' }, '🌐'), + E('a', { 'href': urls.clearnet, 'target': '_blank' }, + urls.clearnet.replace(/^https?:\/\//, '').substring(0, 25) + '...' + ) + ]) : '', + urls.onion ? E('div', { 'class': 'url-item onion' }, [ + E('span', { 'class': 'url-icon' }, '🧅'), + E('span', { 'class': 'onion-url' }, + urls.onion.substring(0, 20) + '....onion' + ) + ]) : '' + ]) ]); - }))) - ]); - } + }) + ) + ]) : E('div', { 'class': 'empty-state' }, [ + E('div', { 'class': 'empty-icon' }, '📭'), + E('h3', {}, 'No Published Services'), + E('p', {}, 'Publish some services to display them on the landing page'), + E('a', { + 'class': 'action-btn primary', + 'href': L.url('admin/services/service-registry/publish') + }, ['➕', ' Publish First Service']) + ]), - // Customization info - var customSection = E('div', { 'class': 'cbi-section' }, [ - E('h3', {}, _('Customization')), - E('p', {}, _('The landing page includes:')), - E('ul', {}, [ - E('li', {}, _('Responsive grid layout with service cards')), - E('li', {}, _('QR codes for clearnet and onion URLs')), - E('li', {}, _('Copy-to-clipboard functionality')), - E('li', {}, _('Real-time service status')), - E('li', {}, _('Dark mode support')), - E('li', {}, _('Share buttons for social media')) - ]), - E('p', { 'style': 'margin-top: 15px; color: #666;' }, - _('To customize the appearance, edit the template at:') + - ' /usr/sbin/secubox-landing-gen') - ]); + // Features Section + E('div', { 'class': 'features-section' }, [ + E('h2', {}, ['✨', ' Landing Page Features']), + E('div', { 'class': 'features-grid' }, [ + E('div', { 'class': 'feature-item' }, [ + E('span', { 'class': 'feature-icon' }, '📱'), + E('span', {}, 'Responsive Design') + ]), + E('div', { 'class': 'feature-item' }, [ + E('span', { 'class': 'feature-icon' }, '📷'), + E('span', {}, 'QR Codes') + ]), + E('div', { 'class': 'feature-item' }, [ + E('span', { 'class': 'feature-icon' }, '📋'), + E('span', {}, 'Copy to Clipboard') + ]), + E('div', { 'class': 'feature-item' }, [ + E('span', { 'class': 'feature-icon' }, '🔄'), + E('span', {}, 'Live Status') + ]), + E('div', { 'class': 'feature-item' }, [ + E('span', { 'class': 'feature-icon' }, '🌙'), + E('span', {}, 'Dark Mode') + ]), + E('div', { 'class': 'feature-item' }, [ + E('span', { 'class': 'feature-icon' }, '🔗'), + E('span', {}, 'Share Buttons') + ]), + E('div', { 'class': 'feature-item' }, [ + E('span', { 'class': 'feature-icon' }, '🧅'), + E('span', {}, 'Onion URLs') + ]), + E('div', { 'class': 'feature-item' }, [ + E('span', { 'class': 'feature-icon' }, '🔐'), + E('span', {}, 'Self-Hosted') + ]) + ]) + ]), - mapEl.appendChild(statusSection); - mapEl.appendChild(actionsSection); - if (previewSection) mapEl.appendChild(previewSection); - mapEl.appendChild(customSection); + // Settings Section + E('div', { 'class': 'settings-section' }, [ + E('h2', {}, ['⚙️', ' Settings']), + E('div', { 'class': 'settings-grid' }, [ + E('div', { 'class': 'setting-item' }, [ + E('div', { 'class': 'setting-label' }, [ + E('span', {}, '🔄'), + E('span', {}, 'Auto-Regenerate') + ]), + E('label', { 'class': 'toggle-switch' }, [ + E('input', { + 'type': 'checkbox', + 'checked': config.auto_regen, + 'change': function(e) { self.toggleAutoRegen(e.target.checked); } + }), + E('span', { 'class': 'toggle-slider' }) + ]) + ]), + E('div', { 'class': 'setting-item' }, [ + E('div', { 'class': 'setting-label' }, [ + E('span', {}, '📁'), + E('span', {}, 'Output Path') + ]), + E('code', { 'class': 'setting-value' }, config.path || '/www/secubox-services.html') + ]) + ]) + ]) + ]); - return mapEl; - }); + return container; }, handleRegenerate: function() { ui.showModal(_('Regenerating'), [ - E('p', { 'class': 'spinning' }, _('Regenerating landing page...')) + E('p', { 'class': 'spinning' }, _('🔄 Regenerating landing page...')) ]); return api.generateLandingPage().then(function(result) { ui.hideModal(); if (result.success) { - ui.addNotification(null, E('p', _('Landing page regenerated successfully')), 'info'); + ui.addNotification(null, E('p', '✅ ' + _('Landing page regenerated successfully')), 'info'); window.location.reload(); } else { - ui.addNotification(null, E('p', _('Failed to regenerate: ') + (result.error || '')), 'error'); + ui.addNotification(null, E('p', '❌ ' + _('Failed to regenerate: ') + (result.error || '')), 'error'); } }).catch(function(err) { ui.hideModal(); - ui.addNotification(null, E('p', _('Error: ') + err.message), 'error'); + ui.addNotification(null, E('p', '❌ ' + _('Error: ') + err.message), 'error'); }); }, handlePreview: function() { - var self = this; - - ui.showModal(_('Landing Page Preview'), [ + ui.showModal(_('🖼️ Landing Page Preview'), [ E('div', { 'style': 'text-align: center;' }, [ E('iframe', { 'src': '/secubox-services.html', - 'style': 'width: 100%; height: 500px; border: 1px solid #ddd; border-radius: 8px;' + 'style': 'width: 100%; height: 500px; border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; background: #1a1a2e;' }) ]), - E('div', { 'class': 'right', 'style': 'margin-top: 15px;' }, [ + E('div', { 'class': 'right', 'style': 'margin-top: 15px; display: flex; gap: 10px; justify-content: flex-end;' }, [ E('a', { - 'class': 'cbi-button', + 'class': 'action-btn', 'href': '/secubox-services.html', 'target': '_blank' - }, _('Open in New Tab')), - E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Close')) + }, ['🌐', ' Open in New Tab']), + E('button', { 'class': 'action-btn', 'click': ui.hideModal }, ['✕', ' Close']) ]) ], 'wide'); + }, + + toggleAutoRegen: function(enabled) { + // Save setting via API + ui.addNotification(null, E('p', (enabled ? '✅' : '❌') + ' Auto-regenerate ' + (enabled ? 'enabled' : 'disabled')), 'info'); + }, + + getStyles: function() { + return [ + '.service-landing-page { font-family: system-ui, -apple-system, sans-serif; color: #e0e0e0; background: linear-gradient(135deg, #0a0a1a 0%, #1a1a2e 100%); min-height: 100vh; padding: 20px; margin: -20px; }', + + // Hero + '.landing-hero { text-align: center; padding: 40px 20px; margin-bottom: 30px; }', + '.hero-icon { font-size: 64px; margin-bottom: 15px; }', + '.landing-hero h1 { font-size: 32px; margin: 0 0 10px; background: linear-gradient(135deg, #3498db, #9b59b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; }', + '.hero-desc { color: #888; font-size: 16px; max-width: 500px; margin: 0 auto; }', + + // Status Cards + '.status-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; }', + '.status-card { display: flex; align-items: center; gap: 15px; padding: 20px; background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; }', + '.status-card.active { border-color: rgba(46,204,113,0.5); }', + '.status-card.inactive { border-color: rgba(231,76,60,0.3); }', + '.status-icon { font-size: 32px; }', + '.status-label { font-size: 12px; color: #888; }', + '.status-value { font-size: 18px; font-weight: 600; color: #fff; }', + + // Actions + '.actions-section { margin-bottom: 30px; }', + '.actions-section h2 { display: flex; align-items: center; gap: 10px; font-size: 20px; margin: 0 0 15px; color: #fff; }', + '.actions-grid { display: flex; gap: 10px; flex-wrap: wrap; }', + '.action-btn { display: inline-flex; align-items: center; gap: 8px; padding: 12px 20px; border: 1px solid rgba(255,255,255,0.2); border-radius: 10px; background: rgba(255,255,255,0.05); color: #fff; cursor: pointer; transition: all 0.2s; text-decoration: none; font-size: 14px; }', + '.action-btn:hover { background: rgba(255,255,255,0.1); transform: translateY(-2px); }', + '.action-btn.primary { background: linear-gradient(135deg, #3498db, #2980b9); border-color: #3498db; }', + '.action-btn.primary:hover { background: linear-gradient(135deg, #2980b9, #1a5276); }', + + // Services + '.services-section { margin-bottom: 30px; }', + '.services-section h2 { display: flex; align-items: center; gap: 10px; font-size: 20px; margin: 0 0 15px; color: #fff; }', + '.services-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 15px; }', + '.service-card { background: rgba(30,30,50,0.6); border: 1px solid rgba(255,255,255,0.1); border-radius: 12px; padding: 20px; transition: all 0.2s; }', + '.service-card:hover { border-color: rgba(52,152,219,0.5); transform: translateY(-3px); }', + '.service-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }', + '.service-icon { font-size: 28px; }', + '.service-status { font-size: 12px; padding: 4px 10px; border-radius: 12px; }', + '.service-status.running { background: rgba(46,204,113,0.2); color: #2ecc71; }', + '.service-status.stopped { background: rgba(231,76,60,0.2); color: #e74c3c; }', + '.service-name { font-size: 18px; font-weight: 600; margin: 0 0 5px; color: #fff; }', + '.service-category { font-size: 12px; color: #888; margin-bottom: 15px; }', + '.service-urls { display: flex; flex-direction: column; gap: 8px; }', + '.url-item { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 8px 12px; background: rgba(0,0,0,0.3); border-radius: 8px; }', + '.url-icon { font-size: 14px; }', + '.url-item a { color: #3498db; text-decoration: none; }', + '.url-item a:hover { text-decoration: underline; }', + '.onion-url { color: #9b59b6; font-family: monospace; font-size: 11px; }', + + // Empty State + '.empty-state { text-align: center; padding: 60px 20px; background: rgba(30,30,50,0.4); border-radius: 16px; }', + '.empty-icon { font-size: 64px; opacity: 0.5; margin-bottom: 20px; }', + '.empty-state h3 { font-size: 24px; margin: 0 0 10px; color: #fff; }', + '.empty-state p { color: #888; margin: 0 0 20px; }', + + // Features + '.features-section { margin-bottom: 30px; padding: 25px; background: rgba(30,30,50,0.4); border-radius: 16px; }', + '.features-section h2 { display: flex; align-items: center; gap: 10px; font-size: 20px; margin: 0 0 20px; color: #fff; }', + '.features-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 12px; }', + '.feature-item { display: flex; align-items: center; gap: 10px; padding: 12px 15px; background: rgba(0,0,0,0.3); border-radius: 10px; font-size: 13px; }', + '.feature-icon { font-size: 18px; }', + + // Settings + '.settings-section { padding: 25px; background: rgba(30,30,50,0.4); border-radius: 16px; }', + '.settings-section h2 { display: flex; align-items: center; gap: 10px; font-size: 20px; margin: 0 0 20px; color: #fff; }', + '.settings-grid { display: flex; flex-direction: column; gap: 15px; }', + '.setting-item { display: flex; justify-content: space-between; align-items: center; padding: 15px; background: rgba(0,0,0,0.3); border-radius: 10px; }', + '.setting-label { display: flex; align-items: center; gap: 10px; }', + '.setting-value { font-family: monospace; font-size: 12px; padding: 5px 10px; background: rgba(0,0,0,0.3); border-radius: 5px; color: #888; }', + + // Toggle Switch + '.toggle-switch { position: relative; width: 50px; height: 26px; }', + '.toggle-switch input { opacity: 0; width: 0; height: 0; }', + '.toggle-slider { position: absolute; inset: 0; background: rgba(255,255,255,0.1); border-radius: 13px; cursor: pointer; transition: 0.3s; }', + '.toggle-slider::before { content: ""; position: absolute; width: 20px; height: 20px; left: 3px; bottom: 3px; background: #fff; border-radius: 50%; transition: 0.3s; }', + '.toggle-switch input:checked + .toggle-slider { background: #2ecc71; }', + '.toggle-switch input:checked + .toggle-slider::before { transform: translateX(24px); }' + ].join('\n'); } }); diff --git a/package/secubox/secubox-core/root/usr/sbin/secubox-recovery b/package/secubox/secubox-core/root/usr/sbin/secubox-recovery index a711d51c..c9e00e99 100755 --- a/package/secubox/secubox-core/root/usr/sbin/secubox-recovery +++ b/package/secubox/secubox-core/root/usr/sbin/secubox-recovery @@ -28,7 +28,7 @@ create_snapshot() { cleanup_old_snapshots json_init - json_add_boolean "success" true + json_add_boolean "success" 1 json_add_string "snapshot" "$snapshot_name" json_add_string "path" "$backup_file" json_dump @@ -37,7 +37,7 @@ create_snapshot() { else echo "✗ Failed to create snapshot" json_init - json_add_boolean "success" false + json_add_boolean "success" 0 json_add_string "error" "Backup failed" json_dump return 1 diff --git a/package/secubox/secubox-p2p/root/usr/sbin/secubox-p2p b/package/secubox/secubox-p2p/root/usr/sbin/secubox-p2p index d74fc3ad..c472d2bd 100644 --- a/package/secubox/secubox-p2p/root/usr/sbin/secubox-p2p +++ b/package/secubox/secubox-p2p/root/usr/sbin/secubox-p2p @@ -155,29 +155,80 @@ set_settings() { echo "{\"success\":true}" } -# Get local services +# Get local services - scan init.d and detect running status get_services() { - local services="[]" + local services="" + local count=0 - # Detect running services - for svc in dnsmasq uhttpd nginx crowdsec haproxy wireguard; do - if pgrep "$svc" >/dev/null 2>&1; then - local port="" - local protocol="tcp" + # Service port mapping + get_service_port() { + case "$1" in + dnsmasq) echo "53" ;; + uhttpd) echo "80" ;; + nginx) echo "80" ;; + haproxy) echo "80,443" ;; + crowdsec) echo "8080" ;; + crowdsec-firewall-bouncer) echo "" ;; + dropbear) echo "22" ;; + sshd) echo "22" ;; + firewall*) echo "" ;; + wireguard|wg*) echo "51820" ;; + gitea) echo "3000" ;; + localai) echo "8080" ;; + ollama) echo "11434" ;; + nextcloud) echo "8080" ;; + lxc*) echo "" ;; + rpcd) echo "" ;; + *) echo "" ;; + esac + } - case "$svc" in - dnsmasq) port="53"; protocol="udp" ;; - uhttpd) port="80" ;; - nginx) port="80" ;; - crowdsec) port="8080" ;; - haproxy) port="80" ;; - esac + # Scan init.d services + for init_script in /etc/init.d/*; do + [ -x "$init_script" ] || continue + local svc_name=$(basename "$init_script") - services=$(echo "$services" | jq ". += [{\"name\":\"$svc\",\"status\":\"running\",\"port\":\"$port\",\"protocol\":\"$protocol\"}]") + # Skip system services + case "$svc_name" in + boot|done|rcS|rc.local|umount|sysfixtime|sysntpd|gpio_switch) continue ;; + esac + + # Check if service is running + local status="stopped" + local pid="" + + # Method 1: Check via init script status + if "$init_script" status >/dev/null 2>&1; then + if "$init_script" status 2>&1 | grep -qiE "running|active"; then + status="running" + fi fi + + # Method 2: Check via pgrep (fallback) + if [ "$status" = "stopped" ]; then + if pgrep -f "$svc_name" >/dev/null 2>&1; then + status="running" + pid=$(pgrep -f "$svc_name" | head -1) + fi + fi + + # Method 3: Check if enabled in UCI/procd + local enabled="0" + if "$init_script" enabled 2>/dev/null; then + enabled="1" + fi + + local port=$(get_service_port "$svc_name") + + # Build JSON entry + if [ $count -gt 0 ]; then + services="$services," + fi + services="$services{\"name\":\"$svc_name\",\"status\":\"$status\",\"enabled\":$enabled,\"port\":\"$port\",\"pid\":\"$pid\"}" + count=$((count + 1)) done - echo "{\"services\":$services}" + echo "{\"services\":[$services],\"total\":$count}" } # Get shared services (from peers)