Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/MHSanaei/3x-ui.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--sub/sub.go55
-rw-r--r--sub/subController.go85
-rw-r--r--sub/subService.go191
-rw-r--r--web/assets/css/custom.min.css2836
-rw-r--r--web/assets/js/subscription.js125
-rw-r--r--web/html/modals/xray_rule_modal.html26
-rw-r--r--web/html/settings/xray/routing.html12
-rw-r--r--web/html/subscription.html274
-rw-r--r--web/html/xray.html5
-rw-r--r--web/locale/locale.go22
-rw-r--r--web/service/tgbot.go343
-rw-r--r--web/translation/translate.ar_EG.toml14
-rw-r--r--web/translation/translate.en_US.toml14
-rw-r--r--web/translation/translate.es_ES.toml14
-rw-r--r--web/translation/translate.fa_IR.toml14
-rw-r--r--web/translation/translate.id_ID.toml14
-rw-r--r--web/translation/translate.ja_JP.toml14
-rw-r--r--web/translation/translate.pt_BR.toml14
-rw-r--r--web/translation/translate.ru_RU.toml14
-rw-r--r--web/translation/translate.tr_TR.toml14
-rw-r--r--web/translation/translate.uk_UA.toml14
-rw-r--r--web/translation/translate.vi_VN.toml14
-rw-r--r--web/translation/translate.zh_CN.toml14
-rw-r--r--web/translation/translate.zh_TW.toml14
26 files changed, 4091 insertions, 68 deletions
diff --git a/go.mod b/go.mod
index 26f45c61..78b6f09c 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.8
+ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.65.0
github.com/xlzd/gotp v0.1.0
github.com/xtls/xray-core v1.250911.0
diff --git a/go.sum b/go.sum
index e45b4986..7cf8d118 100644
--- a/go.sum
+++ b/go.sum
@@ -142,6 +142,8 @@ github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
+github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
diff --git a/sub/sub.go b/sub/sub.go
index 4f8f5672..dce57243 100644
--- a/sub/sub.go
+++ b/sub/sub.go
@@ -6,11 +6,14 @@ import (
"io"
"net"
"net/http"
+ "os"
+ "path/filepath"
"strconv"
"x-ui/config"
"x-ui/logger"
"x-ui/util/common"
+ "x-ui/web/locale"
"x-ui/web/middleware"
"x-ui/web/network"
"x-ui/web/service"
@@ -57,6 +60,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
}
+ // Provide base_path in context for templates
+ engine.Use(func(c *gin.Context) {
+ c.Set("base_path", "/")
+ })
+
LinksPath, err := s.settingService.GetSubPath()
if err != nil {
return nil, err
@@ -112,6 +120,29 @@ func (s *Server) initRouter() (*gin.Engine, error) {
SubTitle = ""
}
+ // init i18n for sub server using disk FS so templates can use {{ i18n }}
+ // Root FS is project root; translation files are under web/translation
+ if err := locale.InitLocalizerFS(os.DirFS("web"), &s.settingService); err != nil {
+ logger.Warning("sub: i18n init failed:", err)
+ }
+ // set per-request localizer from headers/cookies
+ engine.Use(locale.LocalizerMiddleware())
+
+ // load HTML templates needed for subscription page (common layout + page + component + subscription)
+ if files, err := s.getHtmlFiles(); err != nil {
+ logger.Warning("sub: getHtmlFiles failed:", err)
+ } else {
+ // register i18n function similar to web server
+ i18nWebFunc := func(key string, params ...string) string {
+ return locale.I18n(locale.Web, key, params...)
+ }
+ engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
+ engine.LoadHTMLFiles(files...)
+ }
+
+ // serve assets from web/assets to use shared JS/CSS like other pages
+ engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
+
g := engine.Group("/")
s.sub = NewSUBController(
@@ -121,6 +152,30 @@ func (s *Server) initRouter() (*gin.Engine, error) {
return engine, nil
}
+// getHtmlFiles loads templates from local folder (used in debug mode)
+func (s *Server) getHtmlFiles() ([]string, error) {
+ dir, _ := os.Getwd()
+ files := []string{}
+ // common layout
+ common := filepath.Join(dir, "web", "html", "common", "page.html")
+ if _, err := os.Stat(common); err == nil {
+ files = append(files, common)
+ }
+ // components used
+ theme := filepath.Join(dir, "web", "html", "component", "aThemeSwitch.html")
+ if _, err := os.Stat(theme); err == nil {
+ files = append(files, theme)
+ }
+ // page itself
+ page := filepath.Join(dir, "web", "html", "subscription.html")
+ if _, err := os.Stat(page); err == nil {
+ files = append(files, page)
+ } else {
+ return nil, err
+ }
+ return files, nil
+}
+
func (s *Server) Start() (err error) {
// This is an anonymous function, no function name
defer func() {
diff --git a/sub/subController.go b/sub/subController.go
index 3f053740..c37ff5a9 100644
--- a/sub/subController.go
+++ b/sub/subController.go
@@ -2,8 +2,8 @@ package sub
import (
"encoding/base64"
- "net"
"strings"
+ "x-ui/config"
"github.com/gin-gonic/gin"
)
@@ -58,21 +58,8 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
func (a *SUBController) subs(c *gin.Context) {
subId := c.Param("subid")
- var host string
- if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
- host = h
- }
- if host == "" {
- host = c.GetHeader("X-Real-IP")
- }
- if host == "" {
- var err error
- host, _, err = net.SplitHostPort(c.Request.Host)
- if err != nil {
- host = c.Request.Host
- }
- }
- subs, header, err := a.subService.GetSubs(subId, host)
+ scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
+ subs, header, lastOnline, err := a.subService.GetSubs(subId, host)
if err != nil || len(subs) == 0 {
c.String(400, "Error!")
} else {
@@ -81,10 +68,38 @@ func (a *SUBController) subs(c *gin.Context) {
result += sub + "\n"
}
+ // If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here
+ accept := c.GetHeader("Accept")
+ if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
+ // Build page data in service
+ subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
+ page := a.subService.BuildPageData(subId, hostHeader, header, lastOnline, subs, subURL, subJsonURL)
+ c.HTML(200, "subscription.html", gin.H{
+ "title": "subscription.title",
+ "cur_ver": config.GetVersion(),
+ "host": page.Host,
+ "base_path": page.BasePath,
+ "sId": page.SId,
+ "download": page.Download,
+ "upload": page.Upload,
+ "total": page.Total,
+ "used": page.Used,
+ "remained": page.Remained,
+ "expire": page.Expire,
+ "lastOnline": page.LastOnline,
+ "datepicker": page.Datepicker,
+ "downloadByte": page.DownloadByte,
+ "uploadByte": page.UploadByte,
+ "totalByte": page.TotalByte,
+ "subUrl": page.SubUrl,
+ "subJsonUrl": page.SubJsonUrl,
+ "result": page.Result,
+ })
+ return
+ }
+
// Add headers
- c.Writer.Header().Set("Subscription-Userinfo", header)
- c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
- c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
+ a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
if a.subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
@@ -96,41 +111,21 @@ func (a *SUBController) subs(c *gin.Context) {
func (a *SUBController) subJsons(c *gin.Context) {
subId := c.Param("subid")
- var host string
- if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
- host = h
- }
- if host == "" {
- host = c.GetHeader("X-Real-IP")
- }
- if host == "" {
- var err error
- host, _, err = net.SplitHostPort(c.Request.Host)
- if err != nil {
- host = c.Request.Host
- }
- }
+ _, host, _, _ := a.subService.ResolveRequest(c)
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
if err != nil || len(jsonSub) == 0 {
c.String(400, "Error!")
} else {
// Add headers
- c.Writer.Header().Set("Subscription-Userinfo", header)
- c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
- c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
+ a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
c.String(200, jsonSub)
}
}
-func getHostFromXFH(s string) (string, error) {
- if strings.Contains(s, ":") {
- realHost, _, err := net.SplitHostPort(s)
- if err != nil {
- return "", err
- }
- return realHost, nil
- }
- return s, nil
+func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
+ c.Writer.Header().Set("Subscription-Userinfo", header)
+ c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
+ c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
}
diff --git a/sub/subService.go b/sub/subService.go
index e6e25e3a..a47cb6be 100644
--- a/sub/subService.go
+++ b/sub/subService.go
@@ -3,10 +3,15 @@ package sub
import (
"encoding/base64"
"fmt"
+ "net"
"net/url"
+ "strconv"
"strings"
"time"
+ "github.com/gin-gonic/gin"
+ "github.com/goccy/go-json"
+
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
@@ -14,8 +19,6 @@ import (
"x-ui/util/random"
"x-ui/web/service"
"x-ui/xray"
-
- "github.com/goccy/go-json"
)
type SubService struct {
@@ -34,19 +37,20 @@ func NewSubService(showInfo bool, remarkModel string) *SubService {
}
}
-func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
+func (s *SubService) GetSubs(subId string, host string) ([]string, string, int64, error) {
s.address = host
var result []string
var header string
var traffic xray.ClientTraffic
+ var lastOnline int64
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
- return nil, "", err
+ return nil, "", 0, err
}
if len(inbounds) == 0 {
- return nil, "", common.NewError("No inbounds found with ", subId)
+ return nil, "", 0, common.NewError("No inbounds found with ", subId)
}
s.datepicker, err = s.settingService.GetDatepicker()
@@ -73,7 +77,11 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
if client.Enable && client.SubID == subId {
link := s.getLink(inbound, client.Email)
result = append(result, link)
- clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
+ ct := s.getClientTraffics(inbound.ClientStats, client.Email)
+ clientTraffics = append(clientTraffics, ct)
+ if ct.LastOnline > lastOnline {
+ lastOnline = ct.LastOnline
+ }
}
}
}
@@ -101,7 +109,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
}
}
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
- return result, header, nil
+ return result, header, lastOnline, nil
}
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
@@ -1001,3 +1009,172 @@ func searchHost(headers any) string {
return ""
}
+
+// PageData is a view model for subscription.html
+type PageData struct {
+ Host string
+ BasePath string
+ SId string
+ Download string
+ Upload string
+ Total string
+ Used string
+ Remained string
+ Expire int64
+ LastOnline int64
+ Datepicker string
+ DownloadByte int64
+ UploadByte int64
+ TotalByte int64
+ SubUrl string
+ SubJsonUrl string
+ Result []string
+}
+
+// ResolveRequest extracts scheme and host info from request/headers consistently.
+func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
+ // scheme
+ scheme = "http"
+ if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") {
+ scheme = "https"
+ }
+
+ // base host (no port)
+ if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil && h != "" {
+ host = h
+ }
+ if host == "" {
+ host = c.GetHeader("X-Real-IP")
+ }
+ if host == "" {
+ var err error
+ host, _, err = net.SplitHostPort(c.Request.Host)
+ if err != nil {
+ host = c.Request.Host
+ }
+ }
+
+ // host:port for URLs
+ hostWithPort = c.GetHeader("X-Forwarded-Host")
+ if hostWithPort == "" {
+ hostWithPort = c.Request.Host
+ }
+ if hostWithPort == "" {
+ hostWithPort = host
+ }
+
+ // header display host
+ hostHeader = c.GetHeader("X-Forwarded-Host")
+ if hostHeader == "" {
+ hostHeader = c.GetHeader("X-Real-IP")
+ }
+ if hostHeader == "" {
+ hostHeader = host
+ }
+ return
+}
+
+// BuildURLs constructs absolute subscription and json URLs.
+func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
+ if strings.HasSuffix(subPath, "/") {
+ subURL = scheme + "://" + hostWithPort + subPath + subId
+ } else {
+ subURL = scheme + "://" + hostWithPort + strings.TrimRight(subPath, "/") + "/" + subId
+ }
+ if strings.HasSuffix(subJsonPath, "/") {
+ subJsonURL = scheme + "://" + hostWithPort + subJsonPath + subId
+ } else {
+ subJsonURL = scheme + "://" + hostWithPort + strings.TrimRight(subJsonPath, "/") + "/" + subId
+ }
+ return
+}
+
+// BuildPageData parses header and prepares the template view model.
+func (s *SubService) BuildPageData(subId, hostHeader, header string, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
+ // Parse header values
+ var uploadByte, downloadByte, totalByte, expire int64
+ parts := strings.Split(header, ";")
+ for _, p := range parts {
+ kv := strings.Split(strings.TrimSpace(p), "=")
+ if len(kv) != 2 {
+ continue
+ }
+ key := strings.ToLower(strings.TrimSpace(kv[0]))
+ val := strings.TrimSpace(kv[1])
+ switch key {
+ case "upload":
+ if v, err := parseInt64(val); err == nil {
+ uploadByte = v
+ }
+ case "download":
+ if v, err := parseInt64(val); err == nil {
+ downloadByte = v
+ }
+ case "total":
+ if v, err := parseInt64(val); err == nil {
+ totalByte = v
+ }
+ case "expire":
+ if v, err := parseInt64(val); err == nil {
+ expire = v
+ }
+ }
+ }
+
+ download := common.FormatTraffic(downloadByte)
+ upload := common.FormatTraffic(uploadByte)
+ total := "∞"
+ used := common.FormatTraffic(uploadByte + downloadByte)
+ remained := ""
+ if totalByte > 0 {
+ total = common.FormatTraffic(totalByte)
+ left := totalByte - (uploadByte + downloadByte)
+ if left < 0 {
+ left = 0
+ }
+ remained = common.FormatTraffic(left)
+ }
+
+ datepicker := s.datepicker
+ if datepicker == "" {
+ datepicker = "gregorian"
+ }
+
+ return PageData{
+ Host: hostHeader,
+ BasePath: "/",
+ SId: subId,
+ Download: download,
+ Upload: upload,
+ Total: total,
+ Used: used,
+ Remained: remained,
+ Expire: expire,
+ LastOnline: lastOnline,
+ Datepicker: datepicker,
+ DownloadByte: downloadByte,
+ UploadByte: uploadByte,
+ TotalByte: totalByte,
+ SubUrl: subURL,
+ SubJsonUrl: subJsonURL,
+ Result: subs,
+ }
+}
+
+func getHostFromXFH(s string) (string, error) {
+ if strings.Contains(s, ":") {
+ realHost, _, err := net.SplitHostPort(s)
+ if err != nil {
+ return "", err
+ }
+ return realHost, nil
+ }
+ return s, nil
+}
+
+func parseInt64(s string) (int64, error) {
+ // handle potential quotes
+ s = strings.Trim(s, "\"'")
+ n, err := strconv.ParseInt(s, 10, 64)
+ return n, err
+}
diff --git a/web/assets/css/custom.min.css b/web/assets/css/custom.min.css
index 0eb2b409..c078177a 100644
--- a/web/assets/css/custom.min.css
+++ b/web/assets/css/custom.min.css
@@ -1 +1,2835 @@
-: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,.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,.2);--dark-color-text-primary:rgba(255,255,255,.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,.05);--dark-color-tag-border:rgba(255,255,255,.15);--dark-color-tag-color:rgba(255,255,255,.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,.2);--dark-color-codemirror-line-selection:rgba(0,135,113,.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,.9);--dark-color-back-top:rgba(61,76,104,.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,.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,.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,.2);--dark-color-codemirror-line-selection:rgba(82,84,94,.3);--dark-color-login-background:#0a2227;--dark-color-login-wave:#0f2d32;--dark-color-tooltip:rgba(88,93,100,.9);--dark-color-back-top:rgba(88,93,100,.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{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 .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 .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}@-webkit-keyframes ma-bg-move{0%{background-position:-500px 0}100%{background-position:1000px 0}}@keyframes ma-bg-move{0%{background-position:-500px 0}50%{background-position:1000px 0}100%{background-position:1000px 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 .2s linear;transition:opacity .2s linear}.fade-in-linear-enter-active,.fade-in-linear-leave-active{-webkit-transition:opacity .2s linear;transition:opacity .2s linear}.fade-in-enter-active,.fade-in-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .3s cubic-bezier(.55,0,.1,1)}.zoom-in-center-enter-active,.zoom-in-center-leave-active{-webkit-transition:all .3s cubic-bezier(.55,0,.1,1);transition:all .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 .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .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 .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .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 .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1);transition:transform .3s cubic-bezier(.23,1,.32,1),opacity .3s cubic-bezier(.23,1,.32,1),-webkit-transform .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 .3s;transition:all .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}.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(