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 <noreply@anthropic.com>
This commit is contained in:
parent
55ef43aa54
commit
5a40e8a61e
@ -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 <noreply@anthropic.com>\nEOF\n\\)\")",
|
||||
"Bash(gzip:*)"
|
||||
"Bash(gzip:*)",
|
||||
"Bash(python3:*)",
|
||||
"WebFetch(domain:192.168.255.1)",
|
||||
"Bash(ssh-add:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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%); }',
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user