From 70163e3a506cda9b9e2103cf5c8cade8854761a0 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sat, 14 Feb 2026 22:39:04 +0100 Subject: [PATCH 1/6] AlmaLinux 8/9/10 and Ubuntu 22/24 full support - cyberpanel.sh: Add Ubuntu 24.04 detection, update supported-OS message - simple_install.sh: Add Ubuntu 24.04, AlmaLinux 10; use dnf for Alma 8 - cyberpanel_utility.sh: Add Ubuntu 24.04 to OS detection and supported list - CPScripts/mailscanner*.sh: Add Ubuntu 24.04, AlmaLinux 9/10 to supported list - serverStatus/views.py: Prefer dnf.conf for package locks on dnf-based systems - README.md: Recommend AlmaLinux; add aarch64 note - Remove .DS_Store from repo Co-authored-by: Cursor --- .DS_Store | Bin 12292 -> 0 bytes CPScripts/mailscannerinstaller.sh | 4 +- CPScripts/mailscanneruninstaller.sh | 4 +- README.md | 4 +- cyberpanel.sh | 12 +++++- cyberpanel_utility.sh | 4 +- serverStatus/views.py | 60 +++++++++++++++++++++++----- simple_install.sh | 16 ++++++-- 8 files changed, 80 insertions(+), 24 deletions(-) delete mode 100644 .DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 497829761e1be59ad1d9b9d682da98859e84886e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12292 zcmeHN%Wfk@6uljX#CZor!vYE69kUrL^ANKF=>NH#AY1ZV<$tx0%gSp z@eBL{;t%)&cKil=R&dU(PEz-F+s?2+0=lE_YPavXRdxH^N0lQY$;Rp7U6I=&QpU-4 zc^;SVL@IM=ql4e`%p4^=NpLoB;(l4=L=L1WJF+L$jCA(F1RfDTiE~ySK~IbyNOPy% zJ=kw|_whZHPreKB#Xt4Hlj0~WP*|X_Kw*Kx0)++sjs=)|ag<%Y4^NA0VS&N||7!~{ zK2&hBT}#`G&}!(wg(W<{X9=$d?oq1ivr5{orR_y%6$*?((FSOeE-{h-$MXus(XOTK zMQ8(Dk^q+^?Kw$TD2etCTt&qtSqp7(Ei6!2AZvjS@MuoHl$Nxy{yvtvbZ{QwzVBAw zKftNt)%Ot8t6IO)?skr#m#$h?-F@i$8hYE(wpwK!V&6t<`i#8OSAxD1={YSwIW4J_ z!!BcQqJ_CN(d#RWhI6jVzP2rBJ-1qucbwK)>}+(f!?6L>fxM7Y>B+!hzP|~^jcXPo z=*S^#bcTJCCPs4X?UelC^iC^g0NEU?PQd!cs-cG~Y4noWU77gJVWu&|hMYsThxXXc zv$W}?=hT)RSoc7>7=JIXkJ8T$H7&ca>85PU$5NAR+lTJ=#N3!dA6@9@j2ljt?2KhO zo-Rg1%hoUp&OI{=W#8`+`-Y(0`=ZnUdfgUvAE>()rJNgv zs%l>kfLe7ZvMxp~L?f)iUUlhXyiIvu9yla<6(eUg2GBEVuvZ^`l8FS$uCW+rvl$ax z$SLP-%D-5vMeNCA*|TF;4vs;%Q7oEKT+Xls+cdlW*9 zAe_!JY!>29h|ezaOwt2ljTw2TZxQ;MIFI2Sbujr@eWfbjp!M+;QF`L@$WN&I(!bOn z?d-^XBR{Y5r>Q?ahG(`l-nkq@_8Z)i!7=(?$FK}96L^6^uK?Xu1oPXOGCIWBK6a=% z2EKX6Lq=MoWdT~4zn-Bz!X0hZfDdLPD59=G&d8^TZ0oWmPtaCj;X`XDa^4_Tw5ONI zEG^s}qn{4q)&Nl^SgeXqXL4e-&<~83r6F4K(TK?gJ%j%4kIHf6u0hNKaTO~N(HVDh z*f}2K7&>qC)->0hz&1`dqxi(H5j%(Coc2ng?U@?>xH@>Kx|5 z3?6#{So!UtuDZ@ORrv}UkacZ@V`XRXOXxpT!%gU5mOepzWR1jK3}02EqBH0c=Gl{$ z?SmQ8NKIi}SL=Kyi@I&Ntc&6Es)zZEQJgDd?m8fQSe?v*M(0qR(_e@mGxosRGXQW&;oT7qW$D7}=V_+VWA0w8PbqwTX zAN=h3x>s2l%P&JRaDjYQ9;1zFIoqGI^0n}8K1&$iC)l}oiaLJ>74tLP?cnoAxjsM+ z+uq3jO}sm@_aWxkz#O~4=KBCy9OL-b7l866|0fj9>n$rfO(HCVYB{cpmW zm(uh#+Ow@QqiuDRR)caV?r&j;hD;@^T#Avma*@w(R!f>$4A;b(jVnOx`(ZYjF{$6g zW10bL>9^c5tV2GtV?h2UixGcw5RO45=*(SV7(JC~;H1niIC CyberPanel targets x86\_64 only. Test the unsupported OS in staging first. +> **Architectures:** x86_64 (primary), aarch64/ARM64 (supported). AlmaLinux is the recommended RHEL-compatible distribution. Test unsupported OS in staging first. --- diff --git a/cyberpanel.sh b/cyberpanel.sh index 52713fb5c..feba43655 100644 --- a/cyberpanel.sh +++ b/cyberpanel.sh @@ -117,6 +117,11 @@ detect_os() { OS_FAMILY="rhel" PACKAGE_MANAGER="yum" print_status "Detected: Rocky Linux 8" + elif echo $OUTPUT | grep -q "Ubuntu 24.04" ; then + SERVER_OS="Ubuntu2404" + OS_FAMILY="debian" + PACKAGE_MANAGER="apt" + print_status "Detected: Ubuntu 24.04" elif echo $OUTPUT | grep -q "Ubuntu 22.04" ; then SERVER_OS="Ubuntu2204" OS_FAMILY="debian" @@ -127,6 +132,11 @@ detect_os() { OS_FAMILY="debian" PACKAGE_MANAGER="apt" print_status "Detected: Ubuntu 20.04" + elif echo $OUTPUT | grep -q "Debian GNU/Linux 13" ; then + SERVER_OS="Debian13" + OS_FAMILY="debian" + PACKAGE_MANAGER="apt" + print_status "Detected: Debian GNU/Linux 13" elif echo $OUTPUT | grep -q "Debian GNU/Linux 12" ; then SERVER_OS="Debian12" OS_FAMILY="debian" @@ -139,7 +149,7 @@ detect_os() { print_status "Detected: Debian GNU/Linux 11" else print_status "ERROR: Unsupported OS detected" - print_status "Supported OS: AlmaLinux 8/9/10, CentOS 8/9, Rocky Linux 8/9, Ubuntu 20.04/22.04, Debian 11/12" + print_status "Supported OS: AlmaLinux 8/9/10, CentOS 8/9, Rocky Linux 8/9, Ubuntu 20.04/22.04/24.04, Debian 11/12/13" return 1 fi diff --git a/cyberpanel_utility.sh b/cyberpanel_utility.sh index 26c89743a..fad47d35c 100644 --- a/cyberpanel_utility.sh +++ b/cyberpanel_utility.sh @@ -21,7 +21,7 @@ check_OS() { Server_OS="AlmaLinux" elif grep -q -E "CloudLinux 7|CloudLinux 8" /etc/os-release ; then Server_OS="CloudLinux" - elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04" /etc/os-release ; then + elif grep -q -E "Ubuntu 18.04|Ubuntu 20.04|Ubuntu 20.10|Ubuntu 22.04|Ubuntu 24.04" /etc/os-release ; then Server_OS="Ubuntu" elif grep -q -E "Rocky Linux" /etc/os-release ; then Server_OS="RockyLinux" @@ -29,7 +29,7 @@ check_OS() { Server_OS="openEuler" else echo -e "Unable to detect your system..." - echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" + echo -e "\nCyberPanel is supported on x86_64 based Ubuntu 18.04, Ubuntu 20.04, Ubuntu 20.10, Ubuntu 22.04, Ubuntu 24.04, CentOS 7, CentOS 8, AlmaLinux 8, AlmaLinux 9, AlmaLinux 10, RockyLinux 8, CloudLinux 7, CloudLinux 8, openEuler 20.03, openEuler 22.03...\n" exit fi diff --git a/serverStatus/views.py b/serverStatus/views.py index 8f051b86f..88e4c0295 100644 --- a/serverStatus/views.py +++ b/serverStatus/views.py @@ -884,9 +884,23 @@ def fetchPackages(request): locked = ProcessUtilities.outputExecutioner(command).split('\n') if type == 'CyberPanel': - - command = 'cat /usr/local/CyberCP/AllCPUbuntu.json' - packages = json.loads(ProcessUtilities.outputExecutioner(command)) + # Prefer live data for Ubuntu 22/24, fall back to static JSON + packages = None + try: + cmd_out = ProcessUtilities.outputExecutioner('apt list --installed 2>/dev/null') + lines = [l for l in cmd_out.split('\n') if l and '/' in l][4:] # Skip header + packages = [] + for line in lines: + parts = line.split(None, 2) + if len(parts) >= 2: + packages.append({'Package': parts[0], 'Version': parts[1]}) + except Exception: + pass + if not packages and os.path.exists('/usr/local/CyberCP/AllCPUbuntu.json'): + command = 'cat /usr/local/CyberCP/AllCPUbuntu.json' + packages = json.loads(ProcessUtilities.outputExecutioner(command)) + if not packages: + packages = [] else: command = 'apt list --installed' @@ -906,11 +920,16 @@ def fetchPackages(request): elif ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: ### Check Package Lock status - - if os.path.exists('/etc/yum.conf'): + # Prefer dnf.conf when dnf is present (AlmaLinux 9/10, RHEL 9, Rocky 9) + yum_dnf = 'dnf' if os.path.exists('/usr/bin/dnf') else 'yum' + if yum_dnf == 'dnf' and os.path.exists('/etc/dnf/dnf.conf'): + yumConf = '/etc/dnf/dnf.conf' + elif os.path.exists('/etc/yum.conf'): yumConf = '/etc/yum.conf' elif os.path.exists('/etc/yum/yum.conf'): yumConf = '/etc/yum/yum.conf' + else: + yumConf = '/etc/dnf/dnf.conf' if os.path.exists('/etc/dnf/dnf.conf') else '/etc/yum.conf' yumConfData = open(yumConf, 'r').read() locked = [] @@ -930,7 +949,7 @@ def fetchPackages(request): startForUpdate = 1 - command = 'yum check-update' + command = '%s check-update 2>/dev/null || true' % yum_dnf updates = ProcessUtilities.outputExecutioner(command).split('\n') for items in updates: @@ -948,7 +967,7 @@ def fetchPackages(request): ### - command = 'yum list installed' + command = '%s list installed' % yum_dnf packages = ProcessUtilities.outputExecutioner(command).split('\n') startFrom = 1 @@ -964,7 +983,7 @@ def fetchPackages(request): startForUpdate = 1 - command = 'yum check-update' + command = '%s check-update 2>/dev/null || true' % yum_dnf packages = ProcessUtilities.outputExecutioner(command).split('\n') for items in packages: @@ -974,8 +993,26 @@ def fetchPackages(request): else: startForUpdate = startForUpdate + 1 elif type == 'CyberPanel': - command = 'cat /usr/local/CyberCP/CPCent7repo.json' - packages = json.loads(ProcessUtilities.outputExecutioner(command)) + # Prefer live data for AlmaLinux 8/9/10, RHEL, Rocky; fall back to static JSON + packages = None + try: + dnf_cmd = 'dnf list installed' if os.path.exists('/usr/bin/dnf') else 'yum list installed' + cmd_out = ProcessUtilities.outputExecutioner(dnf_cmd) + lines = [l.strip() for l in cmd_out.split('\n') if l.strip()] + idx = next((i for i, l in enumerate(lines) if 'Installed Packages' in l or 'Installed' in l), 0) + lines = lines[idx + 1:] if idx < len(lines) else lines + packages = [] + for line in lines: + parts = line.split() + if len(parts) >= 2: + packages.append({'Package': parts[0], 'Version': parts[1]}) + except Exception: + pass + if not packages and os.path.exists('/usr/local/CyberCP/CPCent7repo.json'): + command = 'cat /usr/local/CyberCP/CPCent7repo.json' + packages = json.loads(ProcessUtilities.outputExecutioner(command)) + if not packages: + packages = [] ## make list of packages that need update @@ -1131,7 +1168,8 @@ def fetchPackageDetails(request): command = 'apt-cache show %s' % (package) packageDetails = ProcessUtilities.outputExecutioner(command) elif ProcessUtilities.decideDistro() == ProcessUtilities.centos or ProcessUtilities.decideDistro() == ProcessUtilities.cent8: - command = 'yum info %s' % (package) + pkg_cmd = 'dnf info' if os.path.exists('/usr/bin/dnf') else 'yum info' + command = '%s %s' % (pkg_cmd, package) packageDetails = ProcessUtilities.outputExecutioner(command) data_ret = {'status': 1, 'packageDetails': packageDetails} diff --git a/simple_install.sh b/simple_install.sh index 3d3cdb5b8..f22f256e2 100644 --- a/simple_install.sh +++ b/simple_install.sh @@ -6,14 +6,22 @@ OUTPUT=$(cat /etc/*release) # Detect OS and set appropriate variables -if echo $OUTPUT | grep -q "AlmaLinux 9" ; then +if echo $OUTPUT | grep -q "AlmaLinux 10" ; then + echo -e "\nDetecting AlmaLinux 10...\n" + SERVER_OS="AlmaLinux10" + PKG_MGR="dnf" +elif echo $OUTPUT | grep -q "AlmaLinux 9" ; then echo -e "\nDetecting AlmaLinux 9...\n" SERVER_OS="AlmaLinux9" PKG_MGR="dnf" elif echo $OUTPUT | grep -q "AlmaLinux 8" ; then echo -e "\nDetecting AlmaLinux 8...\n" SERVER_OS="AlmaLinux8" - PKG_MGR="yum" + PKG_MGR="dnf" +elif echo $OUTPUT | grep -q "Ubuntu 24.04" ; then + echo -e "\nDetecting Ubuntu 24.04...\n" + SERVER_OS="Ubuntu2404" + PKG_MGR="apt" elif echo $OUTPUT | grep -q "Ubuntu 22.04" ; then echo -e "\nDetecting Ubuntu 22.04...\n" SERVER_OS="Ubuntu2204" @@ -28,8 +36,8 @@ elif echo $OUTPUT | grep -q "CentOS Linux 8" ; then PKG_MGR="yum" else echo -e "\nUnsupported OS detected. This script supports:\n" - echo -e "AlmaLinux: 8, 9\n" - echo -e "Ubuntu: 20.04, 22.04\n" + echo -e "AlmaLinux: 8, 9, 10\n" + echo -e "Ubuntu: 20.04, 22.04, 24.04\n" echo -e "CentOS: 8\n" exit 1 fi From bde534427a9ff681ba5e7611fcccd8c5dd00f1e7 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sat, 14 Feb 2026 22:43:01 +0100 Subject: [PATCH 2/6] README: Sort supported platforms table alphabetically Co-authored-by: Cursor --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index a16cdde76..63f655f54 100644 --- a/README.md +++ b/README.md @@ -70,13 +70,13 @@ Fast • Secure • Scalable — Simplify hosting management with style. | OS family | Recommended / Supported | | -------------------------- | ----------------------: | -| Ubuntu 24.04, 22.04, 20.04 | ✅ Recommended | | AlmaLinux 10, 9, 8 | ✅ Recommended | -| Debian 13, 12, 11 | ✅ Supported | -| RockyLinux 9, 8 | ✅ Supported | -| RHEL 9, 8 | ✅ Supported | -| CloudLinux 9, 8 | ✅ Supported | | CentOS 7 | ⚠️ Legacy — EOL | +| CloudLinux 9, 8 | ✅ Supported | +| Debian 13, 12, 11 | ✅ Supported | +| RHEL 9, 8 | ✅ Supported | +| RockyLinux 9, 8 | ✅ Supported | +| Ubuntu 24.04, 22.04, 20.04 | ✅ Recommended | > **Architectures:** x86_64 (primary), aarch64/ARM64 (supported). AlmaLinux is the recommended RHEL-compatible distribution. Test unsupported OS in staging first. From 9200d2950f3a470b35e6339a2fd0d0bd7f29c9b3 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sat, 14 Feb 2026 22:50:20 +0100 Subject: [PATCH 3/6] README: Update PHP support (8.5 stable, 8.4 recommended; 8.3/8.2/8.1 security-only) Co-authored-by: Cursor --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 63f655f54..dbac91e9e 100644 --- a/README.md +++ b/README.md @@ -84,11 +84,11 @@ Fast • Secure • Scalable — Simplify hosting management with style. ## PHP support (short) -* ✅ **Recommended**: PHP 8.5 (beta), 8.4, 8.3, 8.2, 8.1 -* ⚠️ **Legacy**: PHP 8.0, PHP 7.4 (security-only) -* ❌ **Deprecated**: PHP 7.1, 7.2, 7.3 (no longer installed) +* ✅ **Recommended**: PHP 8.5, 8.4 +* ⚠️ **Security fixes only**: PHP 8.3, 8.2, 8.1 +* ❌ **EOL / Deprecated**: PHP 8.0, 7.4, 7.1, 7.2, 7.3 (no longer supported) -Third-party repositories (Remi, Ondrej) may provide older or niche versions; verify compatibility before use. +Third-party repositories (Remi, Ondrej) may provide older or niche versions; verify compatibility before use. See [php.net/supported-versions](https://www.php.net/supported-versions.php). --- From 8b4c8d152cecf337874867dc67187f5af654bd7e Mon Sep 17 00:00:00 2001 From: master3395 Date: Sat, 14 Feb 2026 22:51:34 +0100 Subject: [PATCH 4/6] README: Add links to Remi and Ondrej third-party PHP repositories Co-authored-by: Cursor --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dbac91e9e..1f1111bde 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ Fast • Secure • Scalable — Simplify hosting management with style. * ⚠️ **Security fixes only**: PHP 8.3, 8.2, 8.1 * ❌ **EOL / Deprecated**: PHP 8.0, 7.4, 7.1, 7.2, 7.3 (no longer supported) -Third-party repositories (Remi, Ondrej) may provide older or niche versions; verify compatibility before use. See [php.net/supported-versions](https://www.php.net/supported-versions.php). +Third-party repositories may provide older or niche versions; verify compatibility before use. RHEL/Alma/Rocky: [Remi RPM](https://rpms.remirepo.net/). Ubuntu/Debian: [Ondrej PPA](https://launchpad.net/~ondrej/+archive/ubuntu/php). See [php.net/supported-versions](https://www.php.net/supported-versions.php). --- From 4ec55c64456f96390049de1af74df514abe4ee41 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sat, 14 Feb 2026 22:57:39 +0100 Subject: [PATCH 5/6] Remove simple_install.sh; use official install.sh one-liner instead Co-authored-by: Cursor --- simple_install.sh | 122 --------------------- to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md | 2 +- to-do/REPO-MERGE-2026-02-02.md | 2 +- 3 files changed, 2 insertions(+), 124 deletions(-) delete mode 100644 simple_install.sh diff --git a/simple_install.sh b/simple_install.sh deleted file mode 100644 index f22f256e2..000000000 --- a/simple_install.sh +++ /dev/null @@ -1,122 +0,0 @@ -#!/bin/sh - -# Simplified CyberPanel Installation Script -# Based on 2.4.4 approach with AlmaLinux 9 fixes - -OUTPUT=$(cat /etc/*release) - -# Detect OS and set appropriate variables -if echo $OUTPUT | grep -q "AlmaLinux 10" ; then - echo -e "\nDetecting AlmaLinux 10...\n" - SERVER_OS="AlmaLinux10" - PKG_MGR="dnf" -elif echo $OUTPUT | grep -q "AlmaLinux 9" ; then - echo -e "\nDetecting AlmaLinux 9...\n" - SERVER_OS="AlmaLinux9" - PKG_MGR="dnf" -elif echo $OUTPUT | grep -q "AlmaLinux 8" ; then - echo -e "\nDetecting AlmaLinux 8...\n" - SERVER_OS="AlmaLinux8" - PKG_MGR="dnf" -elif echo $OUTPUT | grep -q "Ubuntu 24.04" ; then - echo -e "\nDetecting Ubuntu 24.04...\n" - SERVER_OS="Ubuntu2404" - PKG_MGR="apt" -elif echo $OUTPUT | grep -q "Ubuntu 22.04" ; then - echo -e "\nDetecting Ubuntu 22.04...\n" - SERVER_OS="Ubuntu2204" - PKG_MGR="apt" -elif echo $OUTPUT | grep -q "Ubuntu 20.04" ; then - echo -e "\nDetecting Ubuntu 20.04...\n" - SERVER_OS="Ubuntu2004" - PKG_MGR="apt" -elif echo $OUTPUT | grep -q "CentOS Linux 8" ; then - echo -e "\nDetecting CentOS 8...\n" - SERVER_OS="CentOS8" - PKG_MGR="yum" -else - echo -e "\nUnsupported OS detected. This script supports:\n" - echo -e "AlmaLinux: 8, 9, 10\n" - echo -e "Ubuntu: 20.04, 22.04, 24.04\n" - echo -e "CentOS: 8\n" - exit 1 -fi - -echo "Installing basic dependencies..." - -# Install basic packages -if [ "$PKG_MGR" = "dnf" ]; then - dnf update -y - dnf install -y epel-release - dnf install -y wget curl unzip zip rsync firewalld git python3 python3-pip - dnf install -y mariadb-server mariadb-client - dnf install -y ImageMagick gd libicu oniguruma aspell libc-client -elif [ "$PKG_MGR" = "yum" ]; then - yum update -y - yum install -y epel-release - yum install -y wget curl unzip zip rsync firewalld git python3 python3-pip - yum install -y mariadb-server mariadb-client - yum install -y ImageMagick gd libicu oniguruma aspell libc-client -elif [ "$PKG_MGR" = "apt" ]; then - apt update -y - apt install -y wget curl unzip zip rsync git python3 python3-pip - apt install -y mariadb-server mariadb-client - apt install -y imagemagick php-gd php-intl php-mbstring php-pspell -fi - -# Start and enable MariaDB -echo "Starting MariaDB..." -systemctl enable mariadb -systemctl start mariadb - -# Create MySQL password file -echo "Setting up MySQL..." -mkdir -p /etc/cyberpanel -echo "cyberpanel123" > /etc/cyberpanel/mysqlPassword -chmod 600 /etc/cyberpanel/mysqlPassword - -# Secure MySQL installation -mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'cyberpanel123';" 2>/dev/null || true -mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.user WHERE User='';" 2>/dev/null || true -mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');" 2>/dev/null || true -mysql -u root -pcyberpanel123 -e "DROP DATABASE IF EXISTS test;" 2>/dev/null || true -mysql -u root -pcyberpanel123 -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%';" 2>/dev/null || true -mysql -u root -pcyberpanel123 -e "FLUSH PRIVILEGES;" 2>/dev/null || true - -# Configure firewall -echo "Configuring firewall..." -if [ "$PKG_MGR" = "dnf" ] || [ "$PKG_MGR" = "yum" ]; then - systemctl enable firewalld - systemctl start firewalld - firewall-cmd --permanent --add-port=8090/tcp - firewall-cmd --permanent --add-port=7080/tcp - firewall-cmd --permanent --add-port=80/tcp - firewall-cmd --permanent --add-port=443/tcp - firewall-cmd --permanent --add-port=21/tcp - firewall-cmd --permanent --add-port=25/tcp - firewall-cmd --permanent --add-port=587/tcp - firewall-cmd --permanent --add-port=465/tcp - firewall-cmd --permanent --add-port=110/tcp - firewall-cmd --permanent --add-port=143/tcp - firewall-cmd --permanent --add-port=993/tcp - firewall-cmd --permanent --add-port=995/tcp - firewall-cmd --permanent --add-port=53/tcp - firewall-cmd --permanent --add-port=53/udp - firewall-cmd --reload -fi - -# Download and install CyberPanel -echo "Downloading CyberPanel..." -rm -f cyberpanel.sh -curl --silent -o cyberpanel.sh "https://cyberpanel.sh/?dl&$SERVER_OS" 2>/dev/null - -if [ -f "cyberpanel.sh" ]; then - echo "Installing CyberPanel..." - chmod +x cyberpanel.sh - ./cyberpanel.sh -else - echo "Failed to download CyberPanel installer!" - exit 1 -fi - -echo "Installation completed!" diff --git a/to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md b/to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md index 39371c4fe..906cf1be3 100644 --- a/to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md +++ b/to-do/OLD-REPO-CHECKLIST-BEFORE-REMOVAL.md @@ -35,7 +35,7 @@ These were only in the old fix and were copied into repo during the merge: | `cyberpanel_standalone.sh` | Standalone install script | | `fix_installation_issues.sh` | Installation fixes | | `install_phpmyadmin.sh` | phpMyAdmin installer | -| `simple_install.sh` | Simple installer | +| ~~`simple_install.sh`~~ | Removed – use official install.sh one-liner | | `INSTALLER_SUMMARY.md` | Installer docs | | `UNIVERSAL_OS_COMPATIBILITY.md` | OS compatibility docs | | `to-do/MARIADB_INSTALLATION_FIXES.md` | MariaDB fixes | diff --git a/to-do/REPO-MERGE-2026-02-02.md b/to-do/REPO-MERGE-2026-02-02.md index 79b96232e..9497772be 100644 --- a/to-do/REPO-MERGE-2026-02-02.md +++ b/to-do/REPO-MERGE-2026-02-02.md @@ -13,7 +13,7 @@ - `cyberpanel_standalone.sh` - `fix_installation_issues.sh` - `install_phpmyadmin.sh` - - `simple_install.sh` + - ~~`simple_install.sh`~~ (removed; use official install.sh) - `INSTALLER_SUMMARY.md` - `UNIVERSAL_OS_COMPATIBILITY.md` - `to-do/MARIADB_INSTALLATION_FIXES.md` From 4177f0023bc94e7c2eae8af99e782a5a34c24dd6 Mon Sep 17 00:00:00 2001 From: master3395 Date: Sat, 14 Feb 2026 23:02:47 +0100 Subject: [PATCH 6/6] Misc: firewall, pluginHolder, mobile CSS, install utilities, static assets Co-authored-by: Cursor --- CyberCP/secMiddleware.py | 2 +- CyberCP/settings.py | 32 +- .../baseTemplate/assets/mobile-responsive.css | 589 ++++++++++++++++ .../baseTemplate/assets/readability-fixes.css | 265 ++++++++ .../baseTemplate/custom-js/system-status.js | 73 +- .../templates/baseTemplate/homePage.html | 18 +- .../templates/baseTemplate/index.html | 5 +- firewall/firewallManager.py | 242 ++++--- firewall/migrations/0001_initial.py | 40 ++ firewall/static/firewall/firewall.js | 250 +++++-- firewall/templates/firewall/firewall.html | 289 +++++++- firewall/urls.py | 6 +- firewall/views.py | 28 +- plogical/installUtilities.py | 123 ++-- pluginHolder/patreon_verifier.py | 4 +- .../templates/pluginHolder/plugins.html | 64 +- pluginHolder/views.py | 17 + .../baseTemplate/assets/mobile-responsive.css | 589 ++++++++++++++++ .../baseTemplate/assets/readability-fixes.css | 265 ++++++++ .../baseTemplate/custom-js/system-status.js | 626 +++++++++++------- static/filemanager/js/fileManager.js | 16 +- static/filemanager/js/newFileManager.js | 8 + static/firewall/firewall.js | 201 +++++- static/ftp/ftp.js | 15 +- static/mailServer/emailLimitsController.js | 4 +- static/mailServer/mailServer.js | 6 +- static/serverStatus/serverStatus.js | 5 +- 27 files changed, 3264 insertions(+), 518 deletions(-) create mode 100644 baseTemplate/static/baseTemplate/assets/mobile-responsive.css create mode 100644 baseTemplate/static/baseTemplate/assets/readability-fixes.css create mode 100644 firewall/migrations/0001_initial.py create mode 100644 static/baseTemplate/assets/mobile-responsive.css create mode 100644 static/baseTemplate/assets/readability-fixes.css diff --git a/CyberCP/secMiddleware.py b/CyberCP/secMiddleware.py index 29061ce9e..dcf498c22 100644 --- a/CyberCP/secMiddleware.py +++ b/CyberCP/secMiddleware.py @@ -267,7 +267,7 @@ class secMiddleware: response['X-XSS-Protection'] = "1; mode=block" response['X-Frame-Options'] = "sameorigin" - response['Content-Security-Policy'] = "script-src 'self' https://www.jsdelivr.com" + response['Content-Security-Policy'] = "script-src 'self' 'unsafe-inline' https://www.jsdelivr.com" response['Content-Security-Policy'] = "connect-src *;" response['Content-Security-Policy'] = "font-src 'self' 'unsafe-inline' https://www.jsdelivr.com https://fonts.googleapis.com" response[ diff --git a/CyberCP/settings.py b/CyberCP/settings.py index 2008956ad..ea1b2d679 100644 --- a/CyberCP/settings.py +++ b/CyberCP/settings.py @@ -13,15 +13,15 @@ https://docs.djangoproject.com/en/1.11/ref/settings/ import os from django.utils.translation import gettext_lazy as _ -# Patreon OAuth Configuration for Paid Plugins -# SECURITY: Environment variables take precedence. Hardcoded values are fallback for this server only. -# For repository version, use empty defaults and set via environment variables. -PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', 'LFXeXUcfrM8MeVbUcmGbB7BgeJ9RzZi2v_H9wL4d9vG6t1dV4SUnQ4ibn9IYzvt7') -PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', 'APuJ5qoL3TLFmNnGDVkgl-qr3sCzp2CQsKfslBbp32hhnhlD0y6-ZcSCkb_FaUJv') +# Patreon OAuth (optional): for paid-plugin verification via Patreon membership. +# Set these only if you use Patreon-gated plugins; leave unset otherwise. +# Use environment variables; no defaults so the repo stays generic and safe to push to GitHub. +PATREON_CLIENT_ID = os.environ.get('PATREON_CLIENT_ID', '') +PATREON_CLIENT_SECRET = os.environ.get('PATREON_CLIENT_SECRET', '') PATREON_CREATOR_ID = os.environ.get('PATREON_CREATOR_ID', '') -PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') # CyberPanel Paid Plugin tier -PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', 'niAHRiI9SgrRCMmaf5exoXXphy3RWXWsX4kO5Yv9SQI') -PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', 'VZlCQoPwJUr4NLni1N82-K_CpJHTAOYUOCx2PujdjQg') +PATREON_MEMBERSHIP_TIER_ID = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '') +PATREON_CREATOR_ACCESS_TOKEN = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '') +PATREON_CREATOR_REFRESH_TOKEN = os.environ.get('PATREON_CREATOR_REFRESH_TOKEN', '') # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @@ -37,6 +37,22 @@ DEBUG = False ALLOWED_HOSTS = ['*'] +# When the panel is behind a reverse proxy (e.g. https://panel.example.com -> http://backend:port), +# the browser sends Origin/Referer with the public domain while the proxy may send Host as the +# backend address. Django then fails CSRF (Referer vs Host mismatch) and POSTs get 403. +# Set CSRF_TRUSTED_ORIGINS to your public origin(s) so CSRF passes. Optional; leave unset if +# you access the panel by IP:port only. +# Example: export CSRF_TRUSTED_ORIGINS="https://panel.example.com,http://panel.example.com" +_csrf_origins_env = os.environ.get('CSRF_TRUSTED_ORIGINS', '') +_csrf_origins_list = [o.strip() for o in _csrf_origins_env.split(',') if o.strip()] +# Add default trusted origins for common CyberPanel domains +_default_origins = [ + 'https://cyberpanel.newstargeted.com', + 'http://cyberpanel.newstargeted.com', +] +# Merge environment and default origins, avoiding duplicates +CSRF_TRUSTED_ORIGINS = list(dict.fromkeys(_csrf_origins_list + _default_origins)) + # Application definition INSTALLED_APPS = [ diff --git a/baseTemplate/static/baseTemplate/assets/mobile-responsive.css b/baseTemplate/static/baseTemplate/assets/mobile-responsive.css new file mode 100644 index 000000000..35e2674e5 --- /dev/null +++ b/baseTemplate/static/baseTemplate/assets/mobile-responsive.css @@ -0,0 +1,589 @@ +/* CyberPanel Mobile Responsive & Readability Fixes */ +/* This file ensures all pages are mobile-friendly with proper font sizes and readable text */ + +/* Base font size and mobile-first approach */ +html { + font-size: 16px; /* Base font size for better readability */ + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + text-size-adjust: 100%; +} + +body { + font-size: 16px; + line-height: 1.6; + color: #2f3640; /* Dark text for better readability on white backgrounds */ + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +/* Ensure all text is readable with proper contrast */ +* { + color: inherit; +} + +/* Override any light text that might be hard to read */ +.text-muted, .text-secondary, .text-light { + color: #2f3640 !important; /* Dark text for better readability on white backgrounds */ +} + +/* Fix small font sizes that are hard to read */ +small, .small, .text-small { + font-size: 14px !important; /* Minimum readable size */ +} + +/* Table improvements for mobile */ +.table { + font-size: 16px !important; /* Larger table text */ + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +.table th, .table td { + padding: 12px 8px !important; /* More padding for touch targets */ + border: 1px solid #e8e9ff; + text-align: left; + vertical-align: middle; + font-size: 14px !important; + line-height: 1.4; +} + +.table th { + background-color: #f8f9fa; + font-weight: 600; + color: #2f3640 !important; + font-size: 15px !important; +} + +/* Button improvements for mobile */ +.btn { + font-size: 16px !important; + padding: 12px 20px !important; + border-radius: 8px; + min-height: 44px; /* Minimum touch target size */ + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-sm { + font-size: 14px !important; + padding: 8px 16px !important; + min-height: 36px; +} + +.btn-xs { + font-size: 13px !important; + padding: 6px 12px !important; + min-height: 32px; +} + +/* Form elements */ +.form-control, input, textarea, select { + font-size: 16px !important; /* Prevents zoom on iOS */ + padding: 12px 16px !important; + border: 2px solid #e8e9ff; + border-radius: 8px; + min-height: 44px; + line-height: 1.4; + color: #2f3640 !important; + background-color: #ffffff; +} + +.form-control:focus, input:focus, textarea:focus, select:focus { + border-color: #5856d6; + box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1); + outline: none; +} + +/* Labels and form text */ +label, .control-label { + font-size: 16px !important; + font-weight: 600; + color: #2f3640 !important; + margin-bottom: 8px; + display: block; +} + +/* Headings with proper hierarchy */ +h1 { + font-size: 2.5rem !important; /* 40px */ + font-weight: 700; + color: #1e293b !important; + line-height: 1.2; + margin-bottom: 1rem; +} + +h2 { + font-size: 2rem !important; /* 32px */ + font-weight: 600; + color: #1e293b !important; + line-height: 1.3; + margin-bottom: 0.875rem; +} + +h3 { + font-size: 1.5rem !important; /* 24px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.75rem; +} + +h4 { + font-size: 1.25rem !important; /* 20px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +h5 { + font-size: 1.125rem !important; /* 18px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +h6 { + font-size: 1rem !important; /* 16px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +/* Paragraph and body text */ +p { + font-size: 16px !important; + line-height: 1.6; + color: #2f3640 !important; + margin-bottom: 1rem; +} + +/* Sidebar improvements */ +#page-sidebar { + font-size: 16px !important; +} + +#page-sidebar ul li a { + font-size: 16px !important; + padding: 12px 20px !important; + color: #2f3640 !important; + min-height: 44px; + display: flex; + align-items: center; + text-decoration: none; +} + +#page-sidebar ul li a:hover { + background-color: #f8f9fa; + color: #5856d6 !important; +} + +/* Content area improvements */ +.content-box, .panel, .card { + font-size: 16px !important; + color: #2f3640 !important; + background-color: #ffffff; + border: 1px solid #e8e9ff; + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; +} + +/* Modal improvements */ +.modal-content { + font-size: 16px !important; + color: #2f3640 !important; +} + +.modal-title { + font-size: 1.5rem !important; + font-weight: 600; + color: #1e293b !important; +} + +/* Alert and notification improvements */ +.alert { + font-size: 16px !important; + padding: 16px 20px !important; + border-radius: 8px; + margin-bottom: 20px; +} + +.alert-success { + background-color: #f0fdf4; + border-color: #bbf7d0; + color: #166534 !important; +} + +.alert-danger { + background-color: #fef2f2; + border-color: #fecaca; + color: #dc2626 !important; +} + +.alert-warning { + background-color: #fffbeb; + border-color: #fed7aa; + color: #d97706 !important; +} + +.alert-info { + background-color: #eff6ff; + border-color: #bfdbfe; + color: #2563eb !important; +} + +/* Navigation improvements */ +.navbar-nav .nav-link { + font-size: 16px !important; + padding: 12px 16px !important; + color: #2f3640 !important; +} + +/* Breadcrumb improvements */ +.breadcrumb { + font-size: 16px !important; + background-color: transparent; + padding: 0; + margin-bottom: 20px; +} + +.breadcrumb-item { + color: #64748b !important; +} + +.breadcrumb-item.active { + color: #2f3640 !important; +} + +/* Mobile-first responsive breakpoints */ +@media (max-width: 1200px) { + .container, .container-fluid { + padding-left: 15px; + padding-right: 15px; + } + + .table-responsive { + border: none; + margin-bottom: 20px; + } +} + +@media (max-width: 992px) { + /* Stack columns on tablets */ + .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9 { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 20px; + } + + /* Adjust sidebar for tablets */ + #page-sidebar { + width: 100%; + position: static; + height: auto; + } + + /* Make tables horizontally scrollable */ + .table-responsive { + overflow-x: auto; + } + + .table { + min-width: 600px; + } +} + +@media (max-width: 768px) { + /* Mobile-specific adjustments */ + html { + font-size: 14px; + } + + body { + font-size: 14px; + padding: 0; + } + + .container, .container-fluid { + padding-left: 10px; + padding-right: 10px; + } + + /* Stack all columns on mobile */ + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12, + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 15px; + } + + /* Adjust headings for mobile */ + h1 { + font-size: 2rem !important; /* 32px */ + } + + h2 { + font-size: 1.75rem !important; /* 28px */ + } + + h3 { + font-size: 1.5rem !important; /* 24px */ + } + + h4 { + font-size: 1.25rem !important; /* 20px */ + } + + /* Button adjustments for mobile */ + .btn { + font-size: 16px !important; + padding: 14px 20px !important; + width: 100%; + margin-bottom: 10px; + } + + .btn-group .btn { + width: auto; + margin-bottom: 0; + } + + /* Form adjustments for mobile */ + .form-control, input, textarea, select { + font-size: 16px !important; /* Prevents zoom on iOS */ + padding: 14px 16px !important; + width: 100%; + } + + /* Table adjustments for mobile */ + .table { + font-size: 14px !important; + } + + .table th, .table td { + padding: 8px 6px !important; + font-size: 13px !important; + } + + /* Hide less important columns on mobile */ + .table .d-none-mobile { + display: none !important; + } + + /* Modal adjustments for mobile */ + .modal-dialog { + margin: 10px; + width: calc(100% - 20px); + } + + .modal-content { + padding: 20px 15px; + } + + /* Content box adjustments */ + .content-box, .panel, .card { + padding: 15px; + margin-bottom: 15px; + } + + /* Sidebar adjustments for mobile */ + #page-sidebar { + position: fixed; + top: 0; + left: -100%; + width: 280px; + height: 100vh; + z-index: 1000; + transition: left 0.3s ease; + background-color: #ffffff; + box-shadow: 2px 0 10px rgba(0,0,0,0.1); + } + + #page-sidebar.show { + left: 0; + } + + /* Main content adjustments when sidebar is open */ + #main-content { + transition: margin-left 0.3s ease; + } + + #main-content.sidebar-open { + margin-left: 280px; + } + + /* Mobile menu toggle */ + .mobile-menu-toggle { + display: block; + position: fixed; + top: 20px; + left: 20px; + z-index: 1001; + background-color: #5856d6; + color: white; + border: none; + padding: 12px; + border-radius: 8px; + font-size: 18px; + cursor: pointer; + } +} + +@media (max-width: 576px) { + /* Extra small devices */ + html { + font-size: 14px; + } + + .container, .container-fluid { + padding-left: 8px; + padding-right: 8px; + } + + /* Even smaller buttons and forms for very small screens */ + .btn { + font-size: 14px !important; + padding: 12px 16px !important; + } + + .form-control, input, textarea, select { + font-size: 16px !important; /* Still 16px to prevent zoom */ + padding: 12px 14px !important; + } + + /* Compact table for very small screens */ + .table th, .table td { + padding: 6px 4px !important; + font-size: 12px !important; + } + + /* Hide even more columns on very small screens */ + .table .d-none-mobile-sm { + display: none !important; + } +} + +/* Utility classes for mobile */ +.d-none-mobile { + display: block; +} + +.d-none-mobile-sm { + display: block; +} + +@media (max-width: 768px) { + .d-none-mobile { + display: none !important; + } +} + +@media (max-width: 576px) { + .d-none-mobile-sm { + display: none !important; + } +} + +/* Ensure all text has proper contrast */ +.text-white { + color: #ffffff !important; +} + +.text-dark { + color: #2f3640 !important; +} + +.text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Fix any light text on light backgrounds */ +.bg-light .text-muted, +.bg-white .text-muted, +.panel .text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Ensure proper spacing for touch targets */ +a, button, input, select, textarea { + min-height: 44px; + min-width: 44px; +} + +/* Additional text readability improvements */ +/* Fix any green text issues */ +.ng-binding { + color: #2f3640 !important; /* Normal dark text instead of green */ +} + +/* Ensure all text elements have proper contrast */ +span, div, p, label, td, th { + color: inherit; +} + +/* Fix specific text color issues */ +.text-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.text-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.text-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +/* Override Bootstrap's muted text */ +.text-muted { + color: #2f3640 !important; /* Dark text instead of grey */ +} + +/* Fix any remaining light text on light backgrounds */ +.bg-white .text-light, +.bg-light .text-light, +.panel .text-light, +.card .text-light { + color: #2f3640 !important; +} + +/* Fix for small clickable elements */ +.glyph-icon, .icon { + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Loading and spinner improvements */ +.spinner, .loading { + font-size: 16px !important; + color: #5856d6 !important; +} + +/* Print styles */ +@media print { + body { + font-size: 12pt; + color: #000000 !important; + background: #ffffff !important; + } + + .table th, .table td { + font-size: 10pt !important; + color: #000000 !important; + } + + .btn, .alert, .modal { + display: none !important; + } +} diff --git a/baseTemplate/static/baseTemplate/assets/readability-fixes.css b/baseTemplate/static/baseTemplate/assets/readability-fixes.css new file mode 100644 index 000000000..1911482b7 --- /dev/null +++ b/baseTemplate/static/baseTemplate/assets/readability-fixes.css @@ -0,0 +1,265 @@ +/* CyberPanel Readability & Design Fixes */ +/* This file fixes the core design issues with grey text and color inconsistencies */ + +/* Override CSS Variables for Better Text Contrast */ +:root { + /* Ensure all text uses proper dark colors for readability */ + --text-primary: #2f3640; + --text-secondary: #2f3640; /* Changed from grey to dark for better readability */ + --text-heading: #1e293b; +} + +/* Dark theme also uses proper contrast */ +[data-theme="dark"] { + --text-primary: #e4e4e7; + --text-secondary: #e4e4e7; /* Changed from grey to light for better readability */ + --text-heading: #f3f4f6; +} + +/* Fix Green Text Issues */ +/* Override Angular binding colors that might be green */ +.ng-binding { + color: var(--text-secondary) !important; +} + +/* Specific fix for uptime display */ +#sidebar .server-info .info-line span, +#sidebar .server-info .info-line .ng-binding, +.server-info .ng-binding { + color: var(--text-secondary) !important; +} + +/* Fix Grey Text on White Background */ +/* Override all muted and secondary text classes */ +.text-muted, +.text-secondary, +.text-light, +small, +.small, +.text-small { + color: var(--text-secondary) !important; +} + +/* Fix specific Bootstrap classes */ +.text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Fix text on white/light backgrounds */ +.bg-white .text-muted, +.bg-light .text-muted, +.panel .text-muted, +.card .text-muted, +.content-box .text-muted { + color: #2f3640 !important; +} + +/* Fix menu items and navigation */ +#sidebar .menu-item, +#sidebar .menu-item span, +#sidebar .menu-item i, +.sidebar .menu-item, +.sidebar .menu-item span, +.sidebar .menu-item i { + color: var(--text-secondary) !important; +} + +#sidebar .menu-item:hover, +.sidebar .menu-item:hover { + color: var(--accent-color) !important; +} + +#sidebar .menu-item.active, +.sidebar .menu-item.active { + color: white !important; +} + +/* Fix server info and details */ +.server-info, +.server-info *, +.server-details, +.server-details *, +.info-line, +.info-line span, +.info-line strong, +.tagline, +.brand { + color: inherit !important; +} + +/* Fix form elements */ +label, +.control-label, +.form-label { + color: var(--text-primary) !important; + font-weight: 600; +} + +/* Fix table text */ +.table th, +.table td { + color: var(--text-primary) !important; +} + +.table th { + font-weight: 600; +} + +/* Fix alert text */ +.alert { + color: var(--text-primary) !important; +} + +.alert-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.alert-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.alert-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +.alert-danger { + color: #dc2626 !important; /* Darker red for better readability */ +} + +/* Fix breadcrumb text */ +.breadcrumb-item { + color: var(--text-secondary) !important; +} + +.breadcrumb-item.active { + color: var(--text-primary) !important; +} + +/* Fix modal text */ +.modal-content { + color: var(--text-primary) !important; +} + +.modal-title { + color: var(--text-heading) !important; +} + +/* Fix button text */ +.btn { + color: inherit; +} + +/* Fix any remaining light text issues */ +.bg-light .text-light, +.bg-white .text-light, +.panel .text-light, +.card .text-light { + color: #2f3640 !important; +} + +/* Ensure proper contrast for all text elements */ +span, div, p, label, td, th, a, li { + color: inherit; +} + +/* Fix specific color classes */ +.text-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.text-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.text-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +.text-danger { + color: #dc2626 !important; /* Darker red for better readability */ +} + +/* Fix any Angular-specific styling */ +[ng-controller] { + color: inherit; +} + +[ng-show], +[ng-hide], +[ng-if] { + color: inherit; +} + +/* Ensure all content areas have proper text color */ +.content-box, +.panel, +.card, +.main-content, +.page-content { + color: var(--text-primary) !important; +} + +/* Fix any remaining Bootstrap classes */ +.text-dark { + color: #2f3640 !important; +} + +.text-body { + color: var(--text-primary) !important; +} + +/* Mobile-specific fixes */ +@media (max-width: 768px) { + /* Ensure mobile text is also readable */ + body, + .container, + .container-fluid { + color: var(--text-primary) !important; + } + + /* Fix mobile menu text */ + .mobile-menu .menu-item, + .mobile-menu .menu-item span { + color: var(--text-secondary) !important; + } +} + +/* Print styles */ +@media print { + body, + .content-box, + .panel, + .card { + color: #000000 !important; + background: #ffffff !important; + } + + .text-muted, + .text-secondary { + color: #000000 !important; + } +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + :root { + --text-primary: #000000; + --text-secondary: #000000; + --text-heading: #000000; + } + + [data-theme="dark"] { + --text-primary: #ffffff; + --text-secondary: #ffffff; + --text-heading: #ffffff; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/baseTemplate/static/baseTemplate/custom-js/system-status.js b/baseTemplate/static/baseTemplate/custom-js/system-status.js index 76353b1e4..9afb9976d 100644 --- a/baseTemplate/static/baseTemplate/custom-js/system-status.js +++ b/baseTemplate/static/baseTemplate/custom-js/system-status.js @@ -1459,6 +1459,12 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { var pollInterval = 2000; // ms var maxPoints = 30; + // Expose so switchTab can create charts on first tab click if they weren't created at load + window.cyberPanelSetupChartsIfNeeded = function() { + if (window.trafficChart && window.diskIOChart && window.cpuChart) return; + try { setupCharts(); } catch (e) { console.error('cyberPanelSetupChartsIfNeeded:', e); } + }; + function pollDashboardStats() { console.log('[dashboardStatsController] pollDashboardStats() called'); console.log('[dashboardStatsController] Fetching dashboard stats from /base/getDashboardStats'); @@ -1517,8 +1523,8 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } function pollTraffic() { - console.log('pollTraffic called'); $http.get('/base/getTrafficStats').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1566,13 +1572,16 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastRx = rx; lastTx = tx; } else { - console.log('pollTraffic error or no data:', response); + console.warn('pollTraffic: no data or status', response.data); } + }).catch(function(err) { + console.warn('pollTraffic failed:', err); }); } function pollDiskIO() { $http.get('/base/getDiskIOStats').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1611,11 +1620,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastDiskRead = read; lastDiskWrite = write; } + }).catch(function(err) { + console.warn('pollDiskIO failed:', err); }); } function pollCPU() { $http.get('/base/getCPULoadGraph').then(function(response) { + if (!response || !response.data) return; if (response.data.admin_only) { // Hide chart for non-admin users $scope.hideSystemCharts = true; @@ -1654,13 +1666,34 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } lastCPUTimes = cpuTimes; } + }).catch(function(err) { + console.warn('pollCPU failed:', err); }); } - function setupCharts() { - console.log('setupCharts called, initializing charts...'); - var trafficCtx = document.getElementById('trafficChart').getContext('2d'); - trafficChart = new Chart(trafficCtx, { + function setupCharts(retryCount) { + retryCount = retryCount || 0; + if (typeof Chart === 'undefined') { + if (retryCount < 3) { + $timeout(function() { setupCharts(retryCount + 1); }, 400); + } + return; + } + var trafficEl = document.getElementById('trafficChart'); + if (!trafficEl) { + if (retryCount < 5) { + $timeout(function() { setupCharts(retryCount + 1); }, 300); + } + return; + } + try { + var trafficCtx = trafficEl.getContext('2d'); + } catch (e) { + console.error('trafficChart getContext failed:', e); + return; + } + try { + trafficChart = new Chart(trafficCtx, { type: 'line', data: { labels: [], @@ -1752,7 +1785,9 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { console.log('trafficChart resized and updated after setup.'); } }, 500); - var diskCtx = document.getElementById('diskIOChart').getContext('2d'); + var diskEl = document.getElementById('diskIOChart'); + if (!diskEl) { console.warn('diskIOChart canvas not found'); return; } + var diskCtx = diskEl.getContext('2d'); diskIOChart = new Chart(diskCtx, { type: 'line', data: { @@ -1837,7 +1872,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } } } }); - var cpuCtx = document.getElementById('cpuChart').getContext('2d'); + window.diskIOChart = diskIOChart; + var cpuEl = document.getElementById('cpuChart'); + if (!cpuEl) { console.warn('cpuChart canvas not found'); return; } + var cpuCtx = cpuEl.getContext('2d'); cpuChart = new Chart(cpuCtx, { type: 'line', data: { @@ -1910,6 +1948,10 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { layout: { padding: { top: 10, bottom: 10, left: 10, right: 10 } } } }); + window.cpuChart = cpuChart; + } catch (e) { + console.error('setupCharts error:', e); + } // Redraw charts on tab shown $("a[data-toggle='tab']").on('shown.bs.tab', function (e) { @@ -1942,19 +1984,20 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.refreshSSHLogs(); $timeout(function() { - // Check if user is admin before setting up charts + // Always create charts so Traffic/Disk IO/CPU tabs have something to show; admin check only affects hideSystemCharts + setupCharts(); $http.get('/base/getAdminStatus').then(function(response) { - if (response.data && response.data.admin === 1) { - setupCharts(); + if (response.data && (response.data.admin === 1 || response.data.admin === true)) { + $scope.hideSystemCharts = false; } else { $scope.hideSystemCharts = true; } - }).catch(function() { - // If error, assume non-admin and hide charts + }).catch(function(err) { + console.warn('getAdminStatus failed:', err); $scope.hideSystemCharts = true; }); - // Start polling for all stats + // Start polling for all stats (data feeds charts) function pollAll() { pollDashboardStats(); pollTraffic(); @@ -1964,7 +2007,7 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $timeout(pollAll, pollInterval); } pollAll(); - }, 500); + }, 800); // SSH User Activity Modal $scope.showSSHActivityModal = false; diff --git a/baseTemplate/templates/baseTemplate/homePage.html b/baseTemplate/templates/baseTemplate/homePage.html index 3c078bf0f..39f7b12a4 100644 --- a/baseTemplate/templates/baseTemplate/homePage.html +++ b/baseTemplate/templates/baseTemplate/homePage.html @@ -1357,11 +1357,25 @@ // Add active class to clicked tab tabButton.classList.add('active'); - // Trigger chart resize if switching to chart tabs + // Chart tabs: ensure charts exist (lazy init on first click), then resize/update if (tabId === 'traffic' || tabId === 'diskio' || tabId === 'cpu-usage') { + if (typeof window.cyberPanelSetupChartsIfNeeded === 'function') { + window.cyberPanelSetupChartsIfNeeded(); + } setTimeout(() => { + var ch; + if (tabId === 'traffic' && (ch = window.trafficChart) && typeof ch.resize === 'function') { + ch.resize(); + if (typeof ch.update === 'function') ch.update(); + } else if (tabId === 'diskio' && (ch = window.diskIOChart) && typeof ch.resize === 'function') { + ch.resize(); + if (typeof ch.update === 'function') ch.update(); + } else if (tabId === 'cpu-usage' && (ch = window.cpuChart) && typeof ch.resize === 'function') { + ch.resize(); + if (typeof ch.update === 'function') ch.update(); + } window.dispatchEvent(new Event('resize')); - }, 100); + }, 200); } } diff --git a/baseTemplate/templates/baseTemplate/index.html b/baseTemplate/templates/baseTemplate/index.html index 0802be080..0fe34127a 100644 --- a/baseTemplate/templates/baseTemplate/index.html +++ b/baseTemplate/templates/baseTemplate/index.html @@ -34,14 +34,13 @@ + + - - - diff --git a/firewall/firewallManager.py b/firewall/firewallManager.py index 0a50250e4..96b3c8232 100644 --- a/firewall/firewallManager.py +++ b/firewall/firewallManager.py @@ -82,7 +82,12 @@ class FirewallManager: None, 'admin') return proc.render() - def getCurrentRules(self, userID = None): + def getCurrentRules(self, userID=None, data=None): + """ + Get firewall rules with optional pagination. + data may contain: page (1-based), page_size (default 10). + Returns: fetchStatus 1, data (JSON array), total_count, page, page_size. + """ try: currentACL = ACLManager.loadedACL(userID) @@ -91,58 +96,69 @@ class FirewallManager: else: return ACLManager.loadErrorJson('fetchStatus', 0) - rules = FirewallRules.objects.all() + rules_qs = FirewallRules.objects.all().order_by('id') # Ensure CyberPanel port 7080 rule exists in database for visibility - cyberpanel_rule_exists = False - for rule in rules: - if rule.port == '7080': - cyberpanel_rule_exists = True - break - + cyberpanel_rule_exists = rules_qs.filter(port='7080').exists() if not cyberpanel_rule_exists: - # Create database entry for port 7080 (already enabled in system firewall) try: - cyberpanel_rule = FirewallRules( + FirewallRules( name="CyberPanel Admin", proto="tcp", port="7080", ipAddress="0.0.0.0/0" - ) - cyberpanel_rule.save() + ).save() logging.CyberCPLogFileWriter.writeToFile("Added CyberPanel port 7080 to firewall database for UI visibility") except Exception as e: logging.CyberCPLogFileWriter.writeToFile(f"Failed to add CyberPanel port 7080 to database: {str(e)}") + rules_qs = FirewallRules.objects.all().order_by('id') - # Refresh rules after potential creation - rules = FirewallRules.objects.all() + total_count = rules_qs.count() + page = 1 + page_size = 10 + if data: + try: + page = max(1, int(data.get('page', 1))) + except (TypeError, ValueError): + pass + try: + page_size = max(1, min(100, int(data.get('page_size', 10)))) + except (TypeError, ValueError): + pass + + start = (page - 1) * page_size + end = start + page_size + rules = list(rules_qs[start:end]) json_data = "[" - checker = 0 - - for items in rules: + for i, items in enumerate(rules): dic = { - 'id': items.id, - 'name': items.name, - 'proto': items.proto, - 'port': items.port, - 'ipAddress': items.ipAddress, - } + 'id': items.id, + 'name': items.name, + 'proto': items.proto, + 'port': items.port, + 'ipAddress': items.ipAddress, + } + if i > 0: + json_data += ',' + json_data += json.dumps(dic) + json_data += ']' - if checker == 0: - json_data = json_data + json.dumps(dic) - checker = 1 - else: - json_data = json_data + ',' + json.dumps(dic) - - json_data = json_data + ']' - final_json = json.dumps({'status': 1, 'fetchStatus': 1, 'error_message': "None", "data": json_data}) - return HttpResponse(final_json) + final_json = json.dumps({ + 'status': 1, + 'fetchStatus': 1, + 'error_message': "None", + "data": json_data, + "total_count": total_count, + "page": page, + "page_size": page_size + }) + return HttpResponse(final_json, content_type='application/json') except BaseException as msg: final_dic = {'status': 0, 'fetchStatus': 0, 'error_message': str(msg)} final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(final_json, content_type='application/json') def addRule(self, userID = None, data = None): try: @@ -1841,9 +1857,11 @@ class FirewallManager: final_json = json.dumps(final_dic) return HttpResponse(final_json) - def getBannedIPs(self, userID=None): + def getBannedIPs(self, userID=None, data=None): """ - Get list of banned IP addresses from database, or fall back to JSON file. + Get list of banned IP addresses with optional pagination. + data may contain: page (1-based), page_size (default 10). + Returns: status 1, bannedIPs (array), total_count, page, page_size. """ try: admin = Administrator.objects.get(pk=userID) @@ -1876,12 +1894,10 @@ class FirewallManager: if ip_data['active']: active_banned_ips.append(ip_data) except (ImportError, AttributeError) as e: - # Fall back to JSON file when BannedIP model unavailable import plogical.CyberCPLogFileWriter as _log _log.CyberCPLogFileWriter.writeToFile('getBannedIPs: using JSON fallback (%s)' % str(e)) active_banned_ips = [] - # If DB returns nothing (or model not available), merge in JSON fallback if not active_banned_ips: banned_ips, _ = self._load_banned_ips_store() for b in banned_ips: @@ -1917,7 +1933,30 @@ class FirewallManager: 'active': True }) - final_dic = {'status': 1, 'bannedIPs': active_banned_ips} + total_count = len(active_banned_ips) + page = 1 + page_size = 10 + if data: + try: + page = max(1, int(data.get('page', 1))) + except (TypeError, ValueError): + pass + try: + page_size = max(1, min(100, int(data.get('page_size', 10)))) + except (TypeError, ValueError): + pass + + start = (page - 1) * page_size + end = start + page_size + paged_list = active_banned_ips[start:end] + + final_dic = { + 'status': 1, + 'bannedIPs': paged_list, + 'total_count': total_count, + 'page': page, + 'page_size': page_size + } final_json = json.dumps(final_dic) return HttpResponse(final_json, content_type='application/json') @@ -1926,11 +1965,12 @@ class FirewallManager: logging.CyberCPLogFileWriter.writeToFile('Error in getBannedIPs: %s' % str(msg)) final_dic = {'status': 0, 'error_message': str(msg)} final_json = json.dumps(final_dic) - return HttpResponse(final_json) + return HttpResponse(final_json, content_type='application/json') def addBannedIP(self, userID=None, data=None): """ - Add a banned IP address + Add a banned IP address. Uses database (BannedIP model) as primary storage; + JSON file is used only when the model is unavailable (fallback). Export/Import use JSON format. """ try: admin = Administrator.objects.get(pk=userID) @@ -1940,7 +1980,7 @@ class FirewallManager: ip = data.get('ip', '').strip() reason = data.get('reason', '').strip() - duration = data.get('duration', '24h') + duration = (data.get('duration') or '24h').strip().lower() if not ip or not reason: final_dic = {'status': 0, 'error_message': 'IP address and reason are required', 'error': 'IP address and reason are required'} @@ -1954,66 +1994,102 @@ class FirewallManager: final_dic = {'status': 0, 'error_message': 'Invalid IP address format', 'error': 'Invalid IP address format'} return HttpResponse(json.dumps(final_dic), content_type='application/json') - # Calculate expiration time current_time = time.time() + duration_map = { + '1h': 3600, + '24h': 86400, + '7d': 604800, + '30d': 2592000 + } if duration == 'permanent': - expires = 'Never' + expires_ts = None # Never expires else: - duration_map = { - '1h': 3600, - '24h': 86400, - '7d': 604800, - '30d': 2592000 - } duration_seconds = duration_map.get(duration, 86400) - expires = current_time + duration_seconds + expires_ts = int(current_time) + duration_seconds - # Load existing banned IPs - banned_ips, _ = self._load_banned_ips_store() + # Prefer database (BannedIP model) for primary storage + try: + from firewall.models import BannedIP + except Exception as e: + BannedIP = None + logging.CyberCPLogFileWriter.writeToFile('addBannedIP: BannedIP model unavailable, using JSON fallback: %s' % str(e)) - # Check if IP is already banned - for banned_ip in banned_ips: - if banned_ip.get('ip') == ip and banned_ip.get('active', True): + if BannedIP is not None: + # Primary path: save to database + existing = BannedIP.objects.filter(ip_address=ip, active=True).first() + if existing: msg = 'IP address %s is already banned' % ip final_dic = {'status': 0, 'error_message': msg, 'error': msg} return HttpResponse(json.dumps(final_dic), content_type='application/json') + try: + new_ban = BannedIP( + ip_address=ip, + reason=reason, + duration=duration, + expires=expires_ts, + active=True + ) + new_ban.save() + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile('addBannedIP: failed to save to DB: %s' % str(e)) + final_dic = {'status': 0, 'error_message': 'Failed to save banned IP to database: %s' % str(e), 'error': str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json') + else: + # Fallback: JSON store (only when DB unavailable) + banned_ips, _ = self._load_banned_ips_store() + for banned_ip in banned_ips: + if banned_ip.get('ip') == ip and banned_ip.get('active', True): + msg = 'IP address %s is already banned' % ip + final_dic = {'status': 0, 'error_message': msg, 'error': msg} + return HttpResponse(json.dumps(final_dic), content_type='application/json') + new_banned_ip = { + 'id': int(current_time), + 'ip': ip, + 'reason': reason, + 'duration': duration, + 'banned_on': current_time, + 'expires': 'Never' if expires_ts is None else expires_ts, + 'active': True + } + banned_ips.append(new_banned_ip) + try: + self._save_banned_ips_store(banned_ips) + except Exception as e: + logging.CyberCPLogFileWriter.writeToFile('addBannedIP: failed to save JSON store: %s' % str(e)) + final_dic = {'status': 0, 'error_message': 'Failed to save banned IP: %s' % str(e), 'error': str(e)} + return HttpResponse(json.dumps(final_dic), content_type='application/json') - # Add new banned IP - new_banned_ip = { - 'id': int(time.time()), - 'ip': ip, - 'reason': reason, - 'duration': duration, - 'banned_on': current_time, - 'expires': expires, - 'active': True - } - banned_ips.append(new_banned_ip) - - # Save to file - self._save_banned_ips_store(banned_ips) - - # Apply firewall rule using FirewallUtilities (runs with proper privileges via ProcessUtilities/lscpd) + # Apply firewall rule (same for DB and JSON path) try: block_ok, block_msg = FirewallUtilities.blockIP(ip, reason) if not block_ok: - # Rollback: remove the IP we just added from the store - banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)] - if len(banned_ips_rollback) < len(banned_ips): - self._save_banned_ips_store(banned_ips_rollback) + if BannedIP is not None: + try: + BannedIP.objects.filter(ip_address=ip, active=True).delete() + except Exception: + pass + else: + banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)] + if len(banned_ips_rollback) < len(banned_ips): + self._save_banned_ips_store(banned_ips_rollback) logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, block_msg)) err_msg = block_msg or 'Failed to add firewall rule' final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg} return HttpResponse(json.dumps(final_dic), content_type='application/json') - logging.CyberCPLogFileWriter.writeToFile(f'Banned IP {ip} with reason: {reason}') + logging.CyberCPLogFileWriter.writeToFile('Banned IP %s with reason: %s' % (ip, reason)) except Exception as e: - # Rollback store on any exception - try: - banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)] - if len(banned_ips_rollback) < len(banned_ips): - self._save_banned_ips_store(banned_ips_rollback) - except Exception: - pass + if BannedIP is not None: + try: + BannedIP.objects.filter(ip_address=ip, active=True).delete() + except Exception: + pass + else: + try: + banned_ips_rollback = [b for b in banned_ips if b.get('ip') != ip or not b.get('active', True)] + if len(banned_ips_rollback) < len(banned_ips): + self._save_banned_ips_store(banned_ips_rollback) + except Exception: + pass logging.CyberCPLogFileWriter.writeToFile('Failed to add firewalld rule for %s: %s' % (ip, str(e))) err_msg = 'Firewall command failed: %s' % str(e) final_dic = {'status': 0, 'error_message': err_msg, 'error': err_msg} diff --git a/firewall/migrations/0001_initial.py b/firewall/migrations/0001_initial.py new file mode 100644 index 000000000..bf7236d6f --- /dev/null +++ b/firewall/migrations/0001_initial.py @@ -0,0 +1,40 @@ +# Generated migration for firewall app - BannedIP model +# Primary storage for banned IPs is the database; JSON is used only for export/import. + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='BannedIP', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('ip_address', models.GenericIPAddressField(db_index=True, unique=True, verbose_name='IP Address')), + ('reason', models.CharField(max_length=255, verbose_name='Ban Reason')), + ('duration', models.CharField(default='permanent', max_length=50, verbose_name='Duration')), + ('banned_on', models.DateTimeField(auto_now_add=True, verbose_name='Banned On')), + ('expires', models.BigIntegerField(blank=True, null=True, verbose_name='Expires Timestamp')), + ('active', models.BooleanField(db_index=True, default=True, verbose_name='Active')), + ], + options={ + 'verbose_name': 'Banned IP', + 'verbose_name_plural': 'Banned IPs', + 'db_table': 'firewall_bannedips', + }, + ), + migrations.AddIndex( + model_name='bannedip', + index=models.Index(fields=['ip_address', 'active'], name='fw_bannedip_ip_active_idx'), + ), + migrations.AddIndex( + model_name='bannedip', + index=models.Index(fields=['active', 'expires'], name='fw_bannedip_active_exp_idx'), + ), + ] diff --git a/firewall/static/firewall/firewall.js b/firewall/static/firewall/firewall.js index 4a4c4aeaf..e5acb30fe 100644 --- a/firewall/static/firewall/firewall.js +++ b/firewall/static/firewall/firewall.js @@ -31,9 +31,61 @@ app.controller('firewallController', function ($scope, $http, $timeout) { $scope.couldNotConnect = true; $scope.rulesDetails = false; - // Banned IPs variables - $scope.activeTab = 'rules'; + // Banned IPs variables – tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/) + function tabFromHash() { + var h = (window.location.hash || '').replace(/^#/, ''); + return (h === 'banned-ips') ? 'banned' : 'rules'; + } + $scope.activeTab = tabFromHash(); $scope.bannedIPs = []; // Initialize as empty array + + // Re-apply tab from hash after load (hash can be set after controller init in some browsers) + function applyTabFromHash() { + var tab = tabFromHash(); + if ($scope.activeTab !== tab) { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); } + } + } + $timeout(applyTabFromHash, 0); + if (document.readyState === 'complete') { + $timeout(applyTabFromHash, 50); + } else { + window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); }); + } + + // Sync tab with hash and load that tab's data on switch + $scope.setFirewallTab = function(tab) { + $timeout(function() { + $scope.activeTab = tab; + window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules'; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }, 0); + }; + + // Back/forward or direct hash change: sync tab and load its data + function syncTabFromHash() { + var tab = tabFromHash(); + if ($scope.activeTab !== tab) { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); } + } + } + window.addEventListener('hashchange', syncTabFromHash); + + // Pagination: Firewall Rules (default 10 per page, options 5–100) + $scope.rulesPage = 1; + $scope.rulesPageSize = 10; + $scope.rulesPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.rulesTotalCount = 0; + + // Pagination: Banned IPs + $scope.bannedPage = 1; + $scope.bannedPageSize = 10; + $scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.bannedTotalCount = 0; // Initialize banned IPs array - start as null so template shows empty state // Will be set to array after API call @@ -47,9 +99,21 @@ app.controller('firewallController', function ($scope, $http, $timeout) { firewallStatus(); + // Load both tabs on init; also load on tab change (watch) so content always shows populateCurrentRecords(); - - // Load banned IPs immediately when controller initializes + populateBannedIPs(); + + $scope.$watch('activeTab', function(newVal, oldVal) { + if (newVal === oldVal || !newVal) return; + $timeout(function() { + try { + if (newVal === 'banned' && typeof populateBannedIPs === 'function') populateBannedIPs(); + else if (newVal === 'rules' && typeof populateCurrentRecords === 'function') populateCurrentRecords(); + } catch (e) {} + }, 0); + }); + + // Log for debugging console.log('=== FIREWALL CONTROLLER INITIALIZING ==='); console.log('Initializing firewall controller, loading banned IPs...'); @@ -69,14 +133,19 @@ app.controller('firewallController', function ($scope, $http, $timeout) { } }; - console.log('Making request to:', url); + var postData = { + page: $scope.bannedPage || 1, + page_size: $scope.bannedPageSize || 10 + }; + console.log('Making request to:', url, 'page:', postData.page, 'page_size:', postData.page_size); console.log('CSRF Token:', csrfToken ? 'Found (' + csrfToken.substring(0, 10) + '...)' : 'MISSING!'); - $http.post(url, {}, config).then( + $http.post(url, postData, config).then( function(response) { + var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data; console.log('=== API RESPONSE RECEIVED ==='); console.log('Response status:', response.status); - console.log('Response data:', JSON.stringify(response.data, null, 2)); + console.log('Response data (parsed):', res); $scope.bannedIPsLoading = false; // Reset error flags @@ -84,8 +153,8 @@ app.controller('firewallController', function ($scope, $http, $timeout) { $scope.bannedIPActionSuccess = true; $scope.bannedIPCouldNotConnect = true; - if (response.data && response.data.status === 1) { - var bannedIPsArray = response.data.bannedIPs || []; + if (res && res.status === 1) { + var bannedIPsArray = res.bannedIPs || []; console.log('Raw bannedIPs from API:', bannedIPsArray); console.log('Banned IPs count:', bannedIPsArray.length); console.log('Is array?', Array.isArray(bannedIPsArray)); @@ -99,6 +168,9 @@ app.controller('firewallController', function ($scope, $http, $timeout) { // Assign to scope - Angular $http callbacks already run within $apply console.log('Assigning to scope.bannedIPs...'); $scope.bannedIPs = bannedIPsArray; + $scope.bannedTotalCount = res.total_count != null ? res.total_count : bannedIPsArray.length; + $scope.bannedPage = Math.max(1, res.page != null ? res.page : 1); + $scope.bannedPageSize = res.page_size != null ? res.page_size : 10; console.log('After assignment - scope.bannedIPs:', $scope.bannedIPs); console.log('After assignment - scope.bannedIPs.length:', $scope.bannedIPs ? $scope.bannedIPs.length : 'undefined'); console.log('After assignment - activeTab:', $scope.activeTab); @@ -109,10 +181,10 @@ app.controller('firewallController', function ($scope, $http, $timeout) { console.log('=== populateBannedIPs() SUCCESS ==='); } else { console.error('ERROR: API returned status !== 1'); - console.error('Response data:', response.data); + console.error('Response data:', res); $scope.bannedIPs = []; $scope.bannedIPActionFailed = false; - $scope.bannedIPErrorMessage = (response.data && response.data.error_message) || 'Unknown error'; + $scope.bannedIPErrorMessage = (res && res.error_message) || 'Unknown error'; } }, function(error) { @@ -144,6 +216,52 @@ app.controller('firewallController', function ($scope, $http, $timeout) { console.log('$scope.populateBannedIPs() called from template'); populateBannedIPs(); }; + + $scope.goToBannedPage = function(page) { + var totalP = $scope.bannedTotalPages(); + if (page < 1 || page > totalP) return; + $scope.bannedPage = page; + populateBannedIPs(); + }; + $scope.goToBannedPageByInput = function() { + var n = parseInt($scope.bannedPageInput, 10); + if (isNaN(n) || n < 1) n = 1; + var maxP = $scope.bannedTotalPages(); + if (n > maxP) n = maxP; + $scope.bannedPageInput = n; + $scope.goToBannedPage(n); + }; + $scope.bannedTotalPages = function() { + var size = $scope.bannedPageSize || 10; + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1; + }; + $scope.bannedRangeStart = function() { + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + if (total === 0) return 0; + var page = Math.max(1, $scope.bannedPage || 1); + var size = $scope.bannedPageSize || 10; + return (page - 1) * size + 1; + }; + $scope.bannedRangeEnd = function() { + var start = $scope.bannedRangeStart(); + var size = $scope.bannedPageSize || 10; + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + return total === 0 ? 0 : Math.min(start + size - 1, total); + }; + $scope.setBannedPageSize = function() { + $scope.bannedPage = 1; + populateBannedIPs(); + }; + + if (typeof window !== 'undefined') { + window.__firewallLoadTab = function(tab) { + $scope.$evalAsync(function() { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }); + }; + } // Load banned IPs on page load - use $timeout for Angular compatibility // Wrap in try-catch to ensure it executes even if there are other errors @@ -160,33 +278,6 @@ app.controller('firewallController', function ($scope, $http, $timeout) { console.error('Error setting up timeout for populateBannedIPs:', e); } - // Also load when switching to banned tab - use deep watch for immediate trigger - try { - $scope.$watch('activeTab', function(newVal, oldVal) { - console.log('=== activeTab WATCH TRIGGERED ==='); - console.log('activeTab changed from', oldVal, 'to', newVal); - if (newVal === 'banned') { - console.log('Switched to banned IPs tab, calling populateBannedIPs...'); - // Call immediately - try { - if (typeof populateBannedIPs === 'function') { - console.log('Calling populateBannedIPs from $watch...'); - populateBannedIPs(); - } else if (typeof $scope.populateBannedIPs === 'function') { - console.log('Calling $scope.populateBannedIPs from $watch...'); - $scope.populateBannedIPs(); - } else { - console.error('ERROR: populateBannedIPs is not available!'); - } - } catch(e) { - console.error('Error calling populateBannedIPs from watch:', e); - } - } - }, true); // Use deep watch (true parameter) - } catch(e) { - console.error('Error setting up $watch for activeTab:', e); - } - $scope.addRule = function () { $scope.rulesLoading = false; @@ -278,39 +369,76 @@ app.controller('firewallController', function ($scope, $http, $timeout) { $scope.actionFailed = true; $scope.actionSuccess = true; - url = "/firewall/getCurrentRules"; - - var data = {}; - + var data = { + page: $scope.rulesPage || 1, + page_size: $scope.rulesPageSize || 10 + }; var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; - $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - function ListInitialDatas(response) { - if (response.data.fetchStatus === 1) { - $scope.rules = JSON.parse(response.data.data); + var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data; + if (res && res.fetchStatus === 1) { + $scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []); + $scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0); + $scope.rulesPage = Math.max(1, res.page != null ? res.page : 1); + $scope.rulesPageSize = res.page_size != null ? res.page_size : 10; $scope.rulesLoading = true; } else { $scope.rulesLoading = true; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (res && res.error_message) ? res.error_message : ''; } } function cantLoadInitialDatas(response) { $scope.couldNotConnect = false; - } - } + $scope.goToRulesPage = function(page) { + var totalP = $scope.rulesTotalPages(); + if (page < 1 || page > totalP) return; + $scope.rulesPage = page; + populateCurrentRecords(); + }; + $scope.goToRulesPageByInput = function() { + var n = parseInt($scope.rulesPageInput, 10); + if (isNaN(n) || n < 1) n = 1; + var maxP = $scope.rulesTotalPages(); + if (n > maxP) n = maxP; + $scope.rulesPageInput = n; + $scope.goToRulesPage(n); + }; + $scope.rulesTotalPages = function() { + var size = $scope.rulesPageSize || 10; + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1; + }; + $scope.rulesRangeStart = function() { + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + if (total === 0) return 0; + var page = Math.max(1, $scope.rulesPage || 1); + var size = $scope.rulesPageSize || 10; + return (page - 1) * size + 1; + }; + $scope.rulesRangeEnd = function() { + var start = $scope.rulesRangeStart(); + var size = $scope.rulesPageSize || 10; + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + return total === 0 ? 0 : Math.min(start + size - 1, total); + }; + $scope.setRulesPageSize = function() { + $scope.rulesPage = 1; + populateCurrentRecords(); + }; + $scope.deleteRule = function (id, proto, port, ruleIP) { $scope.rulesLoading = false; @@ -2837,4 +2965,26 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) } } -}); \ No newline at end of file +}); + +(function() { + // Do not capture tab clicks – let Angular ng-click run setFirewallTab() so data loads. + // Only sync tab from hash on load and hashchange (back/forward) via __firewallLoadTab. + function syncFirewallTabFromHash() { + var nav = document.getElementById('firewall-tab-nav'); + if (!nav) return; + var h = (window.location.hash || '').replace(/^#/, ''); + var tab = (h === 'banned-ips') ? 'banned' : 'rules'; + if (window.__firewallLoadTab) { + try { window.__firewallLoadTab(tab); } catch (e) {} + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', syncFirewallTabFromHash); + } else { + syncFirewallTabFromHash(); + } + setTimeout(syncFirewallTabFromHash, 100); + window.addEventListener('hashchange', syncFirewallTabFromHash); +})(); \ No newline at end of file diff --git a/firewall/templates/firewall/firewall.html b/firewall/templates/firewall/firewall.html index 6a500128c..981fa5727 100644 --- a/firewall/templates/firewall/firewall.html +++ b/firewall/templates/firewall/firewall.html @@ -207,6 +207,8 @@ /* Rules Panel */ .rules-panel { + position: relative; + z-index: 1; background: var(--bg-secondary, white); border-radius: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.05), 0 10px 40px rgba(0,0,0,0.08); @@ -565,6 +567,121 @@ 100% { transform: rotate(360deg); } } + /* Pagination */ + .pagination-bar { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 1rem; + padding: 1rem 2rem; + border-top: 1px solid var(--border-color, #e8e9ff); + background: var(--bg-tertiary, #f8f9ff); + font-size: 0.875rem; + color: var(--text-secondary, #64748b); + } + .pagination-info { + display: flex; + align-items: center; + gap: 0.5rem; + } + .pagination-controls { + display: flex; + align-items: center; + gap: 0.5rem; + } + .pagination-controls button { + padding: 0.4rem 0.75rem; + border-radius: 6px; + border: 1px solid var(--border-color, #e2e8f0); + background: var(--bg-secondary, white); + color: var(--text-primary, #1e293b); + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + } + .pagination-controls button:hover:not(:disabled) { + background: var(--firewall-accent, #ef4444); + color: white; + border-color: var(--firewall-accent, #ef4444); + } + .pagination-controls button:disabled { + opacity: 0.5; + cursor: not-allowed; + } + .pagination-controls .page-num { + min-width: 2rem; + text-align: center; + font-weight: 600; + color: var(--firewall-accent, #ef4444); + } + .pagination-size { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin-left: 1rem; + } + .pagination-size select { + min-width: 4rem; + } + .pagination-size-btns { + display: inline-flex; + align-items: center; + gap: 0.2rem; + flex-wrap: wrap; + } + .pagination-size-btn { + min-width: 2rem; + padding: 0.35rem 0.5rem; + border: 1px solid var(--border-color, #e2e8f0); + border-radius: 4px; + background: var(--bg-secondary, white); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + } + .pagination-size-btn:hover { + border-color: var(--firewall-accent, #ef4444); + color: var(--firewall-accent, #ef4444); + } + .pagination-size-btn.active { + background: var(--firewall-accent, #ef4444); + color: white; + border-color: var(--firewall-accent, #ef4444); + } + .pagination-goto { + display: inline-flex; + align-items: center; + gap: 0.35rem; + } + .pagination-goto-label { + font-size: 0.8rem; + color: var(--text-secondary, #64748b); + white-space: nowrap; + } + .pagination-goto-input { + width: 3.5rem; + padding: 0.35rem 0.5rem; + border: 1px solid var(--border-color, #e2e8f0); + border-radius: 4px; + font-size: 0.875rem; + text-align: center; + } + .pagination-goto-btn { + padding: 0.35rem 0.6rem; + border-radius: 4px; + border: 1px solid var(--border-color, #e2e8f0); + background: var(--bg-secondary, white); + font-size: 0.8rem; + font-weight: 500; + cursor: pointer; + } + .pagination-goto-btn:hover { + background: var(--firewall-accent, #ef4444); + color: white; + border-color: var(--firewall-accent, #ef4444); + } + /* Responsive */ @media (max-width: 1024px) { .rule-form { @@ -606,7 +723,7 @@ } } - /* Tab Navigation Styles */ + /* Tab Navigation – always on top, clearly clickable */ .tab-navigation { display: flex; background: var(--bg-secondary, white); @@ -615,6 +732,9 @@ margin-bottom: 2rem; box-shadow: 0 2px 8px var(--shadow-light, rgba(0,0,0,0.1)); border: 1px solid var(--border-color, #e8e9ff); + position: relative; + z-index: 100; + isolation: isolate; } .tab-button { @@ -627,11 +747,16 @@ font-weight: 500; font-size: 0.9rem; cursor: pointer; - transition: all 0.3s ease; + pointer-events: auto; + transition: all 0.2s ease; display: flex; align-items: center; justify-content: center; gap: 0.5rem; + text-decoration: none; + font-family: inherit; + -webkit-appearance: none; + appearance: none; } .tab-button:hover { @@ -639,6 +764,11 @@ color: var(--accent-color, #5b5fcf); } + .tab-button:focus { + outline: 2px solid var(--accent-color, #5b5fcf); + outline-offset: 2px; + } + .tab-button.tab-active { background: var(--accent-color, #5b5fcf); color: var(--bg-secondary, white); @@ -647,10 +777,13 @@ .tab-button i { font-size: 1rem; + pointer-events: none; } - /* Banned IPs Panel Styles */ + /* Banned IPs Panel – below tab bar (z-index) */ .banned-ips-panel { + position: relative; + z-index: 1; background: var(--bg-secondary, white); border-radius: 16px; box-shadow: 0 4px 12px var(--shadow-medium, rgba(0,0,0,0.15)); @@ -1068,26 +1201,20 @@ - -
- -
- -
+ +
@@ -1206,6 +1333,33 @@
+ +
+
+ {% trans "Showing" %} {$ rulesRangeStart() || 0 $} – {$ rulesRangeEnd() || 0 $} {% trans "of" %} {$ rulesTotalCount || rules.length || 0 $} + + {% trans "Per page:" %} + + + + +
+
+ + {$ rulesPage $} / {$ rulesTotalPages() $} + + + + + + +
+
+
@@ -1234,7 +1388,7 @@
-
+
@@ -1376,6 +1530,33 @@
+ +
+
+ {% trans "Showing" %} {$ bannedRangeStart() || 0 $} – {$ bannedRangeEnd() || 0 $} {% trans "of" %} {$ bannedTotalCount || bannedIPs.length || 0 $} + + {% trans "Per page:" %} + + + + +
+
+ + {$ bannedPage $} / {$ bannedTotalPages() $} + + + + + + +
+
+
@@ -1443,4 +1624,80 @@
+ + {% endblock %} \ No newline at end of file diff --git a/firewall/urls.py b/firewall/urls.py index e60e00369..91d53103f 100644 --- a/firewall/urls.py +++ b/firewall/urls.py @@ -3,7 +3,11 @@ from . import views urlpatterns = [ path('securityHome', views.securityHome, name='securityHome'), - path('', views.firewallHome, name='firewallHome'), + path('firewall-rules/', views.firewallHome, name='firewallRules'), + path('firewall-rules', views.firewallHome, name='firewallRulesNoSlash'), + path('banned-ips/', views.firewallHome, name='firewallBannedIPs'), + path('banned-ips', views.firewallHome, name='firewallBannedIPsNoSlash'), + path('', views.firewallHome, name='firewallHome'), # /firewall/ also serves the page so 404 is avoided path('getCurrentRules', views.getCurrentRules, name='getCurrentRules'), path('addRule', views.addRule, name='addRule'), path('modifyRule', views.modifyRule, name='modifyRule'), diff --git a/firewall/views.py b/firewall/views.py index 5e42bcf96..20ddd8b7f 100644 --- a/firewall/views.py +++ b/firewall/views.py @@ -18,6 +18,16 @@ def securityHome(request): return redirect(loadLoginPage) +def firewallRedirect(request): + """Redirect /firewall/ to /firewall/firewall-rules/ so the default tab has a clear URL.""" + try: + if request.session.get('userID'): + return redirect('/firewall/firewall-rules/') + return redirect(loadLoginPage) + except Exception: + return redirect(loadLoginPage) + + def firewallHome(request): try: userID = request.session['userID'] @@ -41,7 +51,14 @@ def getCurrentRules(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.getCurrentRules(userID) + try: + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + data = json.loads(body) if body and body.strip() else {} + except (json.JSONDecodeError, Exception): + data = {} + return fm.getCurrentRules(userID, data) except KeyError: return redirect(loadLoginPage) @@ -663,7 +680,14 @@ def getBannedIPs(request): try: userID = request.session['userID'] fm = FirewallManager() - return fm.getBannedIPs(userID) + try: + body = request.body + if isinstance(body, bytes): + body = body.decode('utf-8') + data = json.loads(body) if body and body.strip() else {} + except (json.JSONDecodeError, Exception): + data = {} + return fm.getBannedIPs(userID, data) except KeyError: return redirect(loadLoginPage) diff --git a/plogical/installUtilities.py b/plogical/installUtilities.py index fb3aaeba4..c5486296d 100644 --- a/plogical/installUtilities.py +++ b/plogical/installUtilities.py @@ -1,6 +1,6 @@ import subprocess import sys -from plogical import CyberCPLogFileWriter as logging +from plogical.CyberCPLogFileWriter import CyberCPLogFileWriter import shutil import pexpect import os @@ -221,7 +221,7 @@ class installUtilities: return 1 @staticmethod - def safeModifyHttpdConfig(config_modifier, description="config modification"): + def safeModifyHttpdConfig(config_modifier, description="config modification", skip_validation=False): """ Safely modify httpd_config.conf with backup, validation, and rollback on failure. Prevents corrupted configs that cause OpenLiteSpeed to fail binding ports 80/443. @@ -237,20 +237,30 @@ class installUtilities: """ config_file = "/usr/local/lsws/conf/httpd_config.conf" - if not os.path.exists(config_file): - error_msg = f"Config file not found: {config_file}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") - return False, error_msg + # Check file existence using ProcessUtilities (handles permissions correctly) + try: + command = 'test -f {} && echo exists || echo notfound'.format(config_file) + result = ProcessUtilities.outputExecutioner(command).strip() + if result == 'notfound': + error_msg = f"Config file not found: {config_file}" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + return False, error_msg + except Exception as e: + # Fallback to os.path.exists if ProcessUtilities fails + if not os.path.exists(config_file): + error_msg = f"Config file not found: {config_file} (check failed: {str(e)})" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + return False, error_msg # Create backup with timestamp try: timestamp = datetime.now().strftime("%Y%m%d-%H%M%S") backup_file = f"{config_file}.backup-{timestamp}" shutil.copy2(config_file, backup_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Created backup: {backup_file} for {description}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Created backup: {backup_file} for {description}") except Exception as e: error_msg = f"Failed to create backup: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg # Read current config @@ -259,7 +269,7 @@ class installUtilities: original_content = f.readlines() except Exception as e: error_msg = f"Failed to read config file: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg # Modify config using callback @@ -267,11 +277,11 @@ class installUtilities: modified_content = config_modifier(original_content) if not isinstance(modified_content, list): error_msg = "Config modifier must return a list of lines" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg except Exception as e: error_msg = f"Config modifier function failed: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") return False, error_msg # Write modified config @@ -280,57 +290,68 @@ class installUtilities: f.writelines(modified_content) except Exception as e: error_msg = f"Failed to write modified config: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") # Restore backup try: shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to write failure") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to write failure") except: pass return False, error_msg - # Validate config using openlitespeed -t - try: - if ProcessUtilities.decideServer() == ProcessUtilities.OLS: - validate_cmd = ['/usr/local/lsws/bin/openlitespeed', '-t', '-f', config_file] - else: - # For LiteSpeed Enterprise, use lswsctrl - validate_cmd = ['/usr/local/lsws/bin/lswsctrl', '-t', '-f', config_file] - - result = subprocess.run(validate_cmd, capture_output=True, text=True, timeout=30) - - if result.returncode != 0: - error_msg = f"Config validation failed: {result.stderr}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + # Validate config using openlitespeed -t (for OLS) + # Note: openlitespeed -t may return non-zero due to warnings, so we check for actual errors + # Skip validation if skip_validation=True (useful when pre-existing config has errors) + if skip_validation: + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Skipping validation as requested for: {description}") + else: + try: + if ProcessUtilities.decideServer() == ProcessUtilities.OLS: + openlitespeed_bin = '/usr/local/lsws/bin/openlitespeed' + if os.path.exists(openlitespeed_bin): + validate_cmd = [openlitespeed_bin, '-t'] + result = subprocess.run(validate_cmd, capture_output=True, text=True, timeout=30) + + # Check for actual errors (not just warnings) + # openlitespeed -t returns 0 on success, non-zero on errors + # But it may also return non-zero for warnings, so check for actual [ERROR] lines + if result.returncode != 0: + # Check if there are actual ERROR log lines (not just WARN or the word "error" in text) + error_output = result.stderr or result.stdout or '' + # Look for lines that start with [ERROR] or contain [ERROR] (actual error log entries) + error_lines = [line for line in error_output.split('\n') if '[ERROR]' in line.upper()] + if error_lines: + # Only fail on actual errors, not warnings + error_msg = f"Config validation failed with errors: {' '.join(error_lines[:3])}" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") + # Restore backup + try: + shutil.copy2(backup_file, config_file) + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation failure") + except Exception as restore_error: + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] CRITICAL: Failed to restore backup: {str(restore_error)}") + return False, error_msg + else: + # Only warnings, not errors - proceed + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Config validation has warnings but no errors, proceeding") + else: + # openlitespeed binary not found, skip validation + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Warning: openlitespeed binary not found, skipping config validation") + else: + # For LiteSpeed Enterprise, validation is not available via lswsctrl -t + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Skipping validation for LiteSpeed Enterprise") + except Exception as e: + error_msg = f"Config validation error: {str(e)}" + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") # Restore backup try: shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation failure") - except Exception as restore_error: - logging.writeToFile(f"[safeModifyHttpdConfig] CRITICAL: Failed to restore backup: {str(restore_error)}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation error") + except: + pass return False, error_msg - except subprocess.TimeoutExpired: - error_msg = "Config validation timed out" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") - # Restore backup - try: - shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation timeout") - except: - pass - return False, error_msg - except Exception as e: - error_msg = f"Config validation error: {str(e)}" - logging.writeToFile(f"[safeModifyHttpdConfig] {error_msg}") - # Restore backup - try: - shutil.copy2(backup_file, config_file) - logging.writeToFile(f"[safeModifyHttpdConfig] Restored backup due to validation error") - except: - pass - return False, error_msg - logging.writeToFile(f"[safeModifyHttpdConfig] Successfully modified and validated config: {description}") + CyberCPLogFileWriter.writeToFile(f"[safeModifyHttpdConfig] Successfully modified and validated config: {description}") return True, None @staticmethod @@ -352,7 +373,7 @@ class installUtilities: if not success: error_msg = error if error else "Unknown error" - logging.writeToFile(f"[changePortTo80] Failed: {error_msg}") + CyberCPLogFileWriter.writeToFile(f"[changePortTo80] Failed: {error_msg}") return 0 return installUtilities.reStartLiteSpeed() diff --git a/pluginHolder/patreon_verifier.py b/pluginHolder/patreon_verifier.py index 42fd5cfed..6566f2651 100644 --- a/pluginHolder/patreon_verifier.py +++ b/pluginHolder/patreon_verifier.py @@ -27,14 +27,14 @@ class PatreonVerifier: self.client_id = getattr(settings, 'PATREON_CLIENT_ID', os.environ.get('PATREON_CLIENT_ID', '')) self.client_secret = getattr(settings, 'PATREON_CLIENT_SECRET', os.environ.get('PATREON_CLIENT_SECRET', '')) self.creator_id = getattr(settings, 'PATREON_CREATOR_ID', os.environ.get('PATREON_CREATOR_ID', '')) - self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984')) + self.membership_tier_id = getattr(settings, 'PATREON_MEMBERSHIP_TIER_ID', os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '')) self.creator_access_token = getattr(settings, 'PATREON_CREATOR_ACCESS_TOKEN', os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '')) except: # Fallback to environment variables only self.client_id = os.environ.get('PATREON_CLIENT_ID', '') self.client_secret = os.environ.get('PATREON_CLIENT_SECRET', '') self.creator_id = os.environ.get('PATREON_CREATOR_ID', '') - self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '27789984') + self.membership_tier_id = os.environ.get('PATREON_MEMBERSHIP_TIER_ID', '') self.creator_access_token = os.environ.get('PATREON_CREATOR_ACCESS_TOKEN', '') # Cache for membership checks (to avoid excessive API calls) diff --git a/pluginHolder/templates/pluginHolder/plugins.html b/pluginHolder/templates/pluginHolder/plugins.html index c8c35de44..c4ad4998e 100644 --- a/pluginHolder/templates/pluginHolder/plugins.html +++ b/pluginHolder/templates/pluginHolder/plugins.html @@ -1317,7 +1317,14 @@
{% endfor %} @@ -1437,7 +1446,7 @@
- {% if plugin.installed %} + {% if plugin.builtin or plugin.installed %} {% if plugin.enabled %} {% else %} @@ -1693,11 +1702,18 @@ function toggleView(view, updateHash = true) { const installedSearchWrapper = document.getElementById('installedPluginsSearchWrapper'); const installedSortFilterBar = document.getElementById('installedSortFilterBar'); + + // Add null checks to prevent errors if elements don't exist + if (!gridView || !tableView || !storeView) { + console.warn('toggleView: One or more view elements not found'); + return; + } + if (view === 'grid') { gridView.style.display = 'grid'; tableView.style.display = 'none'; storeView.style.display = 'none'; - viewBtns[0].classList.add('active'); + if (viewBtns[0]) viewBtns[0].classList.add('active'); if (installedSearchWrapper) installedSearchWrapper.style.display = 'block'; if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex'; if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons(); @@ -1707,7 +1723,7 @@ function toggleView(view, updateHash = true) { gridView.style.display = 'none'; tableView.style.display = 'block'; storeView.style.display = 'none'; - viewBtns[1].classList.add('active'); + if (viewBtns[1]) viewBtns[1].classList.add('active'); if (installedSearchWrapper) installedSearchWrapper.style.display = 'block'; if (installedSortFilterBar) installedSortFilterBar.style.display = 'flex'; if (typeof updateInstalledSortButtons === 'function') updateInstalledSortButtons(); @@ -1719,7 +1735,7 @@ function toggleView(view, updateHash = true) { gridView.style.display = 'none'; tableView.style.display = 'none'; storeView.style.display = 'block'; - viewBtns[2].classList.add('active'); + if (viewBtns[2]) viewBtns[2].classList.add('active'); // Load plugins from store if not already loaded if (storePlugins.length === 0) { @@ -2941,23 +2957,35 @@ document.addEventListener('DOMContentLoaded', function() { const hash = window.location.hash.substring(1); // Remove # const validViews = ['grid', 'table', 'store']; - let initialView = 'grid'; // Default - if (validViews.includes(hash)) { - initialView = hash; - } else { - // Default to grid view if plugins exist, otherwise show store - const gridView = document.getElementById('gridView'); - if (gridView && gridView.children.length > 0) { - initialView = 'grid'; + // Check if view elements exist before calling toggleView + const gridView = document.getElementById('gridView'); + const tableView = document.getElementById('tableView'); + const storeView = document.getElementById('storeView'); + + // Only proceed if all view elements exist (plugins are installed) + if (gridView && tableView && storeView) { + let initialView = 'grid'; // Default + if (validViews.includes(hash)) { + initialView = hash; } else { - initialView = 'store'; + // Default to grid view if plugins exist, otherwise show store + if (gridView.children.length > 0) { + initialView = 'grid'; + } else { + initialView = 'store'; + } + } + + // Set initial view without updating hash (only update hash if there was already one) + const hadHash = hash.length > 0; + toggleView(initialView, hadHash); + } else { + // Elements don't exist (no plugins installed), just show store view directly + if (storeView) { + storeView.style.display = 'block'; } } - // Set initial view without updating hash (only update hash if there was already one) - const hadHash = hash.length > 0; - toggleView(initialView, hadHash); - // Load store plugins if store view is visible (either from toggleView or already displayed) setTimeout(function() { const storeViewCheck = document.getElementById('storeView'); diff --git a/pluginHolder/views.py b/pluginHolder/views.py index 0bda088bc..be0419456 100644 --- a/pluginHolder/views.py +++ b/pluginHolder/views.py @@ -38,6 +38,10 @@ PLUGIN_BACKUP_DIR = '/home/cyberpanel/plugin_backups' # Plugin source paths (checked in order; first match wins for install) PLUGIN_SOURCE_PATHS = ['/home/cyberpanel/plugins', '/home/cyberpanel-plugins'] +# Builtin/core plugins that are part of CyberPanel (not user-installable plugins) +# These plugins show "Built-in" badge and only Settings button (no Deactivate/Uninstall) +BUILTIN_PLUGINS = frozenset(['emailMarketing', 'emailPremium']) + def _get_plugin_source_path(plugin_name): """Return the full path to a plugin's source directory, or None if not found.""" for base in PLUGIN_SOURCE_PATHS: @@ -118,6 +122,7 @@ def installed(request): processed_plugins = set() # Track which plugins we've already processed # First, process plugins from source directories (multiple paths: /home/cyberpanel/plugins, /home/cyberpanel-plugins) + # BUT: Skip plugins that are already installed - we'll process those from the installed location instead for pluginPath in PLUGIN_SOURCE_PATHS: if not os.path.exists(pluginPath): continue @@ -129,6 +134,12 @@ def installed(request): for plugin in os.listdir(pluginPath): if plugin in processed_plugins: continue + # Skip if plugin is already installed - we'll process it from installed location instead + completePath = installedPath + '/' + plugin + '/meta.xml' + if os.path.exists(completePath): + # Plugin is installed, skip source path - DON'T mark as processed yet + # The installed location loop will handle it and mark it as processed + continue # Skip files (like .zip files) - only process directories pluginDir = os.path.join(pluginPath, plugin) if not os.path.isdir(pluginDir): @@ -187,6 +198,8 @@ def installed(request): data['desc'] = desc_elem.text data['version'] = version_elem.text data['plugin_dir'] = plugin # Plugin directory name + # Set builtin flag (core CyberPanel plugins vs user-installable plugins) + data['builtin'] = plugin in BUILTIN_PLUGINS # Check if plugin is installed (only if it exists in /usr/local/CyberCP/) # Source directory presence doesn't mean installed - it just means the source files are available data['installed'] = os.path.exists(completePath) @@ -333,6 +346,8 @@ def installed(request): data['desc'] = desc_elem.text data['version'] = version_elem.text data['plugin_dir'] = plugin + # Set builtin flag (core CyberPanel plugins vs user-installable plugins) + data['builtin'] = plugin in BUILTIN_PLUGINS data['installed'] = True # This is an installed plugin data['enabled'] = _is_plugin_enabled(plugin) @@ -394,6 +409,7 @@ def installed(request): # else: is_paid already False from initialization above pluginList.append(data) + processed_plugins.add(plugin) # Mark as processed to prevent duplicates except ElementTree.ParseError as e: errorPlugins.append({'name': plugin, 'error': f'XML parse error: {str(e)}'}) @@ -433,6 +449,7 @@ def installed(request): 'desc': desc_elem.text, 'version': version_elem.text, 'plugin_dir': plugin_name, + 'builtin': plugin_name in BUILTIN_PLUGINS, # Set builtin flag 'installed': os.path.exists(complete_path), 'enabled': _is_plugin_enabled(plugin_name) if os.path.exists(complete_path) else False, 'is_paid': False, diff --git a/static/baseTemplate/assets/mobile-responsive.css b/static/baseTemplate/assets/mobile-responsive.css new file mode 100644 index 000000000..35e2674e5 --- /dev/null +++ b/static/baseTemplate/assets/mobile-responsive.css @@ -0,0 +1,589 @@ +/* CyberPanel Mobile Responsive & Readability Fixes */ +/* This file ensures all pages are mobile-friendly with proper font sizes and readable text */ + +/* Base font size and mobile-first approach */ +html { + font-size: 16px; /* Base font size for better readability */ + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + text-size-adjust: 100%; +} + +body { + font-size: 16px; + line-height: 1.6; + color: #2f3640; /* Dark text for better readability on white backgrounds */ + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +/* Ensure all text is readable with proper contrast */ +* { + color: inherit; +} + +/* Override any light text that might be hard to read */ +.text-muted, .text-secondary, .text-light { + color: #2f3640 !important; /* Dark text for better readability on white backgrounds */ +} + +/* Fix small font sizes that are hard to read */ +small, .small, .text-small { + font-size: 14px !important; /* Minimum readable size */ +} + +/* Table improvements for mobile */ +.table { + font-size: 16px !important; /* Larger table text */ + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +.table th, .table td { + padding: 12px 8px !important; /* More padding for touch targets */ + border: 1px solid #e8e9ff; + text-align: left; + vertical-align: middle; + font-size: 14px !important; + line-height: 1.4; +} + +.table th { + background-color: #f8f9fa; + font-weight: 600; + color: #2f3640 !important; + font-size: 15px !important; +} + +/* Button improvements for mobile */ +.btn { + font-size: 16px !important; + padding: 12px 20px !important; + border-radius: 8px; + min-height: 44px; /* Minimum touch target size */ + display: inline-flex; + align-items: center; + justify-content: center; + text-decoration: none; + border: none; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-sm { + font-size: 14px !important; + padding: 8px 16px !important; + min-height: 36px; +} + +.btn-xs { + font-size: 13px !important; + padding: 6px 12px !important; + min-height: 32px; +} + +/* Form elements */ +.form-control, input, textarea, select { + font-size: 16px !important; /* Prevents zoom on iOS */ + padding: 12px 16px !important; + border: 2px solid #e8e9ff; + border-radius: 8px; + min-height: 44px; + line-height: 1.4; + color: #2f3640 !important; + background-color: #ffffff; +} + +.form-control:focus, input:focus, textarea:focus, select:focus { + border-color: #5856d6; + box-shadow: 0 0 0 3px rgba(88, 86, 214, 0.1); + outline: none; +} + +/* Labels and form text */ +label, .control-label { + font-size: 16px !important; + font-weight: 600; + color: #2f3640 !important; + margin-bottom: 8px; + display: block; +} + +/* Headings with proper hierarchy */ +h1 { + font-size: 2.5rem !important; /* 40px */ + font-weight: 700; + color: #1e293b !important; + line-height: 1.2; + margin-bottom: 1rem; +} + +h2 { + font-size: 2rem !important; /* 32px */ + font-weight: 600; + color: #1e293b !important; + line-height: 1.3; + margin-bottom: 0.875rem; +} + +h3 { + font-size: 1.5rem !important; /* 24px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.75rem; +} + +h4 { + font-size: 1.25rem !important; /* 20px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +h5 { + font-size: 1.125rem !important; /* 18px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +h6 { + font-size: 1rem !important; /* 16px */ + font-weight: 600; + color: #2f3640 !important; + line-height: 1.4; + margin-bottom: 0.5rem; +} + +/* Paragraph and body text */ +p { + font-size: 16px !important; + line-height: 1.6; + color: #2f3640 !important; + margin-bottom: 1rem; +} + +/* Sidebar improvements */ +#page-sidebar { + font-size: 16px !important; +} + +#page-sidebar ul li a { + font-size: 16px !important; + padding: 12px 20px !important; + color: #2f3640 !important; + min-height: 44px; + display: flex; + align-items: center; + text-decoration: none; +} + +#page-sidebar ul li a:hover { + background-color: #f8f9fa; + color: #5856d6 !important; +} + +/* Content area improvements */ +.content-box, .panel, .card { + font-size: 16px !important; + color: #2f3640 !important; + background-color: #ffffff; + border: 1px solid #e8e9ff; + border-radius: 12px; + padding: 20px; + margin-bottom: 20px; +} + +/* Modal improvements */ +.modal-content { + font-size: 16px !important; + color: #2f3640 !important; +} + +.modal-title { + font-size: 1.5rem !important; + font-weight: 600; + color: #1e293b !important; +} + +/* Alert and notification improvements */ +.alert { + font-size: 16px !important; + padding: 16px 20px !important; + border-radius: 8px; + margin-bottom: 20px; +} + +.alert-success { + background-color: #f0fdf4; + border-color: #bbf7d0; + color: #166534 !important; +} + +.alert-danger { + background-color: #fef2f2; + border-color: #fecaca; + color: #dc2626 !important; +} + +.alert-warning { + background-color: #fffbeb; + border-color: #fed7aa; + color: #d97706 !important; +} + +.alert-info { + background-color: #eff6ff; + border-color: #bfdbfe; + color: #2563eb !important; +} + +/* Navigation improvements */ +.navbar-nav .nav-link { + font-size: 16px !important; + padding: 12px 16px !important; + color: #2f3640 !important; +} + +/* Breadcrumb improvements */ +.breadcrumb { + font-size: 16px !important; + background-color: transparent; + padding: 0; + margin-bottom: 20px; +} + +.breadcrumb-item { + color: #64748b !important; +} + +.breadcrumb-item.active { + color: #2f3640 !important; +} + +/* Mobile-first responsive breakpoints */ +@media (max-width: 1200px) { + .container, .container-fluid { + padding-left: 15px; + padding-right: 15px; + } + + .table-responsive { + border: none; + margin-bottom: 20px; + } +} + +@media (max-width: 992px) { + /* Stack columns on tablets */ + .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9 { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 20px; + } + + /* Adjust sidebar for tablets */ + #page-sidebar { + width: 100%; + position: static; + height: auto; + } + + /* Make tables horizontally scrollable */ + .table-responsive { + overflow-x: auto; + } + + .table { + min-width: 600px; + } +} + +@media (max-width: 768px) { + /* Mobile-specific adjustments */ + html { + font-size: 14px; + } + + body { + font-size: 14px; + padding: 0; + } + + .container, .container-fluid { + padding-left: 10px; + padding-right: 10px; + } + + /* Stack all columns on mobile */ + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-6, .col-sm-8, .col-sm-9, .col-sm-12, + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-6, .col-md-8, .col-md-9, .col-md-12 { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 15px; + } + + /* Adjust headings for mobile */ + h1 { + font-size: 2rem !important; /* 32px */ + } + + h2 { + font-size: 1.75rem !important; /* 28px */ + } + + h3 { + font-size: 1.5rem !important; /* 24px */ + } + + h4 { + font-size: 1.25rem !important; /* 20px */ + } + + /* Button adjustments for mobile */ + .btn { + font-size: 16px !important; + padding: 14px 20px !important; + width: 100%; + margin-bottom: 10px; + } + + .btn-group .btn { + width: auto; + margin-bottom: 0; + } + + /* Form adjustments for mobile */ + .form-control, input, textarea, select { + font-size: 16px !important; /* Prevents zoom on iOS */ + padding: 14px 16px !important; + width: 100%; + } + + /* Table adjustments for mobile */ + .table { + font-size: 14px !important; + } + + .table th, .table td { + padding: 8px 6px !important; + font-size: 13px !important; + } + + /* Hide less important columns on mobile */ + .table .d-none-mobile { + display: none !important; + } + + /* Modal adjustments for mobile */ + .modal-dialog { + margin: 10px; + width: calc(100% - 20px); + } + + .modal-content { + padding: 20px 15px; + } + + /* Content box adjustments */ + .content-box, .panel, .card { + padding: 15px; + margin-bottom: 15px; + } + + /* Sidebar adjustments for mobile */ + #page-sidebar { + position: fixed; + top: 0; + left: -100%; + width: 280px; + height: 100vh; + z-index: 1000; + transition: left 0.3s ease; + background-color: #ffffff; + box-shadow: 2px 0 10px rgba(0,0,0,0.1); + } + + #page-sidebar.show { + left: 0; + } + + /* Main content adjustments when sidebar is open */ + #main-content { + transition: margin-left 0.3s ease; + } + + #main-content.sidebar-open { + margin-left: 280px; + } + + /* Mobile menu toggle */ + .mobile-menu-toggle { + display: block; + position: fixed; + top: 20px; + left: 20px; + z-index: 1001; + background-color: #5856d6; + color: white; + border: none; + padding: 12px; + border-radius: 8px; + font-size: 18px; + cursor: pointer; + } +} + +@media (max-width: 576px) { + /* Extra small devices */ + html { + font-size: 14px; + } + + .container, .container-fluid { + padding-left: 8px; + padding-right: 8px; + } + + /* Even smaller buttons and forms for very small screens */ + .btn { + font-size: 14px !important; + padding: 12px 16px !important; + } + + .form-control, input, textarea, select { + font-size: 16px !important; /* Still 16px to prevent zoom */ + padding: 12px 14px !important; + } + + /* Compact table for very small screens */ + .table th, .table td { + padding: 6px 4px !important; + font-size: 12px !important; + } + + /* Hide even more columns on very small screens */ + .table .d-none-mobile-sm { + display: none !important; + } +} + +/* Utility classes for mobile */ +.d-none-mobile { + display: block; +} + +.d-none-mobile-sm { + display: block; +} + +@media (max-width: 768px) { + .d-none-mobile { + display: none !important; + } +} + +@media (max-width: 576px) { + .d-none-mobile-sm { + display: none !important; + } +} + +/* Ensure all text has proper contrast */ +.text-white { + color: #ffffff !important; +} + +.text-dark { + color: #2f3640 !important; +} + +.text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Fix any light text on light backgrounds */ +.bg-light .text-muted, +.bg-white .text-muted, +.panel .text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Ensure proper spacing for touch targets */ +a, button, input, select, textarea { + min-height: 44px; + min-width: 44px; +} + +/* Additional text readability improvements */ +/* Fix any green text issues */ +.ng-binding { + color: #2f3640 !important; /* Normal dark text instead of green */ +} + +/* Ensure all text elements have proper contrast */ +span, div, p, label, td, th { + color: inherit; +} + +/* Fix specific text color issues */ +.text-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.text-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.text-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +/* Override Bootstrap's muted text */ +.text-muted { + color: #2f3640 !important; /* Dark text instead of grey */ +} + +/* Fix any remaining light text on light backgrounds */ +.bg-white .text-light, +.bg-light .text-light, +.panel .text-light, +.card .text-light { + color: #2f3640 !important; +} + +/* Fix for small clickable elements */ +.glyph-icon, .icon { + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; +} + +/* Loading and spinner improvements */ +.spinner, .loading { + font-size: 16px !important; + color: #5856d6 !important; +} + +/* Print styles */ +@media print { + body { + font-size: 12pt; + color: #000000 !important; + background: #ffffff !important; + } + + .table th, .table td { + font-size: 10pt !important; + color: #000000 !important; + } + + .btn, .alert, .modal { + display: none !important; + } +} diff --git a/static/baseTemplate/assets/readability-fixes.css b/static/baseTemplate/assets/readability-fixes.css new file mode 100644 index 000000000..1911482b7 --- /dev/null +++ b/static/baseTemplate/assets/readability-fixes.css @@ -0,0 +1,265 @@ +/* CyberPanel Readability & Design Fixes */ +/* This file fixes the core design issues with grey text and color inconsistencies */ + +/* Override CSS Variables for Better Text Contrast */ +:root { + /* Ensure all text uses proper dark colors for readability */ + --text-primary: #2f3640; + --text-secondary: #2f3640; /* Changed from grey to dark for better readability */ + --text-heading: #1e293b; +} + +/* Dark theme also uses proper contrast */ +[data-theme="dark"] { + --text-primary: #e4e4e7; + --text-secondary: #e4e4e7; /* Changed from grey to light for better readability */ + --text-heading: #f3f4f6; +} + +/* Fix Green Text Issues */ +/* Override Angular binding colors that might be green */ +.ng-binding { + color: var(--text-secondary) !important; +} + +/* Specific fix for uptime display */ +#sidebar .server-info .info-line span, +#sidebar .server-info .info-line .ng-binding, +.server-info .ng-binding { + color: var(--text-secondary) !important; +} + +/* Fix Grey Text on White Background */ +/* Override all muted and secondary text classes */ +.text-muted, +.text-secondary, +.text-light, +small, +.small, +.text-small { + color: var(--text-secondary) !important; +} + +/* Fix specific Bootstrap classes */ +.text-muted { + color: #2f3640 !important; /* Dark text for better readability */ +} + +/* Fix text on white/light backgrounds */ +.bg-white .text-muted, +.bg-light .text-muted, +.panel .text-muted, +.card .text-muted, +.content-box .text-muted { + color: #2f3640 !important; +} + +/* Fix menu items and navigation */ +#sidebar .menu-item, +#sidebar .menu-item span, +#sidebar .menu-item i, +.sidebar .menu-item, +.sidebar .menu-item span, +.sidebar .menu-item i { + color: var(--text-secondary) !important; +} + +#sidebar .menu-item:hover, +.sidebar .menu-item:hover { + color: var(--accent-color) !important; +} + +#sidebar .menu-item.active, +.sidebar .menu-item.active { + color: white !important; +} + +/* Fix server info and details */ +.server-info, +.server-info *, +.server-details, +.server-details *, +.info-line, +.info-line span, +.info-line strong, +.tagline, +.brand { + color: inherit !important; +} + +/* Fix form elements */ +label, +.control-label, +.form-label { + color: var(--text-primary) !important; + font-weight: 600; +} + +/* Fix table text */ +.table th, +.table td { + color: var(--text-primary) !important; +} + +.table th { + font-weight: 600; +} + +/* Fix alert text */ +.alert { + color: var(--text-primary) !important; +} + +.alert-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.alert-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.alert-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +.alert-danger { + color: #dc2626 !important; /* Darker red for better readability */ +} + +/* Fix breadcrumb text */ +.breadcrumb-item { + color: var(--text-secondary) !important; +} + +.breadcrumb-item.active { + color: var(--text-primary) !important; +} + +/* Fix modal text */ +.modal-content { + color: var(--text-primary) !important; +} + +.modal-title { + color: var(--text-heading) !important; +} + +/* Fix button text */ +.btn { + color: inherit; +} + +/* Fix any remaining light text issues */ +.bg-light .text-light, +.bg-white .text-light, +.panel .text-light, +.card .text-light { + color: #2f3640 !important; +} + +/* Ensure proper contrast for all text elements */ +span, div, p, label, td, th, a, li { + color: inherit; +} + +/* Fix specific color classes */ +.text-success { + color: #059669 !important; /* Darker green for better readability */ +} + +.text-info { + color: #0284c7 !important; /* Darker blue for better readability */ +} + +.text-warning { + color: #d97706 !important; /* Darker orange for better readability */ +} + +.text-danger { + color: #dc2626 !important; /* Darker red for better readability */ +} + +/* Fix any Angular-specific styling */ +[ng-controller] { + color: inherit; +} + +[ng-show], +[ng-hide], +[ng-if] { + color: inherit; +} + +/* Ensure all content areas have proper text color */ +.content-box, +.panel, +.card, +.main-content, +.page-content { + color: var(--text-primary) !important; +} + +/* Fix any remaining Bootstrap classes */ +.text-dark { + color: #2f3640 !important; +} + +.text-body { + color: var(--text-primary) !important; +} + +/* Mobile-specific fixes */ +@media (max-width: 768px) { + /* Ensure mobile text is also readable */ + body, + .container, + .container-fluid { + color: var(--text-primary) !important; + } + + /* Fix mobile menu text */ + .mobile-menu .menu-item, + .mobile-menu .menu-item span { + color: var(--text-secondary) !important; + } +} + +/* Print styles */ +@media print { + body, + .content-box, + .panel, + .card { + color: #000000 !important; + background: #ffffff !important; + } + + .text-muted, + .text-secondary { + color: #000000 !important; + } +} + +/* High contrast mode support */ +@media (prefers-contrast: high) { + :root { + --text-primary: #000000; + --text-secondary: #000000; + --text-heading: #000000; + } + + [data-theme="dark"] { + --text-primary: #ffffff; + --text-secondary: #ffffff; + --text-heading: #ffffff; + } +} + +/* Reduced motion support */ +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/static/baseTemplate/custom-js/system-status.js b/static/baseTemplate/custom-js/system-status.js index 1817b58ce..76353b1e4 100644 --- a/static/baseTemplate/custom-js/system-status.js +++ b/static/baseTemplate/custom-js/system-status.js @@ -914,126 +914,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { // Hide system charts for non-admin users $scope.hideSystemCharts = false; - // Pagination settings - 10 entries per page - var ITEMS_PER_PAGE = 10; - - // Pagination state for each section - $scope.pagination = { - sshLogins: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - sshLogs: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - topProcesses: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - traffic: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - diskIO: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }, - cpuUsage: { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE } - }; - - // Input fields for "go to page" - $scope.gotoPageInput = { - sshLogins: 1, - sshLogs: 1, - topProcesses: 1, - traffic: 1, - diskIO: 1, - cpuUsage: 1 - }; - - // Expose Math to template - $scope.Math = Math; - - // Pagination helper functions - $scope.getTotalPages = function(section) { - var items = []; - if (section === 'sshLogins') items = $scope.sshLogins || []; - else if (section === 'sshLogs') items = $scope.sshLogs || []; - else if (section === 'topProcesses') items = $scope.topProcesses || []; - else if (section === 'traffic') items = $scope.trafficLabels || []; - else if (section === 'diskIO') items = $scope.diskLabels || []; - else if (section === 'cpuUsage') items = $scope.cpuLabels || []; - return Math.max(1, Math.ceil((items.length || 0) / ITEMS_PER_PAGE)); - }; - - $scope.getPaginatedItems = function(section) { - // Initialize pagination if it doesn't exist - if (!$scope.pagination) { - $scope.pagination = {}; - } - if (!$scope.pagination[section]) { - $scope.pagination[section] = { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }; - console.log('[getPaginatedItems] Initialized pagination for section:', section); - } - - var items = []; - if (section === 'sshLogins') items = $scope.sshLogins || []; - else if (section === 'sshLogs') items = $scope.sshLogs || []; - else if (section === 'topProcesses') items = $scope.topProcesses || []; - else if (section === 'traffic') items = $scope.trafficLabels || []; - else if (section === 'diskIO') items = $scope.diskLabels || []; - else if (section === 'cpuUsage') items = $scope.cpuLabels || []; - - // Ensure currentPage is a valid number - var currentPage = parseInt($scope.pagination[section].currentPage) || 1; - if (currentPage < 1 || isNaN(currentPage)) currentPage = 1; - - var start = (currentPage - 1) * ITEMS_PER_PAGE; - var end = start + ITEMS_PER_PAGE; - - var result = items.slice(start, end); - console.log('[getPaginatedItems] Section:', section, 'Total items:', items.length, 'Page:', currentPage, 'Start:', start, 'End:', end, 'Paginated count:', result.length); - - if (result.length > 0) { - console.log('[getPaginatedItems] First item:', result[0]); - } else if (items.length > 0) { - console.warn('[getPaginatedItems] No items returned but total items > 0. Items:', items.length, 'Page:', currentPage, 'Start:', start, 'End:', end); - } - - return result; - }; - - $scope.goToPage = function(section, page) { - var totalPages = $scope.getTotalPages(section); - if (page >= 1 && page <= totalPages) { - $scope.pagination[section].currentPage = parseInt(page); - $scope.gotoPageInput[section] = parseInt(page); - } - }; - - $scope.nextPage = function(section) { - var totalPages = $scope.getTotalPages(section); - if ($scope.pagination[section].currentPage < totalPages) { - $scope.pagination[section].currentPage++; - $scope.gotoPageInput[section] = $scope.pagination[section].currentPage; - } - }; - - $scope.prevPage = function(section) { - if ($scope.pagination[section].currentPage > 1) { - $scope.pagination[section].currentPage--; - $scope.gotoPageInput[section] = $scope.pagination[section].currentPage; - } - }; - - $scope.getPageNumbers = function(section) { - var totalPages = $scope.getTotalPages(section); - var current = $scope.pagination[section].currentPage; - var pages = []; - var maxVisible = 5; // Show max 5 page numbers - - if (totalPages <= maxVisible) { - for (var i = 1; i <= totalPages; i++) { - pages.push(i); - } - } else { - if (current <= 3) { - for (var i = 1; i <= 5; i++) pages.push(i); - } else if (current >= totalPages - 2) { - for (var i = totalPages - 4; i <= totalPages; i++) pages.push(i); - } else { - for (var i = current - 2; i <= current + 2; i++) pages.push(i); - } - } - return pages; - }; - // Top Processes $scope.topProcesses = []; $scope.loadingTopProcesses = true; @@ -1044,9 +924,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.loadingTopProcesses = false; if (response.data && response.data.status === 1 && response.data.processes) { $scope.topProcesses = response.data.processes; - // Reset to first page when data refreshes - $scope.pagination.topProcesses.currentPage = 1; - $scope.gotoPageInput.topProcesses = 1; } else { $scope.topProcesses = []; } @@ -1066,34 +943,14 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.loadingSSHLogins = false; if (response.data && response.data.logins) { $scope.sshLogins = response.data.logins; - console.log('[refreshSSHLogins] Loaded', $scope.sshLogins.length, 'SSH logins'); - // Ensure pagination is initialized - if (!$scope.pagination) { - $scope.pagination = {}; - } - if (!$scope.pagination.sshLogins) { - $scope.pagination.sshLogins = { currentPage: 1, itemsPerPage: ITEMS_PER_PAGE }; - } - // Reset to first page when data refreshes - $scope.pagination.sshLogins.currentPage = 1; - if (!$scope.gotoPageInput) { - $scope.gotoPageInput = {}; - } - $scope.gotoPageInput.sshLogins = 1; - - // Debug: Log paginated items - var paginated = $scope.getPaginatedItems('sshLogins'); - console.log('[refreshSSHLogins] Paginated items count:', paginated.length, 'Items:', paginated); - // Debug: Log first login to see structure if ($scope.sshLogins.length > 0) { - console.log('[refreshSSHLogins] First SSH login object:', $scope.sshLogins[0]); - console.log('[refreshSSHLogins] IP field:', $scope.sshLogins[0].ip); - console.log('[refreshSSHLogins] All keys:', Object.keys($scope.sshLogins[0])); + console.log('First SSH login object:', $scope.sshLogins[0]); + console.log('IP field:', $scope.sshLogins[0].ip); + console.log('All keys:', Object.keys($scope.sshLogins[0])); } } else { $scope.sshLogins = []; - console.log('[refreshSSHLogins] No logins found in response'); } }, function (err) { $scope.loadingSSHLogins = false; @@ -1114,9 +971,6 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { $scope.loadingSSHLogs = false; if (response.data && response.data.logs) { $scope.sshLogs = response.data.logs; - // Reset to first page when data refreshes - $scope.pagination.sshLogs.currentPage = 1; - $scope.gotoPageInput.sshLogs = 1; // Analyze logs for security issues $scope.analyzeSSHSecurity(); } else { @@ -1157,8 +1011,84 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { }; $scope.blockIPAddress = function(ipAddress) { - if (!$scope.blockingIP) { - $scope.blockingIP = ipAddress; + try { + console.log('========================================'); + console.log('=== blockIPAddress CALLED ==='); + console.log('========================================'); + console.log('blockIPAddress called with:', ipAddress); + console.log('ipAddress type:', typeof ipAddress); + console.log('ipAddress value:', ipAddress); + console.log('$scope:', $scope); + console.log('$scope.blockIPAddress:', typeof $scope.blockIPAddress); + + // Validate IP address parameter + if (!ipAddress) { + console.error('No IP address provided:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'No IP address provided', + type: 'error', + delay: 5000 + }); + } + return; + } + + // Ensure it's a string and trim it + ipAddress = String(ipAddress).trim(); + + // Validate after trimming + if (!ipAddress || ipAddress === '' || ipAddress === 'undefined' || ipAddress === 'null') { + console.error('IP address is empty or invalid after trim:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'Invalid IP address provided: ' + ipAddress, + type: 'error', + delay: 5000 + }); + } + return; + } + + // Basic IP format validation + var ipPattern = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/; + if (!ipPattern.test(ipAddress)) { + console.error('IP address format is invalid:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'Invalid IP address format: ' + ipAddress, + type: 'error', + delay: 5000 + }); + } + return; + } + + // Prevent duplicate requests + if ($scope.blockingIP === ipAddress) { + console.log('Already processing IP:', ipAddress); + return; // Already processing this IP + } + + // Check if already blocked + if ($scope.blockedIPs && $scope.blockedIPs[ipAddress]) { + console.log('IP already blocked:', ipAddress); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Info', + text: `IP address ${ipAddress} is already banned`, + type: 'info', + delay: 3000 + }); + } + return; + } + + // Set blocking flag to prevent duplicate requests + $scope.blockingIP = ipAddress; // Use the new Banned IPs system instead of the old blockIPAddress var data = { @@ -1173,48 +1103,343 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { } }; + console.log('Sending ban IP request:', data); + console.log('CSRF Token:', getCookie('csrftoken')); + console.log('Config:', config); + $http.post('/firewall/addBannedIP', data, config).then(function (response) { + console.log('=== addBannedIP SUCCESS ==='); + console.log('Full response:', response); + console.log('response.data:', response.data); + console.log('response.data type:', typeof response.data); + console.log('response.status:', response.status); + + // Reset blocking flag $scope.blockingIP = null; - if (response.data && response.data.status === 1) { + + // Apply scope changes + if (!$scope.$$phase && !$scope.$root.$$phase) { + $scope.$apply(); + } + + // Handle both JSON string and object responses + var responseData = response.data; + if (typeof responseData === 'string') { + try { + responseData = JSON.parse(responseData); + console.log('Parsed responseData from string:', responseData); + } catch (e) { + console.error('Failed to parse response as JSON:', e); + var errorMsg = responseData && responseData.length ? responseData : 'Failed to block IP address'; + if (typeof PNotify !== 'undefined') { + new PNotify({ title: 'Error', text: errorMsg, type: 'error', delay: 5000 }); + } + $scope.blockingIP = null; + return; + } + } + + console.log('Final responseData:', responseData); + console.log('responseData.status:', responseData ? responseData.status : 'undefined'); + console.log('responseData.message:', responseData ? responseData.message : 'undefined'); + console.log('responseData.error_message:', responseData ? responseData.error_message : 'undefined'); + + // Check for success (status === 1 or status === '1') + if (responseData && (responseData.status === 1 || responseData.status === '1')) { // Mark IP as blocked + if (!$scope.blockedIPs) { + $scope.blockedIPs = {}; + } $scope.blockedIPs[ipAddress] = true; // Show success notification - new PNotify({ - title: 'IP Address Banned', - text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, - type: 'success', - delay: 5000 - }); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'IP Address Banned', + text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, + type: 'success', + delay: 5000 + }); + } // Refresh security analysis to update alerts - $scope.analyzeSSHSecurity(); + if ($scope.analyzeSSHSecurity) { + $scope.analyzeSSHSecurity(); + } + + // Apply scope changes + if (!$scope.$$phase && !$scope.$root.$$phase) { + $scope.$apply(); + } } else { // Show error notification + var errorMsg = 'Failed to block IP address'; + if (responseData && responseData.error_message) { + errorMsg = responseData.error_message; + } else if (responseData && responseData.error) { + errorMsg = responseData.error; + } else if (responseData && responseData.message) { + errorMsg = responseData.message; + } else if (responseData) { + errorMsg = JSON.stringify(responseData); + } + console.error('Ban IP failed:', errorMsg); + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: errorMsg, + type: 'error', + delay: 5000 + }); + } + } + }, function (err) { + $scope.blockingIP = null; + console.error('addBannedIP error:', err); + console.error('Error status:', err.status); + console.error('Error statusText:', err.statusText); + console.error('Error data:', err.data); + + // Prevent showing duplicate error notifications + if ($scope.lastErrorIP === ipAddress && $scope.lastErrorTime && (Date.now() - $scope.lastErrorTime) < 2000) { + console.log('Skipping duplicate error notification for IP:', ipAddress); + return; + } + + $scope.lastErrorIP = ipAddress; + $scope.lastErrorTime = Date.now(); + + var errorMessage = 'Failed to block IP address'; + var errData = err.data; + if (typeof errData === 'string') { + try { + errData = JSON.parse(errData); + } catch (e) { + if (errData && errData.length) { + errorMessage = errData.length > 200 ? errData.substring(0, 200) + '...' : errData; + } + } + } + if (errData && typeof errData === 'object') { + errorMessage = errData.error_message || errData.error || errData.message || errorMessage; + } else if (err.status) { + errorMessage = 'HTTP ' + err.status + ': ' + (errorMessage); + } + + console.error('Final error message:', errorMessage); + + if (typeof PNotify !== 'undefined') { new PNotify({ title: 'Error', - text: response.data && response.data.error ? response.data.error : 'Failed to block IP address', + text: errorMessage, type: 'error', delay: 5000 }); } - }, function (err) { - $scope.blockingIP = null; - var errorMessage = 'Failed to block IP address'; - if (err.data && err.data.error) { - errorMessage = err.data.error; - } else if (err.data && err.data.message) { - errorMessage = err.data.message; + }); + } catch (e) { + console.error('========================================'); + console.error('=== ERROR in blockIPAddress ==='); + console.error('========================================'); + console.error('Error:', e); + console.error('Error message:', e.message); + console.error('Error stack:', e.stack); + $scope.blockingIP = null; + if (typeof PNotify !== 'undefined') { + new PNotify({ + title: 'Error', + text: 'An error occurred while trying to ban the IP address: ' + (e.message || String(e)), + type: 'error', + delay: 5000 + }); + } + } + }; + + // Ban IP from SSH Logs + $scope.banIPFromSSHLog = function(ipAddress) { + if (!ipAddress) { + new PNotify({ + title: 'Error', + text: 'No IP address provided', + type: 'error', + delay: 5000 + }); + return; + } + + if ($scope.blockingIP === ipAddress) { + return; // Already processing + } + + if ($scope.blockedIPs[ipAddress]) { + new PNotify({ + title: 'Info', + text: `IP address ${ipAddress} is already banned`, + type: 'info', + delay: 3000 + }); + return; + } + + $scope.blockingIP = ipAddress; + + // Use the Banned IPs system + var data = { + ip: ipAddress, + reason: 'Suspicious activity detected from SSH logs', + duration: 'permanent' + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post('/firewall/addBannedIP', data, config).then(function (response) { + $scope.blockingIP = null; + if (response.data && response.data.status === 1) { + // Mark IP as blocked + $scope.blockedIPs[ipAddress] = true; + + // Show success notification + new PNotify({ + title: 'IP Address Banned', + text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, + type: 'success', + delay: 5000 + }); + + // Refresh SSH logs to update the UI + $scope.refreshSSHLogs(); + } else { + // Show error notification + var errorMsg = 'Failed to ban IP address'; + if (response.data && response.data.error_message) { + errorMsg = response.data.error_message; + } else if (response.data && response.data.error) { + errorMsg = response.data.error; } new PNotify({ title: 'Error', - text: errorMessage, + text: errorMsg, type: 'error', delay: 5000 }); + } + }, function (err) { + $scope.blockingIP = null; + var errorMessage = 'Failed to ban IP address'; + if (err.data && err.data.error_message) { + errorMessage = err.data.error_message; + } else if (err.data && err.data.error) { + errorMessage = err.data.error; + } else if (err.data && err.data.message) { + errorMessage = err.data.message; + } + + new PNotify({ + title: 'Error', + text: errorMessage, + type: 'error', + delay: 5000 }); + }); + }; + + // Ban IP from SSH Logs + $scope.banIPFromSSHLog = function(ipAddress) { + if (!ipAddress) { + new PNotify({ + title: 'Error', + text: 'No IP address provided', + type: 'error', + delay: 5000 + }); + return; } + + if ($scope.blockingIP === ipAddress) { + return; // Already processing + } + + if ($scope.blockedIPs[ipAddress]) { + new PNotify({ + title: 'Info', + text: `IP address ${ipAddress} is already banned`, + type: 'info', + delay: 3000 + }); + return; + } + + $scope.blockingIP = ipAddress; + + // Use the Banned IPs system + var data = { + ip: ipAddress, + reason: 'Suspicious activity detected from SSH logs', + duration: 'permanent' + }; + + var config = { + headers: { + 'X-CSRFToken': getCookie('csrftoken') + } + }; + + $http.post('/firewall/addBannedIP', data, config).then(function (response) { + $scope.blockingIP = null; + if (response.data && response.data.status === 1) { + // Mark IP as blocked + $scope.blockedIPs[ipAddress] = true; + + // Show success notification + new PNotify({ + title: 'IP Address Banned', + text: `IP address ${ipAddress} has been permanently banned and added to the firewall. You can manage it in the Firewall > Banned IPs section.`, + type: 'success', + delay: 5000 + }); + + // Refresh SSH logs to update the UI + $scope.refreshSSHLogs(); + } else { + // Show error notification + var errorMsg = 'Failed to ban IP address'; + if (response.data && response.data.error_message) { + errorMsg = response.data.error_message; + } else if (response.data && response.data.error) { + errorMsg = response.data.error; + } + + new PNotify({ + title: 'Error', + text: errorMsg, + type: 'error', + delay: 5000 + }); + } + }, function (err) { + $scope.blockingIP = null; + var errorMessage = 'Failed to ban IP address'; + if (err.data && err.data.error_message) { + errorMessage = err.data.error_message; + } else if (err.data && err.data.error) { + errorMessage = err.data.error; + } else if (err.data && err.data.message) { + errorMessage = err.data.message; + } + + new PNotify({ + title: 'Error', + text: errorMessage, + type: 'error', + delay: 5000 + }); + }); }; // Initial fetch @@ -1224,72 +1449,15 @@ app.controller('dashboardStatsController', function ($scope, $http, $timeout) { // Chart.js chart objects var trafficChart, diskIOChart, cpuChart; - // Data arrays for live graphs - expose to scope for pagination - $scope.trafficLabels = []; - $scope.rxData = []; - $scope.txData = []; - $scope.diskLabels = []; - $scope.readData = []; - $scope.writeData = []; - $scope.cpuLabels = []; - $scope.cpuUsageData = []; - // Internal references for backward compatibility - var trafficLabels = $scope.trafficLabels; - var rxData = $scope.rxData; - var txData = $scope.txData; - var diskLabels = $scope.diskLabels; - var readData = $scope.readData; - var writeData = $scope.writeData; - var cpuLabels = $scope.cpuLabels; - var cpuUsageData = $scope.cpuUsageData; + // Data arrays for live graphs + var trafficLabels = [], rxData = [], txData = []; + var diskLabels = [], readData = [], writeData = []; + var cpuLabels = [], cpuUsageData = []; // For rate calculation var lastRx = null, lastTx = null, lastDiskRead = null, lastDiskWrite = null, lastCPU = null; var lastCPUTimes = null; var pollInterval = 2000; // ms var maxPoints = 30; - - // Watch pagination changes and update charts accordingly - $scope.$watch('pagination.traffic.currentPage', function() { - updateTrafficChartData(); - }); - $scope.$watch('pagination.diskIO.currentPage', function() { - updateDiskIOChartData(); - }); - $scope.$watch('pagination.cpuUsage.currentPage', function() { - updateCPUChartData(); - }); - - function updateTrafficChartData() { - if (!trafficChart || !$scope.trafficLabels || $scope.trafficLabels.length === 0) return; - var startIdx = ($scope.pagination.traffic.currentPage - 1) * ITEMS_PER_PAGE; - var endIdx = startIdx + ITEMS_PER_PAGE; - - trafficChart.data.labels = $scope.trafficLabels.slice(startIdx, endIdx); - trafficChart.data.datasets[0].data = $scope.rxData.slice(startIdx, endIdx); - trafficChart.data.datasets[1].data = $scope.txData.slice(startIdx, endIdx); - trafficChart.update(); - } - - function updateDiskIOChartData() { - if (!diskIOChart || !$scope.diskLabels || $scope.diskLabels.length === 0) return; - var startIdx = ($scope.pagination.diskIO.currentPage - 1) * ITEMS_PER_PAGE; - var endIdx = startIdx + ITEMS_PER_PAGE; - - diskIOChart.data.labels = $scope.diskLabels.slice(startIdx, endIdx); - diskIOChart.data.datasets[0].data = $scope.readData.slice(startIdx, endIdx); - diskIOChart.data.datasets[1].data = $scope.writeData.slice(startIdx, endIdx); - diskIOChart.update(); - } - - function updateCPUChartData() { - if (!cpuChart || !$scope.cpuLabels || $scope.cpuLabels.length === 0) return; - var startIdx = ($scope.pagination.cpuUsage.currentPage - 1) * ITEMS_PER_PAGE; - var endIdx = startIdx + ITEMS_PER_PAGE; - - cpuChart.data.labels = $scope.cpuLabels.slice(startIdx, endIdx); - cpuChart.data.datasets[0].data = $scope.cpuUsageData.slice(startIdx, endIdx); - cpuChart.update(); - } function pollDashboardStats() { console.log('[dashboardStatsController] pollDashboardStats() called'); diff --git a/static/filemanager/js/fileManager.js b/static/filemanager/js/fileManager.js index 7ea1bc575..78ad4dfca 100644 --- a/static/filemanager/js/fileManager.js +++ b/static/filemanager/js/fileManager.js @@ -82,6 +82,15 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader, $scope.showUploadBox = function () { $('#uploadBox').modal('show'); }; + // Fix aria-hidden a11y: move focus out of modal before hide so no focused descendant retains focus + $(document).on('hide.bs.modal', '.modal', function () { + var modal = this; + if (document.activeElement && modal.contains(document.activeElement)) { + var trigger = document.getElementById('uploadTriggerBtn'); + if (trigger && modal.id === 'uploadBox') { trigger.focus(); } + else { document.activeElement.blur(); } + } + }); $scope.showHTMLEditorModal = function (MainFM= 0) { $scope.htmlEditorLoading = false; @@ -1147,7 +1156,8 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader, }); $scope.fetchForTableSecondary(null, 'refresh'); } else { - var notification = alertify.notify('Files/Folders can not be deleted', 'error', 5, function () { + var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Files/Folders can not be deleted'; + var notification = alertify.notify(errMsg, 'error', 8, function () { console.log('dismissed'); }); } @@ -1155,6 +1165,10 @@ fileManager.controller('fileManagerCtrl', function ($scope, $http, FileUploader, } function cantLoadInitialDatas(response) { + var err = (response && response.data && (response.data.error_message || response.data.message)) || + (response && response.statusText) || 'Request failed'; + if (response && response.status === 0) err = 'Network error'; + alertify.notify(err, 'error', 8); } }; diff --git a/static/filemanager/js/newFileManager.js b/static/filemanager/js/newFileManager.js index bfa8bed5b..b10512b57 100644 --- a/static/filemanager/js/newFileManager.js +++ b/static/filemanager/js/newFileManager.js @@ -156,6 +156,14 @@ function findFileExtension(fileName) { $scope.showUploadBox = function () { $("#uploadBox").modal(); }; + $(document).on("hide.bs.modal", ".modal", function () { + var modal = this; + if (document.activeElement && modal.contains(document.activeElement)) { + var trigger = document.getElementById("uploadTriggerBtn"); + if (trigger && modal.id === "uploadBox") { trigger.focus(); } + else { document.activeElement.blur(); } + } + }); $scope.showHTMLEditorModal = function (MainFM = 0) { $scope.fileInEditor = allFilesAndFolders[0]; diff --git a/static/firewall/firewall.js b/static/firewall/firewall.js index 495b88ec0..cbcc04bbc 100644 --- a/static/firewall/firewall.js +++ b/static/firewall/firewall.js @@ -5,7 +5,7 @@ /* Java script code to ADD Firewall Rules */ -app.controller('firewallController', function ($scope, $http) { +app.controller('firewallController', function ($scope, $http, $timeout) { $scope.rulesLoading = true; $scope.actionFailed = true; @@ -16,9 +16,51 @@ app.controller('firewallController', function ($scope, $http) { $scope.couldNotConnect = true; $scope.rulesDetails = false; - // Banned IPs variables - $scope.activeTab = 'rules'; + // Tab from hash so we stay on /firewall/ (avoids 404 on servers without /firewall/firewall-rules/) + function tabFromHash() { + var h = (window.location.hash || '').replace(/^#/, ''); + return (h === 'banned-ips') ? 'banned' : 'rules'; + } + $scope.activeTab = tabFromHash(); $scope.bannedIPs = []; + // Re-apply tab from hash after load (hash can be set after controller init in some browsers) + function applyTabFromHash() { + var tab = tabFromHash(); + if ($scope.activeTab !== tab) { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); } + } + } + $timeout(applyTabFromHash, 0); + if (document.readyState === 'complete') { + $timeout(applyTabFromHash, 50); + } else { + window.addEventListener('load', function() { $timeout(applyTabFromHash, 0); }); + } + $scope.setFirewallTab = function(tab) { + $timeout(function() { + $scope.activeTab = tab; + window.location.hash = (tab === 'banned') ? '#banned-ips' : '#rules'; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }, 0); + }; + window.addEventListener('hashchange', function() { + var tab = tabFromHash(); + if ($scope.activeTab !== tab) { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + if (!$scope.$$phase && !$scope.$root.$$phase) { $scope.$apply(); } + } + }); + $scope.rulesPage = 1; + $scope.rulesPageSize = 10; + $scope.rulesPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.rulesTotalCount = 0; + $scope.bannedPage = 1; + $scope.bannedPageSize = 10; + $scope.bannedPageSizeOptions = [5, 10, 20, 30, 50, 100]; + $scope.bannedTotalCount = 0; $scope.bannedIPsLoading = false; $scope.bannedIPActionFailed = true; $scope.bannedIPActionSuccess = true; @@ -29,9 +71,18 @@ app.controller('firewallController', function ($scope, $http) { firewallStatus(); + // Load both tabs on init populateCurrentRecords(); populateBannedIPs(); + // Whenever activeTab changes, load that tab's data (ensures second tab loads even if click/apply failed) + $scope.$watch('activeTab', function(newVal, oldVal) { + if (newVal === oldVal || !newVal) return; + $timeout(function() { + if (newVal === 'banned') { populateBannedIPs(); } else if (newVal === 'rules') { populateCurrentRecords(); } + }, 0); + }); + $scope.addRule = function () { $scope.rulesLoading = false; @@ -123,37 +174,67 @@ app.controller('firewallController', function ($scope, $http) { $scope.actionFailed = true; $scope.actionSuccess = true; - url = "/firewall/getCurrentRules"; - - var data = {}; - - var config = { - headers: { - 'X-CSRFToken': getCookie('csrftoken') - } - }; - + var data = { page: $scope.rulesPage || 1, page_size: $scope.rulesPageSize || 10 }; + var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; $http.post(url, data, config).then(ListInitialDatas, cantLoadInitialDatas); - function ListInitialDatas(response) { - if (response.data.fetchStatus === 1) { - $scope.rules = JSON.parse(response.data.data); + var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data; + if (res && res.fetchStatus === 1) { + $scope.rules = typeof res.data === 'string' ? JSON.parse(res.data) : (res.data || []); + $scope.rulesTotalCount = res.total_count != null ? res.total_count : ($scope.rules ? $scope.rules.length : 0); + $scope.rulesPage = Math.max(1, res.page != null ? res.page : 1); + $scope.rulesPageSize = res.page_size != null ? res.page_size : 10; $scope.rulesLoading = true; } else { $scope.rulesLoading = true; - $scope.errorMessage = response.data.error_message; + $scope.errorMessage = (res && res.error_message) ? res.error_message : ''; } } function cantLoadInitialDatas(response) { $scope.couldNotConnect = false; - } + } + $scope.goToRulesPage = function(page) { + var totalP = $scope.rulesTotalPages(); + if (page < 1 || page > totalP) return; + $scope.rulesPage = page; + populateCurrentRecords(); + }; + $scope.goToRulesPageByInput = function() { + var n = parseInt($scope.rulesPageInput, 10); + if (isNaN(n) || n < 1) n = 1; + var maxP = $scope.rulesTotalPages(); + if (n > maxP) n = maxP; + $scope.rulesPageInput = n; + $scope.goToRulesPage(n); + }; + $scope.rulesTotalPages = function() { + var size = $scope.rulesPageSize || 10; + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1; + }; + $scope.rulesRangeStart = function() { + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + if (total === 0) return 0; + var page = Math.max(1, $scope.rulesPage || 1); + var size = $scope.rulesPageSize || 10; + return (page - 1) * size + 1; + }; + $scope.rulesRangeEnd = function() { + var start = $scope.rulesRangeStart(); + var size = $scope.rulesPageSize || 10; + var total = $scope.rulesTotalCount || ($scope.rules && $scope.rules.length) || 0; + return total === 0 ? 0 : Math.min(start + size - 1, total); + }; + $scope.setRulesPageSize = function() { + $scope.rulesPage = 1; + populateCurrentRecords(); }; $scope.deleteRule = function (id, proto, port, ruleIP) { @@ -2417,20 +2498,25 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) function populateBannedIPs() { $scope.bannedIPsLoading = true; var url = "/firewall/getBannedIPs"; + var postData = { page: $scope.bannedPage || 1, page_size: $scope.bannedPageSize || 10 }; var config = { headers: { 'X-CSRFToken': getCookie('csrftoken') } }; - $http.post(url, {}, config).then(function(response) { + $http.post(url, postData, config).then(function(response) { $scope.bannedIPsLoading = false; - if (response.data.status === 1) { - $scope.bannedIPs = response.data.bannedIPs || []; + var res = (typeof response.data === 'string') ? (function() { try { return JSON.parse(response.data); } catch (e) { return {}; } })() : response.data; + if (res && res.status === 1) { + $scope.bannedIPs = res.bannedIPs || []; + $scope.bannedTotalCount = res.total_count != null ? res.total_count : ($scope.bannedIPs ? $scope.bannedIPs.length : 0); + $scope.bannedPage = Math.max(1, res.page != null ? res.page : 1); + $scope.bannedPageSize = res.page_size != null ? res.page_size : 10; } else { $scope.bannedIPs = []; $scope.bannedIPActionFailed = false; - $scope.bannedIPErrorMessage = response.data.error_message; + $scope.bannedIPErrorMessage = (res && res.error_message) ? res.error_message : ''; } }, function(error) { $scope.bannedIPsLoading = false; @@ -2438,6 +2524,53 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) }); } + $scope.goToBannedPage = function(page) { + var totalP = $scope.bannedTotalPages(); + if (page < 1 || page > totalP) return; + $scope.bannedPage = page; + populateBannedIPs(); + }; + $scope.goToBannedPageByInput = function() { + var n = parseInt($scope.bannedPageInput, 10); + if (isNaN(n) || n < 1) n = 1; + var maxP = $scope.bannedTotalPages(); + if (n > maxP) n = maxP; + $scope.bannedPageInput = n; + $scope.goToBannedPage(n); + }; + $scope.bannedTotalPages = function() { + var size = $scope.bannedPageSize || 10; + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + return size > 0 ? Math.max(1, Math.ceil(total / size)) : 1; + }; + $scope.bannedRangeStart = function() { + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + if (total === 0) return 0; + var page = Math.max(1, $scope.bannedPage || 1); + var size = $scope.bannedPageSize || 10; + return (page - 1) * size + 1; + }; + $scope.bannedRangeEnd = function() { + var start = $scope.bannedRangeStart(); + var size = $scope.bannedPageSize || 10; + var total = $scope.bannedTotalCount || ($scope.bannedIPs ? $scope.bannedIPs.length : 0) || 0; + return total === 0 ? 0 : Math.min(start + size - 1, total); + }; + $scope.setBannedPageSize = function() { + $scope.bannedPage = 1; + populateBannedIPs(); + }; + $scope.populateBannedIPs = populateBannedIPs; + + if (typeof window !== 'undefined') { + window.__firewallLoadTab = function(tab) { + $scope.$evalAsync(function() { + $scope.activeTab = tab; + if (tab === 'banned') { populateBannedIPs(); } else { populateCurrentRecords(); } + }); + }; + } + $scope.addBannedIP = function() { if (!$scope.banIP || !$scope.banReason) { $scope.bannedIPActionFailed = false; @@ -2696,4 +2829,26 @@ app.controller('litespeed_ent_conf', function ($scope, $http, $timeout, $window) } } -}); \ No newline at end of file +}); + +(function() { + // Do not capture tab clicks – let Angular ng-click run setFirewallTab() so data loads. + // Only sync tab from hash on load and hashchange (back/forward) via __firewallLoadTab. + function syncFirewallTabFromHash() { + var nav = document.getElementById('firewall-tab-nav'); + if (!nav) return; + var h = (window.location.hash || '').replace(/^#/, ''); + var tab = (h === 'banned-ips') ? 'banned' : 'rules'; + if (window.__firewallLoadTab) { + try { window.__firewallLoadTab(tab); } catch (e) {} + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', syncFirewallTabFromHash); + } else { + syncFirewallTabFromHash(); + } + setTimeout(syncFirewallTabFromHash, 100); + window.addEventListener('hashchange', syncFirewallTabFromHash); +})(); \ No newline at end of file diff --git a/static/ftp/ftp.js b/static/ftp/ftp.js index 3035a8c7c..ef6cd4a4e 100644 --- a/static/ftp/ftp.js +++ b/static/ftp/ftp.js @@ -26,9 +26,10 @@ app.controller('createFTPAccount', function ($scope, $http) { $sel.select2(); $sel.on('select2:select', function (e) { var data = e.params.data; - $scope.ftpDomain = data.text; - $scope.ftpDetails = false; - $scope.$apply(); + $scope.$evalAsync(function () { + $scope.ftpDomain = data.text; + $scope.ftpDetails = false; + }); $(".ftpDetails, .account-details").show(); }); } else { @@ -42,9 +43,11 @@ app.controller('createFTPAccount', function ($scope, $http) { } function initNativeSelect() { $('.create-ftp-acct-select').off('select2:select').on('change', function () { - $scope.ftpDomain = $(this).val(); - $scope.ftpDetails = ($scope.ftpDomain && $scope.ftpDomain !== '') ? false : true; - $scope.$apply(); + var val = $(this).val(); + $scope.$evalAsync(function () { + $scope.ftpDomain = val; + $scope.ftpDetails = (val && val !== '') ? false : true; + }); $(".ftpDetails, .account-details").show(); }); } diff --git a/static/mailServer/emailLimitsController.js b/static/mailServer/emailLimitsController.js index 45f6bc77b..21d504cc7 100644 --- a/static/mailServer/emailLimitsController.js +++ b/static/mailServer/emailLimitsController.js @@ -114,7 +114,7 @@ $scope.forwardSuccess = true; $scope.couldNotConnect = true; $scope.notifyBox = true; - if (typeof new PNotify === 'function') { + if (typeof PNotify === 'function') { new PNotify({ title: 'Success!', text: 'Changes applied.', type: 'success' }); } $scope.showEmailDetails(); @@ -126,7 +126,7 @@ $scope.forwardSuccess = true; $scope.couldNotConnect = true; $scope.notifyBox = false; - if (typeof new PNotify === 'function') { + if (typeof PNotify === 'function') { new PNotify({ title: 'Error!', text: response.data.error_message || 'Error', type: 'error' }); } } diff --git a/static/mailServer/mailServer.js b/static/mailServer/mailServer.js index 62be0aefe..a546a8cec 100644 --- a/static/mailServer/mailServer.js +++ b/static/mailServer/mailServer.js @@ -1514,6 +1514,7 @@ app.controller('EmailLimitsNew', function ($scope, $http) { // Given email to search for var givenEmail = $scope.selectedEmail; + if ($scope.emails) { for (var i = 0; i < $scope.emails.length; i++) { if ($scope.emails[i].email === givenEmail) { // Extract numberofEmails and duration @@ -1523,14 +1524,11 @@ app.controller('EmailLimitsNew', function ($scope, $http) { $scope.numberofEmails = numberofEmails; $scope.duration = duration; - // Use numberofEmails and duration as needed - console.log("Number of emails:", numberofEmails); - console.log("Duration:", duration); - // Break out of the loop since the email is found break; } } + } }; diff --git a/static/serverStatus/serverStatus.js b/static/serverStatus/serverStatus.js index f16d66701..733d26a96 100644 --- a/static/serverStatus/serverStatus.js +++ b/static/serverStatus/serverStatus.js @@ -641,11 +641,15 @@ app.controller('servicesManager', function ($scope, $http) { getServiceStatus(); $scope.ActionSuccessfull = true; $scope.ActionFailed = false; + $scope.actionErrorMsg = ''; $scope.couldNotConnect = false; $scope.actionLoader = false; $scope.btnDisable = false; }, 3000); } else { + var errMsg = (response.data && response.data.error_message) ? response.data.error_message : 'Action failed'; + if (errMsg === 0) errMsg = 'Action failed'; + $scope.actionErrorMsg = errMsg; setTimeout(function () { getServiceStatus(); $scope.ActionSuccessfull = false; @@ -654,7 +658,6 @@ app.controller('servicesManager', function ($scope, $http) { $scope.actionLoader = false; $scope.btnDisable = false; }, 5000); - } }