From 4c797dc154b2b50f7d2d2bdfd28b7c5e30a78cb4 Mon Sep 17 00:00:00 2001 From: zd <67220210+0zd0@users.noreply.github.com> Date: Tue, 23 Dec 2025 17:43:25 +0300 Subject: [PATCH 1/9] fix: display of outbound traffic (#3604) shows the direction of traffic --- .env.example | 4 ++++ CONTRIBUTING.md | 5 +++++ web/html/xray.html | 4 ++-- 3 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .env.example create mode 100644 CONTRIBUTING.md diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..62aa8c0b --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +XUI_DEBUG=true +XUI_DB_FOLDER=x-ui +XUI_LOG_FOLDER=x-ui +XUI_BIN_FOLDER=x-ui \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..5455e8b5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,5 @@ +## Local Development Setup + +- Create a directory named `x-ui` in the project root +- Rename `.env.example` to `.env ` +- Run `main.go` \ No newline at end of file diff --git a/web/html/xray.html b/web/html/xray.html index 4dacd021..f4d89c84 100644 --- a/web/html/xray.html +++ b/web/html/xray.html @@ -527,10 +527,10 @@ findOutboundTraffic(o) { for (const otraffic of this.outboundsTraffic) { if (otraffic.tag == o.tag) { - return SizeFormatter.sizeFormat(otraffic.up) + ' / ' + SizeFormatter.sizeFormat(otraffic.down); + return `↑ ${SizeFormatter.sizeFormat(otraffic.up)} / ${SizeFormatter.sizeFormat(otraffic.down)} ↓` } } - return SizeFormatter.sizeFormat(0) + ' / ' + SizeFormatter.sizeFormat(0); + return `${SizeFormatter.sizeFormat(0)} / ${SizeFormatter.sizeFormat(0)}` }, findOutboundAddress(o) { serverObj = null; From 69ccdba734226ad6c1b8853d545bf8c8b462bdd6 Mon Sep 17 00:00:00 2001 From: Sanaei Date: Sun, 28 Dec 2025 00:03:33 +0100 Subject: [PATCH 2/9] Self-signed SSL (#3611) --- install.sh | 459 +++++++++++++++++++++++++++++++++++++++++++++++++--- update.sh | 467 ++++++++++++++++++++++++++++++++++++++++++++++++++++- x-ui.sh | 253 +++++++++++++++++++++++++++-- 3 files changed, 1136 insertions(+), 43 deletions(-) diff --git a/install.sh b/install.sh index fd730bca..0cf91b44 100644 --- a/install.sh +++ b/install.sh @@ -39,32 +39,46 @@ arch() { echo "Arch: $(arch)" +# Simple helpers +is_ipv4() { + [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 +} +is_ipv6() { + [[ "$1" =~ : ]] && return 0 || return 1 +} +is_ip() { + is_ipv4 "$1" || is_ipv6 "$1" +} +is_domain() { + [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 +} + install_base() { case "${release}" in ubuntu | debian | armbian) - apt-get update && apt-get install -y -q wget curl tar tzdata + apt-get update && apt-get install -y -q wget curl tar tzdata openssl socat ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update && dnf install -y -q wget curl tar tzdata + dnf -y update && dnf install -y -q wget curl tar tzdata openssl socat ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update && yum install -y wget curl tar tzdata + yum -y update && yum install -y wget curl tar tzdata openssl socat else - dnf -y update && dnf install -y -q wget curl tar tzdata + dnf -y update && dnf install -y -q wget curl tar tzdata openssl socat fi ;; arch | manjaro | parch) - pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata + pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata openssl socat ;; opensuse-tumbleweed | opensuse-leap) - zypper refresh && zypper -q install -y wget curl tar timezone + zypper refresh && zypper -q install -y wget curl tar timezone openssl socat ;; alpine) - apk update && apk add wget curl tar tzdata + apk update && apk add wget curl tar tzdata openssl socat ;; *) - apt-get update && apt-get install -y -q wget curl tar tzdata + apt-get update && apt-get install -y -q wget curl tar tzdata openssl socat ;; esac } @@ -75,10 +89,373 @@ gen_random_string() { echo "$random_string" } +install_acme() { + echo -e "${green}Installing acme.sh for SSL certificate management...${plain}" + cd ~ || return 1 + curl -s https://get.acme.sh | sh >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo -e "${red}Failed to install acme.sh${plain}" + return 1 + else + echo -e "${green}acme.sh installed successfully${plain}" + fi + return 0 +} + +setup_ssl_certificate() { + local domain="$1" + local server_ip="$2" + local existing_port="$3" + local existing_webBasePath="$4" + + echo -e "${green}Setting up SSL certificate...${plain}" + + # Check if acme.sh is installed + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + install_acme + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}" + return 1 + fi + fi + + # Create certificate directory + local certPath="/root/cert/${domain}" + mkdir -p "$certPath" + + # Issue certificate + echo -e "${green}Issuing SSL certificate for ${domain}...${plain}" + echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}" + + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1 + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force + + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to issue certificate for ${domain}${plain}" + echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}" + rm -rf ~/.acme.sh/${domain} 2>/dev/null + rm -rf "$certPath" 2>/dev/null + return 1 + fi + + # Install certificate + ~/.acme.sh/acme.sh --installcert -d ${domain} \ + --key-file /root/cert/${domain}/privkey.pem \ + --fullchain-file /root/cert/${domain}/fullchain.pem \ + --reloadcmd "systemctl restart x-ui" >/dev/null 2>&1 + + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to install certificate${plain}" + return 1 + fi + + # Enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 + chmod 755 $certPath/* 2>/dev/null + + # Set certificate for panel + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1 + echo -e "${green}SSL certificate installed and configured successfully!${plain}" + return 0 + else + echo -e "${yellow}Certificate files not found${plain}" + return 1 + fi +} + +# Fallback: generate a self-signed certificate (not publicly trusted) +setup_self_signed_certificate() { + local name="$1" # domain or IP to place in SAN + local certDir="/root/cert/selfsigned" + + echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${plain}" + + mkdir -p "$certDir" + + local sanExt="" + if is_ip "$name"; then + sanExt="IP:${name}" + else + sanExt="DNS:${name}" + fi + + # Use -addext if supported; fallback to config file if needed + openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout "${certDir}/privkey.pem" \ + -out "${certDir}/fullchain.pem" \ + -subj "/CN=${name}" \ + -addext "subjectAltName=${sanExt}" >/dev/null 2>&1 + + if [[ $? -ne 0 ]]; then + # Fallback via temporary config file (for older OpenSSL versions) + local tmpCfg="${certDir}/openssl.cnf" + cat > "$tmpCfg" </dev/null 2>&1 + rm -f "$tmpCfg" + fi + + if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then + echo -e "${red}Failed to generate self-signed certificate${plain}" + return 1 + fi + + chmod 755 ${certDir}/* 2>/dev/null + /usr/local/x-ui/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1 + echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}" + return 0 +} + +# Comprehensive manual SSL certificate issuance via acme.sh +ssl_cert_issue() { + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]') + + # check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "acme.sh could not be found. Installing now..." + cd ~ || return 1 + curl -s https://get.acme.sh | sh + if [ $? -ne 0 ]; then + echo -e "${red}Failed to install acme.sh${plain}" + return 1 + else + echo -e "${green}acme.sh installed successfully${plain}" + fi + fi + + # get the domain here, and we need to verify it + local domain="" + while true; do + read -rp "Please enter your domain name: " domain + domain="${domain// /}" # Trim whitespace + + if [[ -z "$domain" ]]; then + echo -e "${red}Domain name cannot be empty. Please try again.${plain}" + continue + fi + + if ! is_domain "$domain"; then + echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}" + continue + fi + + break + done + echo -e "${green}Your domain is: ${domain}, checking it...${plain}" + + # check if there already exists a certificate + local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') + if [ "${currentCert}" == "${domain}" ]; then + local certInfo=$(~/.acme.sh/acme.sh --list) + echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}" + echo -e "${yellow}Current certificate details:${plain}" + echo "$certInfo" + return 1 + else + echo -e "${green}Your domain is ready for issuing certificates now...${plain}" + fi + + # create a directory for the certificate + certPath="/root/cert/${domain}" + if [ ! -d "$certPath" ]; then + mkdir -p "$certPath" + else + rm -rf "$certPath" + mkdir -p "$certPath" + fi + + # get the port number for the standalone server + local WebPort=80 + read -rp "Please choose which port to use (default is 80): " WebPort + if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then + echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}" + WebPort=80 + fi + echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}" + + # Stop panel temporarily + echo -e "${yellow}Stopping panel temporarily...${plain}" + systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null + + # issue the certificate + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force + if [ $? -ne 0 ]; then + echo -e "${red}Issuing certificate failed, please check logs.${plain}" + rm -rf ~/.acme.sh/${domain} + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + return 1 + else + echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" + fi + + # Setup reload command + reloadCmd="systemctl restart x-ui || rc-service x-ui restart" + echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}" + echo -e "${green}This command will run on every certificate issue and renew.${plain}" + read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd + if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then + echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui" + echo -e "${green}\t2.${plain} Input your own command" + echo -e "${green}\t0.${plain} Keep default reloadcmd" + read -rp "Choose an option: " choice + case "$choice" in + 1) + echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}" + reloadCmd="systemctl reload nginx ; systemctl restart x-ui" + ;; + 2) + echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}" + read -rp "Please enter your custom reloadcmd: " reloadCmd + echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}" + ;; + *) + echo -e "${green}Keeping default reloadcmd${plain}" + ;; + esac + fi + + # install the certificate + ~/.acme.sh/acme.sh --installcert -d ${domain} \ + --key-file /root/cert/${domain}/privkey.pem \ + --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" + + if [ $? -ne 0 ]; then + echo -e "${red}Installing certificate failed, exiting.${plain}" + rm -rf ~/.acme.sh/${domain} + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + return 1 + else + echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" + fi + + # enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" + ls -lah /root/cert/${domain}/ + chmod 755 $certPath/* + else + echo -e "${green}Auto renew succeeded, certificate details:${plain}" + ls -lah /root/cert/${domain}/ + chmod 755 $certPath/* + fi + + # Restart panel + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + + # Prompt user to set panel paths after successful certificate installation + read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel + if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + echo -e "${green}Certificate paths set for the panel${plain}" + echo -e "${green}Certificate File: $webCertFile${plain}" + echo -e "${green}Private Key File: $webKeyFile${plain}" + echo "" + echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}" + echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}" + systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null + else + echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}" + fi + else + echo -e "${yellow}Skipping panel path setting.${plain}" + fi + + return 0 +} + +# Reusable interactive SSL setup (domain or self-signed) +# Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage +prompt_and_setup_ssl() { + local panel_port="$1" + local web_base_path="$2" # expected without leading slash + local server_ip="$3" + + local ssl_choice="" + + echo -e "${yellow}Choose SSL certificate setup method:${plain}" + echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)" + echo -e "${green}2.${plain} Self-signed certificate (not publicly trusted)" + read -rp "Choose an option (default 2): " ssl_choice + ssl_choice="${ssl_choice// /}" # Trim whitespace + + # Default to 2 (self-signed) if not 1 + if [[ "$ssl_choice" != "1" ]]; then + ssl_choice="2" + fi + + case "$ssl_choice" in + 1) + # User chose Let's Encrypt domain option + echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}" + ssl_cert_issue + # Extract the domain that was used from the certificate + local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') + if [[ -n "${cert_domain}" ]]; then + SSL_HOST="${cert_domain}" + echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" + else + echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" + SSL_HOST="${server_ip}" + fi + ;; + 2) + # User chose self-signed option + # Stop panel if running + if [[ $release == "alpine" ]]; then + rc-service x-ui stop >/dev/null 2>&1 + else + systemctl stop x-ui >/dev/null 2>&1 + fi + echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}" + setup_self_signed_certificate "${server_ip}" + if [ $? -eq 0 ]; then + SSL_HOST="${server_ip}" + echo -e "${green}✓ Self-signed SSL configured successfully${plain}" + else + echo -e "${red}✗ Self-signed SSL setup failed${plain}" + SSL_HOST="${server_ip}" + fi + # Start panel after SSL is configured + if [[ $release == "alpine" ]]; then + rc-service x-ui start >/dev/null 2>&1 + else + systemctl start x-ui >/dev/null 2>&1 + fi + ;; + *) + echo -e "${red}Invalid option. Skipping SSL setup.${plain}" + SSL_HOST="${server_ip}" + ;; + esac +} + config_after_install() { local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}') - local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##') local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + # Properly detect empty cert by checking if cert: line exists and has content after it + local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') local URL_lists=( "https://api4.ipify.org" "https://ipv4.icanhazip.com" @@ -111,20 +488,50 @@ config_after_install() { fi /usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}" - echo -e "This is a fresh installation, generating random login info for security concerns:" - echo -e "###############################################" - echo -e "${green}Username: ${config_username}${plain}" - echo -e "${green}Password: ${config_password}${plain}" - echo -e "${green}Port: ${config_port}${plain}" + + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${yellow}For security, SSL certificate is required for all panels.${plain}" + echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" + echo "" + + prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}" + + # Display final credentials and access information + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} Panel Installation Complete! ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green}Username: ${config_username}${plain}" + echo -e "${green}Password: ${config_password}${plain}" + echo -e "${green}Port: ${config_port}${plain}" echo -e "${green}WebBasePath: ${config_webBasePath}${plain}" - echo -e "${green}Access URL: http://${server_ip}:${config_port}/${config_webBasePath}${plain}" - echo -e "###############################################" + echo -e "${green}Access URL: https://${SSL_HOST}:${config_port}/${config_webBasePath}${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${yellow}⚠ IMPORTANT: Save these credentials securely!${plain}" + echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}" else local config_webBasePath=$(gen_random_string 18) echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" /usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}" echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" - echo -e "${green}Access URL: http://${server_ip}:${existing_port}/${config_webBasePath}${plain}" + + # If the panel is already installed but no certificate is configured, prompt for SSL now + if [[ -z "${existing_cert}" ]]; then + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" + echo "" + prompt_and_setup_ssl "${existing_port}" "${config_webBasePath}" "${server_ip}" + echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${config_webBasePath}${plain}" + else + # If a cert already exists, just show the access URL + echo -e "${green}Access URL: https://${server_ip}:${existing_port}/${config_webBasePath}${plain}" + fi fi else if [[ "$existing_hasDefaultCredential" == "true" ]]; then @@ -139,7 +546,23 @@ config_after_install() { echo -e "${green}Password: ${config_password}${plain}" echo -e "###############################################" else - echo -e "${green}Username, Password, and WebBasePath are properly set. Exiting...${plain}" + echo -e "${green}Username, Password, and WebBasePath are properly set.${plain}" + fi + + # Existing install: if no cert configured, prompt user to set domain or self-signed + # Properly detect empty cert by checking if cert: line exists and has content after it + existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') + if [[ -z "$existing_cert" ]]; then + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} SSL Certificate Setup (RECOMMENDED) ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${yellow}Let's Encrypt requires a domain name (IP certificates are not issued).${plain}" + echo "" + prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" + echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}" + else + echo -e "${green}SSL certificate already configured. No action needed.${plain}" fi fi diff --git a/update.sh b/update.sh index 66c0566c..ff0cea41 100755 --- a/update.sh +++ b/update.sh @@ -70,33 +70,415 @@ arch() { echo "Arch: $(arch)" +# Simple helpers +is_ipv4() { + [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 +} +is_ipv6() { + [[ "$1" =~ : ]] && return 0 || return 1 +} +is_ip() { + is_ipv4 "$1" || is_ipv6 "$1" +} +is_domain() { + [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 +} + +gen_random_string() { + local length="$1" + local random_string=$(LC_ALL=C tr -dc 'a-zA-Z0-9' /dev/null 2>&1 && apt-get install -y -q wget curl tar tzdata >/dev/null 2>&1 + apt-get update >/dev/null 2>&1 && apt-get install -y -q wget curl tar tzdata openssl socat >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1 + dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata openssl socat >/dev/null 2>&1 ;; centos) if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update >/dev/null 2>&1 && yum install -y -q wget curl tar tzdata >/dev/null 2>&1 + yum -y update >/dev/null 2>&1 && yum install -y -q wget curl tar tzdata openssl socat >/dev/null 2>&1 else - dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1 + dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata openssl socat >/dev/null 2>&1 fi ;; arch | manjaro | parch) - pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm wget curl tar tzdata >/dev/null 2>&1 + pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm wget curl tar tzdata openssl socat >/dev/null 2>&1 ;; opensuse-tumbleweed | opensuse-leap) - zypper refresh >/dev/null 2>&1 && zypper -q install -y wget curl tar timezone >/dev/null 2>&1 + zypper refresh >/dev/null 2>&1 && zypper -q install -y wget curl tar timezone openssl socat >/dev/null 2>&1 ;; alpine) - apk update >/dev/null 2>&1 && apk add wget curl tar tzdata >/dev/null 2>&1 + apk update >/dev/null 2>&1 && apk add wget curl tar tzdata openssl socat >/dev/null 2>&1 ;; *) - apt-get update >/dev/null 2>&1 && apt install -y -q wget curl tar tzdata >/dev/null 2>&1 + apt-get update >/dev/null 2>&1 && apt install -y -q wget curl tar tzdata openssl socat >/dev/null 2>&1 + ;; + esac +} + +install_acme() { + echo -e "${green}Installing acme.sh for SSL certificate management...${plain}" + cd ~ || return 1 + curl -s https://get.acme.sh | sh >/dev/null 2>&1 + if [ $? -ne 0 ]; then + echo -e "${red}Failed to install acme.sh${plain}" + return 1 + else + echo -e "${green}acme.sh installed successfully${plain}" + fi + return 0 +} + +setup_ssl_certificate() { + local domain="$1" + local server_ip="$2" + local existing_port="$3" + local existing_webBasePath="$4" + + echo -e "${green}Setting up SSL certificate...${plain}" + + # Check if acme.sh is installed + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + install_acme + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}" + return 1 + fi + fi + + # Create certificate directory + local certPath="/root/cert/${domain}" + mkdir -p "$certPath" + + # Issue certificate + echo -e "${green}Issuing SSL certificate for ${domain}...${plain}" + echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}" + + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt >/dev/null 2>&1 + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force + + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to issue certificate for ${domain}${plain}" + echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}" + rm -rf ~/.acme.sh/${domain} 2>/dev/null + rm -rf "$certPath" 2>/dev/null + return 1 + fi + + # Install certificate + ~/.acme.sh/acme.sh --installcert -d ${domain} \ + --key-file /root/cert/${domain}/privkey.pem \ + --fullchain-file /root/cert/${domain}/fullchain.pem \ + --reloadcmd "systemctl restart x-ui" >/dev/null 2>&1 + + if [ $? -ne 0 ]; then + echo -e "${yellow}Failed to install certificate${plain}" + return 1 + fi + + # Enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 + chmod 755 $certPath/* 2>/dev/null + + # Set certificate for panel + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1 + echo -e "${green}SSL certificate installed and configured successfully!${plain}" + return 0 + else + echo -e "${yellow}Certificate files not found${plain}" + return 1 + fi +} + +# Fallback: generate a self-signed certificate (not publicly trusted) +setup_self_signed_certificate() { + local name="$1" # domain or IP to place in SAN + local certDir="/root/cert/selfsigned" + + echo -e "${yellow}Generating a self-signed certificate (not publicly trusted)...${plain}" + + mkdir -p "$certDir" + + local sanExt="" + if is_ip "$name"; then + sanExt="IP:${name}" + else + sanExt="DNS:${name}" + fi + + # Try -addext; fallback to config if not supported + openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout "${certDir}/privkey.pem" \ + -out "${certDir}/fullchain.pem" \ + -subj "/CN=${name}" \ + -addext "subjectAltName=${sanExt}" >/dev/null 2>&1 + + if [[ $? -ne 0 ]]; then + local tmpCfg="${certDir}/openssl.cnf" + cat > "$tmpCfg" </dev/null 2>&1 + rm -f "$tmpCfg" + fi + + if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then + echo -e "${red}Failed to generate self-signed certificate${plain}" + return 1 + fi + + chmod 755 ${certDir}/* 2>/dev/null + /usr/local/x-ui/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1 + echo -e "${yellow}Self-signed certificate configured. Browsers will show a warning.${plain}" + return 0 +} +# Comprehensive manual SSL certificate issuance via acme.sh +ssl_cert_issue() { + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]') + + # check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + echo "acme.sh could not be found. Installing now..." + cd ~ || return 1 + curl -s https://get.acme.sh | sh + if [ $? -ne 0 ]; then + echo -e "${red}Failed to install acme.sh${plain}" + return 1 + else + echo -e "${green}acme.sh installed successfully${plain}" + fi + fi + + # get the domain here, and we need to verify it + local domain="" + while true; do + read -rp "Please enter your domain name: " domain + domain="${domain// /}" # Trim whitespace + + if [[ -z "$domain" ]]; then + echo -e "${red}Domain name cannot be empty. Please try again.${plain}" + continue + fi + + if ! is_domain "$domain"; then + echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}" + continue + fi + + break + done + echo -e "${green}Your domain is: ${domain}, checking it...${plain}" + + # check if there already exists a certificate + local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') + if [ "${currentCert}" == "${domain}" ]; then + local certInfo=$(~/.acme.sh/acme.sh --list) + echo -e "${red}System already has certificates for this domain. Cannot issue again.${plain}" + echo -e "${yellow}Current certificate details:${plain}" + echo "$certInfo" + return 1 + else + echo -e "${green}Your domain is ready for issuing certificates now...${plain}" + fi + + # create a directory for the certificate + certPath="/root/cert/${domain}" + if [ ! -d "$certPath" ]; then + mkdir -p "$certPath" + else + rm -rf "$certPath" + mkdir -p "$certPath" + fi + + # get the port number for the standalone server + local WebPort=80 + read -rp "Please choose which port to use (default is 80): " WebPort + if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then + echo -e "${yellow}Your input ${WebPort} is invalid, will use default port 80.${plain}" + WebPort=80 + fi + echo -e "${green}Will use port: ${WebPort} to issue certificates. Please make sure this port is open.${plain}" + + # Stop panel temporarily + echo -e "${yellow}Stopping panel temporarily...${plain}" + systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null + + # issue the certificate + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + ~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport ${WebPort} --force + if [ $? -ne 0 ]; then + echo -e "${red}Issuing certificate failed, please check logs.${plain}" + rm -rf ~/.acme.sh/${domain} + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + return 1 + else + echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}" + fi + + # Setup reload command + reloadCmd="systemctl restart x-ui || rc-service x-ui restart" + echo -e "${green}Default --reloadcmd for ACME is: ${yellow}systemctl restart x-ui || rc-service x-ui restart${plain}" + echo -e "${green}This command will run on every certificate issue and renew.${plain}" + read -rp "Would you like to modify --reloadcmd for ACME? (y/n): " setReloadcmd + if [[ "$setReloadcmd" == "y" || "$setReloadcmd" == "Y" ]]; then + echo -e "\n${green}\t1.${plain} Preset: systemctl reload nginx ; systemctl restart x-ui" + echo -e "${green}\t2.${plain} Input your own command" + echo -e "${green}\t0.${plain} Keep default reloadcmd" + read -rp "Choose an option: " choice + case "$choice" in + 1) + echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}" + reloadCmd="systemctl reload nginx ; systemctl restart x-ui" + ;; + 2) + echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}" + read -rp "Please enter your custom reloadcmd: " reloadCmd + echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}" + ;; + *) + echo -e "${green}Keeping default reloadcmd${plain}" + ;; + esac + fi + + # install the certificate + ~/.acme.sh/acme.sh --installcert -d ${domain} \ + --key-file /root/cert/${domain}/privkey.pem \ + --fullchain-file /root/cert/${domain}/fullchain.pem --reloadcmd "${reloadCmd}" + + if [ $? -ne 0 ]; then + echo -e "${red}Installing certificate failed, exiting.${plain}" + rm -rf ~/.acme.sh/${domain} + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + return 1 + else + echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}" + fi + + # enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade + if [ $? -ne 0 ]; then + echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}" + ls -lah /root/cert/${domain}/ + chmod 755 $certPath/* + else + echo -e "${green}Auto renew succeeded, certificate details:${plain}" + ls -lah /root/cert/${domain}/ + chmod 755 $certPath/* + fi + + # Restart panel + systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null + + # Prompt user to set panel paths after successful certificate installation + read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel + if [[ "$setPanel" == "y" || "$setPanel" == "Y" ]]; then + local webCertFile="/root/cert/${domain}/fullchain.pem" + local webKeyFile="/root/cert/${domain}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + echo -e "${green}Certificate paths set for the panel${plain}" + echo -e "${green}Certificate File: $webCertFile${plain}" + echo -e "${green}Private Key File: $webKeyFile${plain}" + echo "" + echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}" + echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}" + systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null + else + echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}" + fi + else + echo -e "${yellow}Skipping panel path setting.${plain}" + fi + + return 0 +} +# Unified interactive SSL setup (domain or self-signed) +# Sets global `SSL_HOST` to the chosen domain/IP +prompt_and_setup_ssl() { + local panel_port="$1" + local web_base_path="$2" # expected without leading slash + local server_ip="$3" + + local ssl_choice="" + + echo -e "${yellow}Choose SSL certificate setup method:${plain}" + echo -e "${green}1.${plain} Let's Encrypt (domain required, recommended)" + echo -e "${green}2.${plain} Self-signed certificate (for testing/local use)" + read -rp "Choose an option (default 2): " ssl_choice + ssl_choice="${ssl_choice// /}" # Trim whitespace + + # Default to 2 (self-signed) if not 1 + if [[ "$ssl_choice" != "1" ]]; then + ssl_choice="2" + fi + + case "$ssl_choice" in + 1) + # User chose Let's Encrypt domain option + echo -e "${green}Using ssl_cert_issue() for comprehensive domain setup...${plain}" + ssl_cert_issue + # Extract the domain that was used from the certificate + local cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}') + if [[ -n "${cert_domain}" ]]; then + SSL_HOST="${cert_domain}" + echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}" + else + echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}" + SSL_HOST="${server_ip}" + fi + ;; + 2) + # User chose self-signed option + # Stop panel if running + if [[ $release == "alpine" ]]; then + rc-service x-ui stop >/dev/null 2>&1 + else + systemctl stop x-ui >/dev/null 2>&1 + fi + echo -e "${yellow}Using server IP for self-signed certificate: ${server_ip}${plain}" + setup_self_signed_certificate "${server_ip}" + if [ $? -eq 0 ]; then + SSL_HOST="${server_ip}" + echo -e "${green}✓ Self-signed SSL configured successfully${plain}" + else + echo -e "${red}✗ Self-signed SSL setup failed${plain}" + SSL_HOST="${server_ip}" + fi + # Start panel after SSL is configured + if [[ $release == "alpine" ]]; then + rc-service x-ui start >/dev/null 2>&1 + else + systemctl start x-ui >/dev/null 2>&1 + fi + ;; + 0) + echo -e "${yellow}Skipping SSL setup${plain}" + SSL_HOST="${server_ip}" + ;; + *) + echo -e "${red}Invalid option. Skipping SSL setup.${plain}" + SSL_HOST="${server_ip}" ;; esac } @@ -105,6 +487,75 @@ config_after_update() { echo -e "${yellow}x-ui settings:${plain}" /usr/local/x-ui/x-ui setting -show true /usr/local/x-ui/x-ui migrate + + # Properly detect empty cert by checking if cert: line exists and has content after it + local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true 2>/dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##') + + # Get server IP + local URL_lists=( + "https://api4.ipify.org" + "https://ipv4.icanhazip.com" + "https://v4.api.ipinfo.io/ip" + "https://ipv4.myexternalip.com/raw" + "https://4.ident.me" + "https://check-host.net/ip" + ) + local server_ip="" + for ip_address in "${URL_lists[@]}"; do + server_ip=$(${curl_bin} -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]') + if [[ -n "${server_ip}" ]]; then + break + fi + done + + # Handle missing/short webBasePath + if [[ ${#existing_webBasePath} -lt 4 ]]; then + echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}" + local config_webBasePath=$(gen_random_string 18) + /usr/local/x-ui/x-ui setting -webBasePath "${config_webBasePath}" + existing_webBasePath="${config_webBasePath}" + echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}" + fi + + # Check and prompt for SSL if missing + if [[ -z "$existing_cert" ]]; then + echo "" + echo -e "${red}═══════════════════════════════════════════${plain}" + echo -e "${red} ⚠ NO SSL CERTIFICATE DETECTED ⚠ ${plain}" + echo -e "${red}═══════════════════════════════════════════${plain}" + echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}" + echo -e "${yellow}Let's Encrypt requires a domain name; IP certs are not issued. Use self-signed for IP.${plain}" + echo "" + + if [[ -z "${server_ip}" ]]; then + echo -e "${red}Failed to detect server IP${plain}" + echo -e "${yellow}Please configure SSL manually using: x-ui${plain}" + return + fi + + # Prompt and setup SSL (domain or self-signed) + prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}" + + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} Panel Access Information ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green}Access URL: https://${SSL_HOST}:${existing_port}/${existing_webBasePath}${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${yellow}⚠ SSL Certificate: Enabled and configured${plain}" + else + echo -e "${green}SSL certificate is already configured${plain}" + # Show access URL with existing certificate + local cert_domain=$(basename "$(dirname "$existing_cert")") + echo "" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green} Panel Access Information ${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + echo -e "${green}Access URL: https://${cert_domain}:${existing_port}/${existing_webBasePath}${plain}" + echo -e "${green}═══════════════════════════════════════════${plain}" + fi } update_x-ui() { diff --git a/x-ui.sh b/x-ui.sh index f8ead9d6..8e62b72a 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -213,6 +213,57 @@ gen_random_string() { echo "$random_string" } +# Generate and configure a self-signed SSL certificate +setup_self_signed_certificate() { + local name="$1" # domain or IP to place in SAN + local certDir="/root/cert/selfsigned" + + LOGI "Generating a self-signed certificate (not publicly trusted)..." + + mkdir -p "$certDir" + + local sanExt="" + if [[ "$name" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ || "$name" =~ : ]]; then + sanExt="IP:${name}" + else + sanExt="DNS:${name}" + fi + + openssl req -x509 -nodes -newkey rsa:2048 -days 365 \ + -keyout "${certDir}/privkey.pem" \ + -out "${certDir}/fullchain.pem" \ + -subj "/CN=${name}" \ + -addext "subjectAltName=${sanExt}" >/dev/null 2>&1 + + if [[ $? -ne 0 ]]; then + local tmpCfg="${certDir}/openssl.cnf" + cat > "$tmpCfg" </dev/null 2>&1 + rm -f "$tmpCfg" + fi + + if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then + LOGE "Failed to generate self-signed certificate" + return 1 + fi + + chmod 755 ${certDir}/* >/dev/null 2>&1 + /usr/local/x-ui/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem" >/dev/null 2>&1 + LOGI "Self-signed certificate configured. Browsers will show a warning." + return 0 +} + reset_webbasepath() { echo -e "${yellow}Resetting Web Base Path${plain}" @@ -256,7 +307,7 @@ check_config() { local existing_webBasePath=$(echo "$info" | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(echo "$info" | grep -Eo 'port: .+' | awk '{print $2}') - local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep -Eo 'cert: .+' | awk '{print $2}') + local existing_cert=$(/usr/local/x-ui/x-ui setting -getCert true | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]') local server_ip=$(curl -s --max-time 3 https://api.ipify.org) if [ -z "$server_ip" ]; then server_ip=$(curl -s --max-time 3 https://4.ident.me) @@ -271,7 +322,22 @@ check_config() { echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" fi else - echo -e "${green}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + echo -e "${red}⚠ WARNING: No SSL certificate configured!${plain}" + read -rp "Generate a self-signed SSL certificate now? [y/N]: " gen_self + if [[ "$gen_self" == "y" || "$gen_self" == "Y" ]]; then + stop >/dev/null 2>&1 + setup_self_signed_certificate "${server_ip}" + if [[ $? -eq 0 ]]; then + restart >/dev/null 2>&1 + echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + else + LOGE "Self-signed SSL setup failed." + echo -e "${yellow}You can try again via option 18 (SSL Certificate Management).${plain}" + fi + else + echo -e "${yellow}Access URL: http://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + echo -e "${yellow}For security, please configure SSL certificate using option 18 (SSL Certificate Management)${plain}" + fi fi } @@ -958,6 +1024,7 @@ ssl_cert_issue_main() { echo -e "${green}\t3.${plain} Force Renew" echo -e "${green}\t4.${plain} Show Existing Domains" echo -e "${green}\t5.${plain} Set Cert paths for the panel" + echo -e "${green}\t6.${plain} Auto SSL for Server IP" echo -e "${green}\t0.${plain} Back to Main Menu" read -rp "Choose an option: " choice @@ -1051,6 +1118,16 @@ ssl_cert_issue_main() { fi ssl_cert_issue_main ;; + 6) + echo -e "${yellow}Automatic SSL Certificate for Server IP${plain}" + echo -e "This will automatically obtain and configure an SSL certificate for your server's IP address." + echo -e "${yellow}Note: Let's Encrypt supports IP certificates. Make sure port 80 is open.${plain}" + confirm "Do you want to proceed?" "y" + if [[ $? == 0 ]]; then + ssl_cert_issue_for_ip + fi + ssl_cert_issue_main + ;; *) echo -e "${red}Invalid option. Please select a valid number.${plain}\n" @@ -1059,6 +1136,134 @@ ssl_cert_issue_main() { esac } +ssl_cert_issue_for_ip() { + LOGI "Starting automatic SSL certificate generation for server IP..." + + local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') + local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') + + # Get server IP + local server_ip=$(curl -s --max-time 3 https://api.ipify.org) + if [ -z "$server_ip" ]; then + server_ip=$(curl -s --max-time 3 https://4.ident.me) + fi + + if [ -z "$server_ip" ]; then + LOGE "Failed to get server IP address" + return 1 + fi + + LOGI "Server IP detected: ${server_ip}" + + # check for acme.sh first + if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then + LOGI "acme.sh not found, installing..." + install_acme + if [ $? -ne 0 ]; then + LOGE "Failed to install acme.sh" + return 1 + fi + fi + + # install socat + case "${release}" in + ubuntu | debian | armbian) + apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 + ;; + fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + ;; + centos) + if [[ "${VERSION_ID}" =~ ^7 ]]; then + yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 + else + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + fi + ;; + arch | manjaro | parch) + pacman -Sy --noconfirm socat >/dev/null 2>&1 + ;; + opensuse-tumbleweed | opensuse-leap) + zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 + ;; + alpine) + apk add socat curl openssl >/dev/null 2>&1 + ;; + *) + LOGW "Unsupported OS for automatic socat installation" + ;; + esac + + # check if certificate already exists for this IP + local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}') + if [ "${currentCert}" == "${server_ip}" ]; then + LOGI "Certificate already exists for IP: ${server_ip}" + certPath="/root/cert/${server_ip}" + else + # create directory for certificate + certPath="/root/cert/${server_ip}" + if [ ! -d "$certPath" ]; then + mkdir -p "$certPath" + else + rm -rf "$certPath" + mkdir -p "$certPath" + fi + + # Use port 80 for certificate issuance + local WebPort=80 + LOGI "Using port ${WebPort} to issue certificate for IP: ${server_ip}" + LOGI "Make sure port ${WebPort} is open and not in use..." + + # issue the certificate for IP + ~/.acme.sh/acme.sh --set-default-ca --server letsencrypt + ~/.acme.sh/acme.sh --issue -d ${server_ip} --listen-v6 --standalone --httpport ${WebPort} --force + if [ $? -ne 0 ]; then + LOGE "Failed to issue certificate for IP: ${server_ip}" + LOGE "Make sure port ${WebPort} is open and the server is accessible from the internet" + rm -rf ~/.acme.sh/${server_ip} + return 1 + else + LOGI "Certificate issued successfully for IP: ${server_ip}" + fi + + # install the certificate + ~/.acme.sh/acme.sh --installcert -d ${server_ip} \ + --key-file /root/cert/${server_ip}/privkey.pem \ + --fullchain-file /root/cert/${server_ip}/fullchain.pem \ + --reloadcmd "x-ui restart" + + if [ $? -ne 0 ]; then + LOGE "Failed to install certificate" + rm -rf ~/.acme.sh/${server_ip} + return 1 + else + LOGI "Certificate installed successfully" + fi + + # enable auto-renew + ~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1 + chmod 755 $certPath/* + fi + + # Set certificate paths for the panel + local webCertFile="/root/cert/${server_ip}/fullchain.pem" + local webKeyFile="/root/cert/${server_ip}/privkey.pem" + + if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then + /usr/local/x-ui/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" + LOGI "Certificate configured for panel" + LOGI " - Certificate File: $webCertFile" + LOGI " - Private Key File: $webKeyFile" + echo -e "${green}Access URL: https://${server_ip}:${existing_port}${existing_webBasePath}${plain}" + LOGI "Panel will restart to apply SSL certificate..." + restart + return 0 + else + LOGE "Certificate files not found after installation" + return 1 + fi +} + ssl_cert_issue() { local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}') local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}') @@ -1072,33 +1277,32 @@ ssl_cert_issue() { fi fi - # install socat second + # install socat case "${release}" in ubuntu | debian | armbian) - apt-get update && apt-get install socat -y + apt-get update >/dev/null 2>&1 && apt-get install socat -y >/dev/null 2>&1 ;; fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol) - dnf -y update && dnf -y install socat + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 ;; centos) - if [[ "${VERSION_ID}" =~ ^7 ]]; then - yum -y update && yum -y install socat - else - dnf -y update && dnf -y install socat - fi + if [[ "${VERSION_ID}" =~ ^7 ]]; then + yum -y update >/dev/null 2>&1 && yum -y install socat >/dev/null 2>&1 + else + dnf -y update >/dev/null 2>&1 && dnf -y install socat >/dev/null 2>&1 + fi ;; arch | manjaro | parch) - pacman -Sy --noconfirm socat + pacman -Sy --noconfirm socat >/dev/null 2>&1 ;; - opensuse-tumbleweed | opensuse-leap) - zypper refresh && zypper -q install -y socat + opensuse-tumbleweed | opensuse-leap) + zypper refresh >/dev/null 2>&1 && zypper -q install -y socat >/dev/null 2>&1 ;; alpine) - apk add socat curl openssl + apk add socat curl openssl >/dev/null 2>&1 ;; *) - echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" - exit 1 + LOGW "Unsupported OS for automatic socat installation" ;; esac if [ $? -ne 0 ]; then @@ -1110,7 +1314,22 @@ ssl_cert_issue() { # get the domain here, and we need to verify it local domain="" - read -rp "Please enter your domain name: " domain + while true; do + read -rp "Please enter your domain name: " domain + domain="${domain// /}" # Trim whitespace + + if [[ -z "$domain" ]]; then + LOGE "Domain name cannot be empty. Please try again." + continue + fi + + if ! is_domain "$domain"; then + LOGE "Invalid domain format: ${domain}. Please enter a valid domain name." + continue + fi + + break + done LOGD "Your domain is: ${domain}, checking it..." # check if there already exists a certificate From 260eedf8c417458b22bb17224035985a213bf3cc Mon Sep 17 00:00:00 2001 From: Wyatt Date: Sun, 28 Dec 2025 23:38:26 +0800 Subject: [PATCH 3/9] fix: add missing is_domain helper function to x-ui.sh (#3612) The is_domain function was being called in ssl_cert_issue() but was never defined in x-ui.sh, causing 'Invalid domain format' errors for valid domains. Added is_ipv4, is_ipv6, is_ip, and is_domain helper functions to match the definitions in install.sh and update.sh. Co-authored-by: wyatt --- x-ui.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/x-ui.sh b/x-ui.sh index 8e62b72a..32ddd8e0 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -19,6 +19,20 @@ function LOGI() { echo -e "${green}[INF] $* ${plain}" } +# Simple helpers for domain/IP validation +is_ipv4() { + [[ "$1" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]] && return 0 || return 1 +} +is_ipv6() { + [[ "$1" =~ : ]] && return 0 || return 1 +} +is_ip() { + is_ipv4 "$1" || is_ipv6 "$1" +} +is_domain() { + [[ "$1" =~ ^([A-Za-z0-9](-*[A-Za-z0-9])*\.)+[A-Za-z]{2,}$ ]] && return 0 || return 1 +} + # check root [[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1 From c061337ce76f4b2dffc8a86340b0133538528576 Mon Sep 17 00:00:00 2001 From: Nebulosa <85841412+nebulosa2007@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:11:32 +0300 Subject: [PATCH 4/9] Set log folder variable to /var/log/3x-ui (#3599) * Set log folder variable to /var/log/3x-ui * Set log folder as x-ui and create the log folder * Create the log folder in install and update scripts --- config/config.go | 2 +- install.sh | 1 + update.sh | 1 + x-ui.sh | 3 ++- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/config/config.go b/config/config.go index 17c9a77f..e5b43a29 100644 --- a/config/config.go +++ b/config/config.go @@ -109,7 +109,7 @@ func GetLogFolder() string { if runtime.GOOS == "windows" { return filepath.Join(".", "log") } - return "/var/log" + return "/var/log/x-ui" } func copyFile(src, dst string) error { diff --git a/install.sh b/install.sh index 0cf91b44..480522ed 100644 --- a/install.sh +++ b/install.sh @@ -641,6 +641,7 @@ install_x-ui() { # Update x-ui cli and se set permission mv -f /usr/bin/x-ui-temp /usr/bin/x-ui chmod +x /usr/bin/x-ui + mkdir -p /var/log/x-ui config_after_install if [[ $release == "alpine" ]]; then diff --git a/update.sh b/update.sh index ff0cea41..6da238b1 100755 --- a/update.sh +++ b/update.sh @@ -653,6 +653,7 @@ update_x-ui() { chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1 chmod +x /usr/bin/x-ui >/dev/null 2>&1 + mkdir -p /var/log/x-ui >/dev/null 2>&1 echo -e "${green}Changing owner...${plain}" chown -R root:root /usr/local/x-ui >/dev/null 2>&1 diff --git a/x-ui.sh b/x-ui.sh index 32ddd8e0..8df28103 100644 --- a/x-ui.sh +++ b/x-ui.sh @@ -53,7 +53,8 @@ os_version="" os_version=$(grep "^VERSION_ID" /etc/os-release | cut -d '=' -f2 | tr -d '"' | tr -d '.') # Declare Variables -log_folder="${XUI_LOG_FOLDER:=/var/log}" +log_folder="${XUI_LOG_FOLDER:=/var/log/x-ui}" +mkdir -p "${log_folder}" iplimit_log_path="${log_folder}/3xipl.log" iplimit_banned_log_path="${log_folder}/3xipl-banned.log" From c881d1015a72fed333bea04fb28b74d1f2517074 Mon Sep 17 00:00:00 2001 From: Zhenyu Qi Date: Fri, 2 Jan 2026 08:12:13 -0700 Subject: [PATCH 5/9] fix: handle GitHub API error responses in GetXrayVersions (#3609) GitHub API returns JSON object instead of array when encountering errors (e.g., rate limit exceeded). This causes JSON unmarshal error: 'cannot unmarshal object into Go value of type []service.Release' Add HTTP status code check to handle error responses gracefully and return user-friendly error messages instead of JSON parsing errors. Fixes issue where getXrayVersion fails with unmarshal error when GitHub API rate limit is exceeded. --- web/service/server.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/web/service/server.go b/web/service/server.go index d48a96c3..970c5c0f 100644 --- a/web/service/server.go +++ b/web/service/server.go @@ -529,6 +529,18 @@ func (s *ServerService) GetXrayVersions() ([]string, error) { } defer resp.Body.Close() + // Check HTTP status code - GitHub API returns object instead of array on error + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(resp.Body) + var errorResponse struct { + Message string `json:"message"` + } + if json.Unmarshal(bodyBytes, &errorResponse) == nil && errorResponse.Message != "" { + return nil, fmt.Errorf("GitHub API error: %s", errorResponse.Message) + } + return nil, fmt.Errorf("GitHub API returned status %d: %s", resp.StatusCode, resp.Status) + } + buffer := bytes.NewBuffer(make([]byte, bufferSize)) buffer.Reset() if _, err := buffer.ReadFrom(resp.Body); err != nil { From 8fe297ef9d519995557add08c1e439381272dbed Mon Sep 17 00:00:00 2001 From: Anton Petrov <63284142+Psychosoc1al@users.noreply.github.com> Date: Fri, 2 Jan 2026 18:12:30 +0300 Subject: [PATCH 6/9] Fix QR codes colors inversion (#3607) --- web/assets/css/custom.min.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/assets/css/custom.min.css b/web/assets/css/custom.min.css index 532d5439..fcbc3a69 100644 --- a/web/assets/css/custom.min.css +++ b/web/assets/css/custom.min.css @@ -1 +1 @@ -:root{--color-primary-100:#008771;--dark-color-background:#0a1222;--dark-color-surface-100:#151f31;--dark-color-surface-200:#222d42;--dark-color-surface-300:#2c3950;--dark-color-surface-400:rgba(65, 85, 119, 0.5);--dark-color-surface-500:#2c3950;--dark-color-surface-600:#313f5a;--dark-color-surface-700:#111929;--dark-color-surface-700-rgb:17, 25, 41;--dark-color-table-hover:rgba(44, 57, 80, 0.2);--dark-color-text-primary:rgba(255, 255, 255, 0.75);--dark-color-stroke:#2c3950;--dark-color-btn-danger:#cd3838;--dark-color-btn-danger-border:transparent;--dark-color-btn-danger-hover:#e94b4b;--dark-color-tag-bg:rgba(255, 255, 255, 0.05);--dark-color-tag-border:rgba(255, 255, 255, 0.15);--dark-color-tag-color:rgba(255, 255, 255, 0.75);--dark-color-tag-green-bg:17, 36, 33;--dark-color-tag-green-border:25, 81, 65;--dark-color-tag-green-color:#3ad3ba;--dark-color-tag-purple-bg:#201425;--dark-color-tag-purple-border:#5a2969;--dark-color-tag-purple-color:#d988cd;--dark-color-tag-red-bg:#291515;--dark-color-tag-red-border:#5c2626;--dark-color-tag-red-color:#e04141;--dark-color-tag-orange-bg:#312313;--dark-color-tag-orange-border:#593914;--dark-color-tag-orange-color:#ffa031;--dark-color-tag-blue-bg:#111a2c;--dark-color-tag-blue-border:#1348ab;--dark-color-tag-blue-color:#529fff;--dark-color-codemirror-line-hover:rgba(0, 135, 113, 0.2);--dark-color-codemirror-line-selection:rgba(0, 135, 113, 0.3);--dark-color-login-background:var(--dark-color-background);--dark-color-login-wave:var(--dark-color-surface-200);--dark-color-tooltip:rgba(61, 76, 104, 0.9);--dark-color-back-top:rgba(61, 76, 104, 0.9);--dark-color-back-top-hover:rgba(61, 76, 104, 1);--dark-color-scrollbar:#313f5a;--dark-color-scrollbar-webkit:#7484a0;--dark-color-scrollbar-webkit-hover:#90a4c7;--dark-color-table-ring:rgb(38 52 77);--dark-color-spin-container:#151f31}html[data-theme-animations='off']{.ant-menu,.ant-layout-sider,.ant-card,.ant-tag,.ant-progress-circle>*,.ant-input,.ant-table-row-expand-icon,.ant-switch,.ant-table-thead>tr>th,.ant-select-selection,.ant-btn,.ant-input-number,.ant-input-group-addon,.ant-checkbox-inner,.ant-progress-bg,.ant-progress-success-bg,.ant-radio-button-wrapper:not(:first-child):before,.ant-radio-button-wrapper,#login,.cm-s-xq.CodeMirror{transition:border 0s,background 0s!important}.ant-menu.ant-menu-inline .ant-menu-item:not(.ant-menu-sub .ant-menu-item),.ant-layout-sider-trigger,.ant-alert-close-icon .anticon-close,.ant-tabs-nav .ant-tabs-tab,.ant-input-number-input,.ant-collapse>.ant-collapse-item>.ant-collapse-header,.Line-Hover,.ant-menu-theme-switch,.ant-menu-submenu-title{transition:color 0s!important}.wave-btn-bg{transition:width 0s!important}}html[data-theme='ultra-dark']{--dark-color-background:#21242a;--dark-color-surface-100:#0c0e12;--dark-color-surface-200:#222327;--dark-color-surface-300:#32353b;--dark-color-surface-400:rgba(255, 255, 255, 0.1);--dark-color-surface-500:#3b404b;--dark-color-surface-600:#505663;--dark-color-surface-700:#101113;--dark-color-surface-700-rgb:16, 17, 19;--dark-color-table-hover:rgba(89, 89, 89, 0.15);--dark-color-text-primary:rgb(255 255 255 / 85%);--dark-color-stroke:#202025;--dark-color-tag-green-bg:17, 36, 33;--dark-color-tag-green-border:29, 95, 77;--dark-color-tag-green-color:#59cbac;--dark-color-tag-purple-bg:#241121;--dark-color-tag-purple-border:#5a2969;--dark-color-tag-purple-color:#d686ca;--dark-color-tag-red-bg:#2a1215;--dark-color-tag-red-border:#58181c;--dark-color-tag-red-color:#e84749;--dark-color-tag-orange-bg:#2b1d11;--dark-color-tag-orange-border:#593815;--dark-color-tag-orange-color:#e89a3c;--dark-color-tag-blue-bg:#111a2c;--dark-color-tag-blue-border:#0f367e;--dark-color-tag-blue-color:#3c89e8;--dark-color-codemirror-line-hover:rgba(82, 84, 94, 0.2);--dark-color-codemirror-line-selection:rgba(82, 84, 94, 0.3);--dark-color-login-background:#0a2227;--dark-color-login-wave:#0f2d32;--dark-color-tooltip:rgba(88, 93, 100, 0.9);--dark-color-back-top:rgba(88, 93, 100, 0.9);--dark-color-back-top-hover:rgba(88, 93, 100, 1);--dark-color-scrollbar:rgb(107,107,107);--dark-color-scrollbar-webkit:#9f9f9f;--dark-color-scrollbar-webkit-hover:#d1d1d1;--dark-color-table-ring:rgb(37 39 42);--dark-color-spin-container:#1d1d1d;.ant-dropdown-menu-dark,.dark .ant-dropdown-menu{background-color:var(--dark-color-surface-500)}.dark .ant-dropdown-menu-submenu-title:hover,.dark .ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled),.dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled){background-color:rgb(0 93 78 / .3)}.dark .waves-header{background-color:#0a2227}.dark .ant-calendar-year-panel-year:hover,.dark .ant-calendar-month-panel-month:hover,.dark .ant-calendar-decade-panel-decade:hover{background-color:var(--dark-color-surface-600)}}html,body{height:100vh;width:100vw;margin:0;padding:0;overflow:hidden}body{color:rgb(0 0 0 / .65);font-size:14px;font-variant:tabular-nums;line-height:1.5;background-color:#fff;font-feature-settings:"tnum"}html{--antd-wave-shadow-color:var(--color-primary-100);line-height:1.15;text-size-adjust:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-moz-tap-highlight-color:#fff0;-webkit-tap-highlight-color:#fff0}@supports (scrollbar-width:auto) and (not selector(::-webkit-scrollbar)){:not(.dark){scrollbar-color:#9a9a9a #fff0;scrollbar-width:thin}.dark *{scrollbar-color:var(--dark-color-scrollbar) #fff0;scrollbar-width:thin}}::-webkit-scrollbar{width:10px;height:10px;background-color:#fff0}::-webkit-scrollbar-track{background-color:#fff0;margin-block:.5em}.ant-modal-wrap::-webkit-scrollbar-track{background-color:#fff;margin-block:0}::-webkit-scrollbar-thumb{border-radius:9999px;background-color:#9a9a9a;border:2px solid #fff0;background-clip:content-box}::-webkit-scrollbar-thumb:hover,::-webkit-scrollbar-thumb:active{background-color:#828282}.dark .ant-modal-wrap::-webkit-scrollbar-track{background-color:var(--dark-color-background)}.dark::-webkit-scrollbar-thumb{background-color:var(--dark-color-scrollbar-webkit)}.dark::-webkit-scrollbar-thumb:hover,.dark::-webkit-scrollbar-thumb:active{background-color:var(--dark-color-scrollbar-webkit-hover)}::-moz-selection{color:var(--color-primary-100);background-color:#cfe8e4}::selection{color:var(--color-primary-100);background-color:#cfe8e4}#app{height:100%;position:fixed;top:0;left:0;right:0;bottom:0;margin:0;padding:0;overflow:auto}.ant-layout,.ant-layout *{box-sizing:border-box}.ant-spin-container:after{border-radius:1.5rem}.dark .ant-spin-container:after{background:var(--dark-color-spin-container)}style attribute{text-align:center}.ant-table-thead>tr>th{padding:12px 8px}.ant-table-tbody>tr>td{padding:10px 8px}.ant-table-thead>tr>th{color:rgb(0 0 0 / .85);font-weight:500;text-align:left;border-bottom:1px solid #e8e8e8;transition:background 0.3s ease}.ant-table table{border-radius:1rem}.ant-table-bordered .ant-table-tbody:not(.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody)>tr:last-child>td:first-child{border-bottom-left-radius:1rem}.ant-table-bordered .ant-table-tbody:not(.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody)>tr:last-child>td:last-child{border-bottom-right-radius:1rem}.ant-table{box-sizing:border-box;margin:0;padding:0;color:rgb(0 0 0 / .65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;clear:both}.ant-table .ant-table-body:not(.ant-table-expanded-row .ant-table-body){overflow-x:auto!important}.ant-card-hoverable{cursor:auto;cursor:pointer}.ant-card{box-sizing:border-box;margin:0;padding:0;color:rgb(0 0 0 / .65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;position:relative;background-color:#fff;border-radius:2px;transition:all 0.3s}.ant-space{width:100%}.ant-layout-sider-zero-width-trigger{display:none}@media (max-width:768px){.ant-layout-sider{display:none}.ant-card,.ant-alert-error{margin:.5rem}.ant-tabs{margin:.5rem;padding:.5rem}.ant-modal-body{padding:20px}.ant-form-item-label{line-height:1.5;padding:8px 0 0}:not(.dark)::-webkit-scrollbar{width:8px;height:8px;background-color:#fff0}.dark::-webkit-scrollbar{width:8px;height:8px;background-color:#fff0}}.ant-layout-content{min-height:auto}.ant-card,.ant-tabs{border-radius:1.5rem}.ant-card-hoverable{cursor:auto}.ant-card+.ant-card{margin-top:20px}.drawer-handle{position:absolute;top:72px;width:41px;height:40px;cursor:pointer;z-index:0;text-align:center;line-height:40px;font-size:16px;display:flex;justify-content:center;align-items:center;background-color:#fff;right:-40px;box-shadow:2px 0 8px rgb(0 0 0 / .15);border-radius:0 4px 4px 0}.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background-color:#006655!important;background-image:linear-gradient(270deg,#fff0 30%,#009980,#fff0 100%);background-repeat:no-repeat;animation:ma-bg-move linear 6.6s infinite;color:#fff;border-radius:.5rem}.ant-layout-sider-collapsed .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{border-radius:0}.ant-menu-item-active,.ant-menu-item:hover,.ant-menu-submenu-title:hover,.ant-menu-item:active,.ant-menu-submenu-title:active{color:var(--color-primary-100);background-color:#e8f4f2}.ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title{border-radius:.5rem}.ant-menu-inline .ant-menu-item:after,.ant-menu{border-right-width:0}.ant-layout-sider-children,.ant-pagination ul{padding:.5rem}.ant-layout-sider-collapsed .ant-layout-sider-children{padding:.5rem 0}.ant-dropdown-menu,.ant-select-dropdown-menu{padding:.5rem}.ant-dropdown-menu-item,.ant-dropdown-menu-item:hover,.ant-select-dropdown-menu-item,.ant-select-dropdown-menu-item:hover,.ant-select-selection--multiple .ant-select-selection__choice{border-radius:.5rem}.ant-select-dropdown--multiple .ant-select-dropdown-menu .ant-select-dropdown-menu-item,.ant-select-dropdown--single .ant-select-dropdown-menu .ant-select-dropdown-menu-item-selected{margin-block:2px}@media (min-width:769px){.drawer-handle{display:none}.ant-tabs{padding:2rem}}.fade-in-enter,.fade-in-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.fade-in-enter-active,.fade-in-leave-active{-webkit-transition:all 0.3s cubic-bezier(.55,0,.1,1);transition:all 0.3s cubic-bezier(.55,0,.1,1)}.zoom-in-center-enter-active,.zoom-in-center-leave-active{-webkit-transition:all 0.3s cubic-bezier(.55,0,.1,1);transition:all 0.3s cubic-bezier(.55,0,.1,1)}.zoom-in-center-enter,.zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.zoom-in-top-enter-active,.zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center top;transform-origin:center top}.zoom-in-top-enter,.zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.zoom-in-bottom-enter-active,.zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center bottom;transform-origin:center bottom}.zoom-in-bottom-enter,.zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.zoom-in-left-enter-active,.zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1);-webkit-transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:top left;transform-origin:top left}.zoom-in-left-enter,.zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45,.45);transform:scale(.45,.45)}.list-enter-active,.list-leave-active{-webkit-transition:all 0.3s;transition:all 0.3s}.list-enter,.list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.ant-tooltip-inner{min-height:0;padding-inline:1rem}.ant-list-item-meta-title{font-size:14px}.ant-progress-inner{background-color:#ebeef5}.deactive-client .ant-collapse-header{color:#ffffff!important;background-color:#ff7f7f}.ant-table-expand-icon-th,.ant-table-row-expand-icon-cell{width:30px;min-width:30px}.ant-tabs{background-color:#fff}.ant-form-item{margin-bottom:0}.ant-setting-textarea{margin-top:1.5rem}.client-table-header{background-color:#f0f2f5}.client-table-odd-row{background-color:#fafafa}.ant-table-pagination.ant-pagination{float:left}.ant-tag{margin-right:0;margin-inline:2px;display:inline-flex;align-items:center;justify-content:space-evenly}.ant-tag:not(.qr-tag){column-gap:4px}#inbound-info-modal .ant-tag{margin-block:2px}.tr-info-table{display:inline-table;margin-block:10px;width:100%}#inbound-info-modal .tr-info-table .ant-tag{margin-block:0;margin-inline:0}.tr-info-row{display:flex;flex-direction:column;row-gap:2px;margin-block:10px}.tr-info-row a{margin-left:6px}.tr-info-row code{padding-inline:8px;max-height:80px;overflow-y:auto}.tr-info-tag{max-width:100%;text-wrap:balance;overflow:hidden;overflow-wrap:anywhere}.tr-info-title{display:inline-flex;align-items:center;justify-content:flex-start;column-gap:4px}.ant-tag-blue{background-color:#edf4fa;border-color:#a9c5e7;color:#0e49b5}.ant-tag-green{background-color:#eafff9;border-color:#76ccb4;color:#199270}.ant-tag-purple{background-color:#f2eaf1;border-color:#d5bed2;color:#7a316f}.ant-tag-orange,.ant-alert-warning{background-color:#ffeee1;border-color:#fec093;color:#f37b24}.ant-tag-red,.ant-alert-error{background-color:#ffe9e9;border-color:#ff9e9e;color:#cf3c3c}.ant-input::placeholder{opacity:.5}.ant-input:hover,.ant-input:focus{background-color:#e8f4f2}.ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled){background-color:#e8f4f2}.delete-icon:hover{color:#e04141}.normal-icon:hover{color:var(--color-primary-100)}.dark ::-moz-selection{color:#fff;background-color:var(--color-primary-100)}.dark ::selection{color:#fff;background-color:var(--color-primary-100)}.dark .normal-icon:hover{color:#fff}.dark .ant-layout-sider,.dark .ant-drawer-content,.ant-menu-dark,.ant-menu-dark .ant-menu-sub,.dark .ant-card,.dark .ant-table,.dark .ant-collapse-content,.dark .ant-tabs{background-color:var(--dark-color-surface-100);color:var(--dark-color-text-primary)}.dark .ant-card-hoverable:hover,.dark .ant-space-item>.ant-tabs:hover{box-shadow:0 2px 8px #fff0}.dark>.ant-layout,.dark .drawer-handle,.dark .ant-table-thead>tr>th,.dark .ant-table-expanded-row,.dark .ant-table-expanded-row:hover,.dark .ant-table-expanded-row .ant-table-tbody,.dark .ant-calendar{background-color:var(--dark-color-background);color:var(--dark-color-text-primary)}.dark .ant-table-expanded-row .ant-table-thead>tr:first-child>th{border-radius:0}.dark .ant-calendar,.dark .ant-card-bordered{border-color:var(--dark-color-background)}.dark .ant-table-bordered,.dark .ant-table-bordered.ant-table-empty .ant-table-placeholder,.dark .ant-table-bordered .ant-table-body>table,.dark .ant-table-bordered .ant-table-fixed-left table,.dark .ant-table-bordered .ant-table-fixed-right table,.dark .ant-table-bordered .ant-table-header>table,.dark .ant-table-bordered .ant-table-thead>tr:not(:last-child)>th,.dark .ant-table-bordered .ant-table-tbody>tr>td,.dark .ant-table-bordered .ant-table-thead>tr>th{border-color:var(--dark-color-surface-400)}.dark .ant-table-tbody>tr>td,.dark .ant-table-thead>tr>th,.dark .ant-card-head,.dark .ant-modal-header,.dark .ant-collapse>.ant-collapse-item,.dark .ant-tabs-bar,.dark .ant-list-split .ant-list-item,.dark .ant-popover-title,.dark .ant-calendar-header,.dark .ant-calendar-input-wrap{border-bottom-color:var(--dark-color-surface-400)}.dark .ant-modal-footer,.dark .ant-collapse-content,.dark .ant-calendar-footer,.dark .ant-divider-horizontal.ant-divider-with-text-left:before,.dark .ant-divider-horizontal.ant-divider-with-text-left:after,.dark .ant-divider-horizontal.ant-divider-with-text-center:before,.dark .ant-divider-horizontal.ant-divider-with-text-center:after{border-top-color:var(--dark-color-surface-300)}.ant-divider-horizontal.ant-divider-with-text-left:before{width:10%}.dark .ant-progress-text,.dark .ant-card-head,.dark .ant-form,.dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,.dark .ant-modal-close-x,.dark .ant-form .anticon,.dark .ant-tabs-tab-arrow-show:not(.ant-tabs-tab-btn-disabled),.dark .anticon-close,.dark .ant-list-item-meta-title,.dark .ant-select-selection i,.dark .ant-modal-confirm-title,.dark .ant-modal-confirm-content,.dark .ant-popover-message,.dark .ant-modal,.dark .ant-divider-inner-text,.dark .ant-popover-title,.dark .ant-popover-inner-content,.dark h2,.dark .ant-modal-title,.dark .ant-form-item-label>label,.dark .ant-checkbox-wrapper,.dark .ant-form-item,.dark .ant-calendar-footer .ant-calendar-today-btn,.dark .ant-calendar-footer .ant-calendar-time-picker-btn,.dark .ant-calendar-day-select,.dark .ant-calendar-month-select,.dark .ant-calendar-year-select,.dark .ant-calendar-date,.dark .ant-calendar-year-panel-year,.dark .ant-calendar-month-panel-month,.dark .ant-calendar-decade-panel-decade{color:var(--dark-color-text-primary)}.dark .ant-pagination-options-size-changer .ant-select-arrow .anticon.anticon-down.ant-select-arrow-icon{color:rgb(255 255 255 / 35%)}.dark .ant-pagination-item a,.dark .ant-pagination-next a,.dark .ant-pagination-prev a{color:var(--dark-color-text-primary)}.dark .ant-pagination-item:focus a,.dark .ant-pagination-item:hover a,.dark .ant-pagination-item-active a,.dark .ant-pagination-next:hover .ant-pagination-item-link{color:var(--color-primary-100)}.dark .ant-pagination-item-active{background-color:#fff0}.dark .ant-list-item-meta-description{color:rgb(255 255 255 / .45)}.dark .ant-pagination-disabled i,.dark .ant-tabs-tab-btn-disabled{color:rgb(255 255 255 / .25)}.dark .ant-input,.dark .ant-input-group-addon,.dark .ant-collapse,.dark .ant-select-selection,.dark .ant-input-number,.dark .ant-input-number-handler-wrap,.dark .ant-table-placeholder,.dark .ant-empty-normal,.dark .ant-select-dropdown,.dark .ant-select-dropdown li,.dark .ant-select-dropdown-menu-item,.dark .client-table-header,.dark .ant-select-selection--multiple .ant-select-selection__choice{background-color:var(--dark-color-surface-200);border-color:var(--dark-color-surface-300);color:var(--dark-color-text-primary)}.dark .ant-select-dropdown--multiple .ant-select-dropdown-menu .ant-select-dropdown-menu-item.ant-select-dropdown-menu-item-selected :not(.ant-dropdown-menu-submenu-title:hover){background-color:var(--dark-color-surface-300)}.dark .ant-select-dropdown-menu-item.ant-select-dropdown-menu-item-selected{background-color:var(--dark-color-surface-300)}.dark .ant-calendar-time-picker-inner{background-color:var(--dark-color-background)}.dark .ant-select-selection:hover,.dark .ant-calendar-picker-clear,.dark .ant-input-number:hover,.dark .ant-input-number:focus,.dark .ant-input:hover,.dark .ant-input:focus{background-color:rgb(0 135 113 / .3);border-color:var(--color-primary-100)}.dark .ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled){border-color:var(--color-primary-100);background-color:rgb(0 135 113 / .3)}.dark .ant-btn:not(.ant-btn-primary):not(.ant-btn-danger){color:var(--dark-color-text-primary);background-color:rgb(10 117 87 / 30%);border:1px solid var(--color-primary-100)}.dark .ant-radio-button-wrapper,.dark .ant-radio-button-wrapper:before{color:var(--dark-color-text-primary);background-color:rgb(0 135 113 / .3);border-color:var(--color-primary-100)}.ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger){background-color:#e8f4f2}.dark .ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),.dark .ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger){color:#fff;background-color:rgb(10 117 87 / 50%);border-color:var(--color-primary-100)}.dark .ant-btn-primary[disabled],.dark .ant-btn-danger[disabled],.dark .ant-calendar-ok-btn-disabled{color:rgb(255 255 255 / 35%);background-color:var(--dark-color-surface-200);border-color:var(--dark-color-surface-300)}.dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.dark .client-table-odd-row{background-color:var(--dark-color-table-hover)}.dark .ant-table-row-expand-icon{color:#fff;background-color:#fff0;border-color:rgb(255 255 255 / 20%)}.dark .ant-table-row-expand-icon:hover{color:var(--color-primary-100);background-color:#fff0;border-color:var(--color-primary-100)}.dark .ant-switch:not(.ant-switch-checked),.dark .ant-progress-line .ant-progress-inner{background-color:var(--dark-color-surface-500)}.dark .ant-progress-circle-trail{stroke:var(--dark-color-stroke)!important}.dark .ant-popover-inner{background-color:var(--dark-color-surface-500)}.dark>.ant-popover-content>.ant-popover-arrow{border-color:var(--dark-color-surface-500)}@media (max-width:768px){.dark .ant-popover-inner{background-color:var(--dark-color-surface-200)}.dark>.ant-popover-content>.ant-popover-arrow{border-color:var(--dark-color-surface-200)}}.ant-dropdown-menu-dark .ant-dropdown-menu-item:hover,.dark .ant-select-dropdown-menu-item-selected,.dark .ant-calendar-time-picker-select-option-selected{background-color:var(--dark-color-surface-600)}.ant-menu-dark .ant-menu-item:hover,.ant-menu-dark .ant-menu-submenu-title:hover{background-color:var(--dark-color-surface-300)}.dark .ant-menu-item:active,.dark .ant-menu-submenu-title:active{color:#fff;background-color:var(--dark-color-surface-300)}.dark .ant-alert-message{color:rgb(255 255 255 / .85)}.dark .ant-tag{color:var(--dark-color-tag-color);background-color:var(--dark-color-tag-bg);border-color:var(--dark-color-tag-border)}.dark .ant-tag-blue{background-color:var(--dark-color-tag-blue-bg);border-color:var(--dark-color-tag-blue-border);color:var(--dark-color-tag-blue-color)}.dark .ant-tag-red,.dark .ant-alert-error{background-color:var(--dark-color-tag-red-bg);border-color:var(--dark-color-tag-red-border);color:var(--dark-color-tag-red-color)}.dark .ant-tag-orange,.dark .ant-alert-warning{background-color:var(--dark-color-tag-orange-bg);border-color:var(--dark-color-tag-orange-border);color:var(--dark-color-tag-orange-color)}.dark .ant-tag-green{background-color:rgb(var(--dark-color-tag-green-bg));border-color:rgb(var(--dark-color-tag-green-border));color:var(--dark-color-tag-green-color)}.dark .ant-tag-purple{background-color:var(--dark-color-tag-purple-bg);border-color:var(--dark-color-tag-purple-border);color:var(--dark-color-tag-purple-color)}.dark .ant-modal-content,.dark .ant-modal-header{background-color:var(--dark-color-surface-700)}.dark .ant-calendar-next-month-btn-day .ant-calendar-date,.dark .ant-calendar-last-month-cell .ant-calendar-date{color:var(--dark-color-surface-300)}.dark .ant-calendar-selected-day .ant-calendar-date{background-color:var(--color-primary-100)!important;color:#fff}.dark .ant-calendar-date:hover,.dark .ant-calendar-time-picker-select li:hover{background-color:var(--dark-color-surface-600);color:#fff}.dark .ant-calendar-header a:hover,.dark .ant-calendar-header a:hover::before,.dark .ant-calendar-header a:hover::after{border-color:#fff}.dark .ant-calendar-time-picker-select{border-right-color:var(--dark-color-surface-300)}.has-warning .ant-select-selection,.has-warning .ant-select-selection:hover,.has-warning .ant-input,.has-warning .ant-input:hover{background-color:#ffeee1;border-color:#fec093}.has-warning .ant-input::placeholder{color:#f37b24}.has-warning .ant-input:not([disabled]):hover{border-color:#fec093}.dark .has-warning .ant-select-selection,.dark .has-warning .ant-select-selection:hover,.dark .has-warning .ant-input,.dark .has-warning .ant-input:hover{border-color:#784e1d;background:#312313}.dark .has-warning .ant-input::placeholder{color:rgb(255 160 49 / 70%)}.dark .has-warning .anticon{color:#ffa031}.dark .has-success .anticon{color:var(--color-primary-100);animation-name:diffZoomIn1!important}.dark .anticon-close-circle{color:#e04141}.dark .ant-spin-nested-loading>div>.ant-spin .ant-spin-text{text-shadow:0 1px 2px #0007}.dark .ant-spin{color:#fff}.dark .ant-spin-dot-item{background-color:#fff}.ant-checkbox-wrapper,.ant-input-group-addon,.ant-tabs-tab,.ant-input::placeholder,.ant-collapse-header,.ant-menu,.ant-radio-button-wrapper{-webkit-user-select:none;user-select:none}.ant-calendar-date,.ant-calendar-year-panel-year,.ant-calendar-decade-panel-decade,.ant-calendar-month-panel-month{border-radius:4px}.ant-checkbox-inner,.ant-checkbox-checked:after,.ant-table-row-expand-icon{border-radius:6px}.ant-calendar-date:hover{background-color:#e8f4f2}.ant-calendar-date:active{background-color:#e8f4f2;color:rgb(0 0 0 / .65)}.ant-calendar-today .ant-calendar-date{color:var(--color-primary-100);font-weight:400;border-color:var(--color-primary-100)}.dark .ant-calendar-today .ant-calendar-date{color:#fff;border-color:var(--color-primary-100)}.ant-calendar-selected-day .ant-calendar-date{background:var(--color-primary-100);color:#fff}li.ant-select-dropdown-menu-item:empty:after{content:"None";font-weight:400;color:rgb(0 0 0 / .25)}.dark li.ant-select-dropdown-menu-item:empty:after{content:"None";font-weight:400;color:rgb(255 255 255 / .3)}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item:hover .ant-select-selected-icon{color:rgb(0 0 0 / .87)}.dark.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item:hover .ant-select-selected-icon{color:#fff}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item-selected .ant-select-selected-icon,.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item-selected:hover .ant-select-selected-icon{color:var(--color-primary-100)}.ant-select-selection:hover,.ant-input-number-focused,.ant-input-number:hover{background-color:#e8f4f2}.dark .ant-input-number-handler:active{background-color:var(--color-primary-100)}.dark .ant-input-number-handler:hover .ant-input-number-handler-down-inner,.dark .ant-input-number-handler:hover .ant-input-number-handler-up-inner{color:#fff}.dark .ant-input-number-handler-down{border-top:1px solid rgb(217 217 217 / .3)}.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-century-select,.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-decade-select,.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-month-select,.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-year-select,.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-century-select,.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-decade-select,.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-month-select,.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-year-select{color:rgb(255 255 255 / .85)}.dark .ant-calendar-year-panel-header{border-bottom:1px solid var(--dark-color-surface-200)}.dark .ant-calendar-year-panel-last-decade-cell .ant-calendar-year-panel-year,.dark .ant-calendar-year-panel-next-decade-cell .ant-calendar-year-panel-year{color:rgb(255 255 255 / .35)}.dark .ant-divider:not(.ant-divider-with-text-center,.ant-divider-with-text-left,.ant-divider-with-text-right),.ant-dropdown-menu-dark,.dark .ant-calendar-year-panel-year:hover,.dark .ant-calendar-month-panel-month:hover,.dark .ant-calendar-decade-panel-decade:hover{background-color:var(--dark-color-surface-200)}.dark .ant-calendar-header a:hover{color:#fff}.dark .ant-calendar-month-panel-header{background-color:var(--dark-color-background);border-bottom:1px solid var(--dark-color-surface-200)}.dark .ant-calendar-year-panel,.dark .ant-calendar table{background-color:var(--dark-color-background)}.dark .ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year,.dark .ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year:hover,.dark .ant-calendar-month-panel-selected-cell .ant-calendar-month-panel-month,.dark .ant-calendar-month-panel-selected-cell .ant-calendar-month-panel-month:hover,.dark .ant-calendar-decade-panel-selected-cell .ant-calendar-decade-panel-decade,.dark .ant-calendar-decade-panel-selected-cell .ant-calendar-decade-panel-decade:hover{color:#fff;background-color:var(--color-primary-100)!important}.dark .ant-calendar-last-month-cell .ant-calendar-date,.dark .ant-calendar-last-month-cell .ant-calendar-date:hover,.dark .ant-calendar-next-month-btn-day .ant-calendar-date,.dark .ant-calendar-next-month-btn-day .ant-calendar-date:hover{color:rgb(255 255 255 / 25%);background:#fff0;border-color:#fff0}.dark .ant-calendar-today .ant-calendar-date:hover{color:#fff;border-color:var(--color-primary-100);background-color:var(--color-primary-100)}.dark .ant-calendar-decade-panel-last-century-cell .ant-calendar-decade-panel-decade,.dark .ant-calendar-decade-panel-next-century-cell .ant-calendar-decade-panel-decade{color:rgb(255 255 255 / 25%)}.dark .ant-calendar-decade-panel-header{border-bottom:1px solid var(--dark-color-surface-200);background-color:var(--dark-color-background)}.dark .ant-checkbox-inner{background-color:rgb(0 135 113 / .3);border-color:rgb(0 135 113 / .3)}.dark .ant-checkbox-checked .ant-checkbox-inner{background-color:var(--color-primary-100);border-color:var(--color-primary-100)}.dark .ant-calendar-input{background-color:var(--dark-color-background);color:var(--dark-color-text-primary)}.dark .ant-calendar-input::placeholder{color:rgb(255 255 255 / .25)}.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(:last-child),.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(:last-child),.ant-input-group.ant-input-group-compact>.ant-input:not(:first-child):not(:last-child),.ant-input-number-handler,.ant-input-number-handler-wrap{border-radius:0}.ant-input-number{overflow:clip}.ant-modal-body,.ant-collapse-content>.ant-collapse-content-box{overflow-x:auto}.ant-modal-body{overflow-y:hidden}.ant-calendar-year-panel-year:hover,.ant-calendar-decade-panel-decade:hover,.ant-calendar-month-panel-month:hover,.ant-dropdown-menu-item:hover,.ant-dropdown-menu-submenu-title:hover,.ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled),.ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),.ant-table-tbody>tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-thead>tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-thead>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td{background-color:#e8f4f2}.dark .ant-dropdown-menu-submenu-title:hover,.dark .ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled),.dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled){background-color:rgb(0 93 78 / .3)}.ant-select-dropdown,.ant-popover-inner{overflow-x:hidden}.ant-popover-inner-content{max-height:450px;overflow-y:auto}@media (max-height:900px){.ant-popover-inner-content{max-height:400px}}@media (max-height:768px){.ant-popover-inner-content{max-height:300px}}@media (max-width:768px){.ant-popover-inner-content{max-height:300px}}.qr-modal{display:flex;align-items:flex-end;gap:10px;flex-direction:column;flex-wrap:wrap;row-gap:24px}.qr-box{width:220px}.qr-cv{width:100%;height:100%}.dark .qr-cv{filter:invert(1)}.qr-bg{background-color:#fff;display:flex;justify-content:center;align-content:center;padding:.8rem;border-radius:1rem;border:solid 1px #e8e8e8;height:220px;width:220px;transition:all 0.1s}.qr-bg:hover{border-color:#76ccb4;background-color:#eafff9}.qr-bg:hover:active{border-color:#76ccb4;background-color:rgb(197 241 228 / 70%)}.dark .qr-bg{background-color:var(--dark-color-surface-700);border-color:var(--dark-color-surface-300)}.dark .qr-bg:hover{background-color:rgb(var(--dark-color-tag-green-bg));border-color:rgb(var(--dark-color-tag-green-border))}.dark .qr-bg:hover:active{background-color:#17322e}@property --tr-rotate{syntax:'';initial-value:45deg;inherits:false}.qr-bg-sub{background-image:linear-gradient(var(--tr-rotate),#76ccb4,transparent,#d5bed2);display:flex;justify-content:center;align-content:center;padding:1px;border-radius:1rem;height:220px;width:220px}.dark .qr-bg-sub{background-image:linear-gradient(var(--tr-rotate),#195141,transparent,#5a2969)}.qr-bg-sub:hover{animation:tr-rotate-gradient 3.5s linear infinite}@keyframes tr-rotate-gradient{from{--tr-rotate:45deg}to{--tr-rotate:405deg}}.qr-bg-sub-inner{background-color:#fff;padding:.8rem;border-radius:1rem;transition:all 0.1s}.qr-bg-sub-inner:hover{background-color:rgb(255 255 255 / 60%);backdrop-filter:blur(25px)}.qr-bg-sub-inner:hover:active{background-color:rgb(255 255 255 / 30%)}.dark .qr-bg-sub-inner{background-color:rgb(var(--dark-color-surface-700-rgb))}.dark .qr-bg-sub-inner:hover{background-color:rgba(var(--dark-color-surface-700-rgb),.5);backdrop-filter:blur(25px)}.dark .qr-bg-sub-inner:hover:active{background-color:rgba(var(--dark-color-surface-700-rgb),.2)}.qr-tag{text-align:center;margin-bottom:10px;width:100%;overflow:hidden;margin-inline:0}@media (min-width:769px){.qr-modal{flex-direction:row;max-width:680px}}.tr-marquee{justify-content:flex-start}.tr-marquee span{padding-right:25%;white-space:nowrap;transform-origin:center}@keyframes move-ltr{0%{transform:translateX(0)}100%{transform:translateX(-100%)}}.ant-input-group-addon:not(:first-child):not(:last-child){border-radius:0rem 1rem 1rem 0rem}b,strong{font-weight:500}.ant-collapse>.ant-collapse-item>.ant-collapse-header{padding:10px 16px 10px 40px}.dark .ant-message-notice-content{background-color:var(--dark-color-surface-200);border:1px solid var(--dark-color-surface-300);color:var(--dark-color-text-primary)}.ant-btn-danger{background-color:var(--dark-color-btn-danger);border-color:var(--dark-color-btn-danger-border)}.ant-btn-danger:focus,.ant-btn-danger:hover{background-color:var(--dark-color-btn-danger-hover);border-color:var(--dark-color-btn-danger-hover)}.dark .ant-alert-close-icon .anticon-close:hover{color:#fff}.ant-empty-small{margin:4px 0;background-color:transparent!important}.ant-empty-small .ant-empty-image{height:20px}.ant-menu-theme-switch,.ant-menu-theme-switch:hover{background-color:transparent!important;cursor:default!important}.dark .ant-tooltip-inner,.dark .ant-tooltip-arrow:before{background-color:var(--dark-color-tooltip)}.ant-select-sm .ant-select-selection__rendered{margin-left:10px}.ant-collapse{-moz-animation:collfade 0.3s ease;-webkit-animation:0.3s collfade 0.3s ease;animation:collfade 0.3s ease}@-webkit-keyframes collfade{0%{transform:scaleY(.8);transform-origin:0% 0%;opacity:0}100%{transform:scaleY(1);transform-origin:0% 0%;opacity:1}}@keyframes collfade{0%{transform:scaleY(.8);transform-origin:0% 0%;opacity:0}100%{transform:scaleY(1);transform-origin:0% 0%;opacity:1}}.ant-table-tbody>tr>td{border-color:#f0f0f0}.ant-table-row-expand-icon{vertical-align:middle;margin-inline-end:8px;position:relative;transform:scale(.9411764705882353)}.ant-table-row-collapsed::before{transform:rotate(-180deg);top:7px;inset-inline-end:3px;inset-inline-start:3px;height:1px;position:absolute;background:currentcolor;transition:transform 0.3s ease-out;content:""}.ant-table-row-collapsed::after{transform:rotate(0deg);top:3px;bottom:3px;inset-inline-start:7px;width:1px;position:absolute;background:currentcolor;transition:transform 0.3s ease-out;content:""}.ant-table-row-expanded::before{top:7px;inset-inline-end:3px;inset-inline-start:3px;height:1px;position:absolute;background:currentcolor;transition:transform 0.3s ease-out;content:""}.ant-table-row-expanded::after{top:3px;bottom:3px;inset-inline-start:7px;width:1px;transform:rotate(90deg);position:absolute;background:currentcolor;transition:transform 0.3s ease-out;content:""}.ant-menu-theme-switch.ant-menu-item .ant-switch:not(.ant-switch-disabled):active:after,.ant-switch:not(.ant-switch-disabled):active:before{width:16px}.dark .ant-select-disabled .ant-select-selection{background:var(--dark-color-surface-100);border-color:var(--dark-color-surface-200);color:rgb(255 255 255 / .25)}.dark .ant-select-disabled .anticon{color:rgb(255 255 255 / .25)}.dark .ant-input-number-handler-down-disabled,.dark .ant-input-number-handler-up-disabled{background-color:rgb(0 0 0 / .1)}.dark .ant-input-number-handler-down-disabled .anticon,.dark .ant-input-number-handler-up-disabled .anticon,.dark .ant-input-number-handler-down:hover.ant-input-number-handler-down-disabled .anticon,.dark .ant-input-number-handler-up:hover.ant-input-number-handler-up-disabled .anticon{color:rgb(255 255 255 / .25)}.dark .ant-input-number-handler-down:active.ant-input-number-handler-down-disabled,.dark .ant-input-number-handler-up:active.ant-input-number-handler-up-disabled{background-color:rgb(0 0 0 / .2)}.ant-menu-dark .ant-menu-inline.ant-menu-sub{background:var(--dark-color-surface-100);box-shadow:none}.dark .ant-layout-sider-trigger{background:var(--dark-color-surface-100);color:rgb(255 255 255 / 65%)}.ant-layout-sider{overflow:auto}.dark .ant-back-top-content{background-color:var(--dark-color-back-top)}.dark .ant-back-top-content:hover{background-color:var(--dark-color-back-top-hover)}.ant-calendar-time .ant-calendar-footer .ant-calendar-time-picker-btn{text-transform:capitalize}.ant-calendar{border-color:#fff0;border-width:0}.ant-calendar-time-picker-select li:focus,li.ant-calendar-time-picker-select-option-selected{color:rgb(0 0 0 / .65);font-weight:400;background-color:#e8f4f2}.dark li.ant-calendar-time-picker-select-option-selected{color:var(--dark-color-text-primary);font-weight:400}.dark .ant-calendar-time-picker-select li:focus{color:#fff;font-weight:400;background-color:var(--color-primary-100)}.ant-calendar-time-picker-select li:hover{background:#f5f5f5}.ant-calendar-date{transition:background .3s ease,color .3s ease}li.ant-calendar-time-picker-select-option-selected{margin-block:2px}.ant-calendar-time-picker-select{padding:4px}.ant-calendar-time-picker-select li{height:28px;line-height:28px;border-radius:4px}@media (min-width:769px){.index-page .ant-layout-content{margin:24px 16px}}.index-page .ant-card-dark h2{color:var(--dark-color-text-primary)}.index-page~div .ant-backup-list-item{gap:10px}.index-page~div .ant-version-list-item{--padding:12px;padding:var(--padding)!important;gap:var(--padding)}.index-page.dark~div .ant-version-list-item svg{color:var(--dark-color-text-primary)}.index-page.dark~div .ant-backup-list-item svg,.index-page.dark .ant-badge-status-text,.index-page.dark .ant-card-extra{color:var(--dark-color-text-primary)}.index-page.dark .ant-card-actions>li{color:rgb(255 255 255 / .55)}.index-page.dark~div .ant-radio-inner{background-color:var(--dark-color-surface-100);border-color:var(--dark-color-surface-600)}.index-page.dark~div .ant-radio-checked .ant-radio-inner{border-color:var(--color-primary-100)}.index-page.dark~div .ant-backup-list,.index-page.dark~div .ant-version-list,.index-page.dark .ant-card-actions,.index-page.dark .ant-card-actions>li:not(:last-child){border-color:var(--dark-color-stroke)}.index-page .ant-card-actions{background:#fff0}.index-page .ip-hidden{-webkit-user-select:none;-moz-user-select:none;user-select:none;filter:blur(10px)}.index-page .xray-running-animation .ant-badge-status-dot,.index-page .xray-processing-animation .ant-badge-status-dot{animation:runningAnimation 1.2s linear infinite}.index-page .xray-running-animation .ant-badge-status-processing:after{border-color:var(--color-primary-100)}.index-page .xray-stop-animation .ant-badge-status-processing:after{border-color:#fa8c16}.index-page .xray-error-animation .ant-badge-status-processing:after{border-color:#f5222d}@keyframes runningAnimation{0%,50%,100%{transform:scale(1);opacity:1}10%{transform:scale(1.5);opacity:.2}}.index-page .card-placeholder{text-align:center;padding:30px 0;margin-top:10px;background:#fff0;border:none}.index-page~div .log-container{height:auto;max-height:500px;overflow:auto;margin-top:.5rem}#app.login-app *{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app.login-app h1{text-align:center;height:110px}#app.login-app .ant-form-item-children .ant-btn,#app.login-app .ant-input{height:50px;border-radius:30px}#app.login-app .ant-input-group-addon{border-radius:0 30px 30px 0;width:50px;font-size:18px}#app.login-app .ant-input-affix-wrapper .ant-input-prefix{left:23px}#app.login-app .ant-input-affix-wrapper .ant-input:not(:first-child){padding-left:50px}#app.login-app .centered{display:flex;text-align:center;align-items:center;justify-content:center;width:100%}#app.login-app .title{font-size:2rem;margin-block-end:2rem}#app.login-app .title b{font-weight:bold!important}#app.login-app{overflow:hidden}#app.login-app #login{animation:charge 0.5s both;background-color:#fff;border-radius:2rem;padding:4rem 3rem;transition:all 0.3s;user-select:none;-webkit-user-select:none;-moz-user-select:none}#app.login-app #login:hover{box-shadow:0 2px 8px rgb(0 0 0 / .09)}@keyframes charge{from{transform:translateY(5rem);opacity:0}to{transform:translateY(0);opacity:1}}#app.login-app .under{background-color:#c7ebe2;z-index:0}#app.login-app.dark .under{background-color:var(--dark-color-login-wave)}#app.login-app.dark #login{background-color:var(--dark-color-surface-100)}#app.login-app.dark h1{color:#fff}#app.login-app .ant-btn-primary-login{width:100%}#app.login-app .ant-btn-primary-login:focus,#app.login-app .ant-btn-primary-login:hover{color:#fff;background-color:#065;border-color:#065;background-image:linear-gradient(270deg,#fff0 30%,#009980,#fff0 100%);background-repeat:no-repeat;animation:ma-bg-move ease-in-out 5s infinite;background-position-x:-500px;width:95%;animation-delay:-0.5s;box-shadow:0 2px 0 rgb(0 0 0 / .045)}#app.login-app .ant-btn-primary-login.active,#app.login-app .ant-btn-primary-login:active{color:#fff;background-color:#065;border-color:#065}@keyframes ma-bg-move{0%{background-position:-500px 0}50%{background-position:1000px 0}100%{background-position:1000px 0}}#app.login-app .wave-btn-bg{position:relative;border-radius:25px;width:100%;transition:all 0.3s cubic-bezier(.645,.045,.355,1)}#app.login-app.dark .wave-btn-bg{color:#fff;position:relative;background-color:#0a7557;border:2px double #fff0;background-origin:border-box;background-clip:padding-box,border-box;background-size:300%;width:100%;z-index:1}#app.login-app.dark .wave-btn-bg:hover{animation:wave-btn-tara 4s ease infinite}#app.login-app.dark .wave-btn-bg-cl{background-image:linear-gradient(#fff0,#fff0),radial-gradient(circle at left top,#006655,#009980,#006655)!important;border-radius:3em}#app.login-app.dark .wave-btn-bg-cl:hover{width:95%}#app.login-app.dark .wave-btn-bg-cl:before{position:absolute;content:"";top:-5px;left:-5px;bottom:-5px;right:-5px;z-index:-1;background:inherit;background-size:inherit;border-radius:4em;opacity:0;transition:0.5s}#app.login-app.dark .wave-btn-bg-cl:hover::before{opacity:1;filter:blur(20px);animation:wave-btn-tara 8s linear infinite}@keyframes wave-btn-tara{to{background-position:300%}}#app.login-app.dark .ant-btn-primary-login{font-size:14px;color:#fff;text-align:center;background-image:linear-gradient(rgb(13 14 33 / .45),rgb(13 14 33 / .35));border-radius:2rem;border:none;outline:none;background-color:#fff0;height:46px;position:relative;white-space:nowrap;cursor:pointer;touch-action:manipulation;padding:0 15px;width:100%;animation:none;background-position-x:0;box-shadow:none}#app.login-app .waves-header{position:fixed;width:100%;text-align:center;background-color:#dbf5ed;color:#fff;z-index:-1}#app.login-app.dark .waves-header{background-color:var(--dark-color-login-background)}#app.login-app .waves-inner-header{height:50vh;width:100%;margin:0;padding:0}#app.login-app .waves{position:relative;width:100%;height:15vh;margin-bottom:-8px;min-height:100px;max-height:150px}#app.login-app .parallax>use{animation:move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite}#app.login-app.dark .parallax>use{fill:var(--dark-color-login-wave)}#app.login-app .parallax>use:nth-child(1){animation-delay:-2s;animation-duration:4s;opacity:.2}#app.login-app .parallax>use:nth-child(2){animation-delay:-3s;animation-duration:7s;opacity:.4}#app.login-app .parallax>use:nth-child(3){animation-delay:-4s;animation-duration:10s;opacity:.6}#app.login-app .parallax>use:nth-child(4){animation-delay:-5s;animation-duration:13s}@keyframes move-forever{0%{transform:translate3d(-90px,0,0)}100%{transform:translate3d(85px,0,0)}}@media (max-width:768px){#app.login-app .waves{height:40px;min-height:40px}}#app.login-app .words-wrapper{width:100%;display:inline-block;position:relative;text-align:center}#app.login-app .words-wrapper b{width:100%;display:inline-block;position:absolute;left:0;top:0}#app.login-app .words-wrapper b.is-visible{position:relative}#app.login-app .headline.zoom .words-wrapper{-webkit-perspective:300px;-moz-perspective:300px;perspective:300px}#app.login-app .headline{display:flex;justify-content:center;align-items:center}#app.login-app .headline.zoom b{opacity:0}#app.login-app .headline.zoom b.is-visible{opacity:1;-webkit-animation:zoom-in 0.8s;-moz-animation:zoom-in 0.8s;animation:cubic-bezier(.215,.61,.355,1) zoom-in 0.8s}#app.login-app .headline.zoom b.is-hidden{-webkit-animation:zoom-out 0.8s;-moz-animation:zoom-out 0.8s;animation:cubic-bezier(.215,.61,.355,1) zoom-out 0.4s}@-webkit-keyframes zoom-in{0%{opacity:0;-webkit-transform:translateZ(100px)}100%{opacity:1;-webkit-transform:translateZ(0)}}@-moz-keyframes zoom-in{0%{opacity:0;-moz-transform:translateZ(100px)}100%{opacity:1;-moz-transform:translateZ(0)}}@keyframes zoom-in{0%{opacity:0;-webkit-transform:translateZ(100px);-moz-transform:translateZ(100px);-ms-transform:translateZ(100px);-o-transform:translateZ(100px);transform:translateZ(100px)}100%{opacity:1;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}}@-webkit-keyframes zoom-out{0%{opacity:1;-webkit-transform:translateZ(0)}100%{opacity:0;-webkit-transform:translateZ(-100px)}}@-moz-keyframes zoom-out{0%{opacity:1;-moz-transform:translateZ(0)}100%{opacity:0;-moz-transform:translateZ(-100px)}}@keyframes zoom-out{0%{opacity:1;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}100%{opacity:0;-webkit-transform:translateZ(-100px);-moz-transform:translateZ(-100px);-ms-transform:translateZ(-100px);-o-transform:translateZ(-100px);transform:translateZ(-100px)}}#app.login-app .setting-section{position:absolute;top:0;right:0;padding:22px}#app.login-app .ant-space-item .ant-switch{margin:2px 0 4px}#app.login-app .ant-layout-content{transition:none}.inbounds-page .ant-table:not(.ant-table-expanded-row .ant-table){outline:1px solid #f0f0f0;outline-offset:-1px;border-radius:1rem;overflow-x:hidden}.inbounds-page.dark .ant-table:not(.ant-table-expanded-row .ant-table){outline-color:var(--dark-color-table-ring)}.inbounds-page .ant-table .ant-table-content .ant-table-scroll .ant-table-body{overflow-y:hidden}.inbounds-page .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper{margin:-10px 22px!important}.inbounds-page .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table{border-bottom-left-radius:1rem;border-bottom-right-radius:1rem}.inbounds-page .ant-table .ant-table-content .ant-table-tbody tr:last-child tr:last-child td{border-bottom-color:#fff0}.inbounds-page .ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:first-child{border-bottom-left-radius:6px}.inbounds-page .ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:last-child{border-bottom-right-radius:6px}@media (min-width:769px){.inbounds-page .ant-layout-content{margin:24px 16px}}@media (max-width:768px){.inbounds-page .ant-card-body{padding:.5rem}.inbounds-page .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper{margin:-10px 2px!important}}.inbounds-page.dark~div .ant-switch-small:not(.ant-switch-checked){background-color:var(--dark-color-surface-100)}.inbounds-page .ant-custom-popover-title{display:flex;align-items:center;gap:10px;margin:5px 0}.inbounds-page .ant-col-sm-24{margin:.5rem -2rem .5rem 2rem}.inbounds-page tr.hideExpandIcon .ant-table-row-expand-icon{display:none}.inbounds-page .infinite-tag,.inbounds-page~div .infinite-tag{padding:0 5px;border-radius:2rem;min-width:50px;min-height:22px}.inbounds-page .infinite-bar .ant-progress-inner .ant-progress-bg{background-color:#F2EAF1;border:#D5BED2 solid 1px}.inbounds-page.dark .infinite-bar .ant-progress-inner .ant-progress-bg{background-color:#7a316f!important;border:#7a316f solid 1px}.inbounds-page~div .ant-collapse{margin:5px 0}.inbounds-page .info-large-tag,.inbounds-page~div .info-large-tag{max-width:200px;overflow:hidden}.inbounds-page .client-comment{font-size:12px;opacity:.75;cursor:help}.inbounds-page .client-email{font-weight:500}.inbounds-page .client-popup-item{display:flex;align-items:center;gap:5px}.inbounds-page .online-animation .ant-badge-status-dot{animation:onlineAnimation 1.2s linear infinite}@keyframes onlineAnimation{0%,50%,100%{transform:scale(1);opacity:1}10%{transform:scale(1.5);opacity:.2}}.inbounds-page .tr-table-box{display:flex;gap:4px;justify-content:center;align-items:center}.inbounds-page .tr-table-rt{flex-basis:70px;min-width:70px;text-align:end}.inbounds-page .tr-table-lt{flex-basis:70px;min-width:70px;text-align:start}.inbounds-page .tr-table-bar{flex-basis:160px;min-width:60px}.inbounds-page .tr-infinity-ch{font-size:14pt;max-height:24px;display:inline-flex;align-items:center}.inbounds-page .ant-table-expanded-row .ant-table .ant-table-body{overflow-x:hidden}.inbounds-page .ant-table-expanded-row .ant-table-tbody>tr>td{padding:10px 2px}.inbounds-page .ant-table-expanded-row .ant-table-thead>tr>th{padding:12px 2px}.idx-cpu-history-svg{display:block;overflow:unset!important}.dark .idx-cpu-history-svg .cpu-grid-line{stroke:rgb(255 255 255 / .08)}.dark .idx-cpu-history-svg .cpu-grid-h-line{stroke:rgb(255 255 255 / .25)}.dark .idx-cpu-history-svg .cpu-grid-y-text,.dark .idx-cpu-history-svg .cpu-grid-x-text{fill:rgb(200 200 200 / .8)}.idx-cpu-history-svg .cpu-grid-text{stroke-width:3;paint-order:stroke;stroke:rgb(0 0 0 / .05)}.dark .idx-cpu-history-svg .cpu-grid-text{fill:#fff;stroke:rgb(0 0 0 / .35)}.inbounds-page~div #inbound-modal form textarea.ant-input{margin:4px 0}@media (min-width:769px){.settings-page .ant-layout-content{margin:24px 16px}}@media (max-width:768px){.settings-page .ant-tabs-nav .ant-tabs-tab{margin:0;padding:12px .5rem}}.settings-page .ant-tabs-bar{margin:0}.settings-page .ant-list-item{display:block}.settings-page .alert-msg{color:#c27512;font-weight:400;font-size:16px;padding:.5rem 1rem;text-align:center;background:rgb(255 145 0 / 15%);margin:1.5rem 2.5rem 0rem;border-radius:.5rem;transition:all 0.5s;animation:settings-page-signal 3s cubic-bezier(.18,.89,.32,1.28) infinite}.settings-page .alert-msg:hover{cursor:default;transition-duration:.3s;animation:settings-page-signal 0.9s ease infinite}@keyframes settings-page-signal{0%{box-shadow:0 0 0 0 rgb(194 118 18 / .5)}50%{box-shadow:0 0 0 6px #fff0}100%{box-shadow:0 0 0 6px #fff0}}.settings-page .alert-msg>i{color:inherit;font-size:24px}.settings-page.dark .ant-input-password-icon{color:var(--dark-color-text-primary)}.settings-page .ant-collapse-content-box .ant-alert{margin-block-end:12px}@media (min-width:769px){.xray-page .ant-layout-content{margin:24px 16px}}@media (max-width:768px){.xray-page .ant-tabs-nav .ant-tabs-tab{margin:0;padding:12px .5rem}.xray-page .ant-table-thead>tr>th,.xray-page .ant-table-tbody>tr>td{padding:10px 0}}.xray-page .ant-tabs-bar{margin:0}.xray-page .ant-list-item{display:block}.xray-page .ant-list-item>li{padding:10px 20px!important}.xray-page .ant-collapse-content-box .ant-alert{margin-block-end:12px}#app.login-app #login input.ant-input:-webkit-autofill{-webkit-box-shadow:0 0 0 100px #f8f8f8 inset;box-shadow:0 0 0 100px #f8f8f8 inset;transition:background-color 9999s ease-in-out 0s,color 9999s ease-in-out 0s;background-clip:text}#app.login-app #login .ant-input-affix-wrapper:hover .ant-input:-webkit-autofill:not(.ant-input-disabled),#app.login-app #login input.ant-input:-webkit-autofill:hover,#app.login-app #login input.ant-input:-webkit-autofill:focus{-webkit-box-shadow:0 0 0 100px #e8f4f2 inset;box-shadow:0 0 0 100px #e8f4f2 inset}#app.login-app.dark #login .ant-input-affix-wrapper:hover .ant-input:-webkit-autofill:not(.ant-input-disabled),#app.login-app.dark #login input.ant-input:-webkit-autofill{-webkit-text-fill-color:var(--dark-color-text-primary);caret-color:var(--dark-color-text-primary);-webkit-box-shadow:0 0 0 1000px var(--dark-color-surface-200) inset;box-shadow:0 0 0 1000px var(--dark-color-surface-200) inset;transition:background-color 9999s ease-in-out 0s,color 9999s ease-in-out 0s}#app.login-app.dark #login .ant-input-affix-wrapper:hover .ant-input:-webkit-autofill:not(.ant-input-disabled),#app.login-app.dark #login input.ant-input:-webkit-autofill:hover,#app.login-app.dark #login input.ant-input:-webkit-autofill:focus{border-color:var(--dark-color-surface-300)}.dark .ant-descriptions-bordered .ant-descriptions-item-label{background-color:var(--dark-color-background)}.dark .ant-descriptions-bordered .ant-descriptions-view,.dark .ant-descriptions-bordered .ant-descriptions-row,.dark .ant-descriptions-bordered .ant-descriptions-item-label,.dark .ant-list-bordered{border-color:var(--dark-color-surface-400)}.dark .ant-descriptions-bordered .ant-descriptions-item-label,.dark .ant-descriptions-bordered .ant-descriptions-item-content{color:var(--dark-color-text-primary)}.dark .ant-dropdown-menu{background-color:var(--dark-color-surface-200)}.dark .ant-dropdown-menu .ant-dropdown-menu-item{color:hsl(0 0% 100% / .65)}.dark .ant-dropdown-menu .ant-dropdown-menu-item:hover{background-color:var(--dark-color-surface-600)}.subscription-page .ant-list.ant-list-split.ant-list-bordered{overflow:hidden}.subscription-page .ant-list.ant-list-split.ant-list-bordered .ant-list-item{overflow-x:auto}.subscription-page .ant-btn.ant-btn-primary.ant-btn-lg.ant-dropdown-trigger{border-radius:4rem;padding:0 20px}.subscription-page .subscription-card{margin:2rem 0}.mb-10{margin-bottom:10px}.mb-12{margin-bottom:12px}.mt-5{margin-top:5px}.mr-8{margin-right:8px}.ml-10{margin-left:10px}.mr-05{margin-right:.5rem}.fs-1rem{font-size:1rem}.w-100{width:100%}.w-70{width:70px}.w-95{width:95px}.text-center{text-align:center}.cursor-pointer{cursor:pointer}.float-right{float:right}.va-middle{vertical-align:middle}.d-flex{display:flex}.justify-end{justify-content:flex-end}.max-w-400{max-width:400px;display:inline-block}.ant-space.jc-center{justify-content:center}.min-h-0{min-height:0}.min-h-100vh{min-height:100vh}.h-100{height:100%}.h-50px{height:50px}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-auto{overflow-x:auto}.mt-1rem{margin-top:1rem}.my-3rem{margin-top:3rem;margin-bottom:3rem} \ No newline at end of file +:root{--color-primary-100:#008771;--dark-color-background:#0a1222;--dark-color-surface-100:#151f31;--dark-color-surface-200:#222d42;--dark-color-surface-300:#2c3950;--dark-color-surface-400:rgba(65, 85, 119, 0.5);--dark-color-surface-500:#2c3950;--dark-color-surface-600:#313f5a;--dark-color-surface-700:#111929;--dark-color-surface-700-rgb:17, 25, 41;--dark-color-table-hover:rgba(44, 57, 80, 0.2);--dark-color-text-primary:rgba(255, 255, 255, 0.75);--dark-color-stroke:#2c3950;--dark-color-btn-danger:#cd3838;--dark-color-btn-danger-border:transparent;--dark-color-btn-danger-hover:#e94b4b;--dark-color-tag-bg:rgba(255, 255, 255, 0.05);--dark-color-tag-border:rgba(255, 255, 255, 0.15);--dark-color-tag-color:rgba(255, 255, 255, 0.75);--dark-color-tag-green-bg:17, 36, 33;--dark-color-tag-green-border:25, 81, 65;--dark-color-tag-green-color:#3ad3ba;--dark-color-tag-purple-bg:#201425;--dark-color-tag-purple-border:#5a2969;--dark-color-tag-purple-color:#d988cd;--dark-color-tag-red-bg:#291515;--dark-color-tag-red-border:#5c2626;--dark-color-tag-red-color:#e04141;--dark-color-tag-orange-bg:#312313;--dark-color-tag-orange-border:#593914;--dark-color-tag-orange-color:#ffa031;--dark-color-tag-blue-bg:#111a2c;--dark-color-tag-blue-border:#1348ab;--dark-color-tag-blue-color:#529fff;--dark-color-codemirror-line-hover:rgba(0, 135, 113, 0.2);--dark-color-codemirror-line-selection:rgba(0, 135, 113, 0.3);--dark-color-login-background:var(--dark-color-background);--dark-color-login-wave:var(--dark-color-surface-200);--dark-color-tooltip:rgba(61, 76, 104, 0.9);--dark-color-back-top:rgba(61, 76, 104, 0.9);--dark-color-back-top-hover:rgba(61, 76, 104, 1);--dark-color-scrollbar:#313f5a;--dark-color-scrollbar-webkit:#7484a0;--dark-color-scrollbar-webkit-hover:#90a4c7;--dark-color-table-ring:rgb(38 52 77);--dark-color-spin-container:#151f31}html[data-theme-animations='off']{.ant-menu,.ant-layout-sider,.ant-card,.ant-tag,.ant-progress-circle>*,.ant-input,.ant-table-row-expand-icon,.ant-switch,.ant-table-thead>tr>th,.ant-select-selection,.ant-btn,.ant-input-number,.ant-input-group-addon,.ant-checkbox-inner,.ant-progress-bg,.ant-progress-success-bg,.ant-radio-button-wrapper:not(:first-child):before,.ant-radio-button-wrapper,#login,.cm-s-xq.CodeMirror{transition:border 0s,background 0s!important}.ant-menu.ant-menu-inline .ant-menu-item:not(.ant-menu-sub .ant-menu-item),.ant-layout-sider-trigger,.ant-alert-close-icon .anticon-close,.ant-tabs-nav .ant-tabs-tab,.ant-input-number-input,.ant-collapse>.ant-collapse-item>.ant-collapse-header,.Line-Hover,.ant-menu-theme-switch,.ant-menu-submenu-title{transition:color 0s!important}.wave-btn-bg{transition:width 0s!important}}html[data-theme='ultra-dark']{--dark-color-background:#21242a;--dark-color-surface-100:#0c0e12;--dark-color-surface-200:#222327;--dark-color-surface-300:#32353b;--dark-color-surface-400:rgba(255, 255, 255, 0.1);--dark-color-surface-500:#3b404b;--dark-color-surface-600:#505663;--dark-color-surface-700:#101113;--dark-color-surface-700-rgb:16, 17, 19;--dark-color-table-hover:rgba(89, 89, 89, 0.15);--dark-color-text-primary:rgb(255 255 255 / 85%);--dark-color-stroke:#202025;--dark-color-tag-green-bg:17, 36, 33;--dark-color-tag-green-border:29, 95, 77;--dark-color-tag-green-color:#59cbac;--dark-color-tag-purple-bg:#241121;--dark-color-tag-purple-border:#5a2969;--dark-color-tag-purple-color:#d686ca;--dark-color-tag-red-bg:#2a1215;--dark-color-tag-red-border:#58181c;--dark-color-tag-red-color:#e84749;--dark-color-tag-orange-bg:#2b1d11;--dark-color-tag-orange-border:#593815;--dark-color-tag-orange-color:#e89a3c;--dark-color-tag-blue-bg:#111a2c;--dark-color-tag-blue-border:#0f367e;--dark-color-tag-blue-color:#3c89e8;--dark-color-codemirror-line-hover:rgba(82, 84, 94, 0.2);--dark-color-codemirror-line-selection:rgba(82, 84, 94, 0.3);--dark-color-login-background:#0a2227;--dark-color-login-wave:#0f2d32;--dark-color-tooltip:rgba(88, 93, 100, 0.9);--dark-color-back-top:rgba(88, 93, 100, 0.9);--dark-color-back-top-hover:rgba(88, 93, 100, 1);--dark-color-scrollbar:rgb(107,107,107);--dark-color-scrollbar-webkit:#9f9f9f;--dark-color-scrollbar-webkit-hover:#d1d1d1;--dark-color-table-ring:rgb(37 39 42);--dark-color-spin-container:#1d1d1d;.ant-dropdown-menu-dark,.dark .ant-dropdown-menu{background-color:var(--dark-color-surface-500)}.dark .ant-dropdown-menu-submenu-title:hover,.dark .ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled),.dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled){background-color:rgb(0 93 78 / .3)}.dark .waves-header{background-color:#0a2227}.dark .ant-calendar-year-panel-year:hover,.dark .ant-calendar-month-panel-month:hover,.dark .ant-calendar-decade-panel-decade:hover{background-color:var(--dark-color-surface-600)}}html,body{height:100vh;width:100vw;margin:0;padding:0;overflow:hidden}body{color:rgb(0 0 0 / .65);font-size:14px;font-variant:tabular-nums;line-height:1.5;background-color:#fff;font-feature-settings:"tnum"}html{--antd-wave-shadow-color:var(--color-primary-100);line-height:1.15;text-size-adjust:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-moz-tap-highlight-color:#fff0;-webkit-tap-highlight-color:#fff0}@supports (scrollbar-width:auto) and (not selector(::-webkit-scrollbar)){:not(.dark){scrollbar-color:#9a9a9a #fff0;scrollbar-width:thin}.dark *{scrollbar-color:var(--dark-color-scrollbar) #fff0;scrollbar-width:thin}}::-webkit-scrollbar{width:10px;height:10px;background-color:#fff0}::-webkit-scrollbar-track{background-color:#fff0;margin-block:.5em}.ant-modal-wrap::-webkit-scrollbar-track{background-color:#fff;margin-block:0}::-webkit-scrollbar-thumb{border-radius:9999px;background-color:#9a9a9a;border:2px solid #fff0;background-clip:content-box}::-webkit-scrollbar-thumb:hover,::-webkit-scrollbar-thumb:active{background-color:#828282}.dark .ant-modal-wrap::-webkit-scrollbar-track{background-color:var(--dark-color-background)}.dark::-webkit-scrollbar-thumb{background-color:var(--dark-color-scrollbar-webkit)}.dark::-webkit-scrollbar-thumb:hover,.dark::-webkit-scrollbar-thumb:active{background-color:var(--dark-color-scrollbar-webkit-hover)}::-moz-selection{color:var(--color-primary-100);background-color:#cfe8e4}::selection{color:var(--color-primary-100);background-color:#cfe8e4}#app{height:100%;position:fixed;top:0;left:0;right:0;bottom:0;margin:0;padding:0;overflow:auto}.ant-layout,.ant-layout *{box-sizing:border-box}.ant-spin-container:after{border-radius:1.5rem}.dark .ant-spin-container:after{background:var(--dark-color-spin-container)}style attribute{text-align:center}.ant-table-thead>tr>th{padding:12px 8px}.ant-table-tbody>tr>td{padding:10px 8px}.ant-table-thead>tr>th{color:rgb(0 0 0 / .85);font-weight:500;text-align:left;border-bottom:1px solid #e8e8e8;transition:background 0.3s ease}.ant-table table{border-radius:1rem}.ant-table-bordered .ant-table-tbody:not(.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody)>tr:last-child>td:first-child{border-bottom-left-radius:1rem}.ant-table-bordered .ant-table-tbody:not(.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody)>tr:last-child>td:last-child{border-bottom-right-radius:1rem}.ant-table{box-sizing:border-box;margin:0;padding:0;color:rgb(0 0 0 / .65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;font-feature-settings:"tnum";position:relative;clear:both}.ant-table .ant-table-body:not(.ant-table-expanded-row .ant-table-body){overflow-x:auto!important}.ant-card-hoverable{cursor:auto;cursor:pointer}.ant-card{box-sizing:border-box;margin:0;padding:0;color:rgb(0 0 0 / .65);font-size:14px;font-variant:tabular-nums;line-height:1.5;list-style:none;position:relative;background-color:#fff;border-radius:2px;transition:all 0.3s}.ant-space{width:100%}.ant-layout-sider-zero-width-trigger{display:none}@media (max-width:768px){.ant-layout-sider{display:none}.ant-card,.ant-alert-error{margin:.5rem}.ant-tabs{margin:.5rem;padding:.5rem}.ant-modal-body{padding:20px}.ant-form-item-label{line-height:1.5;padding:8px 0 0}:not(.dark)::-webkit-scrollbar{width:8px;height:8px;background-color:#fff0}.dark::-webkit-scrollbar{width:8px;height:8px;background-color:#fff0}}.ant-layout-content{min-height:auto}.ant-card,.ant-tabs{border-radius:1.5rem}.ant-card-hoverable{cursor:auto}.ant-card+.ant-card{margin-top:20px}.drawer-handle{position:absolute;top:72px;width:41px;height:40px;cursor:pointer;z-index:0;text-align:center;line-height:40px;font-size:16px;display:flex;justify-content:center;align-items:center;background-color:#fff;right:-40px;box-shadow:2px 0 8px rgb(0 0 0 / .15);border-radius:0 4px 4px 0}.ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{background-color:#006655!important;background-image:linear-gradient(270deg,#fff0 30%,#009980,#fff0 100%);background-repeat:no-repeat;animation:ma-bg-move linear 6.6s infinite;color:#fff;border-radius:.5rem}.ant-layout-sider-collapsed .ant-menu:not(.ant-menu-horizontal) .ant-menu-item-selected{border-radius:0}.ant-menu-item-active,.ant-menu-item:hover,.ant-menu-submenu-title:hover,.ant-menu-item:active,.ant-menu-submenu-title:active{color:var(--color-primary-100);background-color:#e8f4f2}.ant-menu-inline .ant-menu-item,.ant-menu-inline .ant-menu-submenu-title{border-radius:.5rem}.ant-menu-inline .ant-menu-item:after,.ant-menu{border-right-width:0}.ant-layout-sider-children,.ant-pagination ul{padding:.5rem}.ant-layout-sider-collapsed .ant-layout-sider-children{padding:.5rem 0}.ant-dropdown-menu,.ant-select-dropdown-menu{padding:.5rem}.ant-dropdown-menu-item,.ant-dropdown-menu-item:hover,.ant-select-dropdown-menu-item,.ant-select-dropdown-menu-item:hover,.ant-select-selection--multiple .ant-select-selection__choice{border-radius:.5rem}.ant-select-dropdown--multiple .ant-select-dropdown-menu .ant-select-dropdown-menu-item,.ant-select-dropdown--single .ant-select-dropdown-menu .ant-select-dropdown-menu-item-selected{margin-block:2px}@media (min-width:769px){.drawer-handle{display:none}.ant-tabs{padding:2rem}}.fade-in-enter,.fade-in-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active,.fade-in-linear-enter,.fade-in-linear-leave,.fade-in-linear-leave-active{opacity:0}.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity 0.2s linear;transition:opacity 0.2s linear}.fade-in-enter-active,.fade-in-leave-active{-webkit-transition:all 0.3s cubic-bezier(.55,0,.1,1);transition:all 0.3s cubic-bezier(.55,0,.1,1)}.zoom-in-center-enter-active,.zoom-in-center-leave-active{-webkit-transition:all 0.3s cubic-bezier(.55,0,.1,1);transition:all 0.3s cubic-bezier(.55,0,.1,1)}.zoom-in-center-enter,.zoom-in-center-leave-active{opacity:0;-webkit-transform:scaleX(0);transform:scaleX(0)}.zoom-in-top-enter-active,.zoom-in-top-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center top;transform-origin:center top}.zoom-in-top-enter,.zoom-in-top-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.zoom-in-bottom-enter-active,.zoom-in-bottom-leave-active{opacity:1;-webkit-transform:scaleY(1);transform:scaleY(1);-webkit-transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:center bottom;transform-origin:center bottom}.zoom-in-bottom-enter,.zoom-in-bottom-leave-active{opacity:0;-webkit-transform:scaleY(0);transform:scaleY(0)}.zoom-in-left-enter-active,.zoom-in-left-leave-active{opacity:1;-webkit-transform:scale(1,1);transform:scale(1,1);-webkit-transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1);transition:transform 0.3s cubic-bezier(.23,1,.32,1),opacity 0.3s cubic-bezier(.23,1,.32,1),-webkit-transform 0.3s cubic-bezier(.23,1,.32,1);-webkit-transform-origin:top left;transform-origin:top left}.zoom-in-left-enter,.zoom-in-left-leave-active{opacity:0;-webkit-transform:scale(.45,.45);transform:scale(.45,.45)}.list-enter-active,.list-leave-active{-webkit-transition:all 0.3s;transition:all 0.3s}.list-enter,.list-leave-active{opacity:0;-webkit-transform:translateY(-30px);transform:translateY(-30px)}.ant-tooltip-inner{min-height:0;padding-inline:1rem}.ant-list-item-meta-title{font-size:14px}.ant-progress-inner{background-color:#ebeef5}.deactive-client .ant-collapse-header{color:#ffffff!important;background-color:#ff7f7f}.ant-table-expand-icon-th,.ant-table-row-expand-icon-cell{width:30px;min-width:30px}.ant-tabs{background-color:#fff}.ant-form-item{margin-bottom:0}.ant-setting-textarea{margin-top:1.5rem}.client-table-header{background-color:#f0f2f5}.client-table-odd-row{background-color:#fafafa}.ant-table-pagination.ant-pagination{float:left}.ant-tag{margin-right:0;margin-inline:2px;display:inline-flex;align-items:center;justify-content:space-evenly}.ant-tag:not(.qr-tag){column-gap:4px}#inbound-info-modal .ant-tag{margin-block:2px}.tr-info-table{display:inline-table;margin-block:10px;width:100%}#inbound-info-modal .tr-info-table .ant-tag{margin-block:0;margin-inline:0}.tr-info-row{display:flex;flex-direction:column;row-gap:2px;margin-block:10px}.tr-info-row a{margin-left:6px}.tr-info-row code{padding-inline:8px;max-height:80px;overflow-y:auto}.tr-info-tag{max-width:100%;text-wrap:balance;overflow:hidden;overflow-wrap:anywhere}.tr-info-title{display:inline-flex;align-items:center;justify-content:flex-start;column-gap:4px}.ant-tag-blue{background-color:#edf4fa;border-color:#a9c5e7;color:#0e49b5}.ant-tag-green{background-color:#eafff9;border-color:#76ccb4;color:#199270}.ant-tag-purple{background-color:#f2eaf1;border-color:#d5bed2;color:#7a316f}.ant-tag-orange,.ant-alert-warning{background-color:#ffeee1;border-color:#fec093;color:#f37b24}.ant-tag-red,.ant-alert-error{background-color:#ffe9e9;border-color:#ff9e9e;color:#cf3c3c}.ant-input::placeholder{opacity:.5}.ant-input:hover,.ant-input:focus{background-color:#e8f4f2}.ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled){background-color:#e8f4f2}.delete-icon:hover{color:#e04141}.normal-icon:hover{color:var(--color-primary-100)}.dark ::-moz-selection{color:#fff;background-color:var(--color-primary-100)}.dark ::selection{color:#fff;background-color:var(--color-primary-100)}.dark .normal-icon:hover{color:#fff}.dark .ant-layout-sider,.dark .ant-drawer-content,.ant-menu-dark,.ant-menu-dark .ant-menu-sub,.dark .ant-card,.dark .ant-table,.dark .ant-collapse-content,.dark .ant-tabs{background-color:var(--dark-color-surface-100);color:var(--dark-color-text-primary)}.dark .ant-card-hoverable:hover,.dark .ant-space-item>.ant-tabs:hover{box-shadow:0 2px 8px #fff0}.dark>.ant-layout,.dark .drawer-handle,.dark .ant-table-thead>tr>th,.dark .ant-table-expanded-row,.dark .ant-table-expanded-row:hover,.dark .ant-table-expanded-row .ant-table-tbody,.dark .ant-calendar{background-color:var(--dark-color-background);color:var(--dark-color-text-primary)}.dark .ant-table-expanded-row .ant-table-thead>tr:first-child>th{border-radius:0}.dark .ant-calendar,.dark .ant-card-bordered{border-color:var(--dark-color-background)}.dark .ant-table-bordered,.dark .ant-table-bordered.ant-table-empty .ant-table-placeholder,.dark .ant-table-bordered .ant-table-body>table,.dark .ant-table-bordered .ant-table-fixed-left table,.dark .ant-table-bordered .ant-table-fixed-right table,.dark .ant-table-bordered .ant-table-header>table,.dark .ant-table-bordered .ant-table-thead>tr:not(:last-child)>th,.dark .ant-table-bordered .ant-table-tbody>tr>td,.dark .ant-table-bordered .ant-table-thead>tr>th{border-color:var(--dark-color-surface-400)}.dark .ant-table-tbody>tr>td,.dark .ant-table-thead>tr>th,.dark .ant-card-head,.dark .ant-modal-header,.dark .ant-collapse>.ant-collapse-item,.dark .ant-tabs-bar,.dark .ant-list-split .ant-list-item,.dark .ant-popover-title,.dark .ant-calendar-header,.dark .ant-calendar-input-wrap{border-bottom-color:var(--dark-color-surface-400)}.dark .ant-modal-footer,.dark .ant-collapse-content,.dark .ant-calendar-footer,.dark .ant-divider-horizontal.ant-divider-with-text-left:before,.dark .ant-divider-horizontal.ant-divider-with-text-left:after,.dark .ant-divider-horizontal.ant-divider-with-text-center:before,.dark .ant-divider-horizontal.ant-divider-with-text-center:after{border-top-color:var(--dark-color-surface-300)}.ant-divider-horizontal.ant-divider-with-text-left:before{width:10%}.dark .ant-progress-text,.dark .ant-card-head,.dark .ant-form,.dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,.dark .ant-modal-close-x,.dark .ant-form .anticon,.dark .ant-tabs-tab-arrow-show:not(.ant-tabs-tab-btn-disabled),.dark .anticon-close,.dark .ant-list-item-meta-title,.dark .ant-select-selection i,.dark .ant-modal-confirm-title,.dark .ant-modal-confirm-content,.dark .ant-popover-message,.dark .ant-modal,.dark .ant-divider-inner-text,.dark .ant-popover-title,.dark .ant-popover-inner-content,.dark h2,.dark .ant-modal-title,.dark .ant-form-item-label>label,.dark .ant-checkbox-wrapper,.dark .ant-form-item,.dark .ant-calendar-footer .ant-calendar-today-btn,.dark .ant-calendar-footer .ant-calendar-time-picker-btn,.dark .ant-calendar-day-select,.dark .ant-calendar-month-select,.dark .ant-calendar-year-select,.dark .ant-calendar-date,.dark .ant-calendar-year-panel-year,.dark .ant-calendar-month-panel-month,.dark .ant-calendar-decade-panel-decade{color:var(--dark-color-text-primary)}.dark .ant-pagination-options-size-changer .ant-select-arrow .anticon.anticon-down.ant-select-arrow-icon{color:rgb(255 255 255 / 35%)}.dark .ant-pagination-item a,.dark .ant-pagination-next a,.dark .ant-pagination-prev a{color:var(--dark-color-text-primary)}.dark .ant-pagination-item:focus a,.dark .ant-pagination-item:hover a,.dark .ant-pagination-item-active a,.dark .ant-pagination-next:hover .ant-pagination-item-link{color:var(--color-primary-100)}.dark .ant-pagination-item-active{background-color:#fff0}.dark .ant-list-item-meta-description{color:rgb(255 255 255 / .45)}.dark .ant-pagination-disabled i,.dark .ant-tabs-tab-btn-disabled{color:rgb(255 255 255 / .25)}.dark .ant-input,.dark .ant-input-group-addon,.dark .ant-collapse,.dark .ant-select-selection,.dark .ant-input-number,.dark .ant-input-number-handler-wrap,.dark .ant-table-placeholder,.dark .ant-empty-normal,.dark .ant-select-dropdown,.dark .ant-select-dropdown li,.dark .ant-select-dropdown-menu-item,.dark .client-table-header,.dark .ant-select-selection--multiple .ant-select-selection__choice{background-color:var(--dark-color-surface-200);border-color:var(--dark-color-surface-300);color:var(--dark-color-text-primary)}.dark .ant-select-dropdown--multiple .ant-select-dropdown-menu .ant-select-dropdown-menu-item.ant-select-dropdown-menu-item-selected :not(.ant-dropdown-menu-submenu-title:hover){background-color:var(--dark-color-surface-300)}.dark .ant-select-dropdown-menu-item.ant-select-dropdown-menu-item-selected{background-color:var(--dark-color-surface-300)}.dark .ant-calendar-time-picker-inner{background-color:var(--dark-color-background)}.dark .ant-select-selection:hover,.dark .ant-calendar-picker-clear,.dark .ant-input-number:hover,.dark .ant-input-number:focus,.dark .ant-input:hover,.dark .ant-input:focus{background-color:rgb(0 135 113 / .3);border-color:var(--color-primary-100)}.dark .ant-input-affix-wrapper:hover .ant-input:not(.ant-input-disabled){border-color:var(--color-primary-100);background-color:rgb(0 135 113 / .3)}.dark .ant-btn:not(.ant-btn-primary):not(.ant-btn-danger){color:var(--dark-color-text-primary);background-color:rgb(10 117 87 / 30%);border:1px solid var(--color-primary-100)}.dark .ant-radio-button-wrapper,.dark .ant-radio-button-wrapper:before{color:var(--dark-color-text-primary);background-color:rgb(0 135 113 / .3);border-color:var(--color-primary-100)}.ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),.ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger){background-color:#e8f4f2}.dark .ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),.dark .ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger){color:#fff;background-color:rgb(10 117 87 / 50%);border-color:var(--color-primary-100)}.dark .ant-btn-primary[disabled],.dark .ant-btn-danger[disabled],.dark .ant-calendar-ok-btn-disabled{color:rgb(255 255 255 / 35%);background-color:var(--dark-color-surface-200);border-color:var(--dark-color-surface-300)}.dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.dark .client-table-odd-row{background-color:var(--dark-color-table-hover)}.dark .ant-table-row-expand-icon{color:#fff;background-color:#fff0;border-color:rgb(255 255 255 / 20%)}.dark .ant-table-row-expand-icon:hover{color:var(--color-primary-100);background-color:#fff0;border-color:var(--color-primary-100)}.dark .ant-switch:not(.ant-switch-checked),.dark .ant-progress-line .ant-progress-inner{background-color:var(--dark-color-surface-500)}.dark .ant-progress-circle-trail{stroke:var(--dark-color-stroke)!important}.dark .ant-popover-inner{background-color:var(--dark-color-surface-500)}.dark>.ant-popover-content>.ant-popover-arrow{border-color:var(--dark-color-surface-500)}@media (max-width:768px){.dark .ant-popover-inner{background-color:var(--dark-color-surface-200)}.dark>.ant-popover-content>.ant-popover-arrow{border-color:var(--dark-color-surface-200)}}.ant-dropdown-menu-dark .ant-dropdown-menu-item:hover,.dark .ant-select-dropdown-menu-item-selected,.dark .ant-calendar-time-picker-select-option-selected{background-color:var(--dark-color-surface-600)}.ant-menu-dark .ant-menu-item:hover,.ant-menu-dark .ant-menu-submenu-title:hover{background-color:var(--dark-color-surface-300)}.dark .ant-menu-item:active,.dark .ant-menu-submenu-title:active{color:#fff;background-color:var(--dark-color-surface-300)}.dark .ant-alert-message{color:rgb(255 255 255 / .85)}.dark .ant-tag{color:var(--dark-color-tag-color);background-color:var(--dark-color-tag-bg);border-color:var(--dark-color-tag-border)}.dark .ant-tag-blue{background-color:var(--dark-color-tag-blue-bg);border-color:var(--dark-color-tag-blue-border);color:var(--dark-color-tag-blue-color)}.dark .ant-tag-red,.dark .ant-alert-error{background-color:var(--dark-color-tag-red-bg);border-color:var(--dark-color-tag-red-border);color:var(--dark-color-tag-red-color)}.dark .ant-tag-orange,.dark .ant-alert-warning{background-color:var(--dark-color-tag-orange-bg);border-color:var(--dark-color-tag-orange-border);color:var(--dark-color-tag-orange-color)}.dark .ant-tag-green{background-color:rgb(var(--dark-color-tag-green-bg));border-color:rgb(var(--dark-color-tag-green-border));color:var(--dark-color-tag-green-color)}.dark .ant-tag-purple{background-color:var(--dark-color-tag-purple-bg);border-color:var(--dark-color-tag-purple-border);color:var(--dark-color-tag-purple-color)}.dark .ant-modal-content,.dark .ant-modal-header{background-color:var(--dark-color-surface-700)}.dark .ant-calendar-next-month-btn-day .ant-calendar-date,.dark .ant-calendar-last-month-cell .ant-calendar-date{color:var(--dark-color-surface-300)}.dark .ant-calendar-selected-day .ant-calendar-date{background-color:var(--color-primary-100)!important;color:#fff}.dark .ant-calendar-date:hover,.dark .ant-calendar-time-picker-select li:hover{background-color:var(--dark-color-surface-600);color:#fff}.dark .ant-calendar-header a:hover,.dark .ant-calendar-header a:hover::before,.dark .ant-calendar-header a:hover::after{border-color:#fff}.dark .ant-calendar-time-picker-select{border-right-color:var(--dark-color-surface-300)}.has-warning .ant-select-selection,.has-warning .ant-select-selection:hover,.has-warning .ant-input,.has-warning .ant-input:hover{background-color:#ffeee1;border-color:#fec093}.has-warning .ant-input::placeholder{color:#f37b24}.has-warning .ant-input:not([disabled]):hover{border-color:#fec093}.dark .has-warning .ant-select-selection,.dark .has-warning .ant-select-selection:hover,.dark .has-warning .ant-input,.dark .has-warning .ant-input:hover{border-color:#784e1d;background:#312313}.dark .has-warning .ant-input::placeholder{color:rgb(255 160 49 / 70%)}.dark .has-warning .anticon{color:#ffa031}.dark .has-success .anticon{color:var(--color-primary-100);animation-name:diffZoomIn1!important}.dark .anticon-close-circle{color:#e04141}.dark .ant-spin-nested-loading>div>.ant-spin .ant-spin-text{text-shadow:0 1px 2px #0007}.dark .ant-spin{color:#fff}.dark .ant-spin-dot-item{background-color:#fff}.ant-checkbox-wrapper,.ant-input-group-addon,.ant-tabs-tab,.ant-input::placeholder,.ant-collapse-header,.ant-menu,.ant-radio-button-wrapper{-webkit-user-select:none;user-select:none}.ant-calendar-date,.ant-calendar-year-panel-year,.ant-calendar-decade-panel-decade,.ant-calendar-month-panel-month{border-radius:4px}.ant-checkbox-inner,.ant-checkbox-checked:after,.ant-table-row-expand-icon{border-radius:6px}.ant-calendar-date:hover{background-color:#e8f4f2}.ant-calendar-date:active{background-color:#e8f4f2;color:rgb(0 0 0 / .65)}.ant-calendar-today .ant-calendar-date{color:var(--color-primary-100);font-weight:400;border-color:var(--color-primary-100)}.dark .ant-calendar-today .ant-calendar-date{color:#fff;border-color:var(--color-primary-100)}.ant-calendar-selected-day .ant-calendar-date{background:var(--color-primary-100);color:#fff}li.ant-select-dropdown-menu-item:empty:after{content:"None";font-weight:400;color:rgb(0 0 0 / .25)}.dark li.ant-select-dropdown-menu-item:empty:after{content:"None";font-weight:400;color:rgb(255 255 255 / .3)}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item:hover .ant-select-selected-icon{color:rgb(0 0 0 / .87)}.dark.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item:hover .ant-select-selected-icon{color:#fff}.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item-selected .ant-select-selected-icon,.ant-select-dropdown.ant-select-dropdown--multiple .ant-select-dropdown-menu-item-selected:hover .ant-select-selected-icon{color:var(--color-primary-100)}.ant-select-selection:hover,.ant-input-number-focused,.ant-input-number:hover{background-color:#e8f4f2}.dark .ant-input-number-handler:active{background-color:var(--color-primary-100)}.dark .ant-input-number-handler:hover .ant-input-number-handler-down-inner,.dark .ant-input-number-handler:hover .ant-input-number-handler-up-inner{color:#fff}.dark .ant-input-number-handler-down{border-top:1px solid rgb(217 217 217 / .3)}.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-century-select,.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-decade-select,.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-month-select,.dark .ant-calendar-year-panel-header .ant-calendar-year-panel-year-select,.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-century-select,.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-decade-select,.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-month-select,.dark .ant-calendar-month-panel-header .ant-calendar-month-panel-year-select{color:rgb(255 255 255 / .85)}.dark .ant-calendar-year-panel-header{border-bottom:1px solid var(--dark-color-surface-200)}.dark .ant-calendar-year-panel-last-decade-cell .ant-calendar-year-panel-year,.dark .ant-calendar-year-panel-next-decade-cell .ant-calendar-year-panel-year{color:rgb(255 255 255 / .35)}.dark .ant-divider:not(.ant-divider-with-text-center,.ant-divider-with-text-left,.ant-divider-with-text-right),.ant-dropdown-menu-dark,.dark .ant-calendar-year-panel-year:hover,.dark .ant-calendar-month-panel-month:hover,.dark .ant-calendar-decade-panel-decade:hover{background-color:var(--dark-color-surface-200)}.dark .ant-calendar-header a:hover{color:#fff}.dark .ant-calendar-month-panel-header{background-color:var(--dark-color-background);border-bottom:1px solid var(--dark-color-surface-200)}.dark .ant-calendar-year-panel,.dark .ant-calendar table{background-color:var(--dark-color-background)}.dark .ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year,.dark .ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year:hover,.dark .ant-calendar-month-panel-selected-cell .ant-calendar-month-panel-month,.dark .ant-calendar-month-panel-selected-cell .ant-calendar-month-panel-month:hover,.dark .ant-calendar-decade-panel-selected-cell .ant-calendar-decade-panel-decade,.dark .ant-calendar-decade-panel-selected-cell .ant-calendar-decade-panel-decade:hover{color:#fff;background-color:var(--color-primary-100)!important}.dark .ant-calendar-last-month-cell .ant-calendar-date,.dark .ant-calendar-last-month-cell .ant-calendar-date:hover,.dark .ant-calendar-next-month-btn-day .ant-calendar-date,.dark .ant-calendar-next-month-btn-day .ant-calendar-date:hover{color:rgb(255 255 255 / 25%);background:#fff0;border-color:#fff0}.dark .ant-calendar-today .ant-calendar-date:hover{color:#fff;border-color:var(--color-primary-100);background-color:var(--color-primary-100)}.dark .ant-calendar-decade-panel-last-century-cell .ant-calendar-decade-panel-decade,.dark .ant-calendar-decade-panel-next-century-cell .ant-calendar-decade-panel-decade{color:rgb(255 255 255 / 25%)}.dark .ant-calendar-decade-panel-header{border-bottom:1px solid var(--dark-color-surface-200);background-color:var(--dark-color-background)}.dark .ant-checkbox-inner{background-color:rgb(0 135 113 / .3);border-color:rgb(0 135 113 / .3)}.dark .ant-checkbox-checked .ant-checkbox-inner{background-color:var(--color-primary-100);border-color:var(--color-primary-100)}.dark .ant-calendar-input{background-color:var(--dark-color-background);color:var(--dark-color-text-primary)}.dark .ant-calendar-input::placeholder{color:rgb(255 255 255 / .25)}.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(:last-child),.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(:last-child),.ant-input-group.ant-input-group-compact>.ant-input:not(:first-child):not(:last-child),.ant-input-number-handler,.ant-input-number-handler-wrap{border-radius:0}.ant-input-number{overflow:clip}.ant-modal-body,.ant-collapse-content>.ant-collapse-content-box{overflow-x:auto}.ant-modal-body{overflow-y:hidden}.ant-calendar-year-panel-year:hover,.ant-calendar-decade-panel-decade:hover,.ant-calendar-month-panel-month:hover,.ant-dropdown-menu-item:hover,.ant-dropdown-menu-submenu-title:hover,.ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled),.ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),.ant-table-tbody>tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-thead>tr.ant-table-row-hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,.ant-table-thead>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td{background-color:#e8f4f2}.dark .ant-dropdown-menu-submenu-title:hover,.dark .ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled),.dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled){background-color:rgb(0 93 78 / .3)}.ant-select-dropdown,.ant-popover-inner{overflow-x:hidden}.ant-popover-inner-content{max-height:450px;overflow-y:auto}@media (max-height:900px){.ant-popover-inner-content{max-height:400px}}@media (max-height:768px){.ant-popover-inner-content{max-height:300px}}@media (max-width:768px){.ant-popover-inner-content{max-height:300px}}.qr-modal{display:flex;align-items:flex-end;gap:10px;flex-direction:column;flex-wrap:wrap;row-gap:24px}.qr-box{width:220px}.qr-cv{width:100%;height:100%}.dark .qr-cv{background-color:#fff;padding:1px;border-radius:0.25rem}.qr-bg{background-color:#fff;display:flex;justify-content:center;align-content:center;padding:.8rem;border-radius:1rem;border:solid 1px #e8e8e8;height:220px;width:220px;transition:all 0.1s}.qr-bg:hover{border-color:#76ccb4;background-color:#eafff9}.qr-bg:hover:active{border-color:#76ccb4;background-color:rgb(197 241 228 / 70%)}.dark .qr-bg{background-color:var(--dark-color-surface-700);border-color:var(--dark-color-surface-300)}.dark .qr-bg:hover{background-color:rgb(var(--dark-color-tag-green-bg));border-color:rgb(var(--dark-color-tag-green-border))}.dark .qr-bg:hover:active{background-color:#17322e}@property --tr-rotate{syntax:'';initial-value:45deg;inherits:false}.qr-bg-sub{background-image:linear-gradient(var(--tr-rotate),#76ccb4,transparent,#d5bed2);display:flex;justify-content:center;align-content:center;padding:1px;border-radius:1rem;height:220px;width:220px}.dark .qr-bg-sub{background-image:linear-gradient(var(--tr-rotate),#195141,transparent,#5a2969)}.qr-bg-sub:hover{animation:tr-rotate-gradient 3.5s linear infinite}@keyframes tr-rotate-gradient{from{--tr-rotate:45deg}to{--tr-rotate:405deg}}.qr-bg-sub-inner{background-color:#fff;padding:.8rem;border-radius:1rem;transition:all 0.1s}.qr-bg-sub-inner:hover{background-color:rgb(255 255 255 / 60%);backdrop-filter:blur(25px)}.qr-bg-sub-inner:hover:active{background-color:rgb(255 255 255 / 30%)}.dark .qr-bg-sub-inner{background-color:rgb(var(--dark-color-surface-700-rgb))}.dark .qr-bg-sub-inner:hover{background-color:rgba(var(--dark-color-surface-700-rgb),.5);backdrop-filter:blur(25px)}.dark .qr-bg-sub-inner:hover:active{background-color:rgba(var(--dark-color-surface-700-rgb),.2)}.qr-tag{text-align:center;margin-bottom:10px;width:100%;overflow:hidden;margin-inline:0}@media (min-width:769px){.qr-modal{flex-direction:row;max-width:680px}}.tr-marquee{justify-content:flex-start}.tr-marquee span{padding-right:25%;white-space:nowrap;transform-origin:center}@keyframes move-ltr{0%{transform:translateX(0)}100%{transform:translateX(-100%)}}.ant-input-group-addon:not(:first-child):not(:last-child){border-radius:0rem 1rem 1rem 0rem}b,strong{font-weight:500}.ant-collapse>.ant-collapse-item>.ant-collapse-header{padding:10px 16px 10px 40px}.dark .ant-message-notice-content{background-color:var(--dark-color-surface-200);border:1px solid var(--dark-color-surface-300);color:var(--dark-color-text-primary)}.ant-btn-danger{background-color:var(--dark-color-btn-danger);border-color:var(--dark-color-btn-danger-border)}.ant-btn-danger:focus,.ant-btn-danger:hover{background-color:var(--dark-color-btn-danger-hover);border-color:var(--dark-color-btn-danger-hover)}.dark .ant-alert-close-icon .anticon-close:hover{color:#fff}.ant-empty-small{margin:4px 0;background-color:transparent!important}.ant-empty-small .ant-empty-image{height:20px}.ant-menu-theme-switch,.ant-menu-theme-switch:hover{background-color:transparent!important;cursor:default!important}.dark .ant-tooltip-inner,.dark .ant-tooltip-arrow:before{background-color:var(--dark-color-tooltip)}.ant-select-sm .ant-select-selection__rendered{margin-left:10px}.ant-collapse{-moz-animation:collfade 0.3s ease;-webkit-animation:0.3s collfade 0.3s ease;animation:collfade 0.3s ease}@-webkit-keyframes collfade{0%{transform:scaleY(.8);transform-origin:0% 0%;opacity:0}100%{transform:scaleY(1);transform-origin:0% 0%;opacity:1}}@keyframes collfade{0%{transform:scaleY(.8);transform-origin:0% 0%;opacity:0}100%{transform:scaleY(1);transform-origin:0% 0%;opacity:1}}.ant-table-tbody>tr>td{border-color:#f0f0f0}.ant-table-row-expand-icon{vertical-align:middle;margin-inline-end:8px;position:relative;transform:scale(.9411764705882353)}.ant-table-row-collapsed::before{transform:rotate(-180deg);top:7px;inset-inline-end:3px;inset-inline-start:3px;height:1px;position:absolute;background:currentcolor;transition:transform 0.3s ease-out;content:""}.ant-table-row-collapsed::after{transform:rotate(0deg);top:3px;bottom:3px;inset-inline-start:7px;width:1px;position:absolute;background:currentcolor;transition:transform 0.3s ease-out;content:""}.ant-table-row-expanded::before{top:7px;inset-inline-end:3px;inset-inline-start:3px;height:1px;position:absolute;background:currentcolor;transition:transform 0.3s ease-out;content:""}.ant-table-row-expanded::after{top:3px;bottom:3px;inset-inline-start:7px;width:1px;transform:rotate(90deg);position:absolute;background:currentcolor;transition:transform 0.3s ease-out;content:""}.ant-menu-theme-switch.ant-menu-item .ant-switch:not(.ant-switch-disabled):active:after,.ant-switch:not(.ant-switch-disabled):active:before{width:16px}.dark .ant-select-disabled .ant-select-selection{background:var(--dark-color-surface-100);border-color:var(--dark-color-surface-200);color:rgb(255 255 255 / .25)}.dark .ant-select-disabled .anticon{color:rgb(255 255 255 / .25)}.dark .ant-input-number-handler-down-disabled,.dark .ant-input-number-handler-up-disabled{background-color:rgb(0 0 0 / .1)}.dark .ant-input-number-handler-down-disabled .anticon,.dark .ant-input-number-handler-up-disabled .anticon,.dark .ant-input-number-handler-down:hover.ant-input-number-handler-down-disabled .anticon,.dark .ant-input-number-handler-up:hover.ant-input-number-handler-up-disabled .anticon{color:rgb(255 255 255 / .25)}.dark .ant-input-number-handler-down:active.ant-input-number-handler-down-disabled,.dark .ant-input-number-handler-up:active.ant-input-number-handler-up-disabled{background-color:rgb(0 0 0 / .2)}.ant-menu-dark .ant-menu-inline.ant-menu-sub{background:var(--dark-color-surface-100);box-shadow:none}.dark .ant-layout-sider-trigger{background:var(--dark-color-surface-100);color:rgb(255 255 255 / 65%)}.ant-layout-sider{overflow:auto}.dark .ant-back-top-content{background-color:var(--dark-color-back-top)}.dark .ant-back-top-content:hover{background-color:var(--dark-color-back-top-hover)}.ant-calendar-time .ant-calendar-footer .ant-calendar-time-picker-btn{text-transform:capitalize}.ant-calendar{border-color:#fff0;border-width:0}.ant-calendar-time-picker-select li:focus,li.ant-calendar-time-picker-select-option-selected{color:rgb(0 0 0 / .65);font-weight:400;background-color:#e8f4f2}.dark li.ant-calendar-time-picker-select-option-selected{color:var(--dark-color-text-primary);font-weight:400}.dark .ant-calendar-time-picker-select li:focus{color:#fff;font-weight:400;background-color:var(--color-primary-100)}.ant-calendar-time-picker-select li:hover{background:#f5f5f5}.ant-calendar-date{transition:background .3s ease,color .3s ease}li.ant-calendar-time-picker-select-option-selected{margin-block:2px}.ant-calendar-time-picker-select{padding:4px}.ant-calendar-time-picker-select li{height:28px;line-height:28px;border-radius:4px}@media (min-width:769px){.index-page .ant-layout-content{margin:24px 16px}}.index-page .ant-card-dark h2{color:var(--dark-color-text-primary)}.index-page~div .ant-backup-list-item{gap:10px}.index-page~div .ant-version-list-item{--padding:12px;padding:var(--padding)!important;gap:var(--padding)}.index-page.dark~div .ant-version-list-item svg{color:var(--dark-color-text-primary)}.index-page.dark~div .ant-backup-list-item svg,.index-page.dark .ant-badge-status-text,.index-page.dark .ant-card-extra{color:var(--dark-color-text-primary)}.index-page.dark .ant-card-actions>li{color:rgb(255 255 255 / .55)}.index-page.dark~div .ant-radio-inner{background-color:var(--dark-color-surface-100);border-color:var(--dark-color-surface-600)}.index-page.dark~div .ant-radio-checked .ant-radio-inner{border-color:var(--color-primary-100)}.index-page.dark~div .ant-backup-list,.index-page.dark~div .ant-version-list,.index-page.dark .ant-card-actions,.index-page.dark .ant-card-actions>li:not(:last-child){border-color:var(--dark-color-stroke)}.index-page .ant-card-actions{background:#fff0}.index-page .ip-hidden{-webkit-user-select:none;-moz-user-select:none;user-select:none;filter:blur(10px)}.index-page .xray-running-animation .ant-badge-status-dot,.index-page .xray-processing-animation .ant-badge-status-dot{animation:runningAnimation 1.2s linear infinite}.index-page .xray-running-animation .ant-badge-status-processing:after{border-color:var(--color-primary-100)}.index-page .xray-stop-animation .ant-badge-status-processing:after{border-color:#fa8c16}.index-page .xray-error-animation .ant-badge-status-processing:after{border-color:#f5222d}@keyframes runningAnimation{0%,50%,100%{transform:scale(1);opacity:1}10%{transform:scale(1.5);opacity:.2}}.index-page .card-placeholder{text-align:center;padding:30px 0;margin-top:10px;background:#fff0;border:none}.index-page~div .log-container{height:auto;max-height:500px;overflow:auto;margin-top:.5rem}#app.login-app *{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app.login-app h1{text-align:center;height:110px}#app.login-app .ant-form-item-children .ant-btn,#app.login-app .ant-input{height:50px;border-radius:30px}#app.login-app .ant-input-group-addon{border-radius:0 30px 30px 0;width:50px;font-size:18px}#app.login-app .ant-input-affix-wrapper .ant-input-prefix{left:23px}#app.login-app .ant-input-affix-wrapper .ant-input:not(:first-child){padding-left:50px}#app.login-app .centered{display:flex;text-align:center;align-items:center;justify-content:center;width:100%}#app.login-app .title{font-size:2rem;margin-block-end:2rem}#app.login-app .title b{font-weight:bold!important}#app.login-app{overflow:hidden}#app.login-app #login{animation:charge 0.5s both;background-color:#fff;border-radius:2rem;padding:4rem 3rem;transition:all 0.3s;user-select:none;-webkit-user-select:none;-moz-user-select:none}#app.login-app #login:hover{box-shadow:0 2px 8px rgb(0 0 0 / .09)}@keyframes charge{from{transform:translateY(5rem);opacity:0}to{transform:translateY(0);opacity:1}}#app.login-app .under{background-color:#c7ebe2;z-index:0}#app.login-app.dark .under{background-color:var(--dark-color-login-wave)}#app.login-app.dark #login{background-color:var(--dark-color-surface-100)}#app.login-app.dark h1{color:#fff}#app.login-app .ant-btn-primary-login{width:100%}#app.login-app .ant-btn-primary-login:focus,#app.login-app .ant-btn-primary-login:hover{color:#fff;background-color:#065;border-color:#065;background-image:linear-gradient(270deg,#fff0 30%,#009980,#fff0 100%);background-repeat:no-repeat;animation:ma-bg-move ease-in-out 5s infinite;background-position-x:-500px;width:95%;animation-delay:-0.5s;box-shadow:0 2px 0 rgb(0 0 0 / .045)}#app.login-app .ant-btn-primary-login.active,#app.login-app .ant-btn-primary-login:active{color:#fff;background-color:#065;border-color:#065}@keyframes ma-bg-move{0%{background-position:-500px 0}50%{background-position:1000px 0}100%{background-position:1000px 0}}#app.login-app .wave-btn-bg{position:relative;border-radius:25px;width:100%;transition:all 0.3s cubic-bezier(.645,.045,.355,1)}#app.login-app.dark .wave-btn-bg{color:#fff;position:relative;background-color:#0a7557;border:2px double #fff0;background-origin:border-box;background-clip:padding-box,border-box;background-size:300%;width:100%;z-index:1}#app.login-app.dark .wave-btn-bg:hover{animation:wave-btn-tara 4s ease infinite}#app.login-app.dark .wave-btn-bg-cl{background-image:linear-gradient(#fff0,#fff0),radial-gradient(circle at left top,#006655,#009980,#006655)!important;border-radius:3em}#app.login-app.dark .wave-btn-bg-cl:hover{width:95%}#app.login-app.dark .wave-btn-bg-cl:before{position:absolute;content:"";top:-5px;left:-5px;bottom:-5px;right:-5px;z-index:-1;background:inherit;background-size:inherit;border-radius:4em;opacity:0;transition:0.5s}#app.login-app.dark .wave-btn-bg-cl:hover::before{opacity:1;filter:blur(20px);animation:wave-btn-tara 8s linear infinite}@keyframes wave-btn-tara{to{background-position:300%}}#app.login-app.dark .ant-btn-primary-login{font-size:14px;color:#fff;text-align:center;background-image:linear-gradient(rgb(13 14 33 / .45),rgb(13 14 33 / .35));border-radius:2rem;border:none;outline:none;background-color:#fff0;height:46px;position:relative;white-space:nowrap;cursor:pointer;touch-action:manipulation;padding:0 15px;width:100%;animation:none;background-position-x:0;box-shadow:none}#app.login-app .waves-header{position:fixed;width:100%;text-align:center;background-color:#dbf5ed;color:#fff;z-index:-1}#app.login-app.dark .waves-header{background-color:var(--dark-color-login-background)}#app.login-app .waves-inner-header{height:50vh;width:100%;margin:0;padding:0}#app.login-app .waves{position:relative;width:100%;height:15vh;margin-bottom:-8px;min-height:100px;max-height:150px}#app.login-app .parallax>use{animation:move-forever 25s cubic-bezier(.55,.5,.45,.5) infinite}#app.login-app.dark .parallax>use{fill:var(--dark-color-login-wave)}#app.login-app .parallax>use:nth-child(1){animation-delay:-2s;animation-duration:4s;opacity:.2}#app.login-app .parallax>use:nth-child(2){animation-delay:-3s;animation-duration:7s;opacity:.4}#app.login-app .parallax>use:nth-child(3){animation-delay:-4s;animation-duration:10s;opacity:.6}#app.login-app .parallax>use:nth-child(4){animation-delay:-5s;animation-duration:13s}@keyframes move-forever{0%{transform:translate3d(-90px,0,0)}100%{transform:translate3d(85px,0,0)}}@media (max-width:768px){#app.login-app .waves{height:40px;min-height:40px}}#app.login-app .words-wrapper{width:100%;display:inline-block;position:relative;text-align:center}#app.login-app .words-wrapper b{width:100%;display:inline-block;position:absolute;left:0;top:0}#app.login-app .words-wrapper b.is-visible{position:relative}#app.login-app .headline.zoom .words-wrapper{-webkit-perspective:300px;-moz-perspective:300px;perspective:300px}#app.login-app .headline{display:flex;justify-content:center;align-items:center}#app.login-app .headline.zoom b{opacity:0}#app.login-app .headline.zoom b.is-visible{opacity:1;-webkit-animation:zoom-in 0.8s;-moz-animation:zoom-in 0.8s;animation:cubic-bezier(.215,.61,.355,1) zoom-in 0.8s}#app.login-app .headline.zoom b.is-hidden{-webkit-animation:zoom-out 0.8s;-moz-animation:zoom-out 0.8s;animation:cubic-bezier(.215,.61,.355,1) zoom-out 0.4s}@-webkit-keyframes zoom-in{0%{opacity:0;-webkit-transform:translateZ(100px)}100%{opacity:1;-webkit-transform:translateZ(0)}}@-moz-keyframes zoom-in{0%{opacity:0;-moz-transform:translateZ(100px)}100%{opacity:1;-moz-transform:translateZ(0)}}@keyframes zoom-in{0%{opacity:0;-webkit-transform:translateZ(100px);-moz-transform:translateZ(100px);-ms-transform:translateZ(100px);-o-transform:translateZ(100px);transform:translateZ(100px)}100%{opacity:1;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}}@-webkit-keyframes zoom-out{0%{opacity:1;-webkit-transform:translateZ(0)}100%{opacity:0;-webkit-transform:translateZ(-100px)}}@-moz-keyframes zoom-out{0%{opacity:1;-moz-transform:translateZ(0)}100%{opacity:0;-moz-transform:translateZ(-100px)}}@keyframes zoom-out{0%{opacity:1;-webkit-transform:translateZ(0);-moz-transform:translateZ(0);-ms-transform:translateZ(0);-o-transform:translateZ(0);transform:translateZ(0)}100%{opacity:0;-webkit-transform:translateZ(-100px);-moz-transform:translateZ(-100px);-ms-transform:translateZ(-100px);-o-transform:translateZ(-100px);transform:translateZ(-100px)}}#app.login-app .setting-section{position:absolute;top:0;right:0;padding:22px}#app.login-app .ant-space-item .ant-switch{margin:2px 0 4px}#app.login-app .ant-layout-content{transition:none}.inbounds-page .ant-table:not(.ant-table-expanded-row .ant-table){outline:1px solid #f0f0f0;outline-offset:-1px;border-radius:1rem;overflow-x:hidden}.inbounds-page.dark .ant-table:not(.ant-table-expanded-row .ant-table){outline-color:var(--dark-color-table-ring)}.inbounds-page .ant-table .ant-table-content .ant-table-scroll .ant-table-body{overflow-y:hidden}.inbounds-page .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper{margin:-10px 22px!important}.inbounds-page .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table{border-bottom-left-radius:1rem;border-bottom-right-radius:1rem}.inbounds-page .ant-table .ant-table-content .ant-table-tbody tr:last-child tr:last-child td{border-bottom-color:#fff0}.inbounds-page .ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:first-child{border-bottom-left-radius:6px}.inbounds-page .ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:last-child{border-bottom-right-radius:6px}@media (min-width:769px){.inbounds-page .ant-layout-content{margin:24px 16px}}@media (max-width:768px){.inbounds-page .ant-card-body{padding:.5rem}.inbounds-page .ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper{margin:-10px 2px!important}}.inbounds-page.dark~div .ant-switch-small:not(.ant-switch-checked){background-color:var(--dark-color-surface-100)}.inbounds-page .ant-custom-popover-title{display:flex;align-items:center;gap:10px;margin:5px 0}.inbounds-page .ant-col-sm-24{margin:.5rem -2rem .5rem 2rem}.inbounds-page tr.hideExpandIcon .ant-table-row-expand-icon{display:none}.inbounds-page .infinite-tag,.inbounds-page~div .infinite-tag{padding:0 5px;border-radius:2rem;min-width:50px;min-height:22px}.inbounds-page .infinite-bar .ant-progress-inner .ant-progress-bg{background-color:#F2EAF1;border:#D5BED2 solid 1px}.inbounds-page.dark .infinite-bar .ant-progress-inner .ant-progress-bg{background-color:#7a316f!important;border:#7a316f solid 1px}.inbounds-page~div .ant-collapse{margin:5px 0}.inbounds-page .info-large-tag,.inbounds-page~div .info-large-tag{max-width:200px;overflow:hidden}.inbounds-page .client-comment{font-size:12px;opacity:.75;cursor:help}.inbounds-page .client-email{font-weight:500}.inbounds-page .client-popup-item{display:flex;align-items:center;gap:5px}.inbounds-page .online-animation .ant-badge-status-dot{animation:onlineAnimation 1.2s linear infinite}@keyframes onlineAnimation{0%,50%,100%{transform:scale(1);opacity:1}10%{transform:scale(1.5);opacity:.2}}.inbounds-page .tr-table-box{display:flex;gap:4px;justify-content:center;align-items:center}.inbounds-page .tr-table-rt{flex-basis:70px;min-width:70px;text-align:end}.inbounds-page .tr-table-lt{flex-basis:70px;min-width:70px;text-align:start}.inbounds-page .tr-table-bar{flex-basis:160px;min-width:60px}.inbounds-page .tr-infinity-ch{font-size:14pt;max-height:24px;display:inline-flex;align-items:center}.inbounds-page .ant-table-expanded-row .ant-table .ant-table-body{overflow-x:hidden}.inbounds-page .ant-table-expanded-row .ant-table-tbody>tr>td{padding:10px 2px}.inbounds-page .ant-table-expanded-row .ant-table-thead>tr>th{padding:12px 2px}.idx-cpu-history-svg{display:block;overflow:unset!important}.dark .idx-cpu-history-svg .cpu-grid-line{stroke:rgb(255 255 255 / .08)}.dark .idx-cpu-history-svg .cpu-grid-h-line{stroke:rgb(255 255 255 / .25)}.dark .idx-cpu-history-svg .cpu-grid-y-text,.dark .idx-cpu-history-svg .cpu-grid-x-text{fill:rgb(200 200 200 / .8)}.idx-cpu-history-svg .cpu-grid-text{stroke-width:3;paint-order:stroke;stroke:rgb(0 0 0 / .05)}.dark .idx-cpu-history-svg .cpu-grid-text{fill:#fff;stroke:rgb(0 0 0 / .35)}.inbounds-page~div #inbound-modal form textarea.ant-input{margin:4px 0}@media (min-width:769px){.settings-page .ant-layout-content{margin:24px 16px}}@media (max-width:768px){.settings-page .ant-tabs-nav .ant-tabs-tab{margin:0;padding:12px .5rem}}.settings-page .ant-tabs-bar{margin:0}.settings-page .ant-list-item{display:block}.settings-page .alert-msg{color:#c27512;font-weight:400;font-size:16px;padding:.5rem 1rem;text-align:center;background:rgb(255 145 0 / 15%);margin:1.5rem 2.5rem 0rem;border-radius:.5rem;transition:all 0.5s;animation:settings-page-signal 3s cubic-bezier(.18,.89,.32,1.28) infinite}.settings-page .alert-msg:hover{cursor:default;transition-duration:.3s;animation:settings-page-signal 0.9s ease infinite}@keyframes settings-page-signal{0%{box-shadow:0 0 0 0 rgb(194 118 18 / .5)}50%{box-shadow:0 0 0 6px #fff0}100%{box-shadow:0 0 0 6px #fff0}}.settings-page .alert-msg>i{color:inherit;font-size:24px}.settings-page.dark .ant-input-password-icon{color:var(--dark-color-text-primary)}.settings-page .ant-collapse-content-box .ant-alert{margin-block-end:12px}@media (min-width:769px){.xray-page .ant-layout-content{margin:24px 16px}}@media (max-width:768px){.xray-page .ant-tabs-nav .ant-tabs-tab{margin:0;padding:12px .5rem}.xray-page .ant-table-thead>tr>th,.xray-page .ant-table-tbody>tr>td{padding:10px 0}}.xray-page .ant-tabs-bar{margin:0}.xray-page .ant-list-item{display:block}.xray-page .ant-list-item>li{padding:10px 20px!important}.xray-page .ant-collapse-content-box .ant-alert{margin-block-end:12px}#app.login-app #login input.ant-input:-webkit-autofill{-webkit-box-shadow:0 0 0 100px #f8f8f8 inset;box-shadow:0 0 0 100px #f8f8f8 inset;transition:background-color 9999s ease-in-out 0s,color 9999s ease-in-out 0s;background-clip:text}#app.login-app #login .ant-input-affix-wrapper:hover .ant-input:-webkit-autofill:not(.ant-input-disabled),#app.login-app #login input.ant-input:-webkit-autofill:hover,#app.login-app #login input.ant-input:-webkit-autofill:focus{-webkit-box-shadow:0 0 0 100px #e8f4f2 inset;box-shadow:0 0 0 100px #e8f4f2 inset}#app.login-app.dark #login .ant-input-affix-wrapper:hover .ant-input:-webkit-autofill:not(.ant-input-disabled),#app.login-app.dark #login input.ant-input:-webkit-autofill{-webkit-text-fill-color:var(--dark-color-text-primary);caret-color:var(--dark-color-text-primary);-webkit-box-shadow:0 0 0 1000px var(--dark-color-surface-200) inset;box-shadow:0 0 0 1000px var(--dark-color-surface-200) inset;transition:background-color 9999s ease-in-out 0s,color 9999s ease-in-out 0s}#app.login-app.dark #login .ant-input-affix-wrapper:hover .ant-input:-webkit-autofill:not(.ant-input-disabled),#app.login-app.dark #login input.ant-input:-webkit-autofill:hover,#app.login-app.dark #login input.ant-input:-webkit-autofill:focus{border-color:var(--dark-color-surface-300)}.dark .ant-descriptions-bordered .ant-descriptions-item-label{background-color:var(--dark-color-background)}.dark .ant-descriptions-bordered .ant-descriptions-view,.dark .ant-descriptions-bordered .ant-descriptions-row,.dark .ant-descriptions-bordered .ant-descriptions-item-label,.dark .ant-list-bordered{border-color:var(--dark-color-surface-400)}.dark .ant-descriptions-bordered .ant-descriptions-item-label,.dark .ant-descriptions-bordered .ant-descriptions-item-content{color:var(--dark-color-text-primary)}.dark .ant-dropdown-menu{background-color:var(--dark-color-surface-200)}.dark .ant-dropdown-menu .ant-dropdown-menu-item{color:hsl(0 0% 100% / .65)}.dark .ant-dropdown-menu .ant-dropdown-menu-item:hover{background-color:var(--dark-color-surface-600)}.subscription-page .ant-list.ant-list-split.ant-list-bordered{overflow:hidden}.subscription-page .ant-list.ant-list-split.ant-list-bordered .ant-list-item{overflow-x:auto}.subscription-page .ant-btn.ant-btn-primary.ant-btn-lg.ant-dropdown-trigger{border-radius:4rem;padding:0 20px}.subscription-page .subscription-card{margin:2rem 0}.mb-10{margin-bottom:10px}.mb-12{margin-bottom:12px}.mt-5{margin-top:5px}.mr-8{margin-right:8px}.ml-10{margin-left:10px}.mr-05{margin-right:.5rem}.fs-1rem{font-size:1rem}.w-100{width:100%}.w-70{width:70px}.w-95{width:95px}.text-center{text-align:center}.cursor-pointer{cursor:pointer}.float-right{float:right}.va-middle{vertical-align:middle}.d-flex{display:flex}.justify-end{justify-content:flex-end}.max-w-400{max-width:400px;display:inline-block}.ant-space.jc-center{justify-content:center}.min-h-0{min-height:0}.min-h-100vh{min-height:100vh}.h-100{height:100%}.h-50px{height:50px}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-hidden{overflow-x:hidden}.overflow-y-hidden{overflow-y:hidden}.overflow-y-auto{overflow-y:auto}.overflow-x-auto{overflow-x:auto}.mt-1rem{margin-top:1rem}.my-3rem{margin-top:3rem;margin-bottom:3rem} \ No newline at end of file From 278aa1c85c6af86a64326c924a746eeec5865389 Mon Sep 17 00:00:00 2001 From: Vlad Yaroslavlev Date: Fri, 2 Jan 2026 18:13:32 +0300 Subject: [PATCH 7/9] Fix telegram bot issue (#3608) * fix: improve Telegram bot handling for concurrent starts and graceful shutdown - Added logic to stop any existing long-polling loop when Start is called again. - Introduced a mutex to manage access to shared state variables, ensuring thread safety. - Updated the OnReceive method to prevent multiple concurrent executions. - Enhanced Stop method to ensure proper cleanup of resources and state management. * fix: enhance Telegram bot's long-polling management - Improved handling of concurrent starts by stopping existing long-polling loops. - Implemented mutex for thread-safe access to shared state variables. - Updated OnReceive method to prevent multiple executions. - Enhanced Stop method for better resource cleanup and state management. * . --- web/service/tgbot.go | 96 +++++++++++++++++++++++++------------------- 1 file changed, 55 insertions(+), 41 deletions(-) diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 06c51faa..3a98dcb4 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -174,6 +174,10 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { return err } + // If Start is called again (e.g. during reload), ensure any previous long-polling + // loop is stopped before creating a new bot / receiver. + StopBot() + // Initialize hash storage to store callback queries hashStorage = global.NewHashStorage(20 * time.Minute) @@ -207,6 +211,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { return err } + parsedAdminIds := make([]int64, 0) // Parse admin IDs from comma-separated string if tgBotID != "" { for _, adminID := range strings.Split(tgBotID, ",") { @@ -215,9 +220,12 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err) return err } - adminIds = append(adminIds, int64(id)) + parsedAdminIds = append(parsedAdminIds, int64(id)) } } + tgBotMutex.Lock() + adminIds = parsedAdminIds + tgBotMutex.Unlock() // Get Telegram bot proxy URL tgBotProxy, err := t.settingService.GetTgBotProxy() @@ -252,10 +260,12 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { } // Start receiving Telegram bot messages - if !isRunning { + tgBotMutex.Lock() + alreadyRunning := isRunning || botCancel != nil + tgBotMutex.Unlock() + if !alreadyRunning { logger.Info("Telegram bot receiver started") go t.OnReceive() - isRunning = true } return nil @@ -300,6 +310,8 @@ func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*tel // IsRunning checks if the Telegram bot is currently running. func (t *Tgbot) IsRunning() bool { + tgBotMutex.Lock() + defer tgBotMutex.Unlock() return isRunning } @@ -317,34 +329,34 @@ func (t *Tgbot) SetHostname() { // Stop safely stops the Telegram bot's Long Polling operation. // This method now calls the global StopBot function and cleans up other resources. func (t *Tgbot) Stop() { - // Call the global StopBot function to gracefully shut down Long Polling StopBot() - - // Stop the bot handler (in case the goroutine hasn't exited yet) - if botHandler != nil { - botHandler.Stop() - } logger.Info("Stop Telegram receiver ...") - isRunning = false + tgBotMutex.Lock() adminIds = nil + tgBotMutex.Unlock() } // StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context. // This is the global function called from main.go's signal handler and t.Stop(). func StopBot() { + // Don't hold the mutex while cancelling/waiting. tgBotMutex.Lock() - defer tgBotMutex.Unlock() + cancel := botCancel + botCancel = nil + handler := botHandler + botHandler = nil + isRunning = false + tgBotMutex.Unlock() - if botCancel != nil { + if handler != nil { + handler.Stop() + } + + if cancel != nil { logger.Info("Sending cancellation signal to Telegram bot...") - - // Calling botCancel() cancels the context passed to UpdatesViaLongPolling, - // which stops the Long Polling operation and closes the updates channel, - // allowing the th.Start() goroutine to exit cleanly. - botCancel() - - botCancel = nil - // Giving the goroutine a small delay to exit cleanly. + // Cancels the context passed to UpdatesViaLongPolling; this closes updates channel + // and lets botHandler.Start() exit cleanly. + cancel() botWG.Wait() logger.Info("Telegram bot successfully stopped.") } @@ -379,36 +391,38 @@ func (t *Tgbot) OnReceive() { params := telego.GetUpdatesParams{ Timeout: 30, // Increased timeout to reduce API calls } - // --- GRACEFUL SHUTDOWN FIX: Context creation --- + // Strict singleton: never start a second long-polling loop. tgBotMutex.Lock() - - // Create a context with cancellation and store the cancel function. - var ctx context.Context - - // Check if botCancel is already set (to prevent race condition overwrite and goroutine leak) - if botCancel == nil { - ctx, botCancel = context.WithCancel(context.Background()) - } else { - // If botCancel is already set, use a non-cancellable context for this redundant call. - // This prevents overwriting the active botCancel and causing a goroutine leak from the previous call. - logger.Warning("TgBot OnReceive called concurrently. Using background context for redundant call.") - ctx = context.Background() // <<< ИЗМЕНЕНИЕ + if botCancel != nil || isRunning { + tgBotMutex.Unlock() + logger.Warning("TgBot OnReceive called while already running; ignoring.") + return } + ctx, cancel := context.WithCancel(context.Background()) + botCancel = cancel + isRunning = true + // Add to WaitGroup before releasing the lock so StopBot() can't return + // before this receiver goroutine is accounted for. + botWG.Add(1) tgBotMutex.Unlock() // Get updates channel using the context. updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms) - botWG.Go(func() { + go func() { + defer botWG.Done() + h, _ := th.NewBotHandler(bot, updates) + tgBotMutex.Lock() + botHandler = h + tgBotMutex.Unlock() - botHandler, _ = th.NewBotHandler(bot, updates) - botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + h.HandleMessage(func(ctx *th.Context, message telego.Message) error { delete(userStates, message.Chat.ID) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove()) return nil }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) - botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + h.HandleMessage(func(ctx *th.Context, message telego.Message) error { // Use goroutine with worker pool for concurrent command processing go func() { messageWorkerPool <- struct{}{} // Acquire worker @@ -420,7 +434,7 @@ func (t *Tgbot) OnReceive() { return nil }, th.AnyCommand()) - botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { + h.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { // Use goroutine with worker pool for concurrent callback processing go func() { messageWorkerPool <- struct{}{} // Acquire worker @@ -432,7 +446,7 @@ func (t *Tgbot) OnReceive() { return nil }, th.AnyCallbackQueryWithMessage()) - botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + h.HandleMessage(func(ctx *th.Context, message telego.Message) error { if userState, exists := userStates[message.Chat.ID]; exists { switch userState { case "awaiting_id": @@ -578,8 +592,8 @@ func (t *Tgbot) OnReceive() { return nil }, th.AnyMessage()) - botHandler.Start() - }) + h.Start() + }() } // answerCommand processes incoming command messages from Telegram users. From 9a2c1c6b43471316f5cdd15b995f1e47b2f07d52 Mon Sep 17 00:00:00 2001 From: Ilya Kryuchkov <42733472+kr-ilya@users.noreply.github.com> Date: Sat, 3 Jan 2026 05:05:10 +0300 Subject: [PATCH 8/9] Fix: panel redirecting to old port after restart (#3594) * Fix panel redirect logic * Fix panel redirect logic * remove duplicate code * Cr fixes --- web/html/settings.html | 76 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/web/html/settings.html b/web/html/settings.html index e664e6ec..21294da7 100644 --- a/web/html/settings.html +++ b/web/html/settings.html @@ -120,6 +120,10 @@ oldAllSetting: new AllSetting(), allSetting: new AllSetting(), saveBtnDisable: true, + entryHost: null, + entryPort: null, + entryProtocol: null, + entryIsIP: false, user: {}, lang: LanguageManager.getLanguage(), inboundOptions: [], @@ -233,6 +237,31 @@ loading(spinning = true) { this.loadingStates.spinning = spinning; }, + _isIp(h) { + if (typeof h !== "string") return false; + + // IPv4: four dot-separated octets 0-255 + const v4 = h.split("."); + if ( + v4.length === 4 && + v4.every(p => /^\d{1,3}$/.test(p) && Number(p) <= 255) + ) return true; + + // IPv6: hex groups, optional single :: compression + if (!h.includes(":") || h.includes(":::")) return false; + const parts = h.split("::"); + if (parts.length > 2) return false; + + const splitGroups = s => (s ? s.split(":").filter(Boolean) : []); + const head = splitGroups(parts[0]); + const tail = splitGroups(parts[1]); + const validGroup = seg => /^[0-9a-fA-F]{1,4}$/.test(seg); + + if (![...head, ...tail].every(validGroup)) return false; + const groups = head.length + tail.length; + + return parts.length === 2 ? groups < 8 : groups === 8; + }, async getAllSetting() { const msg = await HttpUtil.post("/panel/setting/all"); @@ -307,16 +336,41 @@ this.loading(true); const msg = await HttpUtil.post("/panel/setting/restartPanel"); this.loading(false); - if (msg.success) { - this.loading(true); - await PromiseUtil.sleep(5000); - var { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting; - if (host == this.oldAllSetting.webDomain) host = null; - if (port == this.oldAllSetting.webPort) port = null; - const isTLS = webCertFile !== "" || webKeyFile !== ""; - const url = URLBuilder.buildURL({ host, port, isTLS, base, path: "panel/settings" }); - window.location.replace(url); + if (!msg.success) return; + + this.loading(true); + await PromiseUtil.sleep(5000); + + const { webDomain, webPort, webBasePath, webCertFile, webKeyFile } = this.allSetting; + const newProtocol = (webCertFile || webKeyFile) ? "https:" : "http:"; + + let base = webBasePath ? webBasePath.replace(/^\//, "") : ""; + if (base && !base.endsWith("/")) base += "/"; + + if (!this.entryIsIP) { + const url = new URL(window.location.href); + url.pathname = `/${base}panel/settings`; + url.protocol = newProtocol; + window.location.replace(url.toString()); + return; } + + let finalHost = this.entryHost; + let finalPort = this.entryPort || ""; + + if (webDomain && this._isIp(webDomain)) { + finalHost = webDomain; + } + + if (webPort && Number(webPort) !== Number(this.entryPort)) { + finalPort = String(webPort); + } + + const url = new URL(`${newProtocol}//${finalHost}`); + if (finalPort) url.port = finalPort; + url.pathname = `/${base}panel/settings`; + + window.location.replace(url.toString()); }, toggleTwoFactor(newValue) { if (newValue) { @@ -568,6 +622,10 @@ } }, async mounted() { + this.entryHost = window.location.hostname; + this.entryPort = window.location.port; + this.entryProtocol = window.location.protocol; + this.entryIsIP = this._isIp(this.entryHost); await this.getAllSetting(); await this.loadInboundTags(); while (true) { From 1393f981bc313118b3ffde0b469cc07abdeb24c8 Mon Sep 17 00:00:00 2001 From: weekend sorrow Date: Sat, 3 Jan 2026 09:13:00 +0700 Subject: [PATCH 9/9] feat: Add etckeeper compatibility (#3602) --- install.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/install.sh b/install.sh index 480522ed..1009f047 100644 --- a/install.sh +++ b/install.sh @@ -643,6 +643,20 @@ install_x-ui() { chmod +x /usr/bin/x-ui mkdir -p /var/log/x-ui config_after_install + + # Etckeeper compatibility + if [ -d "/etc/.git" ]; then + if [ -f "/etc/.gitignore" ]; then + if ! grep -q "x-ui/x-ui.db" "/etc/.gitignore"; then + echo "" >> "/etc/.gitignore" + echo "x-ui/x-ui.db" >> "/etc/.gitignore" + echo -e "${green}Added x-ui.db to /etc/.gitignore for etckeeper${plain}" + fi + else + echo "x-ui/x-ui.db" > "/etc/.gitignore" + echo -e "${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}" + fi + fi if [[ $release == "alpine" ]]; then wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc