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:
CyberMind-FR 2026-01-30 15:03:02 +01:00
parent 55ef43aa54
commit 5a40e8a61e
15 changed files with 3225 additions and 1203 deletions

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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